Files
DriverVault/Web App
tajniak81andClaude Opus 5 358ee68f94 Cars: create a car from a manufacturer service, with a per-car data tab
A car can now be imported straight from the account its owner already has
with the manufacturer, and every reading that service exposes shows up on
the car's own tab. MyToyota is the first provider.

API Server — internal/api/vehicleproviders.go adds a generic layer over a
plugin that can enumerate vehicles and read data about them. Adding the
next manufacturer is one vehicleSource adapter plus a line in
vehicleSources(): no new endpoints, no Web App changes.

  GET  /api/vehicle-providers                     providers + connect state
  GET  /api/vehicle-providers/{p}/vehicles        the caller's vehicles
  POST /api/vehicle-providers/{p}/import          create a car from one
  GET  /api/cars/{id}/provider                    live snapshot for the tab
  POST /api/cars/{id}/provider                    link / unlink a car
  POST /api/cars/{id}/provider/sync               re-apply provider data

Two properties shape it. Credentials are always the caller's own, resolved
through the same global -> org -> user cascade as the integration settings,
so a shared car shows provider data only when that vehicle is on the
viewer's account — the owner's credentials are never borrowed. And upstream
shapes are not modelled: these are unofficial APIs, so the layer searches
payloads by key name for the readings worth promoting (odometer, fuel,
battery, range) and flattens the rest to dotted key/value pairs alongside
the raw JSON. A renamed field costs one blank value, not a broken page.

The Toyota gate and its wording now live in toyotaSource, so the older
/api/integrations/toyota/vehicles endpoint and the new ones cannot drift.

Manager.InvokeBatchWith shares one transient plugin instance across a batch
of actions. The tab pulls seven capabilities, and InvokeWith builds a fresh
instance per call — which for a connector that authenticates lazily means a
fresh OAuth login per call. Batching logs in once.

cars gains provider + provider_vehicle_id (schema.go and
setup-pocketbase.mjs both). carPayload deliberately omits them, so an
ordinary car edit can neither reassign the car nor break its link;
carProviderPayload writes the link on its own.

Web App — Dashboard grows an "import from service" button beside "add car",
shown only once an account is connected, opening CarImportModal: pick the
vehicle, choose what to pull (identity / fuel type / dates / odometer, all
on by default), import. ProviderPanel becomes the car's first tab, ahead of
Information, labelled with the service: headline readings, the vehicle
record, one card per capability with its raw response, and an offer to take
the provider's odometer when it is ahead of the stored one. On an unlinked
car the tab instead offers to link it, VIN-matched. Info stays the default
selection — landing on the provider tab would fire a login on every car
page view. Full en/pl/da translations.

Tests cover the payload walking, Toyota normalization, import-selection
defaults, and — through the real handler chain against a stand-in
PocketBase — that every route is registered and that a closed gate is soft
on a listing (200 + a reason the UI can show) but hard on a write (4xx, so
a caller cannot read the reply as a created car).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:13:11 +02:00
..

DriverVault — Web App

