Skip to content

Phase 45/32 bulk actions framework - #61

Merged
atulmgupta merged 47 commits into
mainfrom
phase-45/32-bulk-actions-framework
May 4, 2026
Merged

atulmgupta merged 47 commits into
mainfrom
phase-45/32-bulk-actions-framework

Conversation

@atulmgupta

Copy link
Copy Markdown
Contributor

Description

Closes #

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would break existing functionality)
  • Documentation update
  • Infrastructure / CI change

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or my feature works
  • New and existing tests pass locally
  • I have updated the documentation accordingly
  • My changes generate no new warnings

Screenshots (if applicable)

atulmgupta and others added 30 commits May 3, 2026 01:22
Introduce runtime theming for app icons and Android notification badge support.

- Add web/src/lib/appIcon.ts: pure helpers to build SVG app icons, encode as data URLs, and rasterize to PNGs.
- Add useDynamicAppIcon hook (web/src/hooks/useDynamicAppIcon.ts) to apply themed favicon, apple-touch-icon, and a generated manifest blob (with revoke handling). Exposes data-base-href so badges can composite over the live themed icon.
- Adjust useFaviconBadge to prefer data-base-href when snapshotting/compositing, preventing badge churn over dynamically themed favicons.
- Wire the hook into Layout.tsx so it runs before the favicon badge hook.
- Add unit tests for useDynamicAppIcon (web/src/hooks/__tests__/useDynamicAppIcon.test.tsx).
- Add badge.svg and generated badge-72.png to public icons and document regeneration steps in web/public/icons/README.md.
- Update service worker (sw.ts) to use the monochrome badge (/icons/badge-72.png) and clarify badge vs icon behavior in comments.

This enables on-the-fly favicon/manifest updates to reflect user-selected themes and ensures Android notifications use the correct monochrome badge to avoid the duplicate-icon bug.
Update PlaybackControls help content to avoid hardcoded white text and improve kbd styling. Added a comment explaining why we must inherit the Tooltip's text color (to avoid invisible text in inverted dark-mode tooltips), removed text-white/* classes, set the grid to use opacity-90, and gave each <kbd> a rounded border, subtle background, padding, and smaller monospace text so shortcut tokens are distinct and readable in both themes.
eslint no-useless-escape was failing on [\s_\-] since the dash is
already at end-of-class position; switch to [\s_-]. Pre-existing
issue surfaced by adding lint:i18n to the lint chain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert all 31 JS template-literal placeholders (${var}) in
web/src/i18n/en.json to i18next mustache syntax ({{var}}), updating
every callsite to pass interpolation values via t(key, { var,
defaultValue }). Two legitimate ${{amount}} patterns (literal "$"
followed by a mustache placeholder, used for currency formatting) are
preserved.

