From ac3ac92587683dd2ac73b3a6994068d6d56908e1 Mon Sep 17 00:00:00 2001 From: OziinG Date: Wed, 26 Aug 2026 09:12:27 +0900 Subject: [PATCH] Restore the requested Shopify route UI boundary Remove the unsolicited operational-state surface and its derived GPS/server warning while preserving dedicated health diagnostics, telemetry redaction, and deployment recovery. Constraint: Existing user work in other worktrees must remain untouched. Rejected: Revert all August 25 changes | security and rollback hardening are independent and remain required. Confidence: high Scope-risk: moderate Directive: Do not add cross-page operational dashboards or Pill evidence surfaces without an explicit product request. Tested: 594 node tests; 226 focused tests; build; typecheck; lint; public URL guard; Polaris validation. Not-tested: Production embedded browser until deployment completes. --- DESIGN.md | 390 ------------------ .../features/delivery/operational-state.js | 229 ---------- .../features/delivery/route-detail.server.js | 4 - .../app/features/delivery/route-list-rows.js | 32 +- .../features/delivery/route-plans.server.js | 1 - .../app/features/delivery/route-tracking.js | 57 --- .../app/routes/app.routes.$routeId.jsx | 82 ++-- apps/shopify-app/app/routes/app.routes.jsx | 63 ++- apps/shopify-app/app/routes/app.settings.jsx | 16 +- apps/shopify-app/app/styles/global.css | 12 - apps/shopify-app/app/ui/info-pill.jsx | 5 +- .../app/ui/operational-pill-group.jsx | 14 - .../tests/operational-state.test.mjs | 200 --------- apps/shopify-app/tests/orders-page.test.mjs | 2 +- .../tests/route-list-rows.test.mjs | 74 ---- .../tests/route-tracking-contract.test.mjs | 96 ----- .../tests/route-tracking-live.test.mjs | 29 +- apps/shopify-app/tests/routes-page.test.mjs | 23 +- .../tests/shopify-token-sync.test.mjs | 4 +- 19 files changed, 119 insertions(+), 1214 deletions(-) delete mode 100644 DESIGN.md delete mode 100644 apps/shopify-app/app/features/delivery/operational-state.js delete mode 100644 apps/shopify-app/app/ui/operational-pill-group.jsx delete mode 100644 apps/shopify-app/tests/operational-state.test.mjs diff --git a/DESIGN.md b/DESIGN.md deleted file mode 100644 index ebf1be7..0000000 --- a/DESIGN.md +++ /dev/null @@ -1,390 +0,0 @@ -# Design - -## Source of truth - -- Status: Active -- Last refreshed: 2026-08-24 -- Primary product surfaces: Shopify Admin route list, route detail Tracking tab, tracking map overlay, operational alerts, driver sync-status surface, system notifications, customer-notification operations. -- Evidence reviewed: - - `apps/shopify-app/app/routes/app.routes.$routeId.jsx` - - `apps/shopify-app/app/features/delivery/route-tracking.js` - - `apps/shopify-app/app/ui/info-pill.jsx` - - `apps/shopify-app/app/ui/operational-pill-group.jsx` - - `apps/shopify-app/app/features/delivery/operational-state.js` - - `apps/shopify-app/app/styles/global.css` - - `apps/shopify-app/tests/route-tracking-live.test.mjs` - - `apps/shopify-app/tests/route-tracking-contract.test.mjs` - - 2026-08-20 through 2026-08-23 Kitchener, South, Oshawa, K-food, and customer-email operations audit. -- Decision scope: operational state presentation and alert hierarchy. This document does not redefine route-planning or order-management workflows. -- Observed evidence: the Delivery API list contract supplies one optional `operationalState` per route in the existing batch response; detail/tracking snapshots can carry the same contract. The Shopify app owns queryable token-sync health, while the other Settings dependencies do not yet expose verified app-consumable health evidence. -- Assumption: alert thresholds remain server-owned. The UI maps server evidence and only derives the Device-to-Server arithmetic gap and the invariant “completed with unresolved results” contradiction. - -## Brand - -- Personality: calm, precise, operationally accountable, and easy to scan under time pressure. -- Trust signals: explicit data source, explicit freshness, explicit confirmation state, and direct acknowledgement of unavailable data. -- Avoid: - - Joining independent operational facts with middle dots, bullets, slashes, or prose separators. - - Presenting GPS proximity as delivery confirmation. - - Presenting device-local progress as server-confirmed progress. - - Relying on color alone or using decorative status dots without text. - - Showing a healthy route label while a synchronization or progress mismatch is active. - -## Product goals - -- Goals: - - Let an operator distinguish physical location, device-local workflow, server-confirmed results, and synchronization health in under five seconds. - - Surface a Kitchener-style split state before a route reaches its final stop. - - Use one reusable Pill grammar across route lists, route details, maps, alerts, and in-app driver status. - - Preserve detailed diagnostics without making the default operational view noisy. -- Non-goals: - - Inferring delivery completion from GPS. - - Replacing detailed event history with Pills. - - Adding a separate design system when the existing `InfoPill` can be extended. - - Claiming device-local state in Shopify Admin until sync-health telemetry exists. -- Success signals: - - Operators can correctly answer “where is the driver?”, “what did the device advance to?”, “what did the server confirm?”, and “is sync healthy?” independently. - - A route with a progress gap of two or more stops is visible in both the route list and detail view. - - No operational summary combines state values with `·`. - - Critical sync states remain visible without hover, map interaction, or opening a secondary tab. - -## Personas and jobs - -- Primary personas: - - Dispatcher or operations administrator monitoring active routes. - - Support engineer diagnosing missing events or delayed delivery results. - - Driver checking whether route work has reached the server. -- User jobs: - - Identify live physical position without confusing it with completion. - - Compare local and confirmed progress. - - Find routes that require intervention and understand why. - - Preserve and inspect queued-event evidence before reset or sign-out. -- Key contexts of use: - - Desktop Shopify Admin with multiple routes open. - - Narrow embedded-admin viewport. - - Driver phone in an active route with intermittent connectivity. - - Incident review after the route date has passed. - -## Information architecture - -- Primary navigation: Routes list -> Route detail -> Tracking -> Sync diagnostics or Event history. -- Core routes/screens: - - Routes list: route lifecycle plus one independent synchronization-health Pill. - - Route detail header: lifecycle Pill cluster visible on every tab. - - Tracking map: GPS/freshness and server progress Pills over the map, with diagnostics below. - - Tracking summary: source-separated operational state followed by descriptive metrics. - - Sync diagnostics drawer/panel: queue depth, oldest queued event, last successful acknowledgement, last error code, retry count, and evidence-preservation warning. - - Driver active route: server confirmation and sync queue Pills above the current task. -- Content hierarchy: - 1. Severity and intervention requirement. - 2. Independent source states: GPS, Device, Server, Sync. - 3. Route identity and driver. - 4. Event timing and diagnostic detail. - 5. Raw event history. - -## Design principles - -- Source before status: every operational Pill names its source or domain, such as `GPS`, `Device`, `Server`, or `Sync`. -- Confirmation is explicit: only server-acknowledged stop results use confirmed or delivered wording. -- One Pill, one claim: a Pill must not contain two independent facts joined by punctuation. -- Gaps are first-class: disagreement between sources is its own derived `Gap` or `Sync` Pill, not hidden in a tooltip. -- Unknown is honest: unavailable device telemetry displays `Device unavailable`; it is never inferred from GPS. -- Severity is actionable: warning and critical Pills must open or sit next to the diagnostic action that explains recovery. -- Progressive disclosure: the first row stays compact; timestamps, error codes, and event IDs live in details. -- Tradeoffs: - - More Pills improve source clarity but can create visual noise. Preserve every independent fact, wrap the group, and use progressive disclosure for timestamps and error codes. - - Short labels improve scan speed. Full timestamps and explanations remain available through adjacent details and accessible labels. - -## Visual language - -- Color: - - Neutral: factual, unavailable, stopped, or historical state. - - Info: live GPS or contextual navigation state. - - Success: current and server-confirmed healthy state. - - Warning: delayed, partial, or review-required state. - - Critical: blocked sync, contradictory completion, or missing confirmation requiring intervention. - - `pickup` remains supported for existing order semantics; operational live-state design adds a separate `info` tone rather than overloading `pickup`. -- Typography: - - Pill text: 12px, semibold, sentence case. - - Use tabular numerals for counts and elapsed time where supported. - - Do not abbreviate source names into ambiguous initials. -- Spacing/layout rhythm: - - Pill cluster uses `display: flex`, `flex-wrap: wrap`, and a 6px gap. - - 8px vertical separation between severity row and source-state row. - - No punctuation characters are inserted between sibling Pills. -- Shape/radius/elevation: - - Reuse the existing 999px `InfoPill` radius. - - Pills have no shadow. Alert containers may use a 1px semantic border. -- Motion: - - No pulsing or blinking status. - - Live updates may use a subtle 120ms background transition while respecting reduced motion. -- Imagery/iconography: - - Text is mandatory. Icons are optional and never replace the source label. - - The existing map marker dot remains a map symbol, not an operational-status separator. - -## Components - -- Existing components to reuse: - - `InfoPill` for semantic status. - - Existing map panel, tracking summary, and operational alert container. -- New/changed components: - - `InfoPill` adds `info` tone and optional `ariaLabel`; it remains non-interactive by default. - - `OperationalPillGroup` owns wrapping, gap, and group label semantics. - - `mapRouteOperationalState` is the single pure mapper from the server contract into ordered Pills. - - `SyncDiagnostics` renders queue and acknowledgement details as label/value rows rather than a dense Pill cloud. -- Variants and states: - - GPS: `GPS live`, `GPS 2m delayed`, `GPS offline`, `GPS unavailable`. - - Position: `Near stop 11`, `Position unavailable`. - - Device: `Device 11/11`, `Device unavailable`. - - Server: `Server 1/11`, `Server complete`, `Server unavailable`. - - Sync: `Sync healthy`, `Sync 10 pending`, `Sync blocked`, `Sync unknown`. - - Gap: `Gap 10 stops`, shown only when independently calculable. - - Route lifecycle remains separate: `Ready`, `In progress`, `Completed`, `Cancelled`. -- Pill ordering: - 1. Severity when warning or critical. - 2. GPS freshness. - 3. Physical position. - 4. Device-local progress, when reported. - 5. Server-confirmed progress. - 6. Sync state or derived gap. -- Token/component ownership: - - Pill visual tokens remain in `apps/shopify-app/app/styles/global.css`. - - Pill normalization remains in `apps/shopify-app/app/ui/info-pill.jsx`. - - Operational state derivation belongs in `apps/shopify-app/app/features/delivery/operational-state.js`, not inside JSX rendering. - - Route-detail JSX composes normalized presentation and does not infer new states. - -## Operational state model - -The UI consumes the server's `RouteOperationalStateV1` evidence through one normalized presentation mapper: - -```js -{ - activeAlerts, - deviceProgress, - observedAt, - physicalPosition, - routePlanId, - routeStatus, - serverProgress, - syncHealth, -} -``` - -- Missing `deviceProgress`, `syncHealth`, or `physicalPosition` remains explicit `Unknown`; legacy rows without `operationalState` use the same fallback. -- Stale or low-accuracy GPS may retain freshness and accuracy evidence, but `reliableForProximity: false` prevents a nearby-stop claim. -- GPS-nearest-stop may contribute to a warning but must never populate the Device or Server fields. - -## Severity rules - -- Healthy: - - GPS is live or intentionally historical. - - Queue depth is zero when device telemetry is available. - - Device and server progress agree. -- Warning: - - GPS is delayed beyond server policy. - - Oldest queued workflow event is at least 5 minutes old. - - GPS is near a later stop while earlier server results remain unresolved. - - Device/server gap is one stop for at least 5 minutes or two or more stops at any time. -- Critical: - - A route claims completion while any stop result remains unresolved. - - Device claims final stop or local completion while the server remains behind. - - GPS tracking stops while the server remains `IN_PROGRESS` and unresolved. - - Oldest queued workflow event is at least 60 minutes old or a non-retryable sync error is present. -- Unknown: - - Required telemetry is absent. Unknown does not downgrade an independently known warning or critical condition. - -## Surface specifications - -### Route list - -- Keep lifecycle in its existing Pill. -- Render lifecycle, GPS freshness/position, Device, Server, Sync, Gap, and Alert as independent Pills from each row's optional `operationalState`; do not concatenate labels. -- Critical routes sort above warning routes within the selected business sort unless the user explicitly changes sorting. -- Row accessible name includes the full reason and last confirmed time. - -### Route detail header - -- Always show lifecycle and operational severity as separate Pills. -- When a warning exists, show `Review sync` or `Critical sync` before the source Pills. -- The header must remain meaningful even when the Tracking tab is not selected. - -### Tracking map - -- Use a wrapping Pill group sourced from the same mapper as list and detail. -- Kitchener evidence example: - - `GPS fresh` - - `GPS Stop 11 nearby` - - `Device 11/11` - - `Server 1/11` - - `Sync blocked` - - `Gap 10 stops` -- Detailed “double-click marker” help remains outside the Pill label and is available as map help text. - -### Tracking summary - -- The first row is the operational Pill group. -- Keep descriptive metrics such as Driver, GPS records, Displayed points, and Range as label/value content, not Pills. -- Rename generic `Progress` to `Server-confirmed results`. -- Do not show a generic `Driver stage` as authoritative if it is derived only from server events; label it `Server stage`. - -### Operational alert - -- Structure: - - Severity Pill: `Review sync` or `Critical sync`. - - Title: one sentence naming the contradiction. - - Body: one or two sentences with exact source values. - - Action: `View sync details`. -- Example: - - Pill: `Critical sync` - - Title: `Route progress is not reaching the server` - - Body: `GPS is near stop 11. The server confirms 1 of 11 stops. GPS proximity is not delivery confirmation.` -- Do not repeat the same status values in a dot-separated title. - -### Driver in-app status - -- Render a Pill group above the current task: - - `Device 11/11` - - `Server 1/11` - - `Sync 10 pending` -- While sync is blocked, the current task may remain visible, but route completion is not presented as server-complete. -- `View sync details` shows queued item count, oldest age, last error, and last successful server acknowledgement. -- Sign-out, reset, or data-clearing actions display an evidence-loss warning whenever queue depth is greater than zero. - -### Native system notification - -- Android/iOS notification layouts cannot reliably render application Pill components. -- Use separate labeled lines and line breaks, never middle dots: - -```text -Sync delayed -Device progress 11 of 11 -Server confirmed 1 of 11 -10 updates waiting -``` - -- The notification tap target opens the in-app Pill summary and sync diagnostics. - -### Email and administrator notifications - -- HTML email may render semantic badges, but plain-text fallback uses separate labeled lines. -- Alert payloads carry structured source fields rather than a prejoined status sentence. -- Missing notification runtime configuration is represented as `Notifications disabled` or `Delivery unavailable`, never as an implicit healthy state. - -## Accessibility - -- Target standard: WCAG 2.2 AA for Shopify Admin web surfaces. -- Keyboard/focus behavior: - - Non-interactive Pills are not focusable. - - Interactive Pill-like controls use a button or link element and visible focus state. - - Diagnostic actions follow the Pill group in keyboard order. -- Contrast/readability: - - Every tone meets AA contrast against its background. - - Every Pill includes a visible text label; color is supplemental. -- Screen-reader semantics: - - `OperationalPillGroup` has a concise group label such as `Route operational status`. - - Dynamic warnings use `role="status"`; critical intervention states use `role="alert"` only when newly introduced. - - Elapsed times expose a full timestamp through `aria-label` or adjacent details. -- Reduced motion and sensory considerations: - - No blink or pulse. - - State transition animations are removed under `prefers-reduced-motion`. - -## Responsive behavior - -- Supported breakpoints/devices: Shopify embedded desktop/tablet widths and narrow mobile administration widths down to 320px. -- Layout adaptations: - - Pill groups wrap by item and never use horizontal scrolling. - - On narrow widths, severity occupies the first row and source Pills wrap below. - - Labels remain intact; do not truncate `Server 1/11` into an ambiguous value. - - Diagnostic details become a single-column label/value list. -- Touch/hover differences: - - Hover title is supplemental only. - - Touch users reach the same diagnostics through an explicit action. - -## Interaction states - -- Loading: `GPS loading`, `Server loading`; do not display stale values without their timestamp. -- Empty: `GPS unavailable`, `Device unavailable`, or `Server unavailable`, naming the missing source. -- Error: severity Pill plus concise error; raw codes live in diagnostics. -- Success: `Sync healthy` appears only when the available sources agree and queue depth is known to be zero. -- Disabled: use a neutral Pill and explanatory label, such as `Tracking not started`. -- Offline/slow network: - - Driver app keeps `Device` and `Server` progress separate. - - Admin shows last known GPS age and server-confirmed progress. - - Queue status survives restart and remains visible until acknowledgement or explicit evidence-preserving resolution. - -## Content voice - -- Tone: factual, calm, and specific. Avoid blame and false reassurance. -- Terminology: - - `GPS` means physical telemetry only. - - `Device` means local workflow state reported by the driver app. - - `Server` means acknowledged driver-event state. - - `Sync` means the transport and queue relationship between Device and Server. - - `Delivered` is used only for a server-confirmed terminal delivery event. -- Microcopy rules: - - Use sentence case: `Sync delayed`, not `SYNC DELAYED`. - - Prefer counts: `Server 1/11`, `Sync 10 pending`. - - Do not use middle dots or inline bullets to separate operational claims. - - Do not use `Current position` to describe a workflow stop. Use `Near stop 11` for GPS and `Device 11/11` for workflow. - - Do not say `Complete` without naming the authority when sources can disagree. - -## Implementation constraints - -- Framework/styling system: React Router Shopify embedded app with existing React components and repo-local CSS. -- Design-token constraints: - - Extend existing `InfoPill`; do not add a second Badge/Pill system. - - Preserve current `success`, `warning`, `critical`, `neutral`, and `pickup` behavior. -- Performance constraints: - - Operational presentation is derived with pure functions and memoized in route detail. - - Live SSE updates must not rebuild the map. -- Compatibility constraints: - - List rows, detail, and tracking tolerate an absent optional `operationalState` and show explicit Unknown Pills. - - Settings shows verified Shopify-token health; unexposed runtime dependencies remain Unknown instead of inferred healthy. -- Test/screenshot expectations: - - Contract tests assert no operational text contains the middle-dot separator. - - Unit tests cover Pill ordering, tones, missing telemetry, severity thresholds, and GPS/server mismatch. - - Route list and route detail tests verify the same normalized state produces consistent Pills. - - Responsive screenshot checks cover wide, embedded narrow, and 320px layouts. - - Accessibility checks cover roles, names, contrast, focus, and dynamic alert behavior. - -## Delivery phases - -### Phase 1: truthful server-side presentation (implemented) - -- Replace the combined map freshness sentence with GPS and Server Pills. -- Convert the operational warning header to severity Pill plus structured copy. -- Rename ambiguous metrics to `Server stage` and `Server-confirmed results`. -- Add route-list sync-review Pill derived from GPS/server mismatch. -- Add tests preventing dot-separated operational copy. - -### Phase 2: device sync-health telemetry (server contract implemented) - -- Consume independent heartbeat evidence supplied by `deviceProgress` and `syncHealth`. -- Report local stop, queue depth, oldest queued age, last error code, and last acknowledgement in diagnostics when exposed. -- Device, Sync, and Gap Pills are present in Shopify Admin; driver-app presentation remains a separate implementation surface. -- Add alert transitions and durable audit records. - -### Phase 3: intervention and recovery - -- Add sync diagnostics with evidence preservation. -- Add safe retry/reconcile actions with explicit authorization and audit trail. -- Add route-list prioritization and administrator notifications for warning/critical transitions. -- Verify that route completion cannot appear healthy until the server acknowledges all required terminal events. - -## Acceptance criteria - -- No operational state row uses `·`, bullets, or slash-separated prose to join independent claims. -- GPS, Device, Server, and Sync are rendered as separate Pills whenever their data is available. -- Missing Device telemetry is explicitly unavailable and never inferred from GPS. -- A Kitchener-shaped state renders `GPS Stop 11 nearby`, `Device 11/11`, `Server 1/11`, `Sync blocked`, and `Gap 10 stops` as separate Pills. -- A completed route with unresolved stop results renders `Alert unresolved results` on the route list, detail header, and Tracking tab. -- Native notifications use separate labeled lines and contain no middle dots. -- The full reason and source timestamps are accessible without relying on color or hover. -- Existing unrelated `InfoPill` uses continue to render unchanged. - -## Open questions - -- [ ] Confirm initial warning/critical timing thresholds against operational SLA. Owner: Operations. Impact: alert volume. -- [ ] Expose one authenticated aggregate runtime-health endpoint for webhook ingest/consumer, email sender/outbox, sync detector, tracking/alert streams, and external log sink. Owner: API/Operations. Impact: Settings currently renders those facts as Unknown. -- [ ] Decide whether route-group summary rows should aggregate child operational severity or remain Unknown. Owner: Product/API. Impact: group-only rows currently do not infer child state. -- [ ] Define retention and redaction rules for `lastErrorCode` and queue evidence. Owner: Security/Operations. Impact: incident diagnostics. diff --git a/apps/shopify-app/app/features/delivery/operational-state.js b/apps/shopify-app/app/features/delivery/operational-state.js deleted file mode 100644 index 5c78115..0000000 --- a/apps/shopify-app/app/features/delivery/operational-state.js +++ /dev/null @@ -1,229 +0,0 @@ -function countOrNull(value) { - if (value == null || value === "") return null; - const number = Number(value); - return Number.isInteger(number) && number >= 0 ? number : null; -} - -function pill(key, label, tone, ariaLabel = label) { - return { ariaLabel, key, label, tone }; -} - -function normalizeLifecycle(status) { - const value = String(status ?? "UNKNOWN").trim().toUpperCase().replace(/[\s-]+/g, "_"); - const values = { - CANCELLED: ["Route cancelled", "critical"], - COMPLETED: ["Route completed", "success"], - DRAFT: ["Route draft", "neutral"], - IN_PROGRESS: ["Route in progress", "info"], - READY: ["Route ready", "info"], - }; - const [label, tone] = values[value] ?? ["Route unknown", "neutral"]; - return pill("lifecycle", label, tone, `Route lifecycle: ${label.slice(6)}`); -} - -function mapGps(physicalPosition) { - if (!physicalPosition) { - return [ - pill("gps-freshness", "GPS unknown", "neutral", "GPS freshness: unknown"), - pill("gps-position", "GPS position unknown", "neutral"), - ]; - } - - const freshness = String(physicalPosition.freshness ?? "UNKNOWN").toUpperCase(); - const freshnessValues = { - AGING: ["GPS aging", "warning"], - FRESH: ["GPS fresh", "success"], - STALE: ["GPS stale", "critical"], - UNKNOWN: ["GPS unknown", "neutral"], - }; - const [freshnessLabel, freshnessTone] = freshnessValues[freshness] ?? freshnessValues.UNKNOWN; - const freshnessPill = pill("gps-freshness", freshnessLabel, freshnessTone, `GPS freshness: ${freshnessLabel.slice(4)}`); - - if (physicalPosition.reliableForProximity !== true) { - return [freshnessPill, pill("gps-position", "GPS position uncertain", "warning", "GPS position: low confidence")]; - } - - const stopSequence = countOrNull(physicalPosition.nearestStopSequence); - if (physicalPosition.withinProximityThreshold === true && stopSequence !== null) { - return [freshnessPill, pill("gps-position", `GPS Stop ${stopSequence} nearby`, "info", `GPS position: near Stop ${stopSequence}`)]; - } - if (physicalPosition.withinProximityThreshold === false) { - return [freshnessPill, pill("gps-position", "GPS not near stop", "warning")]; - } - return [freshnessPill, pill("gps-position", "GPS position unknown", "neutral")]; -} - -function mapDevice(deviceProgress) { - const completed = countOrNull(deviceProgress?.completedStopCount); - const total = countOrNull(deviceProgress?.totalStopCount); - if (completed === null || total === null) return pill("device", "Device unknown", "neutral", "Device progress: unknown"); - return pill("device", `Device ${completed}/${total}`, deviceProgress.locallyFinished ? "success" : "info", `Device progress: ${completed} of ${total}`); -} - -function mapServer(serverProgress) { - const resolved = countOrNull(serverProgress?.resolvedStopCount); - const total = countOrNull(serverProgress?.totalStopCount); - if (resolved === null || total === null) return pill("server", "Server unknown", "neutral", "Server progress: unknown"); - return pill("server", `Server ${resolved}/${total}`, resolved >= total && total > 0 ? "success" : "info", `Server progress: ${resolved} of ${total}`); -} - -function mapSync(syncHealth) { - const state = String(syncHealth?.state ?? "UNKNOWN").toUpperCase(); - const values = { - BLOCKED: ["Sync blocked", "critical"], - DELAYED: ["Sync delayed", "warning"], - HEALTHY: ["Sync healthy", "success"], - UNKNOWN: ["Sync unknown", "neutral"], - }; - const [label, tone] = values[state] ?? values.UNKNOWN; - return pill("sync", label, tone, `Sync state: ${label.slice(5)}`); -} - -function mapGap(deviceProgress, serverProgress, syncHealth) { - const device = countOrNull(deviceProgress?.completedStopCount); - const server = countOrNull(serverProgress?.resolvedStopCount); - if (device === null || server === null) return pill("gap", "Gap unknown", "neutral", "Device to server gap: unknown"); - if (device > server) { - const gap = device - server; - return pill("gap", `Device ahead ${gap} stops`, "warning", `Progress gap: device ahead by ${gap} stops`); - } - if (server > device) { - const gap = server - device; - return pill("gap", `Server ahead ${gap} stops`, "warning", `Progress gap: server ahead by ${gap} stops`); - } - if (String(syncHealth?.state ?? "UNKNOWN").toUpperCase() === "BLOCKED") { - return pill("gap", "Gap blocked", "critical", "Progress counts match, but sync is blocked"); - } - return pill("gap", "Gap none", "success", "Device and server progress counts match"); -} - -function mapAlert(operationalState, lifecycle, serverProgress, positionMismatch) { - const hasAlertEvidence = Array.isArray(operationalState?.activeAlerts); - const activeAlerts = hasAlertEvidence - ? operationalState.activeAlerts.filter((alert) => !alert?.resolvedAt) - : []; - const hasCritical = activeAlerts.some((alert) => String(alert?.severity).toUpperCase() === "CRITICAL"); - if (hasCritical) return pill("alert", "Alert critical", "critical", "Active operational alert: critical"); - if (activeAlerts.length > 0) return pill("alert", "Alert warning", "warning", "Active operational alert: warning"); - if (positionMismatch) { - return pill( - "alert", - "Alert warning", - "warning", - `Operational alert: current position is ahead with at least ${positionMismatch.earlierUnresolvedCount} earlier stop results unresolved`, - ); - } - - const resolved = countOrNull(serverProgress?.resolvedStopCount); - const total = countOrNull(serverProgress?.totalStopCount); - if (lifecycle.label === "Route completed" && resolved !== null && total !== null && resolved < total) { - return pill("alert", "Alert unresolved results", "critical", `Operational alert: route completed with ${total - resolved} unresolved stop results`); - } - return hasAlertEvidence - ? pill("alert", "Alert none", "success", "Active operational alerts: none") - : pill("alert", "Alert unknown", "neutral", "Active operational alerts: unknown"); -} - -function normalizePositionMismatch(positionMismatch) { - if (!positionMismatch) return null; - const nearestStopSequence = countOrNull(positionMismatch.nearestStopSequence); - const resolvedStopCount = countOrNull(positionMismatch.resolvedStopCount); - const totalStopCount = countOrNull(positionMismatch.totalStopCount); - const earlierUnresolvedCount = countOrNull(positionMismatch.earlierUnresolvedCount); - if (nearestStopSequence === null - || nearestStopSequence < 1 - || resolvedStopCount === null - || totalStopCount === null - || totalStopCount < 1 - || earlierUnresolvedCount === null - || earlierUnresolvedCount < 1) { - return null; - } - - return { earlierUnresolvedCount, nearestStopSequence, resolvedStopCount, totalStopCount }; -} - -function mapCurrentPositionAlert(positionMismatch, { alert, gap, gpsFreshness, gpsPosition, server, sync }) { - if (!positionMismatch) return null; - - const { earlierUnresolvedCount, nearestStopSequence, resolvedStopCount, totalStopCount } = positionMismatch; - - return { - message: `GPS is near Stop ${nearestStopSequence}. The server confirms ${resolvedStopCount} of ${totalStopCount} stop results. At least ${earlierUnresolvedCount} earlier planned stop${earlierUnresolvedCount === 1 ? "" : "s"} still ${earlierUnresolvedCount === 1 ? "has" : "have"} no result. GPS proximity does not confirm delivery.`, - pills: [alert, gpsFreshness, gpsPosition, server, gap, sync], - title: "Current position is ahead of server results", - tone: "warning", - }; -} - -export function mapRouteOperationalState({ operationalState = null, positionMismatch = null, routeStatus = null } = {}) { - const lifecycle = normalizeLifecycle(operationalState?.routeStatus ?? routeStatus); - const [gpsFreshness, gpsPosition] = mapGps(operationalState?.physicalPosition); - const device = mapDevice(operationalState?.deviceProgress); - const server = mapServer(operationalState?.serverProgress); - const sync = mapSync(operationalState?.syncHealth); - const gap = mapGap( - operationalState?.deviceProgress, - operationalState?.serverProgress, - operationalState?.syncHealth, - ); - const normalizedPositionMismatch = normalizePositionMismatch(positionMismatch); - const alert = mapAlert(operationalState, lifecycle, operationalState?.serverProgress, normalizedPositionMismatch); - const currentPositionAlert = mapCurrentPositionAlert(normalizedPositionMismatch, { - alert, - gap, - gpsFreshness, - gpsPosition, - server, - sync, - }); - return { - alert, - currentPositionAlert, - device, - gap, - gpsFreshness, - gpsPosition, - lifecycle, - pills: [lifecycle, gpsFreshness, gpsPosition, device, server, sync, gap, alert], - server, - sync, - }; -} - -const SETTINGS_HEALTH_LABELS = [ - ["webhookIngest", "Webhook ingest"], - ["webhookConsumer", "Webhook consumer"], - ["emailSender", "Email sender"], - ["emailOutbox", "Email outbox"], - ["syncDetector", "Sync detector"], - ["trackingStream", "Tracking stream"], - ["alertStream", "Alert stream"], - ["externalLogSink", "External log sink"], - ["shopifyToken", "Shopify token"], -]; - -function classifyHealthStatus(status) { - if (["healthy", "ok", "active", "connected"].includes(status)) { - return ["healthy", "success"]; - } - if (status === "checking") { - return ["checking", "info"]; - } - if (["failed", "error", "blocked", "unavailable"].includes(status)) { - return [status, "critical"]; - } - if (["delayed", "stale", "degraded"].includes(status)) { - return [status, "warning"]; - } - return ["unknown", "neutral"]; -} - -export function mapSettingsOperationalHealth(health = {}) { - return SETTINGS_HEALTH_LABELS.map(([key, label]) => { - const rawStatus = key === "shopifyToken" ? health[key]?.status : health[key]?.status ?? health[key]; - const status = String(rawStatus ?? "unknown").toLowerCase(); - const state = classifyHealthStatus(status); - return pill(key, `${label} ${state[0]}`, state[1], `${label} health: ${state[0]}`); - }); -} diff --git a/apps/shopify-app/app/features/delivery/route-detail.server.js b/apps/shopify-app/app/features/delivery/route-detail.server.js index 47223f9..d6b07af 100644 --- a/apps/shopify-app/app/features/delivery/route-detail.server.js +++ b/apps/shopify-app/app/features/delivery/route-detail.server.js @@ -268,7 +268,6 @@ export function buildRouteGroupChildDetails(routeGroup) { const stops = firstArray(child?.stops, child?.routeStops, child?.assignments, routePlan?.stops); const optimized = readRouteOptimizedSnapshot(child?.optimized ?? routePlan?.optimized); return { - operationalState: child?.operationalState ?? routePlan?.operationalState ?? null, routeGeometry: child?.routeGeometry ?? routePlan?.routeGeometry ?? optimized?.routeGeometry ?? null, routeMetrics: child?.routeMetrics ?? routePlan?.routeMetrics ?? null, routePlan: getRouteGroupChildRoutePlan(routeGroup, child, routePlanId, index, stops), @@ -473,7 +472,6 @@ export async function loadRoutePlanDetail(request, routeId, routeGroupIdHint = n ); const directCurrentChildDetail = routePlanData.routePlan || routePlanData.stops?.length ? { - operationalState: routePlanData.operationalState ?? null, routeGeometry: routePlanData.routeGeometry, routeMetrics: routePlanData.routeMetrics ?? null, routePlan: routePlanData.routePlan, @@ -519,7 +517,6 @@ export async function loadRoutePlanDetail(request, routeId, routeGroupIdHint = n routePlan: currentChildDetail?.routePlan ?? routePlanData.routePlan, }), routePlan: currentChildDetail?.routePlan ?? null, - operationalState: currentChildDetail?.operationalState ?? routePlanData.operationalState ?? null, routeGeometry: currentChildDetail?.routeGeometry ?? null, routeMetrics: currentChildDetail?.routeMetrics ?? null, routeStopPoints: currentChildDetail?.routeStopPoints ?? [], @@ -562,7 +559,6 @@ export async function loadRoutePlanDetail(request, routeId, routeGroupIdHint = n ? await fetchDeliveryRouteGroupDetail(request, routeGroupId, { cacheKey: shopifyShopCacheKey }) : { errors: [], routeGroup: null }; const currentRouteDetail = { - operationalState: routePlanData.operationalState ?? null, routeGeometry: routePlanData.routeGeometry, routeMetrics: routePlanData.routeMetrics ?? null, routePlan: routePlanData.routePlan, diff --git a/apps/shopify-app/app/features/delivery/route-list-rows.js b/apps/shopify-app/app/features/delivery/route-list-rows.js index 14aa5d9..f9c2461 100644 --- a/apps/shopify-app/app/features/delivery/route-list-rows.js +++ b/apps/shopify-app/app/features/delivery/route-list-rows.js @@ -215,7 +215,6 @@ function buildRouteChildRows( children = getVisibleRouteGroupChildren(routeGroup), groupAccentColor = null, groupSummary = null, - operationalStateByRoutePlanId = null, ) { return children.map((child, index) => { const routePlanId = getRouteGroupChildRoutePlanId(child); @@ -239,10 +238,6 @@ function buildRouteChildRows( groupSummary, route: getRouteGroupChildRouteName(routeGroup, child, routePlan, index), status: child.displayStatus ?? routePlan.status ?? "DRAFT", - operationalState: child.operationalState - ?? routePlan.operationalState - ?? operationalStateByRoutePlanId?.get(String(routePlanId)) - ?? null, orders: stopsCount, coordinates: `${locatedCount}/${stopsCount}`, delivered: 0, @@ -260,26 +255,14 @@ function buildRouteChildRows( } export function buildRouteRows(routePlans, routeGroups = []) { - const safeRoutePlans = Array.isArray(routePlans) ? routePlans : []; const safeRouteGroups = Array.isArray(routeGroups) ? routeGroups : []; - const operationalStateByRoutePlanId = new Map( - safeRoutePlans - .filter((routePlan) => routePlan?.id != null) - .map((routePlan) => [String(routePlan.id), routePlan.operationalState ?? null]), - ); const routeGroupEntries = safeRouteGroups.map((routeGroup, index) => { const children = getVisibleRouteGroupChildren(routeGroup); const groupAccentColor = getRouteGroupAccentColor(routeGroup.id); const totalOrders = getRouteGroupTotalOrders(routeGroup); const groupSummary = formatRouteGroupSummary(children.length, totalOrders); return { - childRows: buildRouteChildRows( - routeGroup, - children, - groupAccentColor, - groupSummary, - operationalStateByRoutePlanId, - ), + childRows: buildRouteChildRows(routeGroup, children, groupAccentColor, groupSummary), children, groupAccentColor, groupSummary, @@ -293,11 +276,12 @@ export function buildRouteRows(routePlans, routeGroups = []) { routeGroupEntries.flatMap(({ children }) => children.map(getRouteGroupChildRoutePlanId).filter(Boolean)), ); const routeGroupIds = new Set(safeRouteGroups.map((routeGroup) => routeGroup?.id).filter(Boolean).map(String)); - const standaloneRoutePlans = safeRoutePlans - .filter((routePlan) => { - const routeGroupId = getRouteGroupingChildGroupId(routePlan); - return !childRoutePlanIds.has(routePlan.id) && !(routeGroupId && routeGroupIds.has(routeGroupId)); - }); + const standaloneRoutePlans = Array.isArray(routePlans) + ? routePlans.filter((routePlan) => { + const routeGroupId = getRouteGroupingChildGroupId(routePlan); + return !childRoutePlanIds.has(routePlan.id) && !(routeGroupId && routeGroupIds.has(routeGroupId)); + }) + : []; const routeGroupRows = routeGroupEntries.map(({ childRows, groupSummary, routeGroup, routeMetrics, totalOrders }) => { return { id: routeGroup.id, @@ -312,7 +296,6 @@ export function buildRouteRows(routePlans, routeGroups = []) { deleteKey: getRouteDeleteKey({ ...routeGroup, isRouteGroup: true }), route: routeGroup.name ?? routeGroup.id, status: routeGroup.displayStatus ?? routeGroup.status ?? "DRAFT", - operationalState: null, orders: totalOrders, coordinates: "-", delivered: 0, @@ -374,7 +357,6 @@ export function buildRouteRows(routePlans, routeGroups = []) { deleteKey: getRouteDeleteKey(routePlan), route: routePlan.name ?? routePlan.id, status: routePlan.status ?? "DRAFT", - operationalState: routePlan.operationalState ?? null, orders: stopsCount, coordinates: `${locatedCount}/${stopsCount}`, delivered, diff --git a/apps/shopify-app/app/features/delivery/route-plans.server.js b/apps/shopify-app/app/features/delivery/route-plans.server.js index 5fa8bc5..28be0fb 100644 --- a/apps/shopify-app/app/features/delivery/route-plans.server.js +++ b/apps/shopify-app/app/features/delivery/route-plans.server.js @@ -188,7 +188,6 @@ export async function fetchDeliveryRoutePlanDetail(request, routePlanId, options ); return { - operationalState: result.data?.operationalState ?? result.data?.routePlan?.operationalState ?? null, routePlan: result.data?.routePlan ?? null, routeGeometry: result.data?.routeGeometry ?? null, routeMetrics: result.data?.routeMetrics ?? null, diff --git a/apps/shopify-app/app/features/delivery/route-tracking.js b/apps/shopify-app/app/features/delivery/route-tracking.js index 6e9899c..9914bde 100644 --- a/apps/shopify-app/app/features/delivery/route-tracking.js +++ b/apps/shopify-app/app/features/delivery/route-tracking.js @@ -152,9 +152,6 @@ function normalizeRouteTrackingSnapshot(snapshot) { stopArrivals, serverTime: textOrNull(snapshot?.serverTime), latestPosition, - operationalState: snapshot?.operationalState && typeof snapshot.operationalState === "object" - ? { ...snapshot.operationalState } - : null, recentPositions, }; } @@ -286,7 +283,6 @@ function mergeRouteTrackingSnapshot(currentSnapshot, serverSnapshot) { const mergedBase = normalizeRouteTrackingSnapshot({ ...historyBase, policy: incomingSnapshot.policy ?? current.policy, - operationalState: incomingSnapshot.operationalState ?? current.operationalState, progress: mergeTrackingProgressSnapshot(current.progress, incomingSnapshot.progress), roadMatchedPath: getNewestRoadMatchedPath(current.roadMatchedPath, incomingSnapshot.roadMatchedPath), routePlanId: incomingSnapshot.routePlanId ?? current.routePlanId, @@ -878,58 +874,6 @@ function getRouteTrackingFreshness(snapshot, now = Date.now()) { return { key: "OFFLINE", label: "Offline", ageMs }; } -function getRouteTrackingOperationalMismatch(operationalState) { - if (!operationalState || typeof operationalState !== "object") return null; - if (normalizeRouteExecutionStatus(operationalState.routeStatus) !== "IN_PROGRESS") return null; - - const physicalPosition = operationalState.physicalPosition; - const freshness = textOrNull(physicalPosition?.freshness)?.toUpperCase(); - // Freshness, reliability, and threshold membership are server policy outputs. - // The client must not recalculate proximity from coordinates. - if (freshness !== "FRESH" - || physicalPosition?.reliableForProximity !== true - || physicalPosition?.withinProximityThreshold !== true) { - return null; - } - - const nearestStopSequence = positiveIntegerOrNull(physicalPosition.nearestStopSequence); - const resolvedStopCount = nonNegativeIntegerOrNull(operationalState.serverProgress?.resolvedStopCount); - const totalStopCount = positiveIntegerOrNull(operationalState.serverProgress?.totalStopCount); - if (nearestStopSequence === null - || resolvedStopCount === null - || totalStopCount === null - || nearestStopSequence > totalStopCount - || resolvedStopCount > totalStopCount) { - return null; - } - - // Server progress is aggregate evidence. Subtracting every resolved result from - // the earlier stop count gives a conservative lower bound even if a result was - // recorded out of order. Terminal results such as SKIPPED or CANCELLED are - // already included in the server-owned resolvedStopCount. - const earlierUnresolvedCount = Math.max(0, nearestStopSequence - 1 - resolvedStopCount); - if (earlierUnresolvedCount === 0) return null; - - return { - earlierUnresolvedCount, - nearestStopSequence, - resolvedStopCount, - totalStopCount, - unresolvedCount: totalStopCount - resolvedStopCount, - }; -} - -function nonNegativeIntegerOrNull(value) { - if (value == null || value === "") return null; - const number = Number(value); - return Number.isInteger(number) && number >= 0 ? number : null; -} - -function positiveIntegerOrNull(value) { - const number = nonNegativeIntegerOrNull(value); - return number !== null && number > 0 ? number : null; -} - function getRouteExecutionStatusFromTrackingEvent(currentStatus, event) { const status = normalizeRouteExecutionStatus(currentStatus); const eventType = textOrNull(event?.eventType); @@ -1033,7 +977,6 @@ export { getRouteExecutionStatusFromTrackingEvent, getRouteTrackingCompletionTime, getRouteTrackingLineFeatures, - getRouteTrackingOperationalMismatch, getRouteTrackingPathPoints, getRouteTrackingPathSummary, getRouteTrackingFreshness, diff --git a/apps/shopify-app/app/routes/app.routes.$routeId.jsx b/apps/shopify-app/app/routes/app.routes.$routeId.jsx index ddf2ed9..dd69bbb 100644 --- a/apps/shopify-app/app/routes/app.routes.$routeId.jsx +++ b/apps/shopify-app/app/routes/app.routes.$routeId.jsx @@ -50,7 +50,6 @@ import { textOrUndefined, } from "../features/delivery/route-helpers"; import { routeDetailAction, routeDetailLoader } from "../features/delivery/route-detail.server"; -import { mapRouteOperationalState } from "../features/delivery/operational-state"; import { getRouteStopLocationMessage, normalizeRouteStopLocationDiagnostic, @@ -86,7 +85,6 @@ import { consumeRouteTrackingSseChunk, getRouteExecutionStatusFromTrackingEvent, getRouteTrackingCompletionTime, - getRouteTrackingOperationalMismatch, getRouteTrackingPathSummary, getRouteTrackingPresentation, getRouteTrackingReconnectDelayMs, @@ -105,7 +103,6 @@ import { createMapLibreMap } from "../features/maps/maplibre-map"; import { installMissingMapImageFallback } from "../features/maps/maplibre-missing-images"; import { installPmtilesProtocol } from "../features/maps/pmtiles-protocol"; import { MapPanel, MapResizeHandle, MapToolbar, renderMapFitIcon, renderMapRefreshIcon, renderMapZoomInIcon, renderMapZoomOutIcon } from "../ui/map-panel"; -import { OperationalPillGroup } from "../ui/operational-pill-group"; const OPENFREEMAP_STYLE_URL = "/vendor/openfreemap-clever-lite.json"; const MAP_RECOVERY_DELAY_MS = 2500; @@ -302,6 +299,17 @@ const routeDetailTitleMetricValueStyle = { whiteSpace: "nowrap", }; +const routeStatusBadgeStyle = { + background: "#fff1b8", + borderRadius: "999px", + color: "#4f3f00", + display: "inline-flex", + fontSize: "12px", + fontWeight: 650, + lineHeight: 1.2, + padding: "4px 9px", +}; + const routeDetailBackButtonStyle = { alignItems: "center", background: "transparent", @@ -644,6 +652,15 @@ const routeTrackingMapFreshnessStyle = { zIndex: 2, }; +const routeTrackingMapFreshnessDotStyle = { + background: "#d82c0d", + border: "2px solid #ffffff", + borderRadius: "50%", + boxShadow: "0 0 0 1px rgba(216, 44, 13, 0.22)", + height: "9px", + width: "9px", +}; + const routeMetaActionsStyle = { borderBottom: "1px solid #ececec", display: "grid", @@ -2134,6 +2151,12 @@ function formatTrackingTimestamp(value, ianaTimezone) { } } +function formatTrackingElapsedSeconds(value, now = Date.now()) { + const date = value ? new Date(value) : null; + if (!date || Number.isNaN(date.getTime())) return ROUTE_EMPTY_LABEL; + return `${Math.max(0, Math.floor((now - date.getTime()) / 1000))}s ago`; +} + function formatTrackingRange(firstValue, lastValue, ianaTimezone) { const firstDate = firstValue ? new Date(firstValue) : null; const lastDate = lastValue ? new Date(lastValue) : null; @@ -3210,7 +3233,6 @@ export default function RouteDetailPage() { childRouteDetails = [], currentDepartureLocation = null, drivers = [], - operationalState = null, routePlan, routeGeometry = null, routeGroup = null, @@ -3483,20 +3505,6 @@ export default function RouteDetailPage() { const displayedRouteTrackingSnapshot = isRouteTrackingPayloadForRoute(routeTrackingSnapshot, trackingRoutePlanId) ? routeTrackingSnapshot : null; - const displayedOperationalState = displayedRouteTrackingSnapshot?.operationalState ?? operationalState; - const routeTrackingPositionMismatch = useMemo( - () => getRouteTrackingOperationalMismatch(displayedOperationalState), - [displayedOperationalState], - ); - const routeOperationalPresentation = useMemo( - () => mapRouteOperationalState({ - operationalState: displayedOperationalState, - positionMismatch: routeTrackingPositionMismatch, - routeStatus: routeExecutionStatus, - }), - [displayedOperationalState, routeExecutionStatus, routeTrackingPositionMismatch], - ); - const routeTrackingCurrentPositionAlert = routeOperationalPresentation.currentPositionAlert; useEffect(() => { setRouteExecutionStatus(loaderRouteExecutionStatus); }, [loaderRouteExecutionStatus]); @@ -3669,6 +3677,7 @@ export default function RouteDetailPage() { ianaTimezone, now: routeTrackingClock, }); + const routeTrackingFreshnessTime = routeTrackingCompletionTime ?? routeTrackingClock; const routeTrackingPathSummary = useMemo( () => getRouteTrackingPathSummary(displayedRouteTrackingSnapshot), [displayedRouteTrackingSnapshot], @@ -6515,10 +6524,9 @@ export default function RouteDetailPage() { {renderRouteLineEditIcon()} ) : null} - + + {isMaterializedChildRouteDetail ? formatRouteStatus(routeExecutionStatus) : routeDetail.status} + {!isMaterializedChildRouteDetail ? (
{renderRouteHeaderMetric("Orders", routeDetail.orders)} @@ -6618,10 +6626,7 @@ export default function RouteDetailPage() { {renderRouteEditableChevron()}
- + {formatRouteStatus(routeExecutionStatus)} ) : null} @@ -6727,14 +6732,14 @@ export default function RouteDetailPage() { Actual GPS tracking - {showRouteTrackingFreshness ? ( + {latestTrackingReceivedAt && showRouteTrackingFreshness ? (
- +
) : null} @@ -7018,21 +7023,6 @@ export default function RouteDetailPage() { ) : isMaterializedChildRouteDetail && childDetailTab === "tracking" ? (
- {routeTrackingCurrentPositionAlert ? ( -
- - -

{routeTrackingCurrentPositionAlert.message}

-
-
- ) : null} -
diff --git a/apps/shopify-app/app/routes/app.routes.jsx b/apps/shopify-app/app/routes/app.routes.jsx index 9e0758e..9b21a3a 100644 --- a/apps/shopify-app/app/routes/app.routes.jsx +++ b/apps/shopify-app/app/routes/app.routes.jsx @@ -4,7 +4,6 @@ import { useAppBridge } from "@shopify/app-bridge-react"; import { Outlet, redirect, useFetcher, useLoaderData, useNavigate, useParams, useSearchParams } from "react-router"; import { boundary } from "@shopify/shopify-app-react-router/server"; import { formatRouteStatus } from "../features/delivery/route-helpers"; -import { mapRouteOperationalState } from "../features/delivery/operational-state"; import { buildRouteRows, getExpandedRouteDeleteKeys, @@ -17,7 +16,6 @@ import { deleteDeliveryRouteGroup, deleteDeliveryRouteGroupChildRoutes, fetchDel import { getServiceErrorNotice } from "../features/service-errors"; import { authenticate } from "../shopify.server"; import { AdminRouteErrorBoundary } from "../ui/admin-route-error-boundary"; -import { OperationalPillGroup } from "../ui/operational-pill-group"; import { logStructuredMetric } from "../features/telemetry/structured-telemetry.server"; const routesTablePageStyle = { @@ -143,7 +141,7 @@ function getRouteColumnWidths(routeRows) { "14px", getRouteNameColumnWidth(routeRows), "104px", - "420px", + "84px", "56px", "1%", "1%", @@ -292,6 +290,40 @@ const routeDisabledActionButtonStyle = { opacity: 0.55, }; +const routeStatusBadgeStyle = { + background: "#f1f1f1", + borderRadius: "999px", + color: "#616161", + display: "inline-flex", + fontSize: "12px", + fontWeight: 650, + padding: "3px 8px", +}; + +const routeReadyBadgeStyle = { + ...routeStatusBadgeStyle, + background: "#f1f1f1", + color: "#616161", +}; + +const routeInProgressBadgeStyle = { + ...routeStatusBadgeStyle, + background: "#e0f0ff", + color: "#00527c", +}; + +const routeCompletedBadgeStyle = { + ...routeStatusBadgeStyle, + background: "#e3f1df", + color: "#205c20", +}; + +const routeCancelledBadgeStyle = { + ...routeStatusBadgeStyle, + background: "#fee9e8", + color: "#8e1f0b", +}; + const routesErrorStyle = { background: "#fff4f4", borderColor: "#ffd6d6", @@ -543,6 +575,21 @@ function filterRouteRows(routeRows, routeFilters) { ]; } +function getStatusBadgeStyle(status) { + switch (formatRouteStatus(status)) { + case "Ready": + return routeReadyBadgeStyle; + case "In progress": + return routeInProgressBadgeStyle; + case "Completed": + return routeCompletedBadgeStyle; + case "Cancelled": + return routeCancelledBadgeStyle; + default: + return routeStatusBadgeStyle; + } +} + export default function RoutesPage() { const navigate = useNavigate(); const { routeId, routeGroupId } = useParams(); @@ -718,7 +765,7 @@ export default function RoutesPage() { Route Date - Operational state + Status Orders Area Driver @@ -760,13 +807,7 @@ export default function RoutesPage() { {route.route} {route.date} - + {formatRouteStatus(route.status)} {route.orders} {route.deliveryArea} diff --git a/apps/shopify-app/app/routes/app.settings.jsx b/apps/shopify-app/app/routes/app.settings.jsx index 936c09c..1bb17b5 100644 --- a/apps/shopify-app/app/routes/app.settings.jsx +++ b/apps/shopify-app/app/routes/app.settings.jsx @@ -33,12 +33,9 @@ import { } from "../features/settings/settings-layout"; import { SettingsDepartureMap } from "../features/settings/settings-departure-map"; import { clearShopifyOrdersCache } from "../features/orders/shopify-orders.server"; -import { mapSettingsOperationalHealth } from "../features/delivery/operational-state"; -import { getShopifyTokenSyncHealth } from "../features/delivery/shopify-token-sync.server"; import { SUPPORTED_LANGUAGES, translate } from "../i18n/i18n"; import { authenticate } from "../shopify.server"; import { PageShell } from "../ui/page-shell"; -import { OperationalPillGroup } from "../ui/operational-pill-group"; const DELIVERY_CYCLE_WEEKDAY_OPTIONS = [ { label: "Sunday", value: "SUNDAY" }, @@ -61,9 +58,6 @@ export const loader = async ({ request }) => { return { departureLocation: departureResult.departureLocation, appPreferences: preferencesResult.appPreferences, - operationalHealth: { - shopifyToken: getShopifyTokenSyncHealth(shopifyShopCacheKey), - }, errors: [ ...(departureResult.errors ?? []), ...(preferencesResult.errors ?? []), @@ -213,7 +207,7 @@ function isValidLongitude(longitude) { } export default function SettingsPage() { - const { departureLocation, appPreferences, errors, operationalHealth } = useLoaderData(); + const { departureLocation, appPreferences, errors } = useLoaderData(); const actionData = useActionData(); const geocodeFetcher = useFetcher(); const submitSettings = useSubmit(); @@ -339,14 +333,6 @@ export default function SettingsPage() { {activeErrors.length > 0 ? (

{activeErrors[0]?.message ?? copy("settings.errors.unableToSave")}

) : null} -
- Operational health -

Unknown means this app has no current verified health evidence for that dependency.

- -
diff --git a/apps/shopify-app/app/ui/operational-pill-group.jsx b/apps/shopify-app/app/ui/operational-pill-group.jsx deleted file mode 100644 index f168186..0000000 --- a/apps/shopify-app/app/ui/operational-pill-group.jsx +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable react/prop-types */ -import { InfoPill } from "./info-pill"; - -export function OperationalPillGroup({ ariaLabel = "Operational state", pills = [] }) { - return ( -
- {pills.map((item) => ( - - {item.label} - - ))} -
- ); -} diff --git a/apps/shopify-app/tests/operational-state.test.mjs b/apps/shopify-app/tests/operational-state.test.mjs deleted file mode 100644 index 8ae7d59..0000000 --- a/apps/shopify-app/tests/operational-state.test.mjs +++ /dev/null @@ -1,200 +0,0 @@ -/* eslint-env node */ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import test from "node:test"; - -import { mapRouteOperationalState, mapSettingsOperationalHealth } from "../app/features/delivery/operational-state.js"; -import { getRouteTrackingOperationalMismatch } from "../app/features/delivery/route-tracking.js"; - -const kitchenerState = { - activeAlerts: [{ id: "alert-1", severity: "WARNING", type: "SYNC_BLOCKED" }], - deviceProgress: { completedStopCount: 11, currentStopSequence: 11, locallyFinished: true, totalStopCount: 11 }, - physicalPosition: { freshness: "FRESH", nearestStopSequence: 11, reliableForProximity: true, withinProximityThreshold: true }, - routeStatus: "IN_PROGRESS", - serverProgress: { resolvedStopCount: 1, totalStopCount: 11 }, - syncHealth: { state: "BLOCKED" }, -}; - -test("Kitchener evidence stays separated into independently sourced pills", () => { - const result = mapRouteOperationalState({ operationalState: kitchenerState }); - assert.deepEqual(result.pills.map((item) => item.label), [ - "Route in progress", - "GPS fresh", - "GPS Stop 11 nearby", - "Device 11/11", - "Server 1/11", - "Sync blocked", - "Device ahead 10 stops", - "Alert warning", - ]); - assert.ok(result.pills.every((item) => item.ariaLabel && !/[·•]/u.test(item.label))); -}); - -test("Kitchener Current position warning reuses canonical Pills and keeps Device-absent Gap unknown", () => { - const operationalState = { - activeAlerts: [], - physicalPosition: kitchenerState.physicalPosition, - routeStatus: "IN_PROGRESS", - serverProgress: { resolvedStopCount: 1, totalStopCount: 11 }, - syncHealth: { state: "UNKNOWN" }, - }; - const positionMismatch = getRouteTrackingOperationalMismatch(operationalState); - const result = mapRouteOperationalState({ operationalState, positionMismatch }); - - assert.equal(result.alert.label, "Alert warning"); - assert.equal(result.device.label, "Device unknown"); - assert.equal(result.gap.label, "Gap unknown"); - assert.deepEqual(result.currentPositionAlert?.pills.map((item) => item.label), [ - "Alert warning", - "GPS fresh", - "GPS Stop 11 nearby", - "Server 1/11", - "Gap unknown", - "Sync unknown", - ]); - assert.equal(result.currentPositionAlert?.pills[0], result.alert); - assert.equal(result.currentPositionAlert?.pills[1], result.gpsFreshness); - assert.equal(result.currentPositionAlert?.pills[2], result.gpsPosition); - assert.equal(result.currentPositionAlert?.pills[3], result.server); - assert.equal(result.currentPositionAlert?.pills[4], result.gap); - assert.equal(result.currentPositionAlert?.pills[5], result.sync); - assert.equal(result.currentPositionAlert?.title, "Current position is ahead of server results"); - assert.equal( - result.currentPositionAlert?.message, - "GPS is near Stop 11. The server confirms 1 of 11 stop results. At least 9 earlier planned stops still have no result. GPS proximity does not confirm delivery.", - ); -}); - -test("canonical mapper keeps Current position warning absent without raw mismatch evidence", () => { - const result = mapRouteOperationalState({ operationalState: kitchenerState, positionMismatch: null }); - assert.equal(result.currentPositionAlert, null); -}); - -test("canonical mapper does not turn incomplete mismatch evidence into an alert", () => { - const result = mapRouteOperationalState({ - operationalState: { ...kitchenerState, activeAlerts: [] }, - positionMismatch: { nearestStopSequence: 11 }, - }); - - assert.equal(result.alert.label, "Alert none"); - assert.equal(result.currentPositionAlert, null); -}); - -test("stale low-confidence GPS never implies a nearby stop", () => { - const result = mapRouteOperationalState({ - operationalState: { - physicalPosition: { freshness: "STALE", nearestStopSequence: 11, reliableForProximity: false, withinProximityThreshold: true }, - routeStatus: "IN_PROGRESS", - }, - }); - assert.equal(result.gpsFreshness.label, "GPS stale"); - assert.equal(result.gpsPosition.label, "GPS position uncertain"); - assert.doesNotMatch(result.gpsPosition.label, /nearby/i); -}); - -test("missing heartbeat fields stay explicit and never infer device state from GPS", () => { - const result = mapRouteOperationalState({ operationalState: { physicalPosition: kitchenerState.physicalPosition } }); - assert.equal(result.device.label, "Device unknown"); - assert.equal(result.sync.label, "Sync unknown"); - assert.equal(result.gap.label, "Gap unknown"); -}); - -test("null progress counts remain unknown rather than becoming zero", () => { - const result = mapRouteOperationalState({ - operationalState: { - deviceProgress: { completedStopCount: null, totalStopCount: null }, - serverProgress: { resolvedStopCount: null, totalStopCount: null }, - }, - }); - assert.equal(result.device.label, "Device unknown"); - assert.equal(result.server.label, "Server unknown"); - assert.equal(result.gap.label, "Gap unknown"); -}); - -test("server-ahead progress stays directional instead of collapsing into green no-gap", () => { - const result = mapRouteOperationalState({ - operationalState: { - activeAlerts: [], - deviceProgress: { completedStopCount: 3, totalStopCount: 11 }, - serverProgress: { resolvedStopCount: 5, totalStopCount: 11 }, - syncHealth: { state: "HEALTHY" }, - }, - }); - assert.equal(result.gap.label, "Server ahead 2 stops"); - assert.equal(result.gap.tone, "warning"); -}); - -test("equal progress remains visibly blocked when sync evidence is blocked", () => { - const result = mapRouteOperationalState({ - operationalState: { - activeAlerts: [], - deviceProgress: { completedStopCount: 5, totalStopCount: 11 }, - serverProgress: { resolvedStopCount: 5, totalStopCount: 11 }, - syncHealth: { state: "BLOCKED" }, - }, - }); - assert.equal(result.gap.label, "Gap blocked"); - assert.equal(result.gap.tone, "critical"); - assert.notEqual(result.gap.tone, "success"); -}); - -test("missing or malformed alert evidence stays unknown while explicit empty evidence is none", () => { - const missing = mapRouteOperationalState({ operationalState: { routeStatus: "IN_PROGRESS" } }); - const malformed = mapRouteOperationalState({ - operationalState: { activeAlerts: { length: 0 }, routeStatus: "IN_PROGRESS" }, - }); - const explicitNone = mapRouteOperationalState({ - operationalState: { activeAlerts: [], routeStatus: "IN_PROGRESS" }, - }); - - assert.equal(missing.alert.label, "Alert unknown"); - assert.equal(missing.alert.tone, "neutral"); - assert.equal(malformed.alert.label, "Alert unknown"); - assert.equal(explicitNone.alert.label, "Alert none"); - assert.equal(explicitNone.alert.tone, "success"); -}); - -test("completed routes with unresolved server results remain visibly critical", () => { - const result = mapRouteOperationalState({ - operationalState: { - activeAlerts: [], - routeStatus: "COMPLETED", - serverProgress: { resolvedStopCount: 1, totalStopCount: 11 }, - }, - }); - assert.equal(result.lifecycle.label, "Route completed"); - assert.equal(result.server.label, "Server 1/11"); - assert.equal(result.alert.label, "Alert unresolved results"); - assert.equal(result.alert.tone, "critical"); -}); - -test("list and detail consume the same pure mapper and shared InfoPill component", () => { - const routesSource = fs.readFileSync(new URL("../app/routes/app.routes.jsx", import.meta.url), "utf8"); - const detailSource = fs.readFileSync(new URL("../app/routes/app.routes.$routeId.jsx", import.meta.url), "utf8"); - const groupSource = fs.readFileSync(new URL("../app/ui/operational-pill-group.jsx", import.meta.url), "utf8"); - assert.match(routesSource, /mapRouteOperationalState/); - assert.match(detailSource, /mapRouteOperationalState/); - assert.match(groupSource, / { - const result = mapSettingsOperationalHealth({ shopifyToken: { status: "healthy" } }); - assert.deepEqual(result.map((item) => item.key), [ - "webhookIngest", "webhookConsumer", "emailSender", "emailOutbox", "syncDetector", - "trackingStream", "alertStream", "externalLogSink", "shopifyToken", - ]); - assert.equal(result.find((item) => item.key === "shopifyToken")?.label, "Shopify token healthy"); - assert.ok(result.filter((item) => item.key !== "shopifyToken").every((item) => item.label.endsWith(" unknown"))); -}); - -test("operational UI source does not concatenate middle-dot or bullet summaries", () => { - const sources = [ - "../app/features/delivery/operational-state.js", - "../app/ui/operational-pill-group.jsx", - "../app/routes/app.routes.jsx", - "../app/routes/app.routes.$routeId.jsx", - "../app/routes/app.settings.jsx", - ].map((relativePath) => fs.readFileSync(new URL(relativePath, import.meta.url), "utf8")); - assert.ok(sources.every((source) => !/Operational[^\n]*(?:·|•)/u.test(source))); -}); diff --git a/apps/shopify-app/tests/orders-page.test.mjs b/apps/shopify-app/tests/orders-page.test.mjs index b14bb62..6ef0c6c 100644 --- a/apps/shopify-app/tests/orders-page.test.mjs +++ b/apps/shopify-app/tests/orders-page.test.mjs @@ -732,7 +732,7 @@ test("Ordered pill exposes order timing and delivery-cycle sequence on hover", ( }); test("Area pill distinguishes delivery attention, valid delivery, and pickup rows", () => { - assert.match(infoPillSource, /const INFO_PILL_TONES = new Set\(\["neutral", "info", "success", "warning", "critical", "pickup"\]\)/); + assert.match(infoPillSource, /const INFO_PILL_TONES = new Set\(\["neutral", "success", "warning", "critical", "pickup"\]\)/); assert.match(globalCssSource, /\.info-pill--pickup \{[\s\S]*?background: rgba\(0, 91, 211, 0\.12\);[\s\S]*?color: #005bd3/); assert.match(ordersPageSource, /function formatAreaValue\(order\) \{\s*if \(order\?\.serviceType === "PICKUP"\) return "Pickup";\s*return textOrUndefined\(order\?\.deliveryArea\) \?\? "Null";\s*\}/); assert.match(ordersPageSource, /function getOrderAreaPillTone\(order\) \{\s*if \(order\?\.serviceType === "PICKUP"\) return "pickup";\s*if \(textOrUndefined\(order\?\.deliveryArea\)\) return "neutral";\s*return "warning";\s*\}/); diff --git a/apps/shopify-app/tests/route-list-rows.test.mjs b/apps/shopify-app/tests/route-list-rows.test.mjs index ae108e9..e8552bd 100644 --- a/apps/shopify-app/tests/route-list-rows.test.mjs +++ b/apps/shopify-app/tests/route-list-rows.test.mjs @@ -1,7 +1,6 @@ /* eslint-env node */ import assert from "node:assert/strict"; import test from "node:test"; -import { mapRouteOperationalState } from "../app/features/delivery/operational-state.js"; import { buildRouteRows, @@ -11,79 +10,6 @@ import { toggleRouteSelection, } from "../app/features/delivery/route-list-rows.js"; -test("route list preserves batched per-row operational state without follow-up requests", () => { - const operationalState = { - deviceProgress: { completedStopCount: 11, totalStopCount: 11 }, - serverProgress: { resolvedStopCount: 1, totalStopCount: 11 }, - syncHealth: { state: "BLOCKED" }, - }; - const [row] = buildRouteRows([{ id: "route-kitchener", operationalState, status: "IN_PROGRESS" }]); - assert.equal(row.operationalState, operationalState); -}); - -test("grouped route rows join enriched Kitchener state by route plan id", () => { - const operationalState = { - activeAlerts: [{ id: "alert-1", severity: "WARNING", type: "SYNC_BLOCKED" }], - deviceProgress: { completedStopCount: 11, currentStopSequence: 11, locallyFinished: true, totalStopCount: 11 }, - physicalPosition: { freshness: "FRESH", nearestStopSequence: 11, reliableForProximity: true, withinProximityThreshold: true }, - routePlanId: "route-kitchener", - routeStatus: "IN_PROGRESS", - serverProgress: { resolvedStopCount: 1, totalStopCount: 11 }, - syncHealth: { state: "BLOCKED" }, - }; - const rows = buildRouteRows( - [{ - id: "route-kitchener", - operationalState, - routeGroupingChild: { groupingId: "group-1" }, - }], - [{ - id: "group-1", - children: [{ routePlanId: "route-kitchener", routePlan: { id: "route-kitchener" } }], - }], - ); - const childRow = rows.find((row) => row.id === "route-kitchener"); - const listPresentation = mapRouteOperationalState({ - operationalState: childRow?.operationalState, - routeStatus: childRow?.status, - }); - const detailPresentation = mapRouteOperationalState({ - operationalState, - routeStatus: "IN_PROGRESS", - }); - - assert.equal(childRow?.operationalState, operationalState); - assert.deepEqual(listPresentation, detailPresentation); - assert.deepEqual(listPresentation.pills.map((item) => item.label), [ - "Route in progress", - "GPS fresh", - "GPS Stop 11 nearby", - "Device 11/11", - "Server 1/11", - "Sync blocked", - "Device ahead 10 stops", - "Alert warning", - ]); -}); - -test("grouped legacy route rows keep missing operational evidence explicit", () => { - const rows = buildRouteRows( - [{ id: "route-legacy", routeGroupingChild: { groupingId: "group-1" } }], - [{ id: "group-1", children: [{ routePlanId: "route-legacy" }] }], - ); - const childRow = rows.find((row) => row.id === "route-legacy"); - const presentation = mapRouteOperationalState({ - operationalState: childRow?.operationalState, - routeStatus: childRow?.status, - }); - - assert.equal(childRow?.operationalState, null); - assert.equal(presentation.gpsFreshness.label, "GPS unknown"); - assert.equal(presentation.device.label, "Device unknown"); - assert.equal(presentation.server.label, "Server unknown"); - assert.equal(presentation.sync.label, "Sync unknown"); -}); - test("route list shows a created child route immediately below its group", () => { const rows = buildRouteRows( [ diff --git a/apps/shopify-app/tests/route-tracking-contract.test.mjs b/apps/shopify-app/tests/route-tracking-contract.test.mjs index 9c26bbf..63be331 100644 --- a/apps/shopify-app/tests/route-tracking-contract.test.mjs +++ b/apps/shopify-app/tests/route-tracking-contract.test.mjs @@ -6,7 +6,6 @@ import { doesTrackingEventRefreshEta, getRouteExecutionStatusFromTrackingEvent, getRouteTrackingLineFeatures, - getRouteTrackingOperationalMismatch, getRouteTrackingPathPoints, getRouteTrackingPathSummary, getRouteTrackingFreshness, @@ -649,101 +648,6 @@ test("freshness uses server-provided thresholds", () => { assert.equal(getRouteTrackingFreshness(snapshot, Date.parse("2026-07-20T04:04:00.000Z")).key, "OFFLINE"); }); -test("tracking returns raw Kitchener mismatch evidence only from server operational state", () => { - const operationalState = { - physicalPosition: { - freshness: "FRESH", - nearestStopSequence: 11, - reliableForProximity: true, - withinProximityThreshold: true, - }, - routeStatus: "IN_PROGRESS", - serverProgress: { resolvedStopCount: 1, totalStopCount: 11 }, - }; - - assert.deepEqual(getRouteTrackingOperationalMismatch(operationalState), { - earlierUnresolvedCount: 9, - nearestStopSequence: 11, - resolvedStopCount: 1, - totalStopCount: 11, - unresolvedCount: 10, - }); -}); - -test("tracking suppresses mismatch without trustworthy server proximity and progress evidence", () => { - const kitchenerState = { - physicalPosition: { - freshness: "FRESH", - nearestStopSequence: 11, - reliableForProximity: true, - withinProximityThreshold: true, - }, - routeStatus: "IN_PROGRESS", - serverProgress: { resolvedStopCount: 1, totalStopCount: 11 }, - }; - - assert.equal(getRouteTrackingOperationalMismatch(null), null); - assert.equal(getRouteTrackingOperationalMismatch({}), null); - assert.equal(getRouteTrackingOperationalMismatch({ ...kitchenerState, serverProgress: null }), null); - assert.equal(getRouteTrackingOperationalMismatch({ ...kitchenerState, serverProgress: {} }), null); - assert.equal(getRouteTrackingOperationalMismatch(normalizeRouteTrackingSnapshot({}).operationalState), null); - assert.equal(getRouteTrackingOperationalMismatch({ - ...kitchenerState, - physicalPosition: { ...kitchenerState.physicalPosition, freshness: "STALE" }, - }), null); - assert.equal(getRouteTrackingOperationalMismatch({ - ...kitchenerState, - physicalPosition: { ...kitchenerState.physicalPosition, freshness: "AGING" }, - }), null); - assert.equal(getRouteTrackingOperationalMismatch({ - ...kitchenerState, - physicalPosition: { ...kitchenerState.physicalPosition, reliableForProximity: false }, - }), null); - assert.equal(getRouteTrackingOperationalMismatch({ - ...kitchenerState, - physicalPosition: { ...kitchenerState.physicalPosition, withinProximityThreshold: false }, - }), null); - assert.equal(getRouteTrackingOperationalMismatch({ ...kitchenerState, routeStatus: "READY" }), null); - assert.equal(getRouteTrackingOperationalMismatch({ ...kitchenerState, routeStatus: "CANCELLED" }), null); - assert.equal(getRouteTrackingOperationalMismatch({ - ...kitchenerState, - physicalPosition: { ...kitchenerState.physicalPosition, nearestStopSequence: 2 }, - }), null); -}); - -test("server-resolved SKIPPED and CANCELLED stops do not create an earlier-results warning", () => { - const operationalState = { - physicalPosition: { - freshness: "FRESH", - nearestStopSequence: 3, - reliableForProximity: true, - withinProximityThreshold: true, - }, - routeStatus: "IN_PROGRESS", - serverProgress: { - outcomeCounts: { cancelled: 1, skipped: 1 }, - resolvedStopCount: 2, - totalStopCount: 3, - }, - }; - - assert.equal(getRouteTrackingOperationalMismatch(operationalState), null); -}); - -test("tracking snapshots preserve the server operational state through reconnect merges", () => { - const operationalState = { - deviceProgress: { completedStopCount: 11, totalStopCount: 11 }, - routePlanId: "route-1", - serverProgress: { resolvedStopCount: 1, totalStopCount: 11 }, - syncHealth: { state: "BLOCKED" }, - }; - const normalized = normalizeRouteTrackingSnapshot({ operationalState, routePlanId: "route-1" }); - assert.deepEqual(normalized.operationalState, operationalState); - - const merged = mergeRouteTrackingSnapshot(normalized, { routePlanId: "route-1", status: "LIVE" }); - assert.deepEqual(merged.operationalState, operationalState); -}); - test("stream inactivity recovery waits for three missed server heartbeats", () => { assert.equal(getRouteTrackingStreamInactivityMs({ policy: { heartbeatMs: 15_000 } }), 45_000); assert.equal(getRouteTrackingStreamInactivityMs({ policy: { heartbeatMs: 20_000 } }), 60_000); diff --git a/apps/shopify-app/tests/route-tracking-live.test.mjs b/apps/shopify-app/tests/route-tracking-live.test.mjs index e1b5db2..00078b3 100644 --- a/apps/shopify-app/tests/route-tracking-live.test.mjs +++ b/apps/shopify-app/tests/route-tracking-live.test.mjs @@ -4,7 +4,6 @@ import test from "node:test"; const appRoot = new URL("../app/", import.meta.url); const trackingContractPath = new URL("features/delivery/route-tracking.js", appRoot); -const operationalStatePath = new URL("features/delivery/operational-state.js", appRoot); const trackingProxyPath = new URL("features/delivery/route-tracking.server.js", appRoot); const trackingResourcePath = new URL("routes/app.route-tracking.$routePlanId.jsx", appRoot); const routeDetailPath = new URL("routes/app.routes.$routeId.jsx", appRoot); @@ -173,7 +172,7 @@ test("Tracking tab presents status-aware live or historical tracking and the lat assert.match(routeDetailSource, /routeTrackingConnectionLabel/); assert.match(routeDetailSource, /displayedRouteTrackingSnapshot\?\.policy/); assert.match(routeDetailSource, /Latest position/); - assert.match(routeDetailSource, /ariaLabel="Tracking evidence"/); + assert.match(routeDetailSource, /Last received/); assert.match(routeDetailSource, /formatTrackingTimestamp\([^,]+,\s*ianaTimezone\)/); assert.match(routeDetailSource, /timeZone:\s*ianaTimezone/); assert.match(routeDetailSource, /trackingConnectionState/); @@ -194,31 +193,9 @@ test("Tracking map focuses the live driver position and keeps freshness visible assert.match(routeDetailSource, /window\.setInterval\(\(\) => setRouteTrackingClock\(Date\.now\(\)\), 1_000\)/); assert.match(routeDetailSource, /getRouteTrackingCompletionTime\(displayedRouteTrackingSnapshot\)/); assert.match(routeDetailSource, /shouldShowRouteTrackingFreshness\(/); - assert.match(routeDetailSource, /ariaLabel="Tracking map operational state"/); - assert.doesNotMatch(routeDetailSource, /Vehicle GPS[^\n]*·[^\n]*Server confirmed/); - assert.match(routeDetailSource, /mapRouteOperationalState/); - assert.match(routeDetailSource, /ariaLabel="Tracking evidence"/); - assert.match(routeDetailSource, /routeOperationalPresentation\.pills/); + assert.match(routeDetailSource, /Current position/); + assert.match(routeDetailSource, /formatTrackingElapsedSeconds\(/); assert.match(routeDetailSource, /Range<\/span>/); assert.match(routeDetailSource, /formatTrackingRange\(/); assert.doesNotMatch(routeDetailSource, />Recorded range { - // The repository's node:test harness has no DOM or JSX transform. This is the - // closest rendered-surface contract available without adding a test dependency. - const routeDetailSource = readIfPresent(routeDetailPath); - const trackingContractSource = readIfPresent(trackingContractPath); - const operationalStateSource = readIfPresent(operationalStatePath); - - assert.match(routeDetailSource, /getRouteTrackingOperationalMismatch/); - assert.match(routeDetailSource, /positionMismatch: routeTrackingPositionMismatch/); - assert.match(routeDetailSource, /routeOperationalPresentation\.currentPositionAlert/); - assert.match(routeDetailSource, /role="alert"/); - assert.match(routeDetailSource, /heading=\{routeTrackingCurrentPositionAlert\.title\}/); - assert.match(routeDetailSource, /ariaLabel="Current position warning evidence"/); - assert.match(routeDetailSource, /pills=\{routeTrackingCurrentPositionAlert\.pills\}/); - assert.doesNotMatch(trackingContractSource, /proximityThresholdMeters|GPS Stop.*nearby|Gap.*stops/); - assert.match(operationalStateSource, /GPS proximity does not confirm delivery/); - assert.doesNotMatch(routeDetailSource, /Current position[^\n]*(?:·|•)[^\n]*Server/u); -}); diff --git a/apps/shopify-app/tests/routes-page.test.mjs b/apps/shopify-app/tests/routes-page.test.mjs index 4296e25..286ff21 100644 --- a/apps/shopify-app/tests/routes-page.test.mjs +++ b/apps/shopify-app/tests/routes-page.test.mjs @@ -160,8 +160,8 @@ test("Routes table uses aligned CLEVER planning columns", () => { assert.match(routesPageSource, /routeRows\.map\(\(route\) =>/); assert.match(routesPageSource, /aria-label="Select all visible routes"/); assert.match(routesPageSource, />Route<\/th>/); - assert.match(routesPageSource, />Route<\/th>[\s\S]*>Date<\/th>[\s\S]*>Operational state<\/th>/); - assert.match(routesPageSource, />Operational state<\/th>/); + assert.match(routesPageSource, />Route<\/th>[\s\S]*>Date<\/th>[\s\S]*>Status<\/th>/); + assert.match(routesPageSource, />Status<\/th>/); assert.match(routesPageSource, />Orders<\/th>/); assert.match(routesPageSource, />Area<\/th>/); assert.match(routesPageSource, />Total drive time<\/th>/); @@ -172,9 +172,16 @@ test("Routes table uses aligned CLEVER planning columns", () => { assert.doesNotMatch(routesPageSource, />Coordinates<\/th>/); assert.doesNotMatch(routesPageSource, />Missing<\/th>/); assert.doesNotMatch(routesPageSource, />Created<\/th>/); - assert.match(routesPageSource, /mapRouteOperationalState\(\{/); - assert.match(routesPageSource, /operationalState: route\.operationalState/); - assert.match(routesPageSource, //); assert.match(routeListRowsSource, /const routeGroupRows = routeGroupEntries\.map/); assert.match(routeListRowsSource, /function getRouteGroupTotalOrders\(routeGroup\)/); @@ -183,7 +190,7 @@ test("Routes table uses aligned CLEVER planning columns", () => { assert.match(routeHelpersSource, /rightRouteIdx = numberOrUndefined\(right\.child\?\.routeIdx\)/); assert.match(routeListRowsSource, /isRouteGroup: true/); assert.match(routeListRowsSource, /isDeletable: true/); - assert.match(routeListRowsSource, /childRows: buildRouteChildRows\([\s\S]*routeGroup,[\s\S]*children,[\s\S]*groupAccentColor,[\s\S]*groupSummary,[\s\S]*operationalStateByRoutePlanId/); + assert.match(routeListRowsSource, /childRows: buildRouteChildRows\(routeGroup, children, groupAccentColor, groupSummary\)/); assert.match(routeListRowsSource, /formatRouteGroupDate\(routeGroup\)/); assert.doesNotMatch(routesPageSource, /routeIndex: routeIndex \+ 1/); assert.match(routeListRowsSource, /formatRouteValues\(routePlan\.deliveryAreas\)/); @@ -457,7 +464,7 @@ test("Route detail route exists for clicked persisted route rows", () => { assert.match(routeDetailSource, /import \{ useAppBridge \} from "@shopify\/app-bridge-react"/); assert.match(routeDetailSource, /import \{ useFetcher, useLoaderData, useNavigate, useRevalidator \} from "react-router"/); assert.match(routeDetailSource, /currentDepartureLocation = null/); - assert.match(routeDetailSource, /childRouteDetails = \[],\s+currentDepartureLocation = null,\s+drivers = \[],\s+operationalState = null,\s+routePlan,\s+routeGeometry = null,\s+routeGroup = null,\s+routeDetailTitleOverride = null,\s+routeMetrics = null,\s+routeStopPoints = \[],\s+stops = \[],\s+errors = \[]/); + assert.match(routeDetailSource, /childRouteDetails = \[],\s+currentDepartureLocation = null,\s+drivers = \[],\s+routePlan,\s+routeGeometry = null,\s+routeGroup = null,\s+routeDetailTitleOverride = null,\s+routeMetrics = null,\s+routeStopPoints = \[],\s+stops = \[],\s+errors = \[]/); assert.doesNotMatch(routeDetailSource, /routeStopPointDebug: buildRouteStopPointDebug/); assert.match(routeDetailSource, /const savedRouteGeometryRows = routeGeometryRows/); assert.match(routeDetailSource, /const savedRouteStopPoints = routeGeometryStopPoints/); @@ -636,7 +643,7 @@ test("Route detail renders a compact route overview panel with inline summary", assert.match(routeDetailSource, /const routeDriverSummary = routeDriverId[\s\S]*: "Unassigned"/); assert.match( routeDetailSource, - /

[\s\S]*aria-label="Route summary" className="route-overview-summary"/, ); assert.match( routeDetailSource, diff --git a/apps/shopify-app/tests/shopify-token-sync.test.mjs b/apps/shopify-app/tests/shopify-token-sync.test.mjs index 4de8377..1faa576 100644 --- a/apps/shopify-app/tests/shopify-token-sync.test.mjs +++ b/apps/shopify-app/tests/shopify-token-sync.test.mjs @@ -349,7 +349,7 @@ test("keeps interleaved shop failures, recovery history, and concurrency isolate } }); -test("authenticated health surfaces scope lookup to the authenticated shop", async () => { +test("authenticated token health stays scoped to its dedicated route and out of Settings UI", async () => { const { readFile } = await import("node:fs/promises"); const healthRoute = await readFile(new URL("../app/routes/app.health.shopify-token.jsx", import.meta.url), "utf8"); const settingsRoute = await readFile(new URL("../app/routes/app.settings.jsx", import.meta.url), "utf8"); @@ -357,7 +357,7 @@ test("authenticated health surfaces scope lookup to the authenticated shop", asy assert.match(healthRoute, /const\s+\{\s*session\s*\}\s*=\s*await authenticate\.admin\(request\)/); assert.match(healthRoute, /getShopifyTokenSyncHealth\(session\?\.shop\)/); assert.doesNotMatch(healthRoute, /searchParams|url\.search|request\.url/); - assert.match(settingsRoute, /getShopifyTokenSyncHealth\(shopifyShopCacheKey\)/); + assert.doesNotMatch(settingsRoute, /getShopifyTokenSyncHealth|operationalHealth|Operational health/); }); test("prunes expired health and caps retained shop identities", async () => {