Commit Graph
16 Commits
Author SHA1 Message Date
tajniak81andClaude Opus 4.8 19a7d48feb Harden Anker Solix OCPP control (token hashing, step-up, audit, TLS)
Security pass over the OCPP charger-control feature added in a1519f6, since
remotely actuating a physical charger is a real side effect.

Token hygiene:
  - Per-charger control tokens are stored as SHA-256 hashes + a last-4 hint,
    never plaintext. The token is shown once at generation; the status endpoint
    returns only the hint. Added a revoke endpoint that also drops any live
    session using the revoked token.

Step-up + confirmation:
  - Destructive actions (reset, unlock) require confirm:true AND a password
    re-authentication (verified against PocketBase). The Charging UI collects the
    password inline for reset.
  - Per user+charger rate limit (30/min) on control commands.

Transport + provenance:
  - OCPP_REQUIRE_TLS (default on) rejects plaintext ws:// charger connections;
    OCPP_PUBLIC_URL pins the advertised endpoint instead of trusting request
    headers.
  - Proxy-mode upstream URL is validated against a *.anker.com allowlist, so a
    spoofed ocpp-info response can't redirect the proxy.

Durable audit:
  - New control_audit PocketBase collection (added to setup-pocketbase.mjs);
    every control action, token generate/revoke and charger connect is persisted
    best-effort in addition to a structured log line.

Startup:
  - The control-token index is warmed from PocketBase on startup so a charger
    reconnecting after a restart resolves immediately.

Tests:
  - Unit tests for token hashing/eviction/revoke (no plaintext at rest),
    rate limiter, destructive-action classifier, upstream allowlist, TLS
    enforcement, and re-auth guards. A full-stack E2E (control_e2e_test.go)
    drives the real Handler with a stand-in PocketBase and a simulated charge
    point, proving step-up (400/401/200) and audit persistence end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:03:05 +02:00
tajniak81andClaude Opus 4.8 a1519f6e89 Add OCPP control for the Anker Solix EV charger (Own/Proxy CSMS)
The Anker Solix connector was read-only (cloud monitoring only). Add an
OCPP 1.6J control path with a per-user, cascading control mode:

  - off   monitoring only (default, unchanged behavior)
  - own   DriverVault is the charger's Central System (full control)
  - proxy DriverVault relays to Anker's cloud and injects commands

New internal/ocpp subsystem (stdlib-only, hand-rolled RFC 6455): a CSMS
with session management, inbound dispatch, and typed control commands
(RemoteStart/Stop, SetChargingProfile current limit, ChangeAvailability,
Reset, UnlockConnector, TriggerMessage, Get/ChangeConfiguration). Own- and
proxy-mode paths are verified end-to-end against a simulated charge point.

The charger connects to /ocpp/{serial}, authenticated with OCPP Basic auth
(serial + a per-charger control token) resolved to the owning user via an
in-memory token index. Control REST endpoints mirror the monitoring ones and
reuse the same cascade gate plus a live-session check. controlMode is a new
cascade field (global -> org -> user) advertised as a select on the plugin.

Frontend: control-mode select + provisioning card in Settings, and a real
Start/Stop/limit/reset control panel in Charging, gated on the active mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:40:22 +02:00
tajniak81andClaude Opus 4.8 367113f538 Default the Web App to :8090 (BFF) in the API Server
Point the API Server's Web App references at the production BFF port
instead of the Vite dev port (5173): the WEBAPP_URL default probed by
/api/status, and the panel's Web App URL / allowed-origins placeholders.
Update .env.example and the README env table to match. Rebuild the
embedded admin panel dist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:33:26 +02:00
tajniak81andClaude Opus 4.8 52d9614bad Wire the Anker Solix EV charger into the per-user integration cascade
Expose the anker-solix plugin to end users the same way as Toyota: a
global -> org -> user settings cascade so everyone can run it under their
own Anker account, with a superadmin (and org admin) able to impose
settings from above.

API Server: integrations_ankersolix.go mirrors integrations.go with the
Anker fields (email + password resolve as a pair from the highest layer,
country resolves on its own), plus GET/PUT/health/chargers routes under
/api/integrations/anker-solix. Secrets and inherited emails are masked;
the live probe runs server-side under the resolved credentials.

Web App: api.js client calls, a second Integrations card in Settings.vue
(scope switch, enable toggle, email/password/country with locked-field
inheritance notes, save + test), and the en.json strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:46:16 +02:00
tajniak81andClaude Opus 4.8 0793b5ec8e Add a built-in Anker Solix V1 Smart EV Charger plugin
A Go re-implementation of the auth and read-only data flow from
thomluther/anker-solix-api, scoped to the V1 Smart EV Charger (A5191)
and adapted to DriverVault's plugin contract.