Maintenance tracker for your cars: a Vue 3 + Vite + Tailwind CSS v4 SPA served by a small Go backend-for-frontend (BFF). The BFF serves the built SPA and reverse-proxies /api/* to the API Server, so the browser is always same-origin and all data access still flows through the API Server (never PocketBase directly).

Browser ─► Web App BFF (:8090) ──/api/*──► API Server (:8080) ─► PocketBase
                  └── serves embedded Vue SPA

Layout

server/           Go BFF: embeds web/dist, proxies /api -> API_BASE
  main.go
  Run-WebApp.ps1  build frontend, then serve
  .env.example
  dist/           built SPA (generated; embedded at compile time)
web/              Vue 3 + Vite + Tailwind v4 source
  src/
    main.js         app bootstrap
    router.js       /login, / (dashboard), /charging, /cars/:id, /settings, /admin
    api.js          the only place that calls the API Server (base URL resolution)
    auth.js         token/profile state, isAdmin
    prefs.js        theme/locale/date/font preferences -> <html>
    i18n/           en / pl / da translation files + loader
    lib/format.js   date/km formatting + next-service status badges
    lib/attachment.js  upload / fetch / open a record's attached file
    style.css       Tailwind v4 entry (+ dark custom-variant)
    App.vue         layout shell + nav (Charging + Admin links when relevant)
    components/     Modal, AttachmentField, CarFormModal, ServiceFormModal,
                    TechnicalCheckFormModal, MaintenanceFormModal, FuelFormModal,
                    DocumentFormModal, ReminderFormModal, PartFormModal, ShareModal, Logo
    views/          Login, Dashboard, CarDetail, Charging, Settings, AdminUsers

Requirements

  • Node 20.19+ or 22.12+ (Vite 8's floor — Node 18 is end-of-life and will not build) and Go 1.26+ (the go.mod directive). The Docker image builds on node:22-alpine.
  • A running API Server (see ../API Server)

Develop

Two terminals:

# terminal 1 — API Server (see ../API Server/README.md)
cd "../API Server"; .\bin\api-server.exe

# terminal 2 — Vite dev server with hot reload (proxies /api -> :8080)
cd web; npm install; npm run dev      # http://localhost:5173

The dev server proxies /api/* to the API Server (default http://localhost:8080, override with VITE_API_TARGET), so the client uses same-origin relative URLs and avoids CORS. It also listens on all interfaces (host: true) so it's reachable on the LAN (e.g. http://10.2.1.101:5173).

At runtime, users can override the API base URL from the login screen's Server settings (persisted in localStorage as cc_server_url); resolution order is that override → VITE_API_BASE/api.

Build & run (production-style)

./server/Run-WebApp.ps1               # builds frontend, then serves on :8090
# or manually:
cd web; npm run build                 # outputs to ../server/dist
cd ../server; go run .                # http://localhost:8090

Config (server/.env, copy from .env.example):

Variable Purpose Default
WEB_ADDR Listen address :8090
API_BASE API Server base URL http://localhost:8080

Features

  • Dashboard — one card per car: last service, odometer, next-due date/km, and a status badge (OK / due soon ≤30d / overdue) from the Excel formulas. Add a car by hand, or import from service — pick a vehicle off a connected manufacturer account and have its details filled in (the button appears only once an account is connected). Shared cars are labelled and gated by your access level.

  • Car detail — all car spec fields (engine / transmission / differential oil, brake fluid, coolant, VIN, fuel type, …) plus tabbed histories, each with an optional file attachment and add/edit/delete gated by your access level:

    • The connected service (e.g. MyToyota) — the first tab, present for a car linked to a manufacturer account: live readings (odometer, fuel, battery, range, position), the vehicle record, and every section the plugin can fetch with its raw response. Offers the provider's odometer when it is ahead of the stored one. On an unlinked car the tab instead offers to connect it to a vehicle on your account. Read under your account, so a car shared from someone else shows data only if that vehicle is on your account too.
    • Service history — date, km, computed next date/km, and changed-parts flags.
    • Technical checks — roadworthiness inspections; result, cost, station and the certificate's valid-until, which drives the next-due date.
    • Maintenance — workshop visits and repairs (type/status, workshop, parts, labour + parts cost, invoice, warranty-until).
    • Fuel — refills with a summary panel (average / best / worst consumption, cost per km, price per litre), measured between full tanks.
    • Documents — insurance, registration, road tax, … with a renewal badge.
    • Parts — the per-car parts catalog.
    • Reminders — date/odometer, one-off or recurring; server-derived ones are read-only.

    Also share the car with other users (read/write, owner only); edit/delete controls are hidden for read-only shares.

  • Charging — the EV charging screen for connected Anker Solix chargers: live status and, in own/proxy control mode, start/stop and charge-limit controls driven by the API Server's OCPP Central System.

  • Settings — split into tabs: account (name / email verification / password), appearance (theme light/dark/system, locale, date format, currency, font size), profile (avatar, bio), integrations (Toyota, Anker Solix), data export/import, and the account-deletion state machine.

  • Admin/admin user management (list / create / role / reset password / delete), gated by the admin role via a router guard + nav link.

  • Theming — light/dark/system app-wide (Tailwind v4 class strategy); prefs.js toggles .dark on <html> and applies the saved theme/locale/date/font.

Auth & access

Login proxies to the API Server, which relays PocketBase's own token — there is no JWT the server mints and no server-side session list. The token is stored client-side and sent as Authorization on every call. auth.js exposes isAdmin and the current profile; the router guards public / admin routes. Cars are per-user (owned + shared), and the UI mirrors the server's read / write / owner access levels.