Skip to content

feat(parcels): add /parcels tracker with carrier detection (WYA-236) - #1

Merged
charlesponti merged 6 commits into
mainfrom
linear/wya-236
Oct 31, 2026
Merged

charlesponti merged 6 commits into
mainfrom
linear/wya-236

Conversation

@charlesponti

@charlesponti charlesponti commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Ports the standalone parcel tracker into Earth as /parcels and generalises it: the user saves their own tracking numbers in localStorage, the carrier is detected from the number's format, and the app reads status through a server route with a graceful link-out when a carrier blocks the request.

Linear issue: WYA-236

Fixes WYA-236.

What changed

  • /parcels route (app/routes/parcels.tsx) — full-screen map plus status panel. On mobile the panel sits below the map; on desktop the two sit side by side. Add / list / remove numbers, per-item marker type, refresh status.
  • Carrier detection (app/lib/parcels.ts) — normalise then first-match precedence: DHL JJD, UPS 1Z+16, Royal Mail, USPS 20–22 digits, Evri 16 digits, FedEx 12/15 digits, DHL 10 digits, then carrier-unknown. Precedence is documented in the file and tested.
  • Persistence — earth.parcels.v1 in localStorage only. No backend, no account, no sync.
  • Server status route (app/routes/api.parcel-status.ts + app/lib/server/parcel-status.ts) — accepts carrier + number, reads DHL's own tracking JSON (falling back to DHL's page and schema.org ParcelDelivery on other carriers), normalises status / events / ETA. Blocks, challenges, network failures and unreadable pages return state: "unavailable" with the carrier tracking link. An unknown format returns unknown-carrier without attempting a fetch. No status is ever defaulted or invented.
  • Map (app/components/ParcelMap.tsx) — great-circle route arc, endpoint dots, interpolated marker. Named-city scans jump the marker; otherwise position interpolates between collection and estimated delivery. Marker emoji comes from the item's type: flight ✈️, moving 🚚, parcel 📦, clothing 👕, food 🍔, car 🚗, fallback 📍. A route is drawn only when both named endpoints resolve to coordinates; an unmapped destination omits the route rather than mislabelling a scan.
  • Shell — /parcels renders outside the TfL map + bottom sheet shell in root.tsx; all other routes are untouched.
  • Tooling — Vitest + jsdom, test script, and CI now runs pnpm test.

Acceptance criteria → evidence

  • /parcels loads; add persists across reload — tests/parcels-route.test.tsx adds a number, unmounts/remounts the app against the same localStorage, sees it again; tests/parcels.test.ts re-imports the storage module after saving. Live GET /parcels → 200.
  • Detector correctness and documented precedence — tests/parcels.test.ts has a fixture per carrier (DHL 10-digit and JJD, UPS, Royal Mail, FedEx 12/15, USPS 20/22 with 94 prefix, Evri), one unknown format, URL assertions per carrier, and precedence cases for 10/12/15/16/20–22 digit overlaps plus JJD/1Z early wins.
  • Live DHL waybill 1291215726 — live check on 30 Oct 2026: GET /api/parcel-status?number=1291215726&carrier=dhl returned state: "unavailable", reason: "blocked", status: null, with the DHL tracking URL. DHL answered the server fetch with a 403 block page, so the route takes the issue's allowed graceful branch: the UI shows "Live status unavailable" and a "Track on DHL Express" link. No status is displayed or invented. The successful branch is covered by tests/parcel-status.test.ts using a DHL-shaped payload, and the route/marker wiring by tests/parcels-route.test.tsx.
  • Removing removes it from the list and localStorage — route test removes via the UI and asserts the stored array is empty; storage test removes, re-imports, and asserts it is gone.
  • Unknown format saved, labelled carrier-unknown, no fetch — route test asserts "Carrier unknown" and fetch is never called; server test asserts unknown-carrier with zero fetch calls.
  • Each item's type drives its marker emoji — emoji map unit-tested for all six types plus fallback; route test hands the map 🚚 for a moving item and persists a type change to food (🍔).
  • No regressions — live smoke: / 200, /places 200, /parcels 200, /tfl keeps its existing 302 → /. The standalone charlie-parcel checkout was read only; watch.py and shipment.json were not modified.

Validation

  • pnpm test — 53 tests passing
  • pnpm typecheck — passing
  • pnpm lint — passing
  • pnpm build — passing
  • Live API and route smoke checks listed above

Notes

  • No manual carrier-override UI in v1, per the issue. Unknown formats are clearly labelled and can be tracked from the carrier's page once identified; flagging it here in case that hurts.
  • The map draws a route only when the carrier actually returns origin/destination (or scanned city) data, so a blocked carrier shows the link-out panel rather than an invented route.

Saved tracking numbers live in localStorage only; the carrier is detected from the number format with an explicit first-match precedence. A server api/parcel-status route reads DHL's own tracking data and degrades to a carrier link-out when a carrier blocks or returns an unreadable page, never inventing a status. The map reuses the standalone tracker's type emoji and named-scan/time interpolation marker behaviour.

Refs: WYA-236
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T18:52:41.569105Z d38168d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The saved-item type select only showed an emoji; include the type name so the control is readable and screen-reader friendly.

Refs: WYA-236

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d38168dabe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/lib/parcels.ts Resolved
Comment thread app/routes/parcels.tsx Resolved
Match only affirmative completed deliveries so 'Not delivered' or 'will be delivered' do not place the marker at the destination. Draw a route only when both named endpoints resolve to coordinates, rather than substituting an intermediate scan for an unmapped destination.

Refs: WYA-236
Keep mocked fetch implementations from leaking across test cases if an assertion fails early.

Refs: WYA-236
@charlesponti charlesponti changed the title feat(parcels): add /parcels tracker with carrier detection feat(parcels): add /parcels tracker with carrier detection (WYA-236) Oct 30, 2026
Carrier status is live data; prevent intermediaries from caching the api/parcel-status response.

Refs: WYA-236
@charlesponti
charlesponti merged commit 92de6a6 into main Oct 31, 2026
1 check passed
@charlesponti
charlesponti deleted the linear/wya-236 branch October 31, 2026 15:11
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.

1 participant