Login is a custom ECDH (P-256) + AES-256-CBC password exchange against
passport/login, yielding a ~7-day auth token plus gtoken = md5(user_id)
for subsequent requests; a fresh login covers expiry and 401/403. The
country code routes to the EU or global Anker server.

Exposes read-only capabilities (chargers, charger-status, charge-stats,
charge-orders, ocpp-info, devices, sites, vehicles) with a health check
that reports the bound-charger count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:12:45 +02:00
tajniak81andClaude Opus 4.8 5e435c5f77 Add per-user Toyota integration with a settings cascade
Let each user run the Toyota Connected plugin under their own MyToyota
credentials and enable/disable it for themselves in the Web App, while a
superadmin (and, in an organization, an org admin) can impose settings
from above. Resolution is a cascade — top wins, and a lower level only
fills fields the levels above left blank:

  - org user:      API Server (superadmin) -> org admin -> user
  - org-less user: API Server (superadmin) -> user

The MyToyota email + password resolve together as a pair from the highest
layer that supplies an email; brand resolves on its own; enablement is
strictly per-user, gated by the global master switch and the org gate.

API Server:
  - plugins.Manager gains RawConfig / HealthCheckWith / InvokeWith so the
    cascade can read global config and probe/invoke under a per-caller
    resolved config.
  - internal/api/integrations.go resolves the cascade and serves
    GET/PUT /api/integrations/toyota, POST .../health, GET .../vehicles.
    Secrets and inherited usernames are masked before leaving the server.
  - The toyota builtin's credentials are no longer required at the global
    layer, so the master switch can be enabled without global credentials.
  - setup-pocketbase.mjs adds a pluginSettings JSON field to the users and
    organizations collections (the user and org layers of the cascade).

Web App:
  - api.js gains getToyota/saveToyota/testToyota.
  - Settings grows an Integrations section: an enable toggle, credential
    fields with locked / "inherited from" states, a brand select, an
    org-scope switch for admins, and a live test-connection button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 11:08:58 +02:00
tajniak81andClaude Opus 4.8 5a729abd71 Add a built-in Toyota Connected Europe plugin
Port pytoyoda's authentication and read-only data flow to a Go builtin
plugin behind the existing plugin contract. Implements the three-legged
ForgeRock/OAuth2 login (authenticate callback loop, authorize, token
exchange), silent refresh with full re-auth fallback, and the full
Toyota gateway header set with backoff on 429/5xx.

Exposes read-only capabilities: vehicles, telemetry, location, health,
status, electric, notifications, and service-history. Config takes a
MyToyota email/password and a Toyota/Lexus brand select.

Europe-only and read-only, matching pytoyoda's limitations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 09:45:04 +02:00
tajniak81andClaude Opus 4.8 b6bb6b1df0 Add a language-switch system with per-language files
Introduce a hand-rolled i18n layer across all three UIs, each reading its
text from per-language JSON files (English base + Polish + Danish). Nothing
in the converted screens hardcodes English any more.

- Web App (Vue): src/i18n/{en,pl,da}.json + index.js exposing t()/tSplit(),
  reactive to the signed-in profile locale. Every view, component, form and
  the status labels in lib/format.js go through t().
- API Server panel (Vue): src/i18n/ with its own localStorage-persisted
  language (the panel has no user profile) and a header language picker.
  Chrome, cards, login and API section titles translated; endpoint reference
  descriptions intentionally kept in English. Rebuilt embedded dist.
- Phone App (Flutter): assets/i18n/ + lib/i18n.dart loaded at startup,
  driven by AppSettings.locale. Nav, login, lock, dashboard, the full
  Settings panel (incl. language picker) and format.dart status labels
  translated; remaining detail screens fall back to English.

Language = the language half of the existing BCP-47 locale; the region half
still drives date/number/currency formatting. Missing keys fall back to
English, and plurals use Intl.PluralRules / Intl.plural so Polish gets the
correct one/few/many forms. Settings flags languages without a translation.

Tests updated to assert the localized (Polish) status wording; all pass.
See TRANSLATIONS.md for the format and how to add a language.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:07:48 +02:00
tajniak81andClaude Opus 4.8 f77cbf0e73 Add LPG bi-fuel and hydrogen fuel types
Add petrol_lpg, diesel_lpg and hydrogen to the fuel_type choices, across
the six places that define them: the PocketBase schema, the car form and
detail view in both the Web App and the Phone App, and the two doc
comments that enumerate the values. The Go API needed no change — it
passes fuel_type through as a free string, so PocketBase is the only
validator.

Model the LPG conversions as their own choices rather than a separate
"has LPG" flag: the car runs on either tank, so "petrol + LPG" is what an
owner picks it out as. Order each variant next to its base fuel so the
dropdowns read naturally.

