diff --git a/API Server/README.md b/API Server/README.md index 6953a1b..c3d9c64 100644 --- a/API Server/README.md +++ b/API Server/README.md @@ -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. diff --git a/API Server/internal/plugins/README.md b/API Server/internal/plugins/README.md index 4393909..a220906 100644 --- a/API Server/internal/plugins/README.md +++ b/API Server/internal/plugins/README.md @@ -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. diff --git a/Docker AIO/README.md b/Docker AIO/README.md new file mode 100644 index 0000000..cb8abbf --- /dev/null +++ b/Docker AIO/README.md @@ -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. diff --git a/Docker/README.md b/Docker/README.md new file mode 100644 index 0000000..8d16983 --- /dev/null +++ b/Docker/README.md @@ -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. diff --git a/Phone App/README.md b/Phone App/README.md index 4b1a26f..4686982 100644 --- a/Phone App/README.md +++ b/Phone App/README.md @@ -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 ``` diff --git a/README.md b/README.md index aaf35b3..32ce347 100644 --- a/README.md +++ b/README.md @@ -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://: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 diff --git a/Web App/README.md b/Web App/README.md index 567c002..c7f3b5e 100644 --- a/Web App/README.md +++ b/Web App/README.md @@ -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