Commit e155065
Tracing (#70)
* Add PWA manifest and basic service worker
Add web/public/manifest.json to enable PWA metadata and icons (including maskable PNG/SVG entries) and web/public/sw.js as a minimal network-first service worker. The service worker pre-caches '/' and '/manifest.json', performs cache cleanup on activate, uses skipWaiting/clients.claim, caches successful navigation responses and falls back to cache when offline, while leaving non-navigation requests as network-only to avoid caching API/assets.
* feat(observability): full end-to-end tracing coverage across API + 4 workers
Phase-44 extension: lift static trace-coverage audit from 3 flows to 11 and
make every flow emit honest, parent-linked spans across process boundaries.
Foundation (Phase 1)
- internal/tracing/tracing.go: add functional-options Init(ctx, cfg, ...Option)
with WithServiceName; bump default head sampler from 0.01 to 1.0 (single-
tenant self-hosted - tail sampling at collector handles cost)
- internal/platform/telemetry/tracer.go: delete dead InitTracer/Tracer fns
- internal/domain/fsm/engine.go + sub_fsm.go: extend SpanEnder with
RecordError + SetStatus so failed FSM transitions render red in Tempo
- internal/tracing/fsmtracer.go (new): OTel adapter for the domain fsm.Tracer
port - keeps internal/domain/fsm + internal/app/*svc zero-dep on OTel
Worker tracing init (Phase 2)
- cmd/{notification,export,automation}-worker, cmd/resubscribe: each main.go
now calls tracing.Init(ctx, cfg, WithServiceName("teslasync-<worker>"))
immediately after logger setup with a 5s-bounded shutdown defer
MQTT trace propagation (Phase 3)
- internal/mqtt/propagation.go (new): InjectTraceContext / ExtractTraceContext
with versioned `_v:1` envelope; consumer falls back to passthrough for
legacy un-enveloped messages so consumer-first rollout is safe
- notification/export workers wrap publish payloads; consumers extract on
receive so traces span the broker
Per-iteration spans (Phase 4)
- notification.{consume_mqtt,send,dnd_replay_tick,schedule_tick,computed_metric_tick}
- export.{consume_mqtt,publish_status,cleanup_tick,backup_tick,backup_run}
- automation.evaluate; resubscribe.push_vehicle (per vehicle)
- In-API workers gain per-tick spans: gas_price.refresh_tick,
maintenance.tick, unit_drift.validate_tick
SSE per-event spans (Phase 5)
- internal/api/sse_handler.go: BroadcastWithContext + BroadcastSignalChangeWithContext
emit sse.broadcast span with event_type, client_count, dropped_count attrs;
legacy Broadcast retained as // Deprecated: thin wrapper
- Threaded ctx through telemetry handler, alert/automation/lifetime/export
publishers, and the side-effects observer (BroadcastSSEFunc widened)
FSM tracer wiring (Phase 6)
- vehiclesvc/chargingsvc/exportsvc/tripsvc/notificationsvc each expose
SetTracer(fsm.Tracer); router.go wires NewFSMTracer("fsm.<scope>") for
the three currently-instantiated services
Audit gate + tests (Phase 7)
- cmd/trace-coverage-audit: 11 flows (was 3); regex extended to count
tracing.StartSpan and GetTextMapPropagator
- internal/mqtt/propagation_test.go, internal/tracing/fsmtracer_test.go,
internal/api/sse_handler_tracing_test.go: tracetest.SpanRecorder-based
runtime tests verify span emission, attrs, and parent linkage
Docs (Phase 8)
- .github/instructions/observability.instructions.md: 7 new Required rules
(worker init, MQTT inject/extract, SSE WithContext, FSM SetTracer,
per-tick spans, audit gate); new Prohibited rules for deprecated Broadcast
and OTel imports into domain/svc layers
- docs/runbooks/phase-44-trace-coverage-audit.md auto-regenerated
Verification (Phase 9)
- go build ./... + go vet ./... + go test -race -timeout 600s ./... all green
- go run ./cmd/trace-coverage-audit: ALL FLOWS OK
- web tsc --noEmit clean (no frontend changes)
Out of scope (documented):
- Cross-pod trace context through Redis Pub/Sub SSE fanout (separate PR;
fanout spans labelled trace.continuity=false)
- Trace propagation through Tesla Fleet Telemetry MQTT (Tesla owns publisher)
- Browser EventSource traceparent (no header API; defer to SSE-via-fetch swap)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(observability): full Tesla signal-ingest pipeline tracing (MQTT receive -> DB save)
Phase 10 adds 21 OpenTelemetry spans (G1-G21) covering the previously dark zone between MQTT message receipt and DB INSERT for Tesla signals:
- mqtt.vin_resolve (vin_cache.Resolve) + codec.decode_json_field.
- tesla.router.route per atomic with write.role={primary,dual} propagated via a private writeRoleCtxKey.
- tesla.writer.<dest> spans for every writer path (snapshot composed (climate/motor/media/safety/location/charging/drive), signal_log, positions, security_event, tire_pressure timestamp path). Bespoke writers use the shared startWriterSpan helper.
- normalize.observe_setting_unit -> unit_history.record covers the Setting*Unit short-circuit.
- observer.side_effects PARENT span with 6 children: signal.live_store.update_all, fsm.dispatch_signals, signal.live_store.get_all, observer.vin_resolve, sessions.process_signals_at, alerts.evaluate. Children use ctx returned by Start() so parent linkage is preserved (Decision #9).
- signal.redis_cache.update with async={true,false} via private redisAsyncCtxKey set by HybridLiveSignalStore.UpdateNonBlocking before its detached goroutine.
- signal_log read spans (State/SignalAt/Timeline) so cold-path reads from the SPA appear in the same trace.
- signal.redis_pubsub.publish Producer span on the SSE fanout goroutine with trace.continuity=false because the publish intentionally uses context.WithoutCancel + 2s timeout.
Verification:
- go build ./... + go vet ./... + go test -race -timeout 600s ./... all green.
- New tesla_signal_ingest_to_db audit flow PASSES via cmd/trace-coverage-audit; ALL FLOWS OK.
- New runtime tracing contract tests in internal/tesla/router (router_tracing_test.go) and internal/tesla_pipeline (pipeline_tracing_test.go) lock the span tree shape, parent linkage, write.role propagation, async + trace.continuity attrs, and PII guard.
- Pre-existing mqtt tracing test relaxed to acknowledge the new codec.decode_json_field child span (previously asserted 'exactly 1 ended span').
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(observability): Phase-44 batch — DLQ inspector + feature flags + ingest x-ray + drive-end diagnostic + 3 CI gates
Adds the operator-facing observability surface on top of the Phase-10
E2E tracing work so a self-hosted operator can introspect the live
ingest pipeline, replay broken telemetry, flip runtime feature flags,
and answer "why did this drive end?" from the React Diagnostics panel.
Backend (8 new files in internal/api, baselined per ADR-009 Exceptions)
- internal/api/dlq_handler.go + _test.go GET /system/dlq, /system/dlq/{id}, /system/dlq/audit; POST /system/dlq/{id}/replay (sudo + 10/min)
- internal/api/flags_handler.go + _test.go GET/PUT/DELETE /system/flags{,/{key}}; GET /system/flags/changes (write = sudo + 20/min)
- internal/api/ingest_xray_handler.go + _test.go GET /system/ingest-xray/{vehicleID}?window=&bucket= (60/min)
- internal/api/drive_diagnostic_handler.go + _test.go GET /drives/{driveID}/why-ended
Backend infrastructure
- internal/flags/{store,doc}.go + store_test.go Redis-backed FlagStore with Pub/Sub change-notification + miniredis test
- internal/mqtt/dlq_inspector.go + _test.go Ring-buffered DLQ inspector; capacity from FeaturesConfig.DLQRingCapacity
- internal/database/dlq_replay_audit_repo.go dlq_replay_audit table writer (migration 000211)
- internal/database/feature_flag_changes_repo.go feature_flag_changes table writer (migration 000211)
- internal/database/ingest_xray_repo.go FieldStats + SampleCountByBucket + LastSeen over signal_log
- internal/database/drive_diagnostic_repo.go TransitionsAround + SignalsAround with typed-value rendering
- internal/metrics/exemplar.go + _test.go ObserveDurationWithExemplar — Prometheus exemplars for trace/metric correlation
- migrations/000211_dlq_and_feature_flag_audit.{up,down}.sql
Wiring
- internal/api/router.go 5 new routes wired under existing /system + /drives blocks
- internal/api/router_middleware.go RouterOptions gains DLQInspector, DLQReplayAuditRepo, FlagStore, FeatureFlagChangesRepo
- internal/api/telemetry_alerts.go Switched to ObserveDurationWithExemplar
- internal/app/{app,new,run}.go Construct DLQInspector + FlagStore once mqtt + redis ready
- internal/config/config.go FeaturesConfig.{DLQReplayEnabled, DLQRingCapacity}
- internal/tesla_pipeline/span_budget_test.go SpanBudgetPerPayload = 25; gate per-ingest span explosion
Infrastructure
- observability/otel-collector/config.yaml New compose service; tail-sampling (errors / slow>1s / 10% prob)
- docker-compose.yml otel-collector service + OTEL_ENDPOINT + DLQ env vars
- helm/teslasync/templates/configmap.yaml ConfigMap entries for new env vars
- helm/teslasync/values.yaml Defaults + docs for FEATURES_DLQ_REPLAY_ENABLED + ring capacity
- prometheus/prometheus.yml + .env Exemplar storage flag + scrape annotations
- Dockerfile.web VITE_OTLP_HTTP_ENDPOINT + service-name + deploy-env build args
CI gates (.github/workflows/)
- si-canonical-gate.yml Blocks new *_mi/_min/_kwh/_kw/_psi field names
- codec-coverage-gate.yml Locks normalize.Pipeline as the single ingest entry
- span-budget-gate.yml Runs span_budget_test.go on every PR
Frontend (page-builder agent output)
- web/src/types/admin-diagnostics.ts DTOs for all 4 new endpoints
- web/src/api/hooks/{useDLQ,useFeatureFlags,useIngestXRay}.ts
- web/src/api/hooks/useDriving.ts useDriveWhyEnded()
- web/src/api/hooks/useTelemetry.ts useVehicleLiveSignals() refactor (back-compat preserved)
- web/src/features/admin/pages/{DLQInspector,FeatureFlags,IngestXRay,LiveSignalInspector}Page.tsx
- web/src/features/admin/components/{dlq-inspector,feature-flags,ingest-xray,live-signal-inspector}/ (12 components)
- web/src/features/driving/components/drive-detail/WhyEndedPanel.tsx
- web/src/App.tsx + Layout.tsx 4 lazy routes under new Diagnostics sidebar group
- web/src/i18n/en.json +154 keys (admin.{dlq,flags,xray,liveSignals} + driveDetail.whyEnded)
Documentation
- .github/ARCHITECTURE.md New "ADR-009 Exceptions" table cataloguing phase-43a, phase-46, and phase-44 admin handlers + rationale
Validation
- go build ./... exit 0
- go vet ./... exit 0
- go test -race -timeout 600s ./... 158/158 packages pass
- internal/arch baseline refreshed (tools/archmetrics/baseline.{json,md})
- cd web && npx tsc --noEmit exit 0
- cd web && lint exit 0
- Live docker stack (teslasync-api + otel-collector + jaeger)
- /system/dlq 200
- /system/flags 200
- /system/ingest-xray/1?window=1h&bucket=1m 200
- /system/dlq/audit 200
- /drives/5/why-ended 200
- Jaeger sees 4 services emitting spans
Per ADR-009 the 4 new internal/api handlers are recorded in the
"ADR-009 Exceptions" table; they follow the same precedent as
queue_status_handler (phase-46) and signals_catalog_handler /
trips_detail_handler (phase-43a) — thin orchestrators over canonical
repos for the admin diagnostics surface.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(observability): Phase-45 batch — operator confidence (8 features)
Hash-chained audit unification + schema drift + slow queries +
per-vehicle cost telemetry + query budget enforcement + disk
forecast + secret rotation + GDPR data-subject export.
Foundation packages (Layer: platform):
- internal/audit hash-chained Recorder.Write / VerifyChain
- internal/schemacheck public-schema fingerprint Compute / Diff
- internal/rotation HMAC-pepper Tracker.Status (warn/critical)
- internal/export/gdpr streaming tar.gz + sha256 MultiWriter
Database repos:
- audit_log_query_repo filtered List / Distinct / VerifyChain reader
- slow_queries_repo pg_stat_statements + 5-min snapshot ticker
- hypertable_metrics_repo hypertable_size + 30d linear regression
- vehicle_cost_repo per-vehicle ingest cost + DLQ join
- gdpr_artifact_repo manifest CRUD (bytes stay on disk)
- query_budget_tracer composite pgx.QueryTracer wraps otelpgx
App services (Layer: app):
- adminobssvc 5 read-only admin observability use cases
- auditviewersvc audit viewer + chain verification
- gdprexportsvc GDPR artifact manifest + download
Handlers (Layer: handler, internal/handler/v1):
- admin_observability_handler.go (5 routes)
- admin_audit_handler.go (4 routes)
- gdpr_export_handler.go (2 routes)
Middleware:
- internal/handler/middleware/query_budget.go enforces per-route
pg query budgets via composite tracer + chi middleware.
Migration 000212:
- 7 additive audit_logs columns + tamper-evident SHA256 chain
- schema_fingerprint (boot-time seed for drift detection)
- secret_rotation_log (HMAC-pepper fingerprints, no plaintext)
- slow_query_snapshot (5m hypertable for historical perf)
- gdpr_export_artifact (manifest only; bytes on disk/s3)
- export_jobs.{storage_kind,storage_path,sha256} additive
Operator validation (docker compose stack):
- 10 endpoints smoke-tested (200/503/404 all match spec)
- /admin/observability/disk-forecast verified against TimescaleDB 2.26
- /admin/observability/slow-queries returns 503 when
pg_stat_statements installed but not in shared_preload_libraries
(SQLSTATE 55000 now mapped to ErrPgStatStatementsUnavailable)
- /admin/observability/secret-rotation returns 503 when
APP_SECRET_PEPPER unset (operator opt-in)
Tests: 161/161 packages pass with -race -timeout 600s.
trace-coverage-audit: ALL FLOWS OK (incl. tesla_signal_ingest_to_db
which gains newCompositeTracer regex coverage for database.go).
Architecture conformance (Phase-47/10 ADR-009):
- TestHandlerV1Thinness PASS (handlers depend only on app/svc)
- TestForbiddenEdges PASS
- TestBaselineHonoured PASS (7 new doc.go files added)
- TestEveryInternalPackageHasDocGoWithLayer PASS
Known follow-ups (deferred, not blockers):
- APP_SECRET_PEPPER / GDPR_EXPORT_DIR / HYPERTABLE_QUOTA_BYTES
still read via os.Getenv; needs config.go + docker-compose.yml +
helm/ alignment in a follow-up PR per repo-wide convention.
- GDPR S3 download path returns 501 NOT_IMPLEMENTED.
- export-worker integration for gdpr.Exporter pending.
- Frontend pages (page-builder) pending.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(observability): Phase-46 SOTA + Phase-49 profiling
Phase-49 / p49-profiling:
- internal/tracing/profiling.go: Pyroscope continuous profiling SDK
init (CPU/heap/goroutine/mutex/block), no-op when disabled
- Wired into API + 3 worker mains via initTracing / main()
- docker-compose: pyroscope service under profiles=[profiling]
- 3 unit tests for default-disabled + custom upload rate
Phase-46 / p46-slo:
- internal/slo: runtime YAML catalog loader (strict subset matching
cmd/slogen) + Prometheus-backed tier tracker (FastBurn 1h+5m
14.4x, SlowBurn 6h+30m 6x); nil PromQuerier surfaces catalog
metadata so SPA can render without Prometheus
- 4 unit tests covering ratio math, tier eval, isolation
Phase-46 / p46-dq-lineage:
- internal/dataquality: composite per-field score (freshness +
max-gap + duplicate ratio) over signal_log via narrow Querier
interface + pgxpool adapter; static pipeline DAG built from
routing.yaml at boot
Phase-46 / p46-synthetic:
- internal/synthetic: outside-in HTTP probe runner with per-probe
timeout isolation + success/failure streak tracking; HTTP probe
honours optional body-substring assertion; admin board endpoint
reports SUBSYSTEM_NOT_CONFIGURED when runner not wired
Wiring:
- 3 new admin endpoints under /api/v1/admin/observability/:
/slo (live SLO + tier eval)
/data-quality (per-field score)
/lineage (static DAG, always-on)
/synthetic (canary board)
- RouterOptions extended with SLOCatalog/SLOTracker/
DataQualityScorer/SyntheticRunner; each pointer optional;
handler degrades to 503 SUBSYSTEM_NOT_CONFIGURED when nil
- App.initObservabilityPhase46 wires all 4 subsystems; failures
log + continue (isolated per subsystem)
- Config: SLOConfig + DataQualityConfig + SyntheticConfig with
PROMETHEUS_BASE_URL / DATA_QUALITY_ENABLED / SYNTHETIC_*
env vars + docker-compose binding
ADR-009 exception:
- 3 new files under internal/api/ recorded in ARCHITECTURE.md
exceptions table with rationale
- tools/archmetrics/baseline.json + .md regenerated
Build + tests green across slo, dataquality, synthetic, api,
app, tracing, config packages.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(integrations,dr): Phase-47 HomeAssistant MQTT discovery + Phase-49 backup verify drill
Phase-47:
- New internal/integrations/homeassistant package implementing the HA MQTT discovery protocol (Publisher, Vehicle, Entity, PrefixFor, PublishVehicle, UnpublishVehicle).
- 14 default entities per vehicle (battery_level, range, charging_state, climate, locks, sentry, locations, etc.) — see catalog.go.
- Wired into internal/app/new.go via initHomeAssistantPublisher with a per-tick goroutine driven by VehicleRepo.
- Uses MQTT client.Underlying() to bypass the teslasync/ prefix because HA requires raw homeassistant/<component>/<node>/<object>/config topics.
- 5 unit tests (round-trip + topic shape + retained flag + unpublish path).
Phase-49 backup-verify:
- New internal/backupverify package: Verifier exercises the latest successful backup_run, decompresses + checksum-verifies via backup.Processor.RestoreBackup, then asserts the operator-configured critical tables hold >=1 row.
- New cmd/backup-verify one-shot binary (cron / CronJob friendly): JSON output to stdout, exit 1 on failure.
- New BackupRunRepo.LatestSuccessful() helper (errors.Is(err, pgx.ErrNoRows)).
- BACKUP_VERIFY_CRITICAL_TABLES + BACKUP_VERIFY_MAX_AGE env knobs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(chaos,ocpp,v2h): Phase-49 chaos harness + Phase-50 OCPP CSMS + V2H decider
Phase-49 chaos (p49-chaos):
- internal/chaos: Toxiproxy admin client + Scenario primitive with deferred toxic cleanup + DefaultScenarios (mqtt_blackhole_30s, redis_latency_500ms_60s, postgres_throttle_1mbps_45s, redis_blackhole_20s).
- cmd/chaos-runner: operator entrypoint, JSON-per-scenario stdout, /healthz recovery probe with 30s deadline, exit 1 on failure.
- docker-compose chaos profile: toxiproxy 2.9.0 on :8474 with mqtt/redis/postgres proxies on :21883/:26379/:25432.
- 6 unit tests (round-trip + 404 idempotent + cleanup on ctx cancel + invalid-scenario rejection + default-suite validation).
Phase-50 OCPP CSMS (p50-ocpp):
- internal/ocpp: protocol layer (Call/CallResult/CallError envelope parse + encode), strongly-typed messages (BootNotification, Heartbeat, StatusNotification, MeterValues, StartTransaction, StopTransaction, Authorize), Dispatcher with action table + monotonic transaction id + injectable clock, MemorySessionStore (goroutine-safe, defensive Snapshot), Server (gorilla/websocket transport, ocpp1.6 subprotocol enforcement, per-connection write mutex, read deadline).
- cmd/ocpp-server: standalone binary so the WebSocket handshake + OCPP envelope don't bloat the main chi router; /healthz + /ocpp/{chargePointId}.
- Dockerfile.ocpp-server: distroless static, same memory-conservative build flags as notification-worker.
- docker-compose ocpp profile: separate container on :9090 with OCPP_HEARTBEAT_INTERVAL + OCPP_READ_DEADLINE env knobs.
- 10 unit tests covering parse rejection paths, full StartTransaction → StopTransaction lifecycle with meter delta, unknown-action CallError, ISO-8601 timestamp parser variants, heartbeat clock injection.
Phase-50 V2H decider (p50-v2h):
- internal/v2h: pure decision engine (no actuator). Takes hourly inputs (ToU rate, solar forecast, house load, vehicle SoC/capacity/reserve) and returns a 24-hour charge / hold / discharge plan.
- SI-only by Phase-48 convention (W, Wh, SoC fraction 0..1, USD/Wh — no kWh/kW/percent footguns).
- Greedy heuristic: profitable-discharge → solar-surplus charge → cheap-rate grid charge → hold. Round-trip efficiency factored into discharge profitability.
- Hard SoC guardrails: MinSoC floor + MaxSoC ceiling NEVER violated even when discharge would be profitable.
- 7 unit tests covering invalid input rejection, solar surplus charging, TOU cheap-window selection, MinSoC floor enforcement, MaxSoC ceiling enforcement, 97-slice rejection, median rate calc.
Verification:
- go build ./... clean
- go vet ./internal/chaos/... ./internal/ocpp/... ./internal/v2h/... ./cmd/chaos-runner/... ./cmd/ocpp-server/... ./cmd/backup-verify/... clean
- go test -race ./internal/chaos/... ./internal/ocpp/... ./internal/v2h/... all green
- docker compose config --quiet OK
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(outbox,validator,loadtest,sbom): CDC outbox + fleet config validator + k6 nightly + SBOM attestation
Phase-52/53 SOTA additions — closes the four remaining gaps after auditing
the existing codebase against the proposed roadmap (6 of the original 10
candidates were already shipped: anomaly, range, charging-curve ML;
audit hash chain; PII redactor; OpenAPI handler; cosign signing).
## Phase-52: CDC outbox (transactional event bus durability)
- migrations/000213_events_outbox.{up,down}.sql — events_outbox table
with 4 indexes (pending-due, stale-lease, status-created, vehicle-created)
- internal/outbox/{doc,store,dispatcher,mqtt_publisher}.go — package
+ dispatcher_test.go (11 unit tests, race-clean)
- Store.Append uses Writer interface (Exec + QueryRow) satisfied by
both pgx.Tx (transactional) and *pgxpool.Pool (fire-and-forget)
- Dispatcher uses FOR UPDATE SKIP LOCKED CTE so multiple pods poll
concurrently without lock contention; exponential backoff with cap;
stale-lease sweep; OTel spans (outbox.poll/publish/sweep)
- MQTTBusPublisher respects MIN(p.timeout, ctx deadline) for paho WaitTimeout
- NOT YET wired into composition root — implementation is complete and
tested but inert pending follow-up to add producer calls + dispatcher boot
## Phase-53: fleet-config-validator
- cmd/fleet-config-validator/main.go — JSON-or-human output, exit 0/1/2,
validates fleet-telemetry-config.json (8 rules) + routing.yaml (5 rules)
- validDests map mirrors internal/tesla/router/types.go closed Destination
set — MUST be kept in sync (comment in source flags this)
- brokerHostPortRE catches the tcp:// scheme-prefix mistake
- records.V must list "mqtt" — the routing key PipelineSubscriber depends on
- Optional --strict flag fails on dest_unused warnings
- main_test.go — 9 table tests covering happy path + each failure rule
## Phase-53: k6 nightly load test
- tests/k6/api_smoke.js — smoke (1VU/30s) + soak (ramping 1→10VU/5m)
scenarios with scenario-tagged per-endpoint p99 thresholds
(tighter under no contention, looser during ramp)
- handleSummary emits stdout + tests/k6/summary.json artifact
- .github/workflows/load-test.yml — nightly 03:00 UTC cron + workflow_dispatch
boots ephemeral docker-compose stack and uploads summary + api.log
## Phase-53: SBOM via syft + cosign attestation
- .github/workflows/release.yml — adds anchore/sbom-action@v0.17.8
(download-syft) + syft generate (CycloneDX + SPDX) + cosign attest
--type cyclonedx between docker-build and existing cosign sign step
- SBOM artifacts uploaded with 90-day retention per image per version
- Release notes updated with verify-attestation snippet
## Verification
- go build ./... — clean
- go vet ./... — clean
- go test -race ./internal/outbox/... — PASS (2.012s, 11 tests)
- go test ./cmd/fleet-config-validator/... — PASS (2.909s, 9 tests)
- go test -race ./internal/events/... — PASS (no regressions)
- fleet-config-validator run against real repo files — all checks passed
- Workflow YAMLs syntax-valid
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(web): Phase-45 Operator Confidence admin UI — 7 pages
Wires the 11 admin observability endpoints shipped in f93d78e to
the React SPA. Each page consumes its hook via TanStack Query and
follows the shared-component architecture (no inline styles, no
raw HTML, i18n with fallbacks, null-safe rendering, 503 →
AlertBanner empty-state).
Pages:
- /admin/schema-drift SchemaDriftPage useSchemaDrift
- /admin/slow-queries SlowQueriesPage useSlowQueries
- /admin/vehicle-cost VehicleCostPage useVehicleCost
- /admin/disk-forecast DiskForecastPage useDiskForecast
- /admin/secret-rotation SecretRotationPage useSecretRotation
- /admin/audit-log AuditLogPage useAuditLog + 3
- /admin/gdpr-exports GDPRExportPage useGDPRExport
Shared layer:
- web/src/types/admin-operator-confidence.ts (snake_case wire types)
- web/src/api/hooks/useOperatorConfidence.ts (10 hooks + keys)
- Routes registered in App.tsx (admin/*, SafeRoute-wrapped)
- Nav entries added in Layout.tsx Diagnostics section
Verification:
cd web && npx tsc --noEmit # exit 0
cd web && npx eslint <new> # exit 0
i18n strings use the inline-fallback t(key, fallback) pattern;
extraction to en.json is deferred to a separate i18n pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Explore hub & switchable sidebar styles
Introduce a discoverable Explore feature hub and make the app sidebar switchable between a new Linear and Notion style. Adds Explore and Helix routes in App.tsx and moves Helix into Integrations with a dedicated HelixPage. Implement LinearSidebar and NotionSidebar components and a useSidebarStyle hook (localStorage-backed) and wire Layout to render the chosen sidebar style (default 'linear'), preserving the legacy nav as a fallback. Add feature catalog and ExplorePage implementations and unit tests to validate catalog integrity and Explore UI behavior. Small UI/label tweaks and a feature flag to hide the "Recently Used" nav surface are included.
* fix(web): unwrap {data: ...} envelope in admin observability hooks
Phase-45/46 admin observability handlers use the platform
httputil.Respond wrapper which encodes responses as {data: T}, but
the hooks in useOperatorConfidence.ts cast directly to T without
unwrapping. Result: query.data was {data: {has_drift, ...}} so the
pages received undefined when reading the inner fields.
Visible symptoms:
- Schema Drift crashed with 'Cannot read properties of undefined
(reading has_drift)'
- Vehicle Cost rendered the No vehicle cost data empty-state even
though the API returned 97k+ rows
- Disk Forecast showed No hypertables despite 14 hypertables present
- Audit Log Verify Chain rendered Chain broken when the API
responded {intact: true, rows_checked: 0}
Added a small fetchEnvelope<T>() helper that unwraps response.data
when present (no-op when absent so it stays safe if a handler is
ever migrated off httputil.Respond) and applied it to all ten
hooks: useSchemaDrift, useSlowQueries, useVehicleCost,
useDiskForecast, useSecretRotation, useAuditLog, useAuditCategories,
useAuditActions, useAuditChainVerify, useGDPRExport.
Older handlers in internal/api/* use writeJSON directly without the
envelope, so unwrapping at the shared request() client would have
broken every other hook — the fix is scoped to this one file.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(db): preload pg_stat_statements so Slow Queries page can render
The timescaledb-ha:pg17 image default shared_preload_libraries is
'timescaledb,pg_textsearch' — pg_stat_statements is NOT preloaded
even though the init script creates the extension. Without preload
the extension's hooks never attach and pg_stat_statements_view
returns 'function not available', so the Phase-45 Slow Queries
admin page (GET /admin/observability/slow-queries) surfaces the
Subsystem unavailable banner with no rows.
Added shared_preload_libraries=timescaledb,pg_stat_statements,
pg_textsearch (kept timescaledb + pg_textsearch first to preserve
image defaults) plus pg_stat_statements.track=all in both
deployment targets per the config-sync rule in copilot-instructions:
- docker-compose.yml postgres command: extended with -c flags
- helm/teslasync/values.yaml: new postgresql.serverConfig map
- helm/teslasync/templates/deployment-postgresql.yaml: args: block
that iterates the map and emits one -c key=value per setting
Existing deployments need a postgres pod restart for the new args
to take effect. The init script's CREATE EXTENSION IF NOT EXISTS
is idempotent so fresh + existing volumes both work.
Verified with helm lint (0 failures) and helm template renders
the expected -c flags on the postgres container.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(web): remove redundant in-sidebar filter input
The Minimal (LinearSidebar) and Compact (NotionSidebar) variants
each rendered a 'Filter nav…' search box at the top of the tree,
right under the global 'Search… ⌘K' command palette trigger.
Two search boxes stacked vertically with overlapping intent
created visual noise and ambiguity about which to use.
Kept only the command palette (cross-app fuzzy search, the
canonical surface) and removed the per-sidebar tree filter input.
The filter state, filterTokens memo, and matchesFilter helper
are left in place dormant — filter stays the empty string so
matchesFilter accepts every item, equivalent to the prior
behavior with an empty input. This keeps the per-section
.filter() calls and pinned-items rendering untouched, and makes
re-enabling the input behind a setting a one-line change later.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(web): replace ring spinner with self-drawing lightning bolt (B2)
The page-load spinner used a rotating ring around the bolt logo. Replace
with a bare bolt that self-draws via stroke-dasharray animation
(boltDraw keyframe, 2s ease-in-out infinite). Removes the rounded tile
backdrop entirely and lets the bolt glow via a layered drop-shadow
filter driven by --theme-primary / --theme-accent so it picks up the
active theme.
- web/src/index.css: add @Keyframes boltDraw + .spinner-bolt-glow
(drop-shadow stack) + .spinner-bolt-draw (animation binding).
- web/src/components/feedback/Spinner.tsx: rewrite as bare bolt SVG with
pathLength=100 (unit-agnostic), useMotionPreference short-circuit
renders a static filled bolt for reduce-motion users. API preserved
(size sm/md/lg, label, className) so all ~30 consumers (PageContainer,
PageLoader, inline call-sites) work unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(web): merge MQTT stream into TelemetryPipelineCard liveness
The "Telemetry pipeline" card on /system-status derived per-vehicle
liveness solely from /polling/status's last_poll_time. Vehicles that
stream via Fleet Telemetry → MQTT (the phase-42+ default) but are not
REST-polled rendered as "offline · last: —" even when actively sending
240+ signals/min — directly contradicting what MQTT Inspector showed
for the same VIN. Two pages, two sources, opposite verdicts.
Card now reads BOTH ingest paths and takes the union timestamp:
- useMQTTStatus() pulls per-VIN { lastReceived, signalsPerSecond } from
/telemetry (same source MQTT Inspector uses)
- liveness() returns { level, source, lastSeenIso } where source ∈
{ stream | poll | none } so the chip can label which path is alive
- The "polling engine disabled" warning is demoted to neutral
"polling engine off (streaming-only)" when MQTT is healthy —
streaming-only is a valid production configuration, not a fault
- New "Fleet Telemetry connected" chip (cyan) when broker is up,
"MQTT broker disconnected" (amber) when it is not
- Added /mqtt-inspector deep-link in the footer for cross-checks
Tests expanded 5 → 9 in TelemetryPipelineCard.test.tsx:
- streaming-only case (the exact Falcon bug reproducer)
- union-timestamp wins case (newest of poll vs stream)
- both-down warnings case
- broker-connected neutral chip case
- Plus mocks @/api/hooks/useTelemetry so the global useQuery mock no
longer leaks polling data into the MQTT hook
Verified: npx tsc --noEmit clean · 9/9 card tests pass · 121/121
src/features/system pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Bypass unit-history for fixed-mile fields
Treat several distance/range fields as always-emitted-in-miles and avoid per-vehicle unit-history lookups for them. Added fixedMileDistanceFields + IsFixedMileDistanceField and a miles conversion path in units.ToSI, and updated normalize.toSI to convert those atomics directly (so they aren’t dropped on ErrNotFound). This prevents silent loss of Odometer/range samples for fresh vehicles and the 1.609× corruption when a user toggles distance units mid-drive. Tests updated/added to pin the behavior (new fixed-mile pipeline tests, adjusted happy-path to use temperature for the unit-history path, and observer/unit tests updated to reflect the change). Files modified: normalize.go, normalize_test.go, observer_test.go, conversions.go, units.go, units_test.go.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>1 parent fd0ff14 commit e155065
254 files changed
Lines changed: 29889 additions & 634 deletions
File tree
- .github
- instructions
- workflows
- cmd
- automation-worker
- backup-verify
- chaos-runner
- export-worker
- fleet-config-validator
- notification-worker
- ocpp-server
- resubscribe
- trace-coverage-audit
- docs/runbooks
- helm/teslasync
- templates
- internal
- api
- app
- adminobssvc
- auditviewersvc
- chargingsvc
- exportsvc
- gdprexportsvc
- notificationsvc
- tripsvc
- vehiclesvc
- audit
- automation
- backupverify
- chaos
- config
- database
- dataquality
- domain/fsm
- export
- gdpr
- flags
- handler
- middleware
- v1
- integrations/homeassistant
- metrics
- mqtt
- notification
- ocpp
- outbox
- platform/telemetry
- rotation
- schemacheck
- signal
- slo
- synthetic
- tesla_pipeline
- tesla
- codec
- normalize
- router
- writers
- unit_history
- units
- tracing
- v2h
- worker
- migrations
- observability
- otel-collector
- toxiproxy
- prometheus
- tests/k6
- tools/archmetrics
- web
- public
- src
- api/hooks
- components
- feedback
- layout
- sidebar
- features
- admin
- components
- dlq-inspector
- feature-flags
- ingest-xray
- live-signal-inspector
- pages
- driving
- components/drive-detail
- pages
- explore
- __tests__
- pages
- settings
- components
- pages
- system/components/status
- __tests__
- hooks
- i18n
- types
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1254 | 1254 | | |
1255 | 1255 | | |
1256 | 1256 | | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
1257 | 1277 | | |
1258 | 1278 | | |
1259 | 1279 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
19 | 26 | | |
20 | 27 | | |
21 | 28 | | |
22 | 29 | | |
23 | 30 | | |
24 | 31 | | |
25 | 32 | | |
26 | | - | |
| 33 | + | |
27 | 34 | | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
28 | 38 | | |
29 | 39 | | |
30 | 40 | | |
31 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
171 | 171 | | |
172 | 172 | | |
173 | 173 | | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
174 | 216 | | |
175 | 217 | | |
176 | 218 | | |
| |||
257 | 299 | | |
258 | 300 | | |
259 | 301 | | |
260 | | - | |
| 302 | + | |
261 | 303 | | |
262 | 304 | | |
| 305 | + | |
263 | 306 | | |
264 | 307 | | |
265 | 308 | | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
266 | 317 | | |
267 | 318 | | |
268 | 319 | | |
| |||
0 commit comments