Purely additive — existing rows keep their values and need no migration.
The live PocketBase schema does still need scripts/setup-pocketbase.mjs
re-run before the new choices will save, since its select field allows
only the old four; the script reconciles select values on existing
fields, so re-running migrates it in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 12:53:33 +02:00
tajniak81andClaude Opus 4.8 98acd7d121 Add Web App settings to the API Server panel
Mirror the PocketBase settings card with a superadmin-only Web App
section: it edits the Web App base URL (the address /api/status already
probed) and the CORS allowed origins, applying both at runtime and
persisting them to .env so they survive a restart.

The CORS middleware previously built its allow-list once, when Handler()
was constructed, so an edited origin list would not have taken effect
until a restart — which would have made the new field quietly lie. Move
the lookup into a per-request originAllowed helper reading under the
existing lock. Verified behaviour is unchanged: an allowed origin still
gets the headers plus Vary: Origin, an unknown origin gets none, and
preflight still returns 204.

PUT rejects an empty origin list rather than silently keeping the old
one, since an empty list would lock out every browser client.

The authenticated round trip (loading and saving real settings) is not
verified here — it needs a superadmin login.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 12:09:52 +02:00
tajniak81andClaude Opus 4.8 21c99ad762 Add technical check history
A car's mandatory roadworthiness inspections — przegląd techniczny, MOT,
TÜV — had nowhere to live. Log them behind Service history, which this
mirrors minus the odometer: an inspection falls due on a date whatever
the mileage reads.

The cycle is legal rather than mechanical, and it is not constant — a new
car's first check falls due years later than its third. So the car's
technical_check_interval_days only prefills the next date, and any check
overrides it with the valid-until printed on its own certificate. A
failed check derives no next date at all: reading one off a failure would
stamp a reassuring "valid until" on a car that just flunked.

The next-due date and its status are derived on every read rather than
stored, for the same reason documents are — a stored verdict goes stale
as the date passes. The response carries the same expiry shape documents
use, so the web app reuses the existing badge rather than growing a
second one.

Each check records result, cost, station and notes, and holds the
certificate as its single attachment on the same terms as every other
record.

Needs `node scripts/setup-pocketbase.mjs` to create technical_checks and
add the interval to cars. The reformatting in records.go is gofmt
realigning the car struct around a longer field name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:54:26 +02:00
tajniak81andClaude Opus 4.8 03738f08dc Add currency setting and split locale into language + region
Costs rendered as bare numbers because the project had no currency to
render them with. Add one to the profile, beside the existing appearance
preferences:

- users.currency in PocketBase, exposed via /api/me, validated against
  the same 28 codes in the schema, the API and the web app.
- Settings offers the European currencies plus the non-European ones the
  panel already had. Labels come from Intl.DisplayNames rather than a
  hand-kept table, so the lists read in the user's own language and sort
  by what is actually on screen.

The single "Language & region" picker becomes two controls over the one
stored BCP-47 tag, covering European languages and countries, so the two
can be mixed (English in Poland). The API now enforces the
language-REGION shape: the web app feeds the tag straight to Intl, which
throws on a malformed one rather than falling back.

formatKm and the km-count labels passed no locale, so kilometres
followed the browser while the dates and costs beside them followed the
saved preference. They now share one helper that uses the preference.

Rename the "Maintenance log" tab to "Maintenance", the name the
reminder-type list already used.

Amounts are display-only: nothing is converted, so changing currency
reinterprets existing records rather than recalculating them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:32:40 +02:00
tajniak81andClaude Opus 4.8 07192f1238 Add attachments to service, maintenance, fuel and parts records
Documents already carried a single optional file: upload, download, detach,
access-checked on every request and proxied through this server, so PocketBase's
files are never public URLs. Service records, workshop visits, refills and
catalog parts all want the same thing — a receipt, an invoice, a photo of the
box — so extend it to them.

Rather than copy the document handlers four more times, lift them into one
shared layer. Every attachable collection has a car relation and a file field,
which is what lets a single set of handlers authorize and serve all of them. An
upload finishes by delegating to the collection's own GET handler, so the
response carries the full record — derived fields and all — exactly as a re-read
would. The web side gets the same treatment: one picker component and one
upload-after-save helper behind all five forms. Net effect is five features for
about the cost of the one that was already there.

Alongside:
- parts gain a notes field
- Reminders moves behind Parts catalog in the car detail tabs
- the changed-part labels spell out in full ("Oil & Oil filter" rather than
  "Oil & filter"), and the form and table now agree

