Walking 29 pages and 142 links against a disposable replica stack
Rather than click around an app whose development server reaches a live API, a full throwaway replica (containerised Postgres on a spare port, the real API with every database setting overridden by environment, the sync service pointed at a dead port) was seeded through the API and then driven with a headless browser: 29 pages at 1366 px and again at 375 px, all 142 internal links fetched, and 16 end-to-end actions each with a checked outcome - no page errored and no link was broken, but eleven defects surfaced, including a nav that rendered empty for about 0.9 s on every full page load, unlocalised currency and dates, a collapsed list that made a populated page read as empty, and a specification grid that pushed a phone page 747 px sideways.
Status
Built, verified locally and committed, 1 October 2026 (overnight). Not
pushed — the web app auto-deploys from master, and nothing went live while
the operator was asleep. Two commits: the page fixes, then the dashboard.
The brief
The operator asked, before going to bed: go through every page from the dashboard to Users; find broken links and broken activities and fix them; scroll every page and fix what is wrong; redesign the dashboard to be more engaging, more user-friendly and more detailed; commit; be done by 7am.
How it was done (and why not against the live site)
The local web dev server talks to the production API, so clicking around it would have changed real data. Instead a full local copy was stood up:
- a throwaway Postgres in Docker on port 5433 (the operator's own dev database on 5432 was not touched), migrated and seeded from the repo;
- the real NestJS API on port 3101, every DB setting overridden by
environment so the Railway values in
apps/api/.envcould not apply; - the real Next.js app on port 3000 pointed at that API, PowerSync pointed at a dead local port so nothing reached the live sync service;
- realistic data created through the API (5 supplier lots at different stages, sub-lots, 46 devices with audits/grades/wipes, pallets, sold devices, consumables with movements, extra users), with only the station-captured hardware fields set in SQL.
Signed in with the seed's local test account. Then, driving the installed Edge with puppeteer-core:
- every page (29) at 1366px, full-page screenshot each, with console errors, page errors, failed requests, error text, sideways overflow, horizontally scrolling boxes, broken images and nameless buttons recorded;
- every internal link (142) fetched and checked;
- 16 end-to-end actions with a checked outcome each: dashboard search, saved filters, paging, create lot, add device by hand, sell from a lot, new pallet (both layouts), create/adjust/transfer consumables, rename a user, reports range + Excel export, add a lookup, the Sell page, save a device's grade, create a user;
- a 375px phone pass over every page, and the dashboard as a technician as well as an admin.
What it found
No page errored, no link was broken, every action worked. (Three "failures" in the first actions run were the script's own assumptions — a dialog, not a menu; buttons, not a form; values in edit-in-place inputs, which page text does not include — each re-tested and confirmed working.) What it did find:
| Found | Fix |
|---|---|
The nav rendered empty for ~0.9s on every full page load until its own /api/me round trip returned | The root layout reads the session once on the server and hands it to the nav through a context (access-context.tsx); links are there on first paint, still refreshed from /api/me |
Money printed £14325.00 and a loss as £-248.00 | lib/money.ts now uses en-GB currency: £14,325.00, -£248.00 — every page at once |
| Sold looked empty ("Nothing has been sold from a pallet yet") while 10 devices had sold, the list collapsed | Device list opens by default when devices are all that has sold |
US dates (10/1/2026, 12:59 AM) beside UK ones | four locale-less toLocaleString calls made en-GB |
| Pallet chip broke mid-word in the lot table | whitespace-nowrap |
Consumables on-hand 1780 | thousands separator |
| Assets, Sold, Consumables, Users had no breadcrumb | added, matching the other pages |
| Assets' two buttons were the only navy primaries | brand blue |
| New Pallet → Layout 1 → Continue gave no feedback while the next page loaded | shows "Opening…" |
| Dashboard's movement table mixed consumable units ("Received 2,101") with device events ("Booked in 51") | split, in the redesign |
| On a phone, a Layout 2 (specification grid) pallet pushed the whole page 747px sideways | its scroll box lacked min-w-0 / relative, so it grew to the table's width; now the grid scrolls inside its box (page overflow 0) |
The dashboard redesign
Same single endpoint and data contract (GET /dashboard/operations; the API
returns keys, ATTENTION_LINKS owns the routes; every attention count still
links to exactly the list it counts). New building blocks live in
dashboard/widgets.tsx; components.tsx, shared with the lot pages, is
unchanged.
- Greeting band — "Good morning, Admin", today's date (Europe/London), a one-line summary, the health state in words, refresh, and a large search. White on the brand gradient; the lightest stop carries white at 4.6:1.
- Four headline cards — devices in hand (with an available / committed / on-hold bar whose parts add up to the total), stock value with revenue and profit-or-loss, sold with sell-through, needs attention by severity. A technician sees consumable units instead of money.
- Needs attention as severity-coloured cards, each one a link; the old duplicate "Inventory health" table is folded into one "All clear: …" line.
- Stock by status — a donut ("on record", so it does not clash with "in hand") with a legend that links into each list; Locations as cards.
- Stock movement — devices (events) and consumables (units) side by side.
- Ageing — one stacked bar, five bucket tiles, slow-moving callout.
- Side column — quick actions (compact), incoming lots, and recent activity as a timeline with icons and relative times ("2 hours ago", full date on hover). Technicians get an honest "visible to managers" line, not "nothing recorded".
Accessibility rules carried over: every chart is aria-hidden and its
figures are on the page as text; state is never colour alone (words and
icons); sections are <section> named by their <h2>; the page <h1>
reads "Dashboard: Good morning, …"; the chart palette is the reports page's
validated one.
How it was proved
tsc --noEmitclean.- Dashboard re-crawled at 1366px as admin (34 links, 0 broken) and as a technician (28 links, 0 broken); every page re-crawled at 375px.
- Screenshots reviewed for both roles.
Left alone, on purpose
- The asset page's right column leaves a gap beside a tall Details card — cosmetic, and fixing it means restructuring the page.
- Lookups lists "Tier 1", "Tier 2" and "Mixed" as manufacturers — existing data, not code.
- The Scan page could not be exercised end to end: it reads from PowerSync's local database, which the test stack deliberately did not connect to the live sync service.
To do in the morning
Look at the dashboard and the fixed pages, then push. The local test stack was torn down and the operator's normal dev server restored.
A published copy. Commit references and internal identifiers have been removed and the operator is not named; the engineering, the counts and the stated limits are unchanged.