Apprise is a Python library that speaks 100+ notification services behind one URL grammar — mailto://, tgram://, ntfy://, discord://. None of that is portable to a server that takes no dependencies, and none of it needs to be: caronc/apprise-api wraps the library in HTTP and is meant to run as a container beside us. So the connector carries no notification protocols of its own. It posts a body to an endpoint the operator runs and lets Apprise fan it out, which is also why adding a service later costs nothing here. Targets are addressed one of two ways and configKey is the switch. Stateful means the URLs live on the Apprise server under a key, narrowed by a tag expression, and recipients are then edited there — no credential for any downstream service is ever held in DriverVault. Stateless means the URLs travel with the request, from a secret config field, which is simpler for one destination and worse for ten. A call that names its own key or urls takes that destination alone rather than merging with the configured one: honouring a caller's URLs while still falling back to the configured key would deliver the message somewhere nobody asked for. baseUrl is Required, which no other connector's address is. Toyota, Anker and Greencell leave everything blank at the global layer because the superadmin → org → user cascade exists to fill it in, and a blank there means "let the user choose". There is no cascade behind this one — a notification gateway is infrastructure the operator runs, not an account a driver owns — so nothing further down can supply the address, and a blank is simply a plugin that cannot work. Better to fail at enable than at the first notification nobody sees. Three limits are choices rather than gaps. /add and /del are not implemented: the Apprise config belongs to the operator, we post to it, and a connector that can delete a notification config has a wider blast radius than one that can only send through it. privacy=1 is forced on /json/urls rather than offered as a parameter, so a target listing reads mailto://user:****@host and downstream tokens stay on the Apprise side of the wire. Attachments are remote URLs the Apprise server fetches; multipart upload is the API's own path for files and not ours. Health follows the rule Greencell set. A reachable server whose config holds nothing to notify is degraded, not down: the half we address works and the missing half is the operator's config. Two cases earn their own line — a config key set against a server running with stateful mode disabled can never resolve, and /status answers 417 rather than 500 when Apprise finds a problem with itself, so that is a parsed answer and not a transport failure. A proxy that strips our Accept header gets the same codes back as plain text, which is read rather than called unreadable; an HTML error page from something that is not Apprise is not, and a test pins the difference. Notifications needed a category of their own, and that is the one change outside the plugin: the constant, the tab order in PluginsCard.vue, and the label in all three panel languages. The cost is now written down in the plugins README beside the Descriptor example, since the previous five categories predate anyone having to add a sixth. The plugin's tests run against an apprise-api stand-in built from that project's views.py — both notify paths, the override rules, 204-as-empty against 424-as-failure, and every health branch. builtin_test.go is the other half: the blank-import list in builtin.go is a silent failure mode, since a connector left out of it compiles, passes its own tests, and never appears in the panel. What is not covered is a live instance; there is no Docker on this machine, so the wire contract comes from reading upstream's source rather than from running it, and a smoke test against a real deployment is still worth doing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.8 KiB
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), the Anker Solix V1 EV charger and the Greencell HabuDen wallbox (read over the owner's own MQTT broker — no Greencell cloud is involved). Apprise joins them as a server-wide connector rather than a per-user one: it hands a message to an Apprise gateway the operator runs, which fans it out to any of the 100+ services Apprise speaks.
- 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