Docs: refresh every README against the current code

Verified each documented command, path, port and env var against what the
code actually does, and corrected the drift.

Phone App. Was still titled Car Control. The navigation description was
also stale: the app moved to a RootShell bottom nav (Garage, Charging,
Settings, and Users for admins), so the Settings gear and admin action
the dashboard bullet described no longer exist. Adds the Charging screen,
noting its public tab is placeholder data and only the Home tab's OCPP
control is real, and rebuilds the lib/ tree, which had lost i18n.dart,
theme.dart, widgets/ and three screens.

Web App. Node 18+ was wrong. The installed Vite is 8.1.2, whose engines
field is ^20.19.0 || >=22.12.0 - Node 18 is EOL and cannot build this.

API Server. The config table gained OCPP_REQUIRE_TLS, OCPP_PUBLIC_URL,
PB_BOOTSTRAP and DRIVERVAULT_SUPERADMIN_*, plus a note that PLUGINS_FILE
and the panel-written .env resolve against the working directory (a
volume, in Docker).

Plugins. Per-tenant credentials sat under "not yet implemented", but
/api/integrations/* has done exactly that for both built-ins for a while.
Narrowed the roadmap item to the genuinely missing generic version.

New Docker/README.md and Docker AIO/README.md: the root README's
component table linked those directories as documentation but neither had
any. The root README now points at them.

All 8 markdown files pass a relative-link check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-07-21 22:46:21 +02:00
co-authored by Claude Opus 4.8
parent 01d82ebda9
commit a0eb5e4e9d
7 changed files with 244 additions and 21 deletions
+9
View File
@@ -258,10 +258,19 @@ Copy `.env.example` to `.env` and fill in. Summary:
| `WEBAPP_URL` | `http://localhost:8090` | probed by `/api/status` |
| `AUTH_USERS_COLLECTION` | `users` | PocketBase auth collection |
| `PLUGINS_FILE` | `plugins.json` | plugin state store |
| `OCPP_REQUIRE_TLS` | `true` | reject chargers that did not connect over TLS |
| `OCPP_PUBLIC_URL` | — | canonical `ws(s)://` base to point chargers at |
| `PB_BOOTSTRAP` | `true` | run the on-boot schema create/reconcile |
| `DRIVERVAULT_SUPERADMIN_EMAIL` / `_PASSWORD` / `_NAME` | — / — / `Administrator` | first `superadmin`, created on boot when absent |
`PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD`, `PORT` and `CORS_ORIGINS` are
still honoured for older deployments; the modern names win when both are set.
Two paths are resolved **relative to the working directory**: `PLUGINS_FILE` and
the `.env` the panel rewrites when a superadmin retargets PocketBase. In Docker
the working directory is `/data`, a volume, so both survive a container
recreate — see [`Dockerfile`](Dockerfile) and [`../Docker`](../Docker).
`CORS_ALLOW_ORIGINS` only matters for **browser** clients (the web app). Native
mobile apps are not subject to CORS.
+8 -2
View File
@@ -303,8 +303,14 @@ The contract is shaped for these; see [`doc.go`](doc.go):
- **Invocation API** — an endpoint to call `Invoke` from clients, with a normalized
request/response envelope and a provider→internal mapper.
- **Resilience** — retry/backoff, circuit breaker, per-plugin latency/error metrics.
- **Per-tenant credentials** — config keyed by org/user so users connect their own accounts.
- **Audit logging** of plugin access.
- **Per-tenant credentials _for arbitrary plugins_** — the two built-in connectors
already have them, through the hand-written `/api/integrations/toyota` and
`/api/integrations/anker-solix` routes and their **superadmin → org admin →
user** config cascade. What is missing is the generic version: per-org/per-user
config keyed off `ConfigFields`, so a newly registered plugin gets the same
treatment without new endpoints.
- **Audit logging** of plugin access. (Charger *control* commands are already
audited to the `control_audit` collection; this is the wider plugin case.)
Until the invocation API lands, `Invoke` is dormant — plugins are discoverable,
configurable, and health-checked, but not yet callable over HTTP.
+96
View File
@@ -0,0 +1,96 @@
# DriverVault — Docker AIO (all-in-one image)
**PocketBase + API Server + Web App in a single container**, supervised by
`supervisord` with nginx serving the SPA and proxying `/api/` to the API Server
on localhost. One image, one volume set, no compose network — the simplest way
to stand DriverVault up on a single host.
Prefer the three-container stack in [`../Docker`](../Docker) when you want to
scale, upgrade or restart the pieces independently.
```
:80 nginx ─► SPA, and /api/ ─► API Server on 127.0.0.1:8080 ─► PocketBase on :8070
```
| File | Use |
|---|---|
| `Dockerfile` | the all-in-one image (build context must be the **repo root**) |
| `docker-compose.yml` | **builds from source** — for development and local testing |
| `docker-compose.prod.yml` | **pulls the prebuilt image** from the registry |
| `.env.example` / `.env.prod.example` | copy to `.env` for the matching compose file |
## Run it
```bash
cd "Docker AIO"
cp .env.example .env # then edit — PB_ADMIN_* have no safe defaults
docker compose up -d --build
```
Production, from the registry:
```bash
cp .env.prod.example .env # then edit
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
Then: web app on `http://host:8090/`, the API Server's superadmin panel on
`http://host:8080/`, PocketBase admin on `http://host:8070/_/`.
Note the port mapping: inside the container the web app is on **80**, published
as `WEB_PORT` (8090 by default) to line up with the other deployment.
## Building by hand
The build context **must be the repo root** so the Dockerfile can reach both
`API Server/` and `Web App/`:
```bash
docker build -f "Docker AIO/Dockerfile" -t drivervault-aio .
```
Build args: `VITE_API_BASE` (leave empty so the bundle uses same-origin `/api`)
and `PB_VERSION` (pin PocketBase, or leave empty to fetch the latest release at
build time).
## First boot
Identical to the multi-container stack, and idempotent:
1. PocketBase upserts its superuser from `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD`.
2. The API Server waits for PocketBase to report healthy, then creates any
missing collections, reconciles existing ones, and creates the first app
`superadmin` from `DRIVERVAULT_SUPERADMIN_EMAIL` / `_PASSWORD`. Set
`PB_BOOTSTRAP=false` to skip once the database is established.
## Volumes
| Volume | Holds |
|---|---|
| `/pb/pb_data` | the PocketBase SQLite database and uploaded files |
| `/data` | the API Server's `plugins.json`, and the `.env` the panel rewrites when a superadmin retargets the PocketBase connection |
Both default to Docker-managed named volumes; set `PB_DATA` / `API_DATA` to
absolute host paths in the prod file for bind mounts.
## Charger control (OCPP)
Chargers in own/proxy mode dial in to `/ocpp/{serial}` on the **API Server port
(8080)** — not through nginx — authenticating with a per-charger control token
in an OCPP Basic-auth header. Because a plaintext `ws://` would expose that
token, `OCPP_REQUIRE_TLS` defaults to `true`.
This image serves plain HTTP, so charger control needs TLS terminated in front
of it, with `OCPP_PUBLIC_URL` set to the public `wss://` base. Only drop
`OCPP_REQUIRE_TLS` on a trusted network.
## Caveats
- Everything runs as **root** in one container, and a crash of `supervisord`
takes all three services down together. That is the trade for the simplicity.
- Logs from all three processes are interleaved on the container's stdout/stderr
(`docker logs drivervault-aio`).
- `PB_VERSION` empty means the image pulls whatever PocketBase release is latest
**at build time**, so two builds of the same source can differ. Pin it for
reproducibility.
+92
View File
@@ -0,0 +1,92 @@
# DriverVault — Docker (multi-container stack)
Three containers — **PocketBase**, **API Server**, **Web App** — on one compose
network. This is the deployment to use unless you specifically want everything
in a single image; for that see [`../Docker AIO`](../Docker%20AIO).
```
Browser ─► Web App BFF (:8090) ──/api/*──► API Server (:8080) ─► PocketBase (:8070)
```
Only the Web App port is meant to be public. The API Server and the PocketBase
admin UI are published for convenience and, in the prod file, bound to
`127.0.0.1` by default.
| File | Use |
|---|---|
| `docker-compose.yml` | **builds from source** in this repo — for development and local testing |
| `docker-compose.prod.yml` | **pulls prebuilt images** from the registry — for deployment |
| `.env.example` / `.env.prod.example` | copy to `.env` for the matching compose file |
| `pocketbase/` | the PocketBase image (official release binary on alpine) |
## Run it
```bash
cd Docker
cp .env.example .env # then edit — PB_ADMIN_* have no safe defaults
docker compose up -d --build
```
Production, from the registry:
```bash
cp .env.prod.example .env # then edit
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
Then: web app on `http://host:8090/`, the API Server's superadmin panel on
`http://host:8080/`, PocketBase admin on `http://host:8070/_/`.
## First boot
Both steps are idempotent, so restarts and upgrades are safe:
1. **PocketBase** upserts its superuser from `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD`.
This is the only way to create the first superuser — the REST API cannot
bootstrap it. The API Server then authenticates with the same credentials.
2. **The API Server** creates any missing collections and reconciles existing
ones, then creates the first app `superadmin` from
`DRIVERVAULT_SUPERADMIN_EMAIL` / `_PASSWORD` if no such user exists. Set
`PB_BOOTSTRAP=false` to skip once the database is established.
No manual `setup-pocketbase.mjs` step is needed here — the server runs the same
schema reconcile itself.
## Volumes
| Volume | Holds |
|---|---|
| `pb_data` | the PocketBase SQLite database and uploaded files |
| `api_data` | the API Server's `plugins.json`, and the `.env` the panel rewrites when a superadmin retargets the PocketBase connection |
Both default to Docker-managed named volumes. In the prod file, set `PB_DATA` /
`API_DATA` to absolute host paths for bind mounts instead.
> The API Server container runs as an unprivileged user, and a **named** volume
> inherits that ownership from the image. A **bind mount** does not — the host
> directory's ownership wins, so `chown` it to the container's `app` user (or
> `chmod` it writable) before setting `API_DATA` to a host path, otherwise the
> server cannot write `plugins.json`.
## Charger control (OCPP)
Chargers in own/proxy mode dial in to `/ocpp/{serial}` **on the API Server
port**, authenticating with a per-charger control token in an OCPP Basic-auth
header. A plaintext `ws://` would put that token on the wire in the clear, so
`OCPP_REQUIRE_TLS` defaults to `true` and non-TLS connections are rejected.
This stack serves plain HTTP, so to actually use charger control you need to
terminate TLS in a reverse proxy in front of it and set `OCPP_PUBLIC_URL` to the
public `wss://` base (behind a proxy, deriving it from request headers is
unreliable). `OCPP_REQUIRE_TLS=false` is for trusted networks only. You will
also need `API_BIND` set so the proxy can reach the port.
## Notes
- `docker-compose.yml` builds the API Server and Web App from `../API Server`
and `../Web App`, so run it from this directory with the repo checked out.
- The Web App's Vue bundle is built with an empty `VITE_API_BASE`, so the
browser uses same-origin `/api` and the BFF proxies it — no CORS in play.
- `CORS_ALLOW_ORIGINS` therefore only matters if a browser calls the API Server
directly. Native mobile apps are not subject to CORS at all.
+31 -15
View File
@@ -1,6 +1,6 @@
# Car Control — Phone App (Flutter)
# DriverVault — Phone App (Flutter)
A Flutter client for the Car Control maintenance tracker. Talks **only** to the
A Flutter client for the DriverVault maintenance tracker. Talks **only** to the
API Server (same contract and auth as the web app — a PocketBase token relayed by
the server, not a JWT the server mints). At full feature parity with the web app
(data export/import is the only deliberate omission).
@@ -11,12 +11,16 @@ deprecated.
## Features
Once signed in, `RootShell` hosts the app behind a persistent **bottom
navigation bar** — Garage, Charging, Settings, and Users for admins — in an
`IndexedStack`, so each section keeps its state as you switch tabs.
- **Login** — email/password against `/api/auth/login`, password show/hide, and a
collapsible **Server settings** section to override the API base URL on-device.
- **Biometric / face sign-in + app lock** — see the dedicated section below.
- **Dashboard** — car list with next-due status badges (date + km, worst-of),
a "shared" chip on cars owned by someone else, pull-to-refresh, **Add car**
FAB, Settings gear, and an admin action (admins only).
- **Garage (dashboard)** — car list with next-due status badges (date + km,
worst-of), a "shared" chip on cars owned by someone else, pull-to-refresh and
an **Add car** FAB.
- **Car detail** — all spec fields (incl. VIN and transmission / differential /
brake / coolant specs), a **share** sheet (owner only), quick odometer update,
edit car, and delete car (type-to-confirm; cascades). Actions are gated by the
@@ -45,13 +49,20 @@ deprecated.
workshop visit, refill, document and part (PDF or image, up to 10MB). Picked
with `file_picker`, fetched back through the API Server — never a public URL —
and opened with the phone's own viewer via `open_filex`.
- **Charging** — mirrors the web `Charging.vue`, split into two tabs. **Public**
is a discovery map with a demo session and nearby stations: presentational
placeholders, because there is no public-charging API yet (same as the web).
**Home** carries the one real piece — an OCPP control card that drives your
own charger through the Anker Solix control endpoints, once you pick Own/Proxy
CSMS under Settings → Integrations.
- **Settings** — account (name / email verification / password), appearance
(theme + dark mode, **language**, **region**, date format, **currency**, font
size), profile (avatar via `image_picker`, bio), **Security** (biometric
toggle), and the account-deletion state machine. Auth relays PocketBase's own
stateless tokens, so there is no per-device session list to show or revoke.
- **Admin** — user management screen (list / create / role / reset password /
delete), gated by the admin role.
size), profile (avatar via `image_picker`, bio), **integrations** (Toyota,
Anker Solix), **Security** (biometric toggle), and the account-deletion state
machine. Auth relays PocketBase's own stateless tokens, so there is no
per-device session list to show or revoke.
- **Users (admin)** — user management tab (list / create / role / reset password
/ delete), shown only for the admin role.
Sharing/ownership: `Car.access` drives `isOwner` / `canWrite` / `isReadOnly`
getters that gate the UI, mirroring the server's access checks.
@@ -134,15 +145,20 @@ Notes:
```
lib/
├── config.dart # default API base URL (kDefaultApiBase)
├── models.dart # Car (+ access getters), ServiceRecord, Part, AuthUser, UserProfile, Session
├── models.dart # Car (+ access getters), the record types, integrations, profile
├── api.dart # ApiClient — the only thing that calls the API Server
├── auth.dart # AuthService (token persistence, app-lock flag, ChangeNotifier)
├── biometric.dart # BiometricAuth — local_auth + secure storage; biometricAuth singleton
├── app_settings.dart # AppSettings (theme/locale/date/font), persisted; drives MaterialApp
├── i18n.dart # translation lookup — t("key"); en/pl/da with en fallback
├── theme.dart # shared colours/tones (status badges, charging tiles)
├── format.dart # date/km formatting + next-service status (worst-of date/km)
├── main.dart # app root; routes Login / Lock / Dashboard; lifecycle-based re-lock
├── main.dart # app root; routes Login / Lock / RootShell; lifecycle re-lock
├── widgets/
│ └── attachment_field.dart # pick / view / clear a record's attached file
└── screens/
├── login_screen.dart dashboard_screen.dart car_detail_screen.dart
├── car_form_sheet.dart settings_screen.dart admin_users_screen.dart
── lock_screen.dart
├── root_shell.dart # bottom-nav shell: Garage, Charging, Settings, Users
├── login_screen.dart lock_screen.dart dashboard_screen.dart
── car_detail_screen.dart car_form_sheet.dart record_form_sheets.dart
└── charging_screen.dart settings_screen.dart admin_users_screen.dart
```
+5 -3
View File
@@ -25,7 +25,7 @@ nothing talks to PocketBase directly.
| **Database** | PocketBase | ✅ running, schema + seed done | — |
| **Web App** | Vue 3 + Vite + Tailwind v4 | ✅ full feature set (below) | [Web App/README.md](Web%20App/README.md) |
| **Phone App** | Flutter (Android) | ✅ web parity + biometric login | [Phone App/README.md](Phone%20App/README.md) |
| **Docker** | Compose (server / all-in-one) | ✅ deployment configs | [Docker](Docker) · [Docker AIO](Docker%20AIO) |
| **Docker** | Compose (multi-container / all-in-one) | ✅ deployment configs | [Docker/README.md](Docker/README.md) · [Docker AIO/README.md](Docker%20AIO/README.md) |
| **Car Agent Device** | ESP32 + SIM7600 (LILYGO TTGO) | 🚧 firmware in progress | [Car Agent Device](Car%20Agent%20Device) |
| **Home Assistant Plugin** | — | ⬜ later | — |
@@ -113,8 +113,10 @@ Bring up the stack in this order — each app's README has the details:
3. **[Phone App](Phone%20App/README.md)** — `flutter build apk` /
`flutter run` with `--dart-define=API_BASE=http://<server-ip>:8080/api`.
Or bring the whole stack up with **Docker** — see [Docker](Docker) (server +
web) and [Docker AIO](Docker%20AIO) (single all-in-one image).
Or skip all of that and bring the whole stack up with **Docker**, which runs the
schema setup itself — see [Docker](Docker/README.md) (PocketBase + API Server +
Web App as three containers) or [Docker AIO](Docker%20AIO/README.md) (all three
in a single image).
## Layout
+3 -1
View File
@@ -38,7 +38,9 @@ web/ Vue 3 + Vite + Tailwind v4 source
## Requirements
- Node 18+ and Go 1.26+
- **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