Skip to content
The record
Finding1 October 20265 min read

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.

methodsystemsitad-lifecycle

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/.env could 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:

FoundFix
The nav rendered empty for ~0.9s on every full page load until its own /api/me round trip returnedThe 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.00lib/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 collapsedDevice list opens by default when devices are all that has sold
US dates (10/1/2026, 12:59 AM) beside UK onesfour locale-less toLocaleString calls made en-GB
Pallet chip broke mid-word in the lot tablewhitespace-nowrap
Consumables on-hand 1780thousands separator
Assets, Sold, Consumables, Users had no breadcrumbadded, matching the other pages
Assets' two buttons were the only navy primariesbrand blue
New Pallet → Layout 1 → Continue gave no feedback while the next page loadedshows "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 sidewaysits 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 --noEmit clean.
  • 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.