Add web/scripts/lint-i18n-no-template-literals.mjs as a CI guardrail
that fails if any web/src/i18n/*.json file ever reintroduces this
syntax. The script is chained into "npm run lint" so existing CI
workflows pick it up without further wiring.

Affected user-visible strings include Year-in-Review, Cost-Per-Distance,
Battery Health insights, Notification Rules cadence, Confirm dialogs,
Pin/Unpin tooltip, and Drive efficiency labels.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a centralized broadcast topic registry (TOPICS in
web/src/lib/broadcastTopics.ts) and a <FormatterPrefsBridge /> mounted
near the React root that anchors a permanent useSettings() subscription
so module-level formatter globals (numberFormat locale + decimal
precision) stay current on every page — including pages that never
call useSettings() themselves.

Extends BroadcastMessage with an umbrella settings.changed topic that
non-React-Query mutators can fire to invalidate the ['settings'] query
on this and peer tabs, and refactors useSettings() to apply globals
inside useEffect rather than during render so commits are deterministic
under React.StrictMode.

Adds 9 broadcast-propagation tests covering: derived useSettings
applies/refreshes globals on data change, the bridge anchors globals
without a page-level consumer, settings.changed cross-tab broadcasts
trigger refetch + global re-application, and invalidateAndBroadcast
emits queryInvalidate envelopes on the wire.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erfiles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds end-to-end measurement of Core Web Vitals (LCP, INP, CLS, FCP, TTFB)
from real browser sessions, the prerequisite for treating the engineering
guideline performance budget (FCP < 1.5s on 4G) as an observable property
rather than a one-off check.

Frontend:
- web/src/lib/webVitalsReporter.ts: idempotent reporter that batches
  metrics, ships them via navigator.sendBeacon (with fetch fallback),
  flushes on visibilitychange/pagehide, and never propagates errors.
- main.tsx: starts the reporter in PROD; keeps the existing dev-console
  reporter for development to avoid HMR noise.

Backend:
- internal/api/web_vitals_handler.go: POST /api/v1/web-vitals ingest
  handler with strict JSON decoding, 64 KiB body cap, 100-metric batch
  cap, closed allow-list of metric names + ratings, Prometheus
  histogram (teslasync_web_vitals_value{name,rating,route}), and route
  normalization (numeric/UUID/hex segments collapsed to :id, length cap
  50) so label cardinality stays bounded.
- internal/api/router.go: mounted public (no auth) outside the /api/v1
  ForwardAuth subrouter and rate-limited to 120 req/min/IP.

UI surface:
- features/system/pages/SystemStatusPage.tsx: new ClientPerformancePanel
  documenting that Web Vitals are now collected and linking to a Grafana
  dashboard via VITE_GRAFANA_URL when set.
- i18n/en.json: admin.systemStatus.clientPerformance.* keys.

Tests: 9 frontend tests (mocks web-vitals callbacks and exercises
sendBeacon/fetch/error paths) + 8 Go test groups (valid/invalid/empty/
oversize batches, unknown name/rating handling, strict decoding, 14
route-normalization cases, length cap).

Note: the prompt's Files-touched list referenced
features/admin/pages/SystemStatusPage.tsx, but the actual file lives at
features/system/pages/SystemStatusPage.tsx (App.tsx:124,
lazyRoutes.list.ts:135). The git_status allow-list in the gate was
adjusted to match the real path; see PREFLIGHT NOTE in the log.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the missing mobile-aware shared primitives required by the
Phase-45/14 mobile responsive audit:

- web/src/hooks/useMediaQuery.ts: reactive matchMedia hook with
  SSR-safe defaults plus useIsMobile() / useIsCoarsePointer()
  convenience aliases. Lets chart consumers opt into tap-to-tooltip
  behaviour on touch devices without ad-hoc matchMedia plumbing.
- web/src/__tests__/mobile.viewport.test.tsx: smoke test that
  exercises the shared primitives at a 375px phone viewport
  (Modal as bottom sheet, BottomTabBar safe-area + touch targets,
  PageHeader stacking, DataTable mobileColumns) and verifies the
  useMediaQuery contract (initial state, reactive updates, listener
  cleanup).

Verified: tsc --noEmit, npm run lint, npm test -- --run (1238 passed),
audit: viewport-fit=cover + safe-area-inset CSS already present.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a shared useOptimisticMutation helper that snapshots the cache,
writes the optimistic update, restores on error, and invalidates on
settle. Supports multi-key updates and an optional cross-tab broadcast.

Migrate 7 high-frequency mutations to use it so the UI updates
instantly instead of waiting for the server round-trip:

- useToggleAutomation (useAutomations.ts) - flip enabled instantly
- useDeleteSavedView (useSavedViews.ts)   - row removed instantly
- useMarkAlertRead (useNotifications.ts)
- useToggleAlertRule (useNotifications.ts)
- useMarkNotificationsRead (useNotifications.ts)
- useMarkNotificationsUnread (useNotifications.ts)
- useArchiveNotifications (useNotifications.ts)

Notes:
- useTogglePin remains on plain useMutation: its mutationFn reads the
  pinned-list cache to look up the row id for DELETE, which requires
  pre-mutation cache access. Documented inline; revisit if the backend
  exposes a delete-by-(type,item_id) endpoint.
- Toasts stay on onSuccess/onError, not onMutate, so a rolled-back
  optimistic write does not mislead the user.
- Adds useAlerts.ts re-export shim so consumers can import alert hooks
  from a dedicated module without churning useNotifications.ts.

Tests:
- useOptimisticMutation.test.tsx (9 tests): immediate optimistic write,
  rollback on error, invalidation on settle, multi-key, function-form
  keys, caller onMutate ordering, double-toggle race, broadcast.
- useMarkNotificationRead.test.tsx (6 tests): bulk mark-read +
  single useMarkAlertRead happy + rollback paths.

All gates green: TS clean, lint clean, 1253/1253 frontend tests pass,
13 onMutate occurrences across hooks/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… guards

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace useState-based filter state with useUrlString/Number/Boolean/Enum
helpers in 10 filter-bearing pages so filter selections are bookmarkable,
shareable, and survive a page refresh. Also adds a static-source guard
test that asserts every listed page imports a useUrl* hook.

Pages converted:
- charging/ChargingListPage (sort, charger, search, page, dates)
- admin/ApiLogsPage (page, method, status, endpoint, service, dates)
- admin/DevToolsPage (refactor useTabParam to useUrlEnum)
- analytics/StatisticsPage (vehicle, dates)
- analytics/PeriodComparePage (vehicle, periodA, periodB)
- maps/LocationsPage (vehicle, page, search)
- system/CommandHistoryPage (vehicle, status, search, page)
- telemetry/SignalExplorerPage (signals, dates, page)
- telemetry/SignalDiffPage (vehicle, atA, atB, filter, category)
- trips/TripListPage (vehicle, page, dates)

Total adopters in features/: 10 -> 20.

NEW src/__tests__/url-state-adoption.test.ts asserts the 13 canonical
filter pages each call a useUrl* helper.

Adds top-level i18n key 'filters.reset' = 'Reset filters'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…script

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ulse

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rDisplay

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ency audit

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… /settings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dialog + checklist task

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pReplayPage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
atulmgupta and others added 17 commits May 3, 2026 16:07
…update + undo

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…vanced settings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ken expiry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ss feature pages

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t across 4 list pages

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…down + breaker UX

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…I script + dev-time warn

- NEW web/scripts/audit-tooltip-text-color.mjs flags <Tooltip|HelpTooltip>
  callsites whose JSX content hardcodes text-white/N or text-gray-{100..400}
  (collides with the tooltip's inverted surface — light card in dark mode,
  dark card in light mode + light-mode global text-white overrides).
- Wire the audit into the lint chain (npm run audit:tooltip-text).
- web/src/components/ui/Tooltip.tsx: JSDoc the inverted-surface text colour
  contract and add a dev-only warnIfHardcodedTextColor backstop that
  console.warn's once per offending callsite (zero cost in production
  builds via import.meta.env.PROD short-circuit).
- NEW web/src/components/ui/__tests__/Tooltip.contract.test.tsx — 10 tests
  covering string content, JSX without overrides, decorative shades
  (text-amber-300, text-emerald-300), nested-children walking, dev vs
  production behaviour, and the text-gray-500 negative case.
- 0 existing offenders — PlaybackControls helpContent (the post-Phase-40
  fix) remains the canonical reference for downstream tooltip authors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
State Machine debugger no longer shows the contradictory "23 buffered" /
"No transitions in window" pair. The toolbar now reads
"{n} in window . {N} in 24 h" so both scopes are explicit, and the
empty-state timeline + snapshot inspector get an actionable hint with
"Widen window to {label}" and "Jump to last transition" buttons.

Phase-45 / Prompt 35.

Changes:
- new windowTransitions(transitions, minutes, anchor?) helper as the
  single source of truth for page, timeline, and toolbar
- new nextWiderPreset(lastTs, anchor, currentMinutes) that picks the
  smallest preset (5m..24h) that would surface the last transition
- StateMachineDebuggerPage now feeds pre-windowed transitions to the
  timeline and threads lastTransition / widerPreset / handlers into
  StateTimeline + SnapshotInspector
- LiveControls renders dual counter + Tooltip explaining the dropdown
  vs fetch distinction; bufferCount kept as a one-phase deprecated
  fallback (no external consumers)
- StateTimeline empty-state now flex-row with hint + Widen / Jump
  buttons; component is purely presentational (callers pre-window)
- SnapshotInspector emptyOutsideWindow branch matches the timeline
  hint when there is nothing selectable
- new i18n keys under debugger.{window,timeline,controls,inspector}

Tests: 14 helper + 5 timeline empty + 5 inspector empty + 3 page
integration; full suite 1548/1548 pass; tsc + lint clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… + active-state on chips

Fixes the same-tick URL setter race in react-router-dom v6 that caused
preset chips on Signal Log Viewer (and 9 other pages) to silently lose
one of the two URL writes when applying a time-range preset.

Changes:
- Add useUrlBatch() to hooks/useUrlState.ts: single setter that
  atomically commits N URL key changes via one setSearchParams updater
  call, eliminating the structural race where two same-tick navigate
  (replace) calls discard each other.
- Document the same-tick race in the UrlStateSetter JSDoc.
- Extend <DateRangeFilter> with optional onRangeChange prop. When
  provided, preset chip clicks call it instead of dual onStart/onEnd
  setters, allowing pages to wire it to a useUrlBatch-backed setter.
- Add matchTimeRangePreset() to lib/constants.ts: matches a (from, to)
  datetime pair to a TIME_RANGE_PRESETS entry within +-60s tolerance.
- Add visual active-state to TIME_RANGE_PRESETS chips on
  SignalLogViewerPage, SignalExplorerPage, and SignalQueryControls
  (variant=primary + aria-pressed when matchTimeRangePreset matches).
- Migrate 10 affected sites: SignalLogViewerPage, SignalExplorerPage
  and NotificationsPage use useUrlBatch directly; the 8
  <DateRangeFilter> consumers (DrivesList, ChargingList, MyActivity,
  CostAnalysis, Statistics, Energy, Efficiency, TripList) wire
  onRangeChange. CostAnalysis/Energy/Efficiency also migrated from
  useState to useUrlString for date filters.
- Collapse NotificationsPage.handleFiltersChange (7 setters) into one
  setFiltersBatch call, fixing silent data loss when applying saved
  views or clearing all filters.
- Add i18n key signalQuery.preset.aria for chip aria-label.

Tests:
- useUrlBatch: 5 new tests (atomic write, deletion semantics, push
  option, plus a REGRESSION test documenting the same-tick race that
  useUrlBatch fixes).
- matchTimeRangePreset: 7 new tests (each preset matches, non-match,
  tolerance, custom tolerance, empty/invalid input).
- DateRangeFilter onRangeChange: 3 new tests (precedence, back-compat
  fallback, onApply still fires).
- All 1567 frontend tests pass; tsc clean; lint (incl. all 4
  sub-audits) clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eta + add keys endpoint + structured empty-state diagnostic

Backend
- internal/signal/redis_cache.go: add HLen + Scan to redisSignalClient
  interface; add RawFieldCount (HLEN raw HSET size, distinguishes
  "Redis empty" from "fields stored but undecodable") and
  ScanVehicleKeys (cursor-based SCAN, NEVER KEYS) helpers.
- internal/signal/store.go: add LastSeenAt(vehicleID) returning newest
  L1 timestamp; complement of L2 GetAllValues max-Timestamp scan.
- internal/api/devtools_handler.go: wire signalStore + vehicleRepo via
  new WithSignalStore option; enrich GET /dev-tools/redis-signals with
  meta block (live_signal_store_mode, redis_key, redis_field_count,
  l1_signal_count, l1/l2_last_seen_at, vehicle_vin); add sister
  GET /dev-tools/redis-signals/keys handler that lists vehicleIDs with
  cached Redis HSETs via SCAN.
- internal/api/router.go: pass WithSignalStore(opt.SignalStore) to the
  dev-tools handler; register the new /redis-signals/keys route.
- internal/api/devtools_handler_test.go (NEW): table-driven tests using
  miniredis covering 503-no-redis, 400-bad-vehicle-id, four meta
  permutations (mode-local, hybrid+L1-only, hybrid+populated, both
  empty), and keys endpoint (filters malformed keys, respects limit).

Frontend
- web/src/api/devtools.ts: add RedisSignalsMeta, RedisSignalKeyEntry,
  RedisSignalKeysResponse types; getRedisSignalKeys hook function.
- web/src/features/admin/components/RedisDiagnosticEmptyState.tsx
  (NEW): structured 4-branch diagnostic banner — mode=local danger,
  L1>0+L2=0 mirror-broken warning, no-recent-telemetry info, neutral
  fallthrough — plus quick-switch chips listing OTHER vehicles that DO
  have cached signals (from the keys endpoint), and a meta KV list.
- web/src/features/admin/pages/RedisSignalViewerPage.tsx: render
  persistent header chips (Mode/VIN/L1-last) whenever a vehicle is
  selected so engineers don't have to clear the table to see them;
  replace the generic empty-state branch with the new diagnostic.
- web/src/i18n/en.json: add redis.diagnostic.* (modeLocal, mirrorBroken,
  noTelemetry, empty, otherVehicles, meta) and redis.headerChip.*
  blocks.

The page stops being a black box: the engineer now gets a specific
actionable hint for each of the five known empty-state root causes
documented in internal/signal/live_store.go and redis_cache.go.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ts.by_service; trim dead options; add live-writer SERVICE_CONFIG entries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stop rescoring each keyword as if it were the label in CommandPalette: score the label once with keywords passed in to preserve label-vs-keyword ranking and avoid keyword ties. Add regression tests to CommandPalette and commandRegistry to lock in the behavior.

Revamp service dropdown generation: deriveServiceOptions now accepts an optional knownServices catalog and returns the union of knownServices, by_service keys, and the active service; results are deduplicated and sorted alphabetically (case-insensitive). Add extensive tests to cover union, dedupe, and ordering behaviors.

Update ApiLogsPage to use a URL-batch writer (useUrlBatch) so multiple search params (filters + page reset) are written atomically; wire selects/inputs to a setFilter helper and add selectService helper. Introduce KNOWN_SERVICES (from SERVICE_CONFIG) and pass it to deriveServiceOptions; show tracked vs known counts and add several geocoder services to SERVICE_CONFIG.

Polish RedisSignalViewerPage value rendering with per-type color classes for better readability. Update i18n string for service count to include tracked and known counts.

Tests updated/added accordingly to cover these fixes and prevent regressions.
Drive Detail page (/drives/{id}) was rendering blank Odometer From->To,

blank Range Start->End, blank Range Used, and a flat Power Profile chart

even on drives with valid signal_log telemetry. Two root causes:

1. driveTelemetryFieldMappings was missing the canonical Tesla signals

   that the frontend chartData mapper reads: Odometer, IdealBatteryRange,

   RatedRange, EstBatteryRange, Soc, HvacFanStatus, HvacLeftTempRequest,

   HvacRightTempRequest. The frontend's tp.odometer / tp.idealRange /

   tp.ratedRange were always undefined.

2. Tesla Fleet Telemetry does not emit a per-row PackPower signal, so the

   chart's dataKey='power' had nothing to render. derivePowerKw() now

   computes power = pack_voltage * pack_current / 1000.0 per row (sign

   preserved so regen stays negative on the chart). Same formula already

   used by enrichLiveDrive() for live drive AvgPowerKw and signalPowerKW()

   in telemetry_sessions_signal_helpers.go.

Frontend follow-ups:

- EnergySummaryPanel: replace hardcoded '--' for Range Used with

  fmtWithUnit(stats.startRange - stats.endRange, distanceUnit).

- useDriveDetailData: powerMax / powerMin now come from chartData (the

  legacy approximation hard-coded powerMax = avgPowerKw and powerMin = 0,

  which gave nonsense MaxRegen badges).

Tests:

- TestDriveDetail_Telemetry_DerivesPowerKw: V*I/1000 with sign preservation,

  rows missing pack_current have no derived power.

- TestDriveDetail_Telemetry_FieldMappingsCoverPageFields: regression guard

  for the 21 fields the frontend reads from each telemetry row.

All 1592 vitest tests + drive detail Go tests pass; tsc clean; audit clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
My Activity page rendered 'key activity.myActivity.disabled (en) returned an

object instead of string' as the empty-state body. Same class of bug

in 3 other call sites:

- activity.myActivity.disabled / .unauthorized (MyActivityPage)

- admin.security.timeline (EventTimeline heading)

- widget.chargeHistory (ChargeHistoryWidget title)

- search.noResults / .placeholder / .label (SettingsSearch — also wrong

  namespace; CommandPalette owns the search.* tree, settings.search.*

  is the right namespace)

Each call resolved its dotted key to an OBJECT (parent of .title /

siblings) in en.json. i18next returns the object verbatim and ignores

the fallback string, so users saw the raw error text in the UI.

Fix:

- Add .description / .title siblings under disabled, unauthorized,

  admin.security.timeline, widget.chargeHistory

- Repoint SettingsSearch to settings.search.* (correct namespace)

- New scripts/audit-i18n-shadowed-keys.mjs walks every t('a.b.c') call,

  resolves against en.json, fails on any leaf-pointing-to-object.

  Wired into npm run lint so this can't regress.

tsc clean; vitest 1592/1592 pass; audit:i18n-shadowed exits 0; audit_code

clean on all 4 touched components.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Settings struct was missing TimeFormatDefault (Phase-45/22) and
ChartPalette (Phase-45/23) fields. When the Appearance settings page
PUT them, json.NewDecoder silently dropped the unknown JSON keys, the
Upsert never wrote the rows, and the next GET returned the old value -
so the radio cards appeared inert when clicked.

Wires both fields through the four canonical settings layers:
- models.Settings struct (with phase-tagged doc comments)
- settingsDefaults() (relative + cb_safe)
- applySettingsRow() switch cases
- Upsert() textRows
- Update() handler validation (rejects values outside the typed enum)

No migration needed: the settings key/value table is forward-
compatible and will create the rows on first Upsert.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The driving-dynamics cold-signal panels (G-Force, Pedal Usage, Autopilot &
Cruise) and the SignalLogWidget event feed surfaced empty / stale values
because /signals/observations had two coordinated bugs:

1. The repo ordered ASC by ts. The frontend latestNumeric() helper reads
   data[0] expecting the most recent observation, so callers asking with
   limit:1 got the OLDEST row in the window, and SignalLogWidget showed
   the oldest 20 events of the day instead of the most recent 20.

2. The handler defaulted since to now-24h. Cold signals (Lateral/Long
   Acceleration, PedalPosition, BrakePedalPos, BrakePedal, CruiseSetSpeed,
   CruiseFollowDistance) only emit while driving, so the panels went empty
   when the car had been parked > 24h despite the data being on disk.

Fix:
- Repo: ListByName + ListByVehicle now ORDER BY ts DESC and accept zero
  time.Time{} bounds to mean 'unbounded' on that side. Extracted the SQL
  composition into buildObservationQuery (covered by 5 new unit tests).
  The (vehicle_id, signal_name, ts DESC) index serves the bounded LIMIT
  case in index-only fashion.
- Handler: omit the lower/upper bound from the predicate when since/until
  query params are absent. Reject non-RFC3339 since/until with 400 instead
  of silently widening to 'all history' (subtle behavior regression flagged
  by rubber-duck review).

Affected callers (all want latest-first): GForcePanel, PedalUsage,
AutopilotSection, SignalLogWidget, PowersharePage. SignalCatalogWidget's
per-signal counts now reflect 'recently active signals' (newest 100 of
all-time) instead of 'oldest 100 of last 24h' — strictly more useful for
the widget's UX.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@atulmgupta
atulmgupta merged commit a201040 into main May 4, 2026
2 of 4 checks passed
@atulmgupta
atulmgupta deleted the phase-45/32-bulk-actions-framework branch May 4, 2026 05:24
atulmgupta added a commit that referenced this pull request May 7, 2026
…outs)

Adds web/src/api/hooks/orphan-allowlist.ts documenting two intentional
ORPHAN waivers per phase-43a/0001:

  - useAlerts.ts: domain-named re-export shim from Phase-45/32 (PR #61);
    call sites still import from useNotifications. Adoption pending.
  - useDashboardLayouts.ts: named-layout library hooks with LIVE backend
    routes (/dashboard/layouts/*) but the UI integration with
    LayoutSwitcher / DashboardPage is not yet wired (out of scope for
    this prompt; deferred to a future re-mount prompt).

Plus a documented baseline-fix sweep for 8 pre-existing lint failures
(4 ESLint no-extra-semi/unused-disable, 4 audit:query-signal AbortSignal
threading) so cd web && npm run lint passes end-to-end. Each baseline
fix is a single-line edit unrelated to orphan disposition; explicit
scope expansion documented in the prompt log per Honesty Covenant 8.

See .github/prompts/db-refactor/logs/phase-43a-0001-orphans.log for the
full audit evidence, design rationale, and gate output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant