Skip to content

Fix/charging null safety#29

Open
atulmgupta wants to merge 40 commits intomainfrom
fix/charging-null-safety
Open

Fix/charging null safety#29
atulmgupta wants to merge 40 commits intomainfrom
fix/charging-null-safety

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 March 30, 2026 07:14
- Fix TypeError 'Cannot read properties of undefined (reading toFixed)'
  in Charging.tsx by using typeof checks and nullish coalescing
- Add nil checks to all Latest() API handlers (motor, climate, security,
  media, location) to return 404 instead of 200 with null body
- Prevents frontend crash when API returns null/undefined for nullable fields

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix Energy.tsx: guard .toFixed() calls on session.cost,
  charge_energy_added, and charger breakdown data
- Fix Dashboard.tsx: guard .toFixed() on inside_temp, outside_temp,
  charge_rate, and time_to_full_charge which can be null from API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 5-second context timeout was too short for 12+ sequential DB
operations in the telemetry goroutine. When the first operation
(UpdateState) exceeded 5s, the context was cancelled and ALL
subsequent tracking functions (motor, climate, location, etc.)
silently failed — no data was persisted to snapshot tables.

Also add logging for VIN lookup failures which were previously
completely silent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comprehensive telemetry panels feature (#27) added new columns
and tables to the Go code but no database migration was created.
This caused all API endpoints to return 500 errors:
- column 'hvac_ac_enabled' does not exist
- column 'di_torque_actual_f' does not exist
- column 'homelink_device_count' does not exist
- relation 'location_snapshots' does not exist
- relation 'media_snapshots' does not exist

Adds 34 columns to motor_snapshots, 21 to climate_snapshots,
17 to security_events. Creates 7 new tables: location_snapshots,
media_snapshots, safety_snapshots, user_preference_snapshots,
vehicle_config_snapshots, tire_pressure_snapshots, charging_telemetry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With fleet telemetry batching every 100ms, each batch was spawning a
goroutine with 12+ sequential DB operations. This saturated the 25-conn
pool and caused context deadline exceeded on every write — including
vehicle state updates and health checks.

Now snapshot writes (motor, climate, security, charging, location, etc.)
are throttled to once every 10 seconds per vehicle. This reduces DB
write load by ~50-100x while still providing near-real-time data for
the UI panels. Vehicle state updates are also gated to prevent the
UpdateState call from consuming connections on every batch.

SSE/MQTT broadcasting and signal counting remain unthrottled for
real-time UI updates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Vehicle in standby/charging mode sends signals like ChargeRateMilePerHour,
BatteryLevel, Soc, OutsideTemp, Gear, Odometer — but the tracking
functions only gated on a narrow set of signals, causing all writes
to be skipped during standby.

Expanded gate conditions:
- trackCharging: add BatteryLevel, Soc, ChargeRateMilePerHour, ChargeAmps
- trackClimate: add OutsideTemp
- trackMotor: add Gear, Odometer

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

- Extract detectVehicleState() helper used by both UpdateState and trackStateTransition
- Add ChargeRateMilePerHour and ChargeAmps as charging state indicators
- Vehicle now correctly shows 'charging' when charge rate > 0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Backend returns 'is_read' (JSON) but frontend expected 'read', causing
alerts to always appear unread. Updated all frontend references to use
'is_read'. Also fixed chatbot SQL query using wrong column name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
404 causes React Query to retry infinitely, resulting in the Security
page flickering/skeleton looping. Changed all 5 latest handlers
(security, climate, motor, media, location) to return 200 with null
body when no data exists yet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix division-by-zero in Charging, Drives, BatteryHealth, BatteryCells
- Fix React hooks-in-loop violation in Vehicles.tsx (FleetSummary, BatteryComparison)
- Fix InsightsEngine null safety on trend data access
- Add retry:1 to Layout sidebar queries to prevent backend hammering
- Invalidate vehicle state caches on vehicle delete
- Add nil check to tire_pressure_handler Latest()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Backend:
- analytics_handler: check DB errors instead of ignoring, reduce limit 10K→2K
- alert_handler: check GetByID error after UpdateRule
- search_handler: remove duplicate rows.Close() (defer handles it)
- tire_pressure_handler: nil check on Latest (prev commit)

Telemetry:
- Fix TOCTTOU race in write throttle - use single Lock for check+set
- Log alert rule DB query failures instead of silent swallow

Router:
- Wire missing /api-keys routes (List, Create, Delete, Revoke)

Database:
- geofence_repo: wrap Update in transaction, check errors instead of _, _
- vehicle_state_repo: distinguish ErrNoRows from real DB errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- MotorSnapshot: add 34 fields (di_torque_actual, di_axle_speed, di_state,
  di_stator_temp, di_heatsink_t, di_inverter_t, di_motor_current, di_v_bat
  for all motor positions, plus hvil, brake_pedal_pos, cruise_set_speed, drive_rail)
- ClimateSnapshot: add 21 fields (hvac_ac_enabled, seat heaters (5 positions),
  seat cooling/vent, steering wheel heat, climate keeper, defrost, wiper heat,
  rear display hvac)
- SecurityEvent: add 17 fields (valet mode, speed limit, seat belts, tonneau,
  lights, guest mode mobile access, center display, paired phone keys)
- AlertRule: remove phantom notify_push/notify_mqtt (not in Go model)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add StartCleanup() goroutine that removes stale streaming state entries
  every 10 minutes (3x past stale timeout threshold)
- Cleans both streamingState and lastWriteAt maps to prevent unbounded growth
- Simplify lastWriteMu from RWMutex to Mutex (only uses Lock now)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Position: add fan_status field
- Geofence: add created_at, updated_at fields
- NotificationLog: add scheduled_at, latency_ms fields
- Dashboard: sort vehicle IDs in query key for cache stability

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When fleet telemetry is actively streaming for a vehicle, CurrentState()
now builds the VehicleState from DB tables (positions, climate_snapshots,
security_events, charging_telemetry) instead of making a Tesla API call.

Priority order:
1. Fleet Telemetry (if IsVehicleStreaming) → build from DB snapshots
2. Fleet API (if token valid) → live Tesla API call
3. Cached position (if API suspended/unavailable)

Benefits:
- ~150x faster response (DB query vs Tesla API roundtrip)
- Saves Tesla API quota (no unnecessary calls for streaming vehicles)
- Prevents vehicle wake-up from API polling
- data_source field in response indicates source for debugging

The response now includes 'data_source' field: 'fleet_telemetry',
'fleet_api', or 'cached'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migration 18 adds SQL functions that mirror frontend conversion logic:
- convert_distance(val, 'mi') — km → miles
- convert_speed(val, 'mi') — km/h → mph
- convert_temp(val, 'F') — °C → °F
- convert_efficiency(val, 'mi') — Wh/km → Wh/mi
- convert_pressure(val, 'mi') — bar → psi

When target is NULL, reads user preference from settings table.
Also adds unit_*() helpers that return the label ('mph', '°F', etc).

Grafana usage:
  SELECT convert_temp(inside_temp) FROM climate_snapshots;
  SELECT convert_distance(distance) || ' ' || unit_distance() FROM drives;

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adding fan_status field accidentally dropped the 'export interface Drive {'
line, causing TS1128 build error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Alerts.tsx: remove notify_push/notify_mqtt from createAlertRule call
  (fields removed from AlertRule interface)
- api.ts: make Geofence created_at/updated_at optional (server-generated,
  not sent on create/update)

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

buildStateFromDB now:
- Always checks charging_telemetry (not gated on state=='charging')
- Overrides stale position battery_level with fresh charging telemetry value
- Detects charging from ChargeRateMph/ChargeAmps/ChargeState in telemetry
- Uses charging telemetry range values when available
- Sets state to 'charging' and is_charging=true based on telemetry data

Fixes: Dashboard showing 59% (stale position) instead of 75% (real from
charging_telemetry), showing 'Online' instead of 'Charging'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Vehicle may report IsStreaming=true but only send 1 signal (e.g.
ChargeRateMilePerHour while charging). This left position data 17h stale
and all panels empty because the worker skipped the vehicle.

buildStateFromDB now checks if position is >5min old. If so, falls
through to Fleet API to get complete fresh data. Charging telemetry
enrichment still applies when fresh.

This ensures Dashboard always shows accurate battery%, temps, lock
status etc. regardless of how many telemetry signals the vehicle sends.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
State endpoint fixes:
- Battery: use Soc when BatteryLevel is nil AND position is stale
  (was checking BatteryLevel==0 which missed stale 59% from position)
- Charging detection: add ChargerVoltage>0, DCChargingPower>0,
  ACChargingPower>0 as indicators. Also treat fresh charging_telemetry
  record (<2min) as proof of charging
- Lock status: remains false when no security snapshot exists (vehicle
  doesn't send Locked signal while sleeping/charging) — will be
  populated by Fleet API fallback when needed

EnergyFlow page:
- Add .toFixed(1) to charger_voltage and charge_amps display
  (was showing 112.001999... instead of 112.0)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
extractPosition() required GPS Location to create a position record.
While charging/parked, vehicle doesn't send Location but DOES send
Soc, OutsideTemp, BatteryLevel, ChargerVoltage, etc. All these signals
were silently discarded because the GPS gate returned nil.

Now creates position records when battery, temp, speed, or odometer
signals are present even without GPS.

Also expanded trackCharging gate conditions to include ChargerVoltage,
EstBatteryRange, IdealBatteryRange, EnergyRemaining, PackVoltage,
PackCurrent, ChargeLimitSoc — signals the vehicle actually sends
while charging but were being rejected by the gate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Sidebar vehicle card: use convertDistance + distanceUnit for range
  (was hardcoded 'km')
- Dashboard: fix hardcoded 'mi' on miles_to_arrival — now converts
  to user's preferred unit

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When data is nil, writeJSON wrote nothing (empty body). Frontend
res.json() throws SyntaxError parsing empty string, React Query
treats it as error, retries every 3s → infinite skeleton loop.

Now writes literal 'null' JSON which parses correctly to null.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migration 19 adds to settings table:
- gas_price_per_unit (default 3.50) — price per gallon or liter
- gas_unit (gallon/liter) — fuel volume unit
- gas_efficiency_mpg (default 25) — comparison ICE vehicle MPG

Updated: Go model, settings repo (SELECT/INSERT/Upsert), TS interface,
Settings page UI with gas price, unit selector, and comparison MPG fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds gas_price_history table with effective_from/effective_to periods.
When gas price is changed in Settings, the old period is closed and a
new one is opened. Old charging sessions compare with the gas price
that was active during that period.

Includes gas_price_at(timestamp) SQL function for Grafana:
  SELECT g.price_per_unit FROM gas_price_at(session.start_date) g;

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add automated gas price polling from the U.S. Energy Information
Administration (EIA) API for EV vs ICE cost comparison.

Backend:
- Add GasPriceConfig to config with env vars (GAS_PRICE_ENABLED,
  GAS_PRICE_POLL_INTERVAL, GAS_PRICE_API_KEY)
- Create GasPriceWorker with time.Ticker pattern, runtime stop/resume
  via channels, and persistent state across restarts
- Create gas_price_handler with 5 endpoints: GET /status, POST /poll,
  POST /toggle, PUT /config, GET /history
- Wire worker in main.go with SafeGoLoop pattern
- Add migration 20 for gas_price_poll_state table

Helm:
- Add gasPrice section to values.yaml (enabled, pollInterval, apiKey)
- Add GAS_PRICE_ENABLED/POLL_INTERVAL to configmap.yaml
- Add GAS_PRICE_API_KEY to secret.yaml

Frontend:
- Add GasPriceStatus/GasPriceHistory interfaces and API functions
- Add Gas Price Auto-Poll section to Settings page with toggle,
  interval selector, poll-now button, and price display

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fixed facets[duession] → facets[duoarea][]=NUS (US national average)
- Use context.Background() instead of request context for poll HTTP call
  (request context gets cancelled when handler returns, killing the poll)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Path: /petroleum/pri/gnd/data/ (not /pri/grt/)
Product: EMM_EPMR_PTE_NUS_DPG (US avg regular gasoline, weekly, $/gal)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Verified live: returns US Regular Gasoline .961/gal (2026-03-23).
Previous facet EMM_EPMR_PTE_NUS_DPG was the series ID, not the
product facet code. Need both product and duoarea facets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
atulmgupta and others added 10 commits March 30, 2026 13:49
Vehicle sends different signals in each batch (Soc in one, PackVoltage
in next, ChargerVoltage in another). GetLatest returns only the newest
record which may have just PackVoltage — losing Soc, ChargeRate, Power.

GetLatestMerged looks at last 20 records and fills nil fields from older
records to build a composite view. Now charger_power, charge_rate,
battery_level, time_to_full all show correctly even when the latest
single record is sparse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signals arrive as individual MQTT messages, each creating a separate
batch. The 10s write throttle only writes on the first batch, losing
all signals from subsequent batches (climate, security, motor, tire).

Added signal accumulator per vehicle: signals from all batches within
the throttle window are merged into a single map. When the throttle
timer fires, ALL accumulated signals are written together.

Before: 37 signals → only BatteryLevel written, 5 tables empty
After:  37 signals → all 37 written, all 6 tables populated

Verified locally with Docker compose + mosquitto_pub test suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Test scripts with VIN should not be in the repo. Added to .gitignore.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Added ?? 0 guards to CostComparisonCard props and all metric display
values. Prevents 'Cannot read properties of undefined (toFixed)' crash.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Motor cards were hardcoded with null for all temps/currents. Now maps:
- Front Motor: di_stator_temp_f, di_heatsink_t_f, di_inverter_t_f, di_motor_current_f
- Rear Motor: di_stator_temp, di_heatsink_t_r, di_inverter_t_r, di_motor_current_r
- Rear-Left: di_stator_temp_rel, di_heatsink_t_rel, di_inverter_t_rel, di_motor_current_rel
- Rear-Right: di_stator_temp_rer, di_heatsink_t_rer, di_inverter_t_rer, di_motor_current_rer

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
End date '2026-03-30' was parsed as 00:00:00, excluding sessions
created later that day. Now adds 24h-1s to include the full day.

Fixes Charging page showing 'No charging sessions yet' when sessions
exist on the end date.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- seed_snapshots.sql: realistic data for integration testing
  (motor temps/currents, climate, security, charging, tire pressure)
- .gitignore: exclude package.json (Playwright test dependency)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace all hardcoded unit conversions and raw metric values in 26 dashboard
JSON files with calls to the PostgreSQL conversion functions from migration 18:

- convert_distance() for distance/range/odometer fields
- convert_speed() for speed/speed_max/vehicle_speed fields
- convert_temp() for inside/outside/stator/heatsink/inverter temp fields
- convert_pressure() for tire pressure fields
- convert_efficiency() for Wh/km computed efficiency values

Also:
- Replace inline CASE WHEN unit_length conversions with function calls
- Wrap computed speed (distance/time) and efficiency (Wh/km) expressions
- Update SQL aliases to remove hardcoded unit labels (km/h, °C, PSI, etc.)
- Neutralize Grafana unit fields (celsius, velocitykmh, pressurepsi, km)
  to prevent double-conversion
- Update byName override matchers to match new alias names

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add seed_comprehensive.sql: a large, realistic dataset generator for TeslaSync (Model Y) covering 2020-01-01 to 2026-03-31. The script truncates existing tables, creates monthly position partitions, seeds vehicle, tokens, settings, addresses, geofences and electricity rates, and generates extensive time series data (hourly positions ~54k rows), drives, charging sessions, charging telemetry, vehicle states, daily mileage, motor/climate/security/tire/battery snapshots, alerts and rules, notification channels/preferences/logs/metrics, visited locations, trips, gas price history, software updates, API/audit logs, API keys, command logs, vampire drain events, and other auxiliary tables. Includes setval calls for sequences and comments with docker psql instructions; uses fake tokens/keys for testing. Intended for local/dev testing and data visualization.
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