The PocketBase schema must be migrated before the new attachments work:
scripts/setup-pocketbase.mjs adds the file fields and parts.notes. It is
additive and safe to re-run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 10:41:01 +02:00
tajniak81andClaude Opus 4.8 e64c89a564 Add fuel, maintenance, document and reminder tracking
Four features layered onto cars, each following the existing parts/services
pattern: a Go handler gated on requireCarAccess, snake_case PocketBase
mappers, a Vue form modal, and a tab on CarDetail (now driven by an array
rather than repeated markup).

Fuel: refills logged with odometer, litres and cost. Consumption is derived
on read from the whole history rather than stored, so correcting an old fill
re-derives every window it touches with no rows to migrate. Efficiency uses
the full-tank method — two consecutive full tanks are the same known level,
so the fuel burned between them is exactly what was poured in. Partial fills
roll into the window that closes them; a missed-fill flag leaves that window
uncomputed rather than reporting an implausibly good figure. Averages in the
stats rollup are distance-weighted, so a long motorway run counts for more
than a trip across town — which is what actually happened to the fuel.

Maintenance: workshop visits and repairs, deliberately separate from
service_records. That collection is the routine interval schedule and drives
next-service-due; this one is unplanned garage work with a workshop, an
invoice and a labour bill, and no bearing on the interval.

Documents: insurance, pollution certificates and registration papers. The
renewal date is the point of the record, so expiry is assessed live on every
read instead of stored and left to go stale. Scans are proxied through the
API — PocketBase's collections have no public read rule, so an attachment is
never a public URL and car access is re-checked per fetch.

Reminders: fire on a date, an odometer reading, or both (whichever comes
first). Stored reminders sit alongside read-only ones derived from document
expiry and next-service-due, so a renewal date is never typed twice and can
never drift from the document it came from. Derived ids are namespaced
"auto:" and every write endpoint rejects them.

A refill or a completed visit also writes the car's odometer forward, since
it is the freshest reading there is — never backwards, so backfilling old
history can't rewind the car.

Adds fuel_entries, maintenance_entries, car_documents and reminders to the
idempotent schema script, plus a file-field builder for attachments.

Verified end-to-end against a live PocketBase with a throwaway account: 39
checks covering the efficiency maths, expiry states, the derived reminders,
the upload/download round-trip, and that a stranger can reach none of it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 09:50:11 +02:00
tajniak81andClaude Opus 4.8 ae6ed4ac1e Rebuild API Server on the PilotVault structure
Mirror PilotVault's API Server layout and add the superadmin console,
plugin system, runtime PocketBase settings, and user/organization
management. The car domain (cars, service records, parts, sharing) is
carried over unchanged apart from the auth switch.

Layout: main.go -> cmd/server/main.go; module carcontrol/api ->
drivervault/apiserver. internal/api is split by concern (auth, users,
orgs, settings, plugins, status, health, respond).

Auth: replace the server-minted HS256 JWT and the sessions collection
with a PocketBase token proxy. /api/auth/login relays PocketBase's
{token, record}, and every protected request re-resolves that token
against PocketBase, so a role change or deletion takes effect at once
instead of waiting out a token. AUTH_SECRET is obsolete and internal/auth
is gone. Per-device session listing/revocation goes with it: PocketBase
tokens are stateless. Changing a password rotates the user's token key,
which invalidates every token already issued.

Roles: add superadmin alongside user/admin, plus an organizations
collection and users.organization. Admins are scoped to their own
organization; superadmins span all of them. Guards prevent changing your
own role, deleting your own account, an admin touching a superadmin, and
deleting an organization that still has members.

Plugins: new internal/plugins package with one contract over two kinds --
builtin (compiled in) and external (any HTTP service, registered at
runtime with no rebuild). State persists to plugins.json; secrets are
masked on read and preserved when saved back at the mask.

PocketBase settings: /api/admin/pb-config applies a new connection at
runtime and persists it to .env. It deliberately does not require a
working service account, so a wrong or unreachable connection can still
be fixed from the panel.

Panel: rebuilt as the superadmin console -- login gate, status, users,
organizations, PocketBase, plugins, and the endpoint reference.

Clients: update the Web App and Phone App for the PocketBase token shape,
the move of user management to /api/users ({users}/{user} envelopes, with
password resets folded into PATCH), and the removal of sessions. Both now
mirror the server's real guards rather than the old last-admin rule, and
parse PocketBase's field-level error shape.

Config: modern POCKETBASE_*/API_ADDR names with legacy PB_*/PORT
fallbacks, so existing .env files keep working. Also fixes /api/status
probing the Web App on 8090 instead of DriverVault's 5173.

Run scripts/setup-pocketbase.mjs to add the organizations collection and
grow users.role; every client must log in once more.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 22:29:45 +02:00
tajniak81 ba3f227361 Initial commit 2026-07-06 08:50:52 +02:00