Two changes, both about showing what is actually known rather than a tidier version of it. The build date asked for a day. A car's build date is often only a year, or a month and a year - the VIN plate is stamped with a month, the papers carry a day, a grey import neither - so a field insisting on all three is answered either with an invented day or with nothing, and both throw away what the owner did know. The field now picks its own precision: a full date, a month and year, or a year, each with the control that suits it. A year is typed rather than picked, because a date picker that makes you walk back to 1998 is worse than four keystrokes. Stored as the ISO prefix - "2015", "2015-03", "2015-03-10" - which is ISO 8601 reduced precision, and printed back at exactly that precision. The three shapes sort and compare as strings in date order, which is why the prefix is stored rather than a date with a precision field beside it. The formatter takes the string apart rather than parsing it: "2015-03" read as a UTC instant and printed in local time hands back February west of Greenwich. Narrowing the precision keeps what is still true, so a day dropped from "2015-03-10" leaves "2015-03". Widening clears the field. That is the awkward half of the control and it is deliberate: there is nothing to widen a year with, and leaving "2015" behind an empty month box would store a date the screen is not showing. The column was free text with no validation at all, which was tolerable while only a date picker could write it and is not now that three shapes are legal. normalizeBuildDate parses rather than pattern-matches, so "2015-13" and "2015-02-31" are refused instead of stored as something no reader can print. The phone needed changing to avoid destroying this. It parsed buildDate with DateTime.tryParse, which returns null for "2015" - so a half-known date would have shown as a dash, and saving the car from the phone would have written "" back over it. It holds both date fields as the string they arrived as now, prints them at their own precision, and hands back anything it cannot set. Its picker still only makes full dates; a precision control there is a separate job. Separately: an empty cell of the service table had three different looks in one row. The dash under Notes was body-coloured, as though it were content; the one under File was 12px, having borrowed the size of the Download button that would otherwise be there; the one under Changed parts was muted at 14px. They are one constant now, muted at the row's own size, which is what Next date and Next km already did for a missing value. The Download link keeps its own styling - it is an action, not a value. Verified in a browser: a stored "2015-03" loads as month precision in a month picker, month to year narrows to "2015", year to day clears, "19x98abc" typed into the year box sanitises to "1998", saving sends buildDate:"1998" and the Information tab then reads "1998" - while a full first-registration date beside it still reads 06-08-2026. All five empty cells across the three columns now compute to the same size, colour and weight, with the filled ones unchanged. go vet and go test ./... pass with a new test over the three valid shapes and six rejects; flutter analyze is clean and 22 tests pass, one new, covering a half-known date in two date formats and the time zone that could shift it; npm run build is clean. Not verified: First registration still demands a full date. The same argument applies to it and the field is now a reusable component, but it was not asked for and is one line away. The web formatter's month-name paths - the DMY and MDY formats, which spell the month out - are covered only by the phone's mirror of the logic, the web app still having no test runner. A car created through the Toyota import bypasses the new validation; it only ever produces full dates, so nothing invalid gets in that way, but it is not guarded. Both apps need redeploying before any of this is visible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DriverVault
A car control & service-tracking system (originally "Car Control Project").
Built incrementally — starting with a car maintenance tracker (modeled on
Car Service.xlsx) and growing toward live integration with the car via a
cellular ESP32 device and third-party services (Toyota Connected, EV chargers).
Architecture
All clients communicate with the database only through the API Server — nothing talks to PocketBase directly.
┌──────────────────┐
Web App (Vue) ─────▶│ │
Phone App (Flutter)▶│ API Server │────▶ PocketBase
Home Assistant ────▶│ (Go, stdlib) │ (10.2.1.10:8027)
Car Agent (ESP32) ─▶│ │
└──────────────────┘
| Component | Stack | Status | Docs |
|---|---|---|---|
| API Server | Go (stdlib) | ✅ built, running, verified | API Server/README.md |
| Database | PocketBase | ✅ running, schema + seed done | — |
| Web App | Vue 3 + Vite + Tailwind v4 | ✅ full feature set (below) | Web App/README.md |
| Phone App | Flutter (Android) | ✅ web parity + biometric login | Phone App/README.md |
| Docker | Compose (multi-container / all-in-one) | ✅ deployment configs | Docker/README.md · Docker-AIO/README.md |
| Car Agent Device | ESP32 + SIM7600 (LILYGO TTGO) | 🚧 firmware in progress | Car Agent Device |
| Home Assistant Plugin | — | ⬜ later | — |
The Web and Phone apps are at feature parity.
Features
- Maintenance tracking — cars, service history (date/odometer + which parts were changed), and a per-car parts catalog, with next-due date/km status from the spreadsheet formulas.
- Technical checks — the mandatory roadworthiness inspections (przegląd techniczny / MOT / TÜV): result, cost, station and the certificate's valid-until, which overrides the car's interval and drives the next-due date.
- Maintenance log — workshop visits and repairs outside the routine schedule: type/status, workshop, parts used, labour + parts cost, invoice, warranty-until.
- Fuel tracking — refills with derived efficiency (average / best / worst consumption, cost per km, price per litre). Consumption is measured between full tanks, so partial fills roll into the next full one.
- Documents — insurance, registration, road tax and the rest, each with a server-computed renewal/expiry state.
- Reminders — date- and/or odometer-triggered, one-off or recurring, plus read-only reminders the server derives from documents and service records.
- Attachments — one optional file (PDF or image) per service record, technical check, maintenance entry, refill, document and part; fetched back through the API Server, never a public URL.
- Accounts — PocketBase-token login, profile + appearance preferences (theme/locale/date format/currency/font), avatar, email verification, data export/import, and an account-deletion state machine.
- Organizations & roles — multi-tenant
user/admin/superadminroles; admins manage users within their own organization, superadmins span all. Any user without an organization can create one and becomes its admin. - Per-user ownership & sharing — each car has an owner and can be shared with other users as read or write; the UI mirrors the server's access checks.
- Integrations — per-user connectors under a superadmin → org-admin → user cascade. Built-in today: Toyota Connected (read-only vehicle data) and the Anker Solix V1 EV charger.
- Cars from the manufacturer's own service — import a car straight off a connected account (MyToyota today), choosing what to pull in, and read everything that service knows about it from a dedicated first tab on the car. Generic over providers: the next manufacturer is one adapter in the API Server.
- EV charging control — for Anker Solix chargers the API Server runs an OCPP 1.6J Central System; in own/proxy mode the charger dials back in and the owner can start/stop and set limits from the Charging screen.
- Translated UI — the interface reads its text from per-language files (English, Polish, Danish today), with English as the fallback for any untranslated string. See TRANSLATIONS.md.
- Phone biometric login & app lock — fingerprint / face sign-in with an app-lock that requires an unlock on relaunch. See the Phone App README.
Auth model
All apps share one auth model: authentication is PocketBase's own.
POST /api/auth/login is proxied to the PocketBase users collection and the
client keeps the token PocketBase minted — the API Server does not issue its
own JWT. Every protected request carries Authorization: <token> (both
Bearer <token> and a raw token are accepted) and the server re-resolves it
against PocketBase on each call, so a role change or a deletion takes effect
immediately. Tokens are stateless, so there is no per-device session list;
changing an account's password rotates its token key and invalidates every token
already issued. Access to cars/records is gated by per-user ownership and shares;
user management requires the admin or superadmin role. Creating an organization
is the one management action open to a plain user — it promotes them to admin of
the organization they just created.
Domain (from Car Service.xlsx)
- Cars — one per vehicle (was: one spreadsheet sheet), with spec fields (engine / transmission / differential oil, brake fluid, coolant, VIN, fuel type, build / first-registration dates, …) and configurable service intervals.
- Service records — date + odometer per service, plus which parts were changed (oil & oil filter, engine air filter, cabin air filter).
- Parts — per-car catalog of part numbers.
Key spreadsheet formulas, reproduced by the API Server on read:
Next Service Date = service date + serviceIntervalDays (default 365; Excel: =A+365)
Next Service Km = service km + serviceIntervalKm (default 15 000; Excel: =B+15000)
Intervals are configurable per car.
Getting started
Bring up the stack in this order — each app's README has the details:
- API Server — configure
.env, runsetup-pocketbase.mjs, start the server. This must be running for either app. - Web App —
npm install && npm run dev(proxies/apito the server). - Phone App —
flutter build apk/flutter runwith--dart-define=API_BASE=http://<server-ip>:8080/api.
Or skip all of that and bring the whole stack up with Docker, which runs the schema setup itself — see Docker (PocketBase + API Server + Web App as three containers) or Docker-AIO (all three in a single image).
Layout
DriverVault/
├── API Server/ # Go gateway to PocketBase (the only DB client)
├── Web App/ # Vue 3 + Vite + Tailwind v4 SPA + Go BFF
├── Phone App/ # Flutter (Android)
├── Car Agent Device/ # ESP32 + SIM7600 firmware (LILYGO TTGO T-SIM7600)
├── Home Assistant Plugin/ # later phase
├── Docker/ # Compose deployment (API Server + Web App)
└── Docker-AIO/ # single all-in-one image