edrsc.com had no entry point — a visitor could not tell which of the two
apps they wanted. This adds @edr/landing: one static page whose job is to
make that choice obvious, with the two doors above the fold and the rest
kept to supporting context.
Fills in apps/edr-landing/, which until now held an orphaned next-env.d.ts
from an abandoned Next 15 start and was built by nothing.
- Next.js 14 static export. Versions mirror the passenger portal exactly,
so they resolve to what the lockfile already had and add 6 packages.
Depends on no workspace package — @edr/ui-common's Tailwind 4 tokens do
not fit this Tailwind 3 setup.
- Destinations come from NEXT_PUBLIC_PASSENGER_URL / NEXT_PUBLIC_FREIGHT_URL
as origins, with the entry path appended in src/lib/apps.ts. Freight links
to /portal rather than /, which is that app's own marketing landing.
- Design lifted from EDRFreightLandingPage so the two public surfaces read
as one railway. Figures (752 km, ~16 h) are the ones freight already
asserts publicly.
- Fonts load as a stylesheet, not next/font: next/font fetches from
fonts.googleapis.com during `next build` and failed the build outright on
a machine without network. Verified deterministic over repeated builds.
- CountUp renders its final value server-side and the reveal animation's
hidden state is scoped behind html.js, so the page is complete with
JavaScript off instead of blank.
turbo.json: a static export's artifact is out/, which the build task did not
list — a cache hit would have restored a build with the artifact missing.
Also gitignored.
Dockerfile.web: passes the two NEXT_PUBLIC_* build args through (a static
export inlines them at build time, so runtime env does nothing), and exempts
this app from the VITE_* required-check that guards the freight Vite apps and
would otherwise fail its build.
Claude-Session: https://claude.ai/code/session_011ZMMHwK4Ham1Dt19FVZQj3
Nuke the 17 hand-written raw-SQL reports (no pagination, hard LIMITs) and
the reports module built around them. Replace with a resolver contract:
a report declares columns/filters/permission and a TypeORM QueryBuilder;
ReportRunnerService applies filtering, a whitelisted sort, offset/limit
paging, and a COUNT(*) FROM (query) wrapper for the total (getCount() is
wrong for GROUP BY). ReportExportService re-runs the same resolver
unpaginated for xlsx (exceljs) and pdf (existing PdfRenderService, now
landscape-capable) exports.
Ships with 4 reports: bookings-list, revenue-by-customer,
aging-receivables, contract-utilization. Catalog + per-report permission
checks live in the controller; adding a report is one new definitions/
file plus a REPORT_KEYS entry, no frontend change.
Hermetic E2E harness targeting pricing integrity and backoffice config:
- e2e/ docker Postgres (5544) + prepare.sh/run.sh one-command runner + HTML report
- 6 suites / 23 tests reproducing pricing, FX, wallet, refund, config and auth
defects (see docs/ISSUES.md); docs/e2e-test-matrix.md documents the matrix
- two-tier harness (slim module boot + direct service instantiation) to work
around the IAM/RabbitMQ/file-type boot wall
- .env.test.example tracked; loader falls back to it for fresh checkouts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>