Skip to content

HI-Fi: driver assist and summary page - #174

Open
hir-al-14 wants to merge 15 commits into
benevolentbandwidth:mainfrom
hir-al-14:driver_assist_hifi
Open

hir-al-14 wants to merge 15 commits into
benevolentbandwidth:mainfrom
hir-al-14:driver_assist_hifi

Conversation

@hir-al-14

@hir-al-14 hir-al-14 commented May 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds the high-fidelity driver_assist PWA flow for importing route JSON, completing/reporting stops, finishing a route, and exporting a route summary JSON.

Motivation

  • Drivers need a browser-based mobile workflow that matches the existing delivery flow while preserving route progress locally and producing a usable end-of-route summary for dispatch or review.

Changes

  • Frontend:

    • Connected /upload-route to the driver assist route import flow so uploaded JSON is transformed into driver route state instead of looping back to upload.
    • Updated /driver_assist with the mobile-first stop workflow: route summary, remaining/delivered/reported sections, expandable stop cards, notes, delivered status, issue reporting, and navigation.
    • Added local progress persistence so driver notes, completed stops, and reported failures survive refreshes.
    • Added /driver_assist/summary with total/complete/remaining counts, per-stop completion badges, b2 footer branding, and sticky route summary export.
    • Connected route summary export to the shared JSON download helper.
    • Split driver assist UI into focused component, style, storage, summary, import, transform, and export helper files.
    • Added practical comments across the driver assist route, summary, storage, import, transform, and export code paths.
    • Ran Prettier across changed app/ui files and kept the actual content diff focused to the driver assist files.
  • Backend:

    • No backend changes in this driver assist UI work.
  • Mobile:

    • No native app/mobile changes in this driver assist UI work.

Validation

Frontend

  • npm --prefix app/ui run lint
  • npm --prefix app/ui run format:check
  • npm --prefix app/ui run typecheck
  • npm --prefix app/ui run test
  • npm --prefix app/ui run build
  • npm --prefix app/mobile run lint
  • npm --prefix app/mobile run typecheck

Backend

  • cmake --preset dev
  • .github/scripts/check-backend-static.sh build/dev
  • cmake --build --preset dev --parallel
  • ctest --preset dev --output-on-failure --no-tests=error -LE 'e2e|docker'
  • docker compose -f deploy/compose/docker-compose.arm64.yml --env-file deploy/env/http-server.arm64.env config

Risk

  • Main risk is browser storage state: stale or invalid saved driver route JSON could send a driver back to upload, but invalid local data is cleared and invalid uploads show user-facing errors.
  • Route summary export depends on the browser download path, so download behavior may vary slightly by browser/PWA install mode.

Rollout and Recovery

  • Roll out by merging the PWA UI changes and having drivers open /driver_assist or install the PWA from the browser.
  • Recovery is to revert this PR; existing route-manager JSON save files and native mobile app behavior remain separate.

High-Signal PR Checklist

  • The summary states the user-visible or operational outcome, not just file names.
  • The motivation explains why this change is needed now.
  • The change list separates frontend, backend, infrastructure, and documentation work where applicable.
  • Validation includes exact commands run, relevant output, and any checks intentionally skipped.
  • Risks, migrations, feature flags, and rollback steps are called out when relevant.
  • Screenshots or request/response examples are included for UI and API behavior changes.

Screenshots for driver assist

Driver assist stops screen Driver assist route progress Driver assist summary export

@hir-al-14 hir-al-14 changed the title Driver assist hifi Hifi: driver assist May 18, 2026
@hir-al-14 hir-al-14 changed the title Hifi: driver assist HI-Fi: driver assist and summary page May 18, 2026
@hir-al-14
hir-al-14 force-pushed the driver_assist_hifi branch from 6f361ec to fc0f6ff Compare May 25, 2026 18:34
@markboenigk

Copy link
Copy Markdown
Collaborator

Great work on the driver assist flow — the library boundary in lib/driver-route/, the Zod validation, and the localStorage resilience are all solid. A few things to fix before merge:

Bugs

  • CSV is accepted but not handled. upload-route/page.tsx validates .json and .csv extensions and the dropzone copy says "Drag and drop .json or .csv files here", but driver_assist/page.tsx calls loadSessionFromText() which only understands JSON. A driver uploading a CSV will leave the upload page and then hit "This file is not valid JSON." Either remove CSV from the accepted types, or add a CSV parse path.

  • Warning button in summary is a dead stub. driver_assist/summary/page.tsx renders a <button aria-label="View remaining deliveries"> with no onClick. Wire it up or remove it.

  • export const dynamic = "force-dynamic" is misplaced and ineffective. In upload-route/page.tsx it appears after "use client" and the imports, which is the wrong order for Next.js to pick it up. It also has no effect in a client component — route segment config only applies to server components. Remove it.

Minor

  • The setTimeout(0) in driver_assist/summary/page.tsx is unnecessary. persistRoute is a synchronous localStorage.setItem, so the write is already committed before router.push fires. Replace with a direct call.

  • upload-route/page.tsx injects styles via a raw inline <style> string while every other driver_assist/ file uses CSSProperties objects from styles.ts. Ideally this is consistent — or at least call out the intentional split.

@hir-al-14
hir-al-14 force-pushed the driver_assist_hifi branch from 0f51ab7 to c7e8172 Compare June 27, 2026 11:55
@hir-al-14

Copy link
Copy Markdown
Contributor Author

Rebased this branch on top of fix-driver-route-export-import branch which is PR #213, so it includes the latest Driver Assist upload fixes first.
Then added the review fixes: CSV route uploads now work, the dead summary warning button was removed, and the unnecessary summary setTimeout(0) was replaced with a cleaner route load.

@markboenigk

Copy link
Copy Markdown
Collaborator

Thanks for addressing all the feedback — CSV handling, the dead warning button, the dynamic export, and the style comment are all good.

Two things to fix before merge:

CSV lat/lng accepts empty columns as 0,0
In routeUploadValidation.ts, Number("") === 0 which passes Number.isFinite, so a CSV row with a missing lat or lng column silently places the stop at coordinates 0,0 instead of rejecting the file. The existing check needs a non-empty guard before converting:

```ts
const latStr = value(["lat", "latitude"]);
const lngStr = value(["lng", "lon", "long", "longitude"]);
if (!latStr || !lngStr || !Number.isFinite(Number(latStr)) || !Number.isFinite(Number(lngStr))) {
throw new Error("CSV route stops must include valid lat and lng columns.");
}
```

queueMicrotask in summary/page.tsx still defers unnecessarily
The setTimeout(0) was there to wait for a localStorage write — but persistRoute is a synchronous setItem, so by the time router.push fires the write is already committed. The queueMicrotask still defers setRoute/setHasCheckedRoute, which means if savedRoute is null, router.replace fires synchronously while the state updates are still pending — so the component can briefly re-render with hasCheckedRoute=true and route=null before navigation finishes. A plain direct call is safe here:

```ts
const savedRoute = readSavedRoute();
setRoute(savedRoute);
setHasCheckedRoute(true);
if (!savedRoute) {
router.replace("/upload-route");
}
```

@hir-al-14
hir-al-14 force-pushed the driver_assist_hifi branch from c7e8172 to c9c2bcf Compare August 8, 2026 00:34

@markboenigk markboenigk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review (medium effort) — 2 correctness bugs, 2 cleanup items

Reviewed the full diff on driver_assist_hifi, including the latest commits (fix(driver): reject CSV route uploads missing coordinates, fix(driver): load summary route synchronously).

Blocking:

  1. app/ui/src/lib/driver-route/importSession.ts:47 — resultsRouteStopSchema.capacityUsed uses z.number().positive(), rejecting the legitimate value 0. vroomToRoutes.ts:73 computes capacityUsed as step.load?.[0] ?? 0, so a pickup-only/zero-demand stop legitimately has capacityUsed: 0. Uploading such a valid Results-page export fails validation, and the driver sees "This file is not a recognized route or session JSON file" for a perfectly valid file. Suggest .nonnegative() instead of .positive().

  2. app/ui/src/app/upload-route/routeUploadValidation.ts:64 — CSV import silently coerces packageCount: 0 to 1 via packageCount > 0 ? packageCount : 1. A CSV row for a zero-package stop gets its data corrupted with no error surfaced. Same falsy-zero root cause as #1.

Non-blocking cleanup:

  1. importSession.ts:173, transformSession.ts, routeUploadValidation.ts:29-63 — three near-identical functions duplicate the same stop-defaulting logic (address/customerName/packageCount/notes/status). Already diverged, as seen in finding #2 — worth consolidating into one helper.

  2. importSession.ts:60 — loadSessionFromFile/loadSessionFromText appear to be dead code now that driver_assist/page.tsx uses parseRouteUploadFile instead; only test references remain.

@markboenigk markboenigk mentioned this pull request Aug 21, 2026
17 of 23 tasks

@markboenigk markboenigk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid refactor — well-tested, no correctness blockers. A few things a maintainer would want addressed before merge, grouped by priority.

Removed guards

1. Size cap + MIME check dropped from the import path — app/ui/src/lib/driver-route/importSession.ts:58
The old loadSessionFromFile rejected route files >1MB and non-JSON before parsing. loadDriverRouteFromText has no size guard now — the only bound is upload-route's 10MB limit, and parsing runs synchronously inside queueMicrotask on the PWA main thread. A 10MB route file freezes the driver's UI for seconds during import instead of being rejected early. Please restore an early size/type check on this path.

2. sequence > 0 treats an explicit 0 as missing — app/ui/src/app/upload-route/routeUploadValidation.ts:68
For a CSV with a 0-based sequence column whose rows aren't pre-sorted, the sequence=0 row falls back to file position while the rest get sequence-1, so after the sort/renumber that stop lands in the wrong place. Use Number.isFinite(sequence) && sequence >= 0 and key ordering off the parsed value, or handle zero-vs-absent explicitly.

Efficiency

3. Route file parsed twice — app/ui/src/app/upload-route/page.tsx:86
handleContinue runs a full parseRouteUploadFile parse + Zod validation just to check validity, discards the DriverRoute, stores the raw text, and /driver_assist re-parses on mount. Two full passes of a multi-MB file on a phone — store or reuse the parsed route instead.

4. Per-row closure + linear header scan — app/ui/src/app/upload-route/routeUploadValidation.ts:30
parseRouteCsvText rebuilds a value() closure per row and calls headers.findIndex on every field lookup — O(rows × fields × headers). Precompute a normalized header→index map once before the row loop.

Structure

5. Validation code imports from a React-hook module — app/ui/src/app/upload-route/routeUploadValidation.ts:1
parseCsvToRows is pulled from @/app/edit/hooks/useCSVImport into code loaded by the driver PWA. Move the pure RFC-4180 parser to a leaf util so the edit-page hook doesn't enter the driver bundle's dependency graph.

6. Near-duplicate route transforms — app/ui/src/lib/driver-route/importSession.ts:131
transformResultsRouteToDriverRoute and parseRouteCsvText both sort by sequence, map through createPendingDeliveryStop, and build { driverName, routeLabel, stops } — already diverging in style. Future ordering/shape changes have to be made in two places.

7. Undocumented queueMicrotask-wrapped setState — app/ui/src/app/driver_assist/page.tsx:69 (also :95, upload-route/page.tsx:38)
Three sites defer state updates by a microtask with no comment. Add one explaining what it guards against, or drop it if it's not load-bearing.


🤖 Generated with Claude Code

@markboenigk

Copy link
Copy Markdown
Collaborator

Stepping back from line-level notes this round — there's a flow-level problem that makes most of the file-by-file feedback moot until it's resolved.

Blocking: the uploaded route never reaches Driver Assist

upload-route/page.tsx:87 writes "routeFile" and pushes to /driver-view, which is a stub that renders "route display coming soon" — and calls removeItem("routeFile") on mount, destroying the payload.

Nothing in the app navigates to /driver_assist:

grep -rn driver_assist app/ui/src
→ manifest.ts start_url, sw.js cache, FeedbackLauncher pathname check. No push/href.

So everything this PR adds — CSV parsing, Zod validation, the sessionStorage handoff, the error-surfacing path — is only reachable by launching the installed PWA or typing the URL. A driver who uploads a route today sees a placeholder.

Acceptance: uploading a .csv and a .json from /upload-route lands on /driver_assist with stops rendered. /driver-view is deleted in this PR, or /upload-route keeps pointing at it and the Driver Assist handoff is removed — one owner for the "routeFile" key, not two. Please say which you chose and why.

Blocking: storage access, as a rule not a patch

Last round I flagged an unguarded removeItem; the fix wrapped that call and left the identical pattern at storage.ts:20 and driver_assist/page.tsx:81. My fault for citing a line instead of the rule, so stating it as a rule:

Add lib/session/safeStorage.ts exporting safeGet/safeSet/safeRemove that swallow throws and return null/false. Route every storage call in driver_assist/, upload-route/, and driver-view/ through it.

Acceptance: grep -rn 'localStorage\.\|sessionStorage\.' <those dirs> returns only safeStorage.ts. The other ~20 call sites in edit/, results/, and upload-save-point/ are out of scope — I'll file a follow-up issue.

Why it matters: in blocked-cookie or quota-exhausted browsers the current error path throws while handling a storage error, so the page white-screens instead of showing the driver an explanation.

Blocking: CSV stop ids can collide

id: getRowValue(row, headerIndexes, ["id", "stopid"]) || String(index + 1)

A CSV with an id column that's blank on some rows generates "1", "2", … which can equal real ids on other rows. updateStop matches by stop.id, so marking one stop delivered silently flips the collider too.

Acceptance: reject the file with a clear message on duplicate ids after fallback, or make the fallback non-colliding (row-${index + 1}). A test with a CSV whose row 3 has id=1 and whose row 1 has a blank id.

Explicitly not blocking

Noted so they don't come back as surprises — please don't fix them here:

  • summary/page.tsx:24 passes an uncached getSnapshot to useSyncExternalStore. Real, pre-existing on main, separate issue.
  • JSON_ROUTE_FILE_TYPES rejecting text/plain. Cosmetic; the extension check already ran.
  • importSession.ts:70 reporting the session-save Zod error for a malformed results route. Confusing message, not a correctness bug.

What I'd like back

Not a diff — a short note saying which routing option you picked, plus the two grep/test results above. If the flow works end to end and those pass, I'll merge without another line-by-line pass.

@markboenigk markboenigk mentioned this pull request Sep 16, 2026
11 of 20 tasks
@markboenigk

Copy link
Copy Markdown
Collaborator

This solves the same CSV-import/upload-error problem as the #181→#212→#213 chain via a separate implementation, and #213 has since gone further (route-handoff hardening, traffic departure clamping) — recommend that chain be the one we merge, not this branch.

One thing here is worth keeping: this PR's routeUploadValidation.ts correctly handles sequence = 0 (Number.isFinite(sequence) && sequence >= 0), while #181/#212/#213 all regressed to sequence > 0, which drops valid zero-indexed sequences and scrambles stop order. Requesting that check be ported over to #213 before this PR is closed, so the fix isn't lost.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants