From 1e76c2b7f9c4bfc70faa8162f36462029b752b2f Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:05:46 +0200 Subject: [PATCH] Refresh docs and fix Docker builds for current layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit READMEs: correct the auth model (PocketBase token relay, not JWT/sessions), document the full feature set (technical checks, fuel, maintenance, documents, reminders, attachments, integrations, OCPP charging control), the shipping built-in connectors (toyota, anker-solix), and the current endpoint surface. Docker: build against the current repo layout — Go 1.26, cmd/server entry point, Web App source under web/. Add the missing Web App Dockerfile (Go BFF) and .dockerignore, drop the obsolete AUTH_SECRET, modernise CORS var naming, and standardise on drivervault-* naming. Co-Authored-By: Claude Opus 4.8 --- API Server/Dockerfile | 7 +- API Server/README.md | 88 ++++++++++++++----- API Server/docker-compose.yml | 10 ++- API Server/internal/plugins/README.md | 7 +- .../internal/plugins/builtin/builtin.go | 8 +- Docker AIO/.env.example | 2 - Docker AIO/Dockerfile | 37 ++++---- Docker AIO/docker-compose.yml | 10 +-- Docker/.env.example | 8 +- Docker/docker-compose.yml | 33 ++++--- Phone App/README.md | 2 +- README.md | 88 +++++++++++++------ Web App/.dockerignore | 17 ++++ Web App/Dockerfile | 51 +++++++++++ Web App/README.md | 71 +++++++++------ 15 files changed, 301 insertions(+), 138 deletions(-) create mode 100644 Web App/.dockerignore create mode 100644 Web App/Dockerfile diff --git a/API Server/Dockerfile b/API Server/Dockerfile index e300eab..daf187f 100644 --- a/API Server/Dockerfile +++ b/API Server/Dockerfile @@ -3,7 +3,7 @@ # --- Build stage ------------------------------------------------------------- # Compile a static Go binary. The Vue panel is pre-built into internal/api/dist # and embedded via //go:embed, so no Node toolchain is needed here. -FROM golang:1.22-alpine AS build +FROM golang:1.26-alpine AS build WORKDIR /src @@ -15,8 +15,9 @@ RUN go mod download COPY . . -# CGO_ENABLED=0 produces a static binary that runs on a bare alpine image. -RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api-server . +# CGO_ENABLED=0 produces a static binary that runs on a bare alpine image. The +# entry point is the cmd/server package. +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api-server ./cmd/server # --- Runtime stage ----------------------------------------------------------- FROM alpine:latest diff --git a/API Server/README.md b/API Server/README.md index 9c385af..38c68ef 100644 --- a/API Server/README.md +++ b/API Server/README.md @@ -23,12 +23,17 @@ internal/ │ ├── status.go # upstream health probes │ ├── health.go respond.go panel.go │ ├── cars.go records.go services.go parts.go shares.go me.go -│ └── dist/ # built panel, embedded via go:embed +│ ├── technical.go fuel.go maintenance.go documents.go reminders.go +│ ├── attachments.go # one optional file per record (shared handlers) +│ ├── integrations*.go # per-user Toyota / Anker Solix settings + OCPP control +│ └── dist/ # built panel, embedded via go:embed ├── config/config.go # env + .env load, .env write-back +├── models/models.go # domain types + derived-field computation +├── ocpp/ # OCPP 1.6J Central System (Anker Solix charging control) ├── pb/client.go # PocketBase superuser client (runtime-retargetable) └── plugins/ # plugin system — see plugins/README.md ├── plugin.go manager.go external.go doc.go - └── builtin/ # built-in connectors (none yet) + └── builtin/ # built-in connectors: toyota, ankersolix panel/ # Vue 3 + Tailwind panel source scripts/ # Node/Python maintenance scripts bin/api-server.exe # prebuilt binary the deployment runs @@ -85,14 +90,23 @@ other users `read` or `write` access. Every car/service/part handler is gated by | Collection | Purpose | Key fields | |---|---|---| -| `cars` | one per car | name, make, model, year, registration, vin, `currentKm`, `serviceIntervalDays` (365), `serviceIntervalKm` (15000), `technicalCheckIntervalDays` (365), `oilSpec`, `transmissionOilSpec`, `differentialOilSpec`, `brakeFluidSpec`, `coolantSpec`, `owner` | +| `cars` | one per car | name, make, model, year, registration, `registrationCountry`, vin, `fuelType`, `buildDate`, `firstRegistrationDate`, `currentKm`, `serviceIntervalDays` (365), `serviceIntervalKm` (15000), `technicalCheckIntervalDays` (365), `oilSpec`, `transmissionOilSpec`, `differentialOilSpec`, `brakeFluidSpec`, `coolantSpec`, `owner` | | `service_records` | the service log | car, date, km, changed_oil, changed_engine_air_filter, changed_cabin_air_filter, notes | | `technical_checks` | roadworthiness inspections (przegląd techniczny / MOT / TÜV) | car, date, `result` (passed \| failed), cost, station, `valid_until`, notes | -| `parts` | per-car parts catalog | car, name, part_number, category | +| `parts` | per-car parts catalog | car, name, part_number, category, notes | +| `fuel_entries` | refuelling log (efficiency derived on read) | car, date, km, liters, cost, `full_tank`, `missed_fill`, station, notes | +| `maintenance_entries` | workshop visits & repairs (outside routine service) | car, date, km, type, status, workshop, parts_used, labor_cost, parts_cost, invoice_number, warranty_until, notes | +| `car_documents` | paperwork (insurance, registration, road tax, …) | car, type, title, provider, reference, issue_date, expiry_date, cost, notes | +| `reminders` | date/odometer reminders (some auto-derived) | car, title, type, due_date, due_km, repeat_days, repeat_km, done, done_at, notes | | `car_shares` | grants another user access to a car | car, user, `permission` (read \| write) | | `organizations` | tenants | name (unique) | +| `control_audit` | OCPP control-command audit trail | user, charger, action, result, timestamp | | `users` | login + profile (built-in auth collection) | name, email, avatar, `role` (user \| admin \| superadmin), `organization`, bio, theme, locale, date_format, currency, font_size, deletion_requested_at | +Every record collection except `car_shares` / `organizations` / `control_audit` +carries **one optional file attachment**, served only through the API Server +(`GET /api/{records}/{id}/file`) — never a public PocketBase URL. + **Spreadsheet formulas** (from the original `Car Service.xlsx`), reproduced by the API on read: @@ -118,12 +132,12 @@ GET /api/auth/me GET /api/identity # current user (profile / appearance / avatar / data / account lifecycle) -GET /api/me PATCH /api/me +GET /api/me PATCH /api/me DELETE /api/me POST /api/me/password POST /api/me/avatar GET /api/me/avatar DELETE /api/me/avatar POST /api/me/verify/request GET /api/me/export POST /api/me/import -POST /api/me/delete POST /api/me/delete/cancel DELETE /api/me +POST /api/me/delete POST /api/me/delete/cancel # users + organizations (admin or superadmin; org writes are superadmin-only) GET /api/users POST /api/users @@ -131,31 +145,47 @@ PATCH /api/users/{id} DELETE /api/users/{id} GET /api/orgs POST /api/orgs PATCH /api/orgs/{id} DELETE /api/orgs/{id} -# superadmin -GET /api/admin/pb-config PUT /api/admin/pb-config -POST /api/admin/pb-config/test +# superadmin — connection + plugin management +GET /api/admin/pb-config PUT /api/admin/pb-config POST /api/admin/pb-config/test +GET /api/admin/webapp-config PUT /api/admin/webapp-config POST /api/admin/webapp-config/test GET /api/admin/plugins POST /api/admin/plugins GET /api/admin/plugins/{name} PUT /api/admin/plugins/{name} DELETE /api/admin/plugins/{name} POST /api/admin/plugins/{name}/health +# integrations (per-user plugin settings; superadmin → org admin → user cascade) +GET /api/integrations/toyota PUT /api/integrations/toyota POST /api/integrations/toyota/health +GET /api/integrations/toyota/vehicles +GET /api/integrations/anker-solix PUT /api/integrations/anker-solix POST /api/integrations/anker-solix/health +GET /api/integrations/anker-solix/chargers + +# Anker Solix OCPP charging control (own/proxy mode + a live CSMS session) +GET /api/integrations/anker-solix/chargers/{sn}/control +POST /api/integrations/anker-solix/chargers/{sn}/control/token +DELETE /api/integrations/anker-solix/chargers/{sn}/control/token +POST /api/integrations/anker-solix/chargers/{sn}/{action} +GET /ocpp/{serial} # charger dials in here (OCPP Basic auth, not bearer) + # cars + sharing GET /api/cars POST /api/cars GET /api/cars/{id} PATCH /api/cars/{id} DELETE /api/cars/{id} -GET /api/cars/{id}/service-records -GET /api/cars/{id}/parts -GET /api/cars/{id}/shares POST /api/cars/{id}/shares -DELETE /api/cars/{id}/shares/{userId} +GET /api/cars/{id}/service-records GET /api/cars/{id}/technical-checks +GET /api/cars/{id}/parts GET /api/cars/{id}/fuel-entries GET /api/cars/{id}/fuel-stats +GET /api/cars/{id}/maintenance GET /api/cars/{id}/documents GET /api/cars/{id}/reminders +GET /api/cars/{id}/shares POST /api/cars/{id}/shares DELETE /api/cars/{id}/shares/{userId} -# service records + parts -GET /api/service-records POST /api/service-records -GET /api/service-records/{id} PATCH /api/service-records/{id} DELETE /api/service-records/{id} -GET /api/parts POST /api/parts -GET /api/parts/{id} PATCH /api/parts/{id} DELETE /api/parts/{id} +# per-record collections — each is GET(list) POST / GET PATCH DELETE {id} +/api/service-records /api/technical-checks /api/parts +/api/fuel-entries /api/maintenance /api/car-documents +/api/reminders (+ POST /api/reminders/{id}/complete) + +# attachments — one optional file per record, on every collection that takes one. +# {records} = car-documents | service-records | technical-checks | maintenance | fuel-entries | parts +POST /api/{records}/{id}/file GET /api/{records}/{id}/file DELETE /api/{records}/{id}/file ``` `GET /api/cars` returns the caller's owned cars plus any shared with them, each -annotated with an `access` field. `GET /api/service-records?car={id}` and -`GET /api/parts?car={id}` filter by car. +annotated with an `access` field. The per-record list endpoints also accept a +`?car={id}` filter (e.g. `GET /api/service-records?car={id}`). > **Gotcha:** `updateCar` rewrites **all** car columns from the payload, so a > `PATCH /api/cars/{id}` must send the **full** car object — omitted spec fields @@ -198,8 +228,22 @@ superadmin. Two kinds share one contract: **built-in** (Go, compiled in) and persists to `plugins.json`. See **[`internal/plugins/README.md`](internal/plugins/README.md)** for the full -guide. DriverVault ships no built-in connectors yet; the external kind is the -place to start. +guide. Two built-in connectors ship today — **Toyota Connected** (`toyota`, +read-only MyToyota vehicle data) and the **Anker Solix** V1 EV charger +(`anker-solix`) — and any number of external HTTP plugins can be registered at +runtime with no rebuild. + +### Integrations & charging control + +Beyond the superadmin plugin registry, the built-in connectors are exposed +per-user through `/api/integrations/*` under a **superadmin → org admin → user** +cascade (each layer supplies defaults the next can override). For Anker Solix +chargers the server additionally runs an **OCPP 1.6J Central System** +(`internal/ocpp`): when the owner sets a control mode of own/proxy, the charger +dials back in at `GET /ocpp/{serial}` (authenticated with OCPP Basic auth using a +per-charger control token, not a bearer token) and the owner can start/stop and +set charge limits, with every command rate-limited and written to a +`control_audit` trail. ## Configuration diff --git a/API Server/docker-compose.yml b/API Server/docker-compose.yml index 8c4a0d0..4e1dabf 100644 --- a/API Server/docker-compose.yml +++ b/API Server/docker-compose.yml @@ -3,8 +3,8 @@ services: build: context: . dockerfile: Dockerfile - image: carcontrol-api - container_name: carcontrol-api + image: drivervault-api + container_name: drivervault-api restart: unless-stopped ports: - "${PORT:-8080}:8080" @@ -13,8 +13,10 @@ services: PORT: "8080" # External PocketBase instance (all DB access goes through this server). PB_URL: "${PB_URL:-http://10.2.1.10:8027}" + # Superuser service account. Leave unset and the server still starts — a + # superadmin can configure it from the panel; management endpoints 503 until then. PB_ADMIN_EMAIL: "${PB_ADMIN_EMAIL}" PB_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD}" - CORS_ORIGINS: "${CORS_ORIGINS:-http://localhost:5173}" - AUTH_SECRET: "${AUTH_SECRET}" + # Browser origins allowed by CORS (native apps are exempt). + CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:5173}" AUTH_USERS_COLLECTION: "${AUTH_USERS_COLLECTION:-users}" diff --git a/API Server/internal/plugins/README.md b/API Server/internal/plugins/README.md index 905d313..4393909 100644 --- a/API Server/internal/plugins/README.md +++ b/API Server/internal/plugins/README.md @@ -164,9 +164,10 @@ go build -o bin/api-server.exe ./cmd/server Restart the server. The plugin appears in the panel's **Plugins** card, **disabled** by default. -> DriverVault ships no built-in connectors yet, so `builtin/builtin.go` has an -> empty import block. The **external** kind below needs no rebuild and is the -> easier place to start. +> DriverVault ships two built-in connectors today — `toyota` (Toyota Connected / +> MyToyota, read-only vehicle data) and `anker-solix` (Anker Solix V1 EV charger) +> — both blank-imported from `builtin/builtin.go`. The **external** kind below +> needs no rebuild and is the easier place to start a new one. --- diff --git a/API Server/internal/plugins/builtin/builtin.go b/API Server/internal/plugins/builtin/builtin.go index 3494d39..519fcdb 100644 --- a/API Server/internal/plugins/builtin/builtin.go +++ b/API Server/internal/plugins/builtin/builtin.go @@ -2,13 +2,13 @@ // register them with the plugin registry. Import this package once (from the api // package) to make all built-in connectors available. // -// DriverVault ships no built-in connectors yet — add one under -// internal/plugins/builtin// and blank-import it here, e.g. +// Built-in connectors that ship today: toyota and ankersolix (imported below). +// Add another under internal/plugins/builtin// and blank-import it here, e.g. // // import _ "drivervault/apiserver/internal/plugins/builtin/acme" // -// Until then, plugins are added at runtime as the "external" HTTP kind, which -// needs no rebuild. See ../README.md. +// Plugins can also be added at runtime as the "external" HTTP kind, which needs +// no rebuild. See ../README.md. package builtin import ( diff --git a/Docker AIO/.env.example b/Docker AIO/.env.example index 5a03269..d565e38 100644 --- a/Docker AIO/.env.example +++ b/Docker AIO/.env.example @@ -4,8 +4,6 @@ # PocketBase superuser, also used by the API Server to authenticate. PB_ADMIN_EMAIL=admin@example.com PB_ADMIN_PASSWORD=change-me-long-password -# Signs auth tokens. Generate e.g.: openssl rand -hex 32 -AUTH_SECRET=change-me-to-a-long-random-value # --- Host port mappings (optional; defaults shown) -------------------------- WEB_PORT=80 diff --git a/Docker AIO/Dockerfile b/Docker AIO/Dockerfile index e4e7975..355643f 100644 --- a/Docker AIO/Dockerfile +++ b/Docker AIO/Dockerfile @@ -5,42 +5,43 @@ # The build context MUST be the project root so this file can reach both # "API Server/" and "Web App/". Build it with: # -# docker build -f "Docker AIO/Dockerfile" -t carcontrol-aio . +# docker build -f "Docker AIO/Dockerfile" -t drivervault-aio . # # Run it (all three services start together): # -# docker run -d --name carcontrol -p 80:80 -p 8090:8090 \ +# docker run -d --name drivervault -p 80:80 -p 8090:8090 \ # -e PB_ADMIN_EMAIL=admin@example.com \ # -e PB_ADMIN_PASSWORD=change-me \ -# -e AUTH_SECRET=$(openssl rand -hex 32) \ -# -v carcontrol_pb:/pb/pb_data \ -# carcontrol-aio +# -v drivervault_pb:/pb/pb_data \ +# drivervault-aio # # Then: web app on http://host/ and PocketBase admin on http://host:8090/_/ # --- Stage 1: build the Go API Server --------------------------------------- -FROM golang:1.22-alpine AS api-build +FROM golang:1.26-alpine AS api-build WORKDIR /src COPY ["API Server/go.mod", "./"] COPY ["API Server/go.su[m]", "./"] RUN go mod download -# Only main.go + internal are needed; the panel is already built into -# internal/api/dist and embedded via //go:embed. -COPY ["API Server/main.go", "./"] +# Only cmd/ + internal are needed; the panel is already built into +# internal/api/dist and embedded via //go:embed. Entry point is cmd/server. +COPY ["API Server/cmd", "./cmd"] COPY ["API Server/internal", "./internal"] -RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api-server . +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api-server ./cmd/server # --- Stage 2: build the Vue Web App ----------------------------------------- +# The Vue source lives under "Web App/web/". FROM node:22-alpine AS web-build WORKDIR /app -COPY ["Web App/package.json", "Web App/package-lock.json", "./"] +COPY ["Web App/web/package.json", "Web App/web/package-lock.json", "./"] RUN npm ci -COPY ["Web App/index.html", "Web App/vite.config.js", "./"] -COPY ["Web App/src", "./src"] -COPY ["Web App/public", "./public"] +COPY ["Web App/web/index.html", "Web App/web/vite.config.js", "./"] +COPY ["Web App/web/src", "./src"] +COPY ["Web App/web/public", "./public"] # Empty -> bundle uses same-origin "/api", proxied to the API Server by nginx. ARG VITE_API_BASE -RUN npm run build +# vite.config writes to ../server/dist by default; emit into ./dist here. +RUN npm run build -- --outDir dist --emptyOutDir # --- Stage 3: runtime (all services) ---------------------------------------- FROM alpine:latest @@ -148,11 +149,11 @@ SUPERVISOR # API Server config: everything is local to this container. ENV PORT=8080 \ PB_URL=http://127.0.0.1:8090 \ - CORS_ORIGINS=http://localhost \ + CORS_ALLOW_ORIGINS=http://localhost \ AUTH_USERS_COLLECTION=users -# Required at runtime (no safe defaults): PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, -# AUTH_SECRET. Pass them with `docker run -e ...`. +# Required at runtime (no safe defaults): PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD. +# Pass them with `docker run -e ...`. VOLUME /pb/pb_data # 80 = Web App, 8090 = PocketBase admin, 8080 = API Server + embedded API panel. diff --git a/Docker AIO/docker-compose.yml b/Docker AIO/docker-compose.yml index 989adda..0ea644c 100644 --- a/Docker AIO/docker-compose.yml +++ b/Docker AIO/docker-compose.yml @@ -1,11 +1,11 @@ -name: carcontrol-aio +name: drivervault-aio # Single all-in-one container: PocketBase + API Server + Web App (nginx). # The build context is the project root so the Dockerfile can reach both # "API Server/" and "Web App/". Copy .env.example to .env before starting. services: - carcontrol: + drivervault: build: # Project root (one level up from this compose file). context: .. @@ -15,15 +15,13 @@ services: VITE_API_BASE: "${VITE_API_BASE:-}" # Optional: pin PocketBase; empty fetches the latest release at build. PB_VERSION: "${PB_VERSION:-}" - image: carcontrol-aio - container_name: carcontrol-aio + image: drivervault-aio + container_name: drivervault-aio restart: unless-stopped environment: # Superuser (also used by the API Server to authenticate to PocketBase). PB_ADMIN_EMAIL: "${PB_ADMIN_EMAIL:?set PB_ADMIN_EMAIL in .env}" PB_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD:?set PB_ADMIN_PASSWORD in .env}" - # Signs auth tokens — set to a long random value. - AUTH_SECRET: "${AUTH_SECRET:?set AUTH_SECRET in .env}" ports: - "${WEB_PORT:-80}:80" # Web App - "${PB_PORT:-8090}:8090" # PocketBase admin UI / API diff --git a/Docker/.env.example b/Docker/.env.example index ab5733e..cb574e4 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -5,11 +5,9 @@ PB_ADMIN_EMAIL=admin@example.com PB_ADMIN_PASSWORD=change-me-long-password # --- API Server ------------------------------------------------------------- -# Long random value used to sign auth tokens. Generate e.g.: -# openssl rand -hex 32 -AUTH_SECRET=change-me-to-a-long-random-value # Allowed CORS origin(s) for the web app (match WEB_PORT / your public URL). -CORS_ORIGINS=http://localhost:8081 +# Native mobile apps are not subject to CORS. +CORS_ALLOW_ORIGINS=http://localhost:8081 AUTH_USERS_COLLECTION=users # --- Host port mappings (optional; defaults shown) -------------------------- @@ -18,5 +16,5 @@ API_PORT=8080 WEB_PORT=8081 # --- Web App build ----------------------------------------------------------- -# Leave empty so the browser uses same-origin /api (proxied by nginx). +# Leave empty so the browser uses same-origin /api (proxied by the BFF). VITE_API_BASE= diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml index 9b89851..3489acc 100644 --- a/Docker/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -1,15 +1,15 @@ -name: carcontrol +name: drivervault -# Full Car Control / DriverVault stack: PocketBase (database) + API Server + Web App. -# Traffic flow (browser): Web App (nginx) --/api--> API Server --> PocketBase. +# Full DriverVault stack: PocketBase (database) + API Server + Web App. +# Traffic flow (browser): Web App BFF --/api--> API Server --> PocketBase. # Copy .env.example to .env and fill in the secrets before `docker compose up`. services: pocketbase: build: context: ./pocketbase - image: carcontrol-pocketbase - container_name: carcontrol-pocketbase + image: drivervault-pocketbase + container_name: drivervault-pocketbase restart: unless-stopped environment: # Superuser is created/updated on boot so the API Server can authenticate. @@ -30,8 +30,8 @@ services: api-server: build: context: ../API Server - image: carcontrol-api - container_name: carcontrol-api + image: drivervault-api + container_name: drivervault-api restart: unless-stopped depends_on: pocketbase: @@ -42,21 +42,20 @@ services: PB_URL: "http://pocketbase:8090" PB_ADMIN_EMAIL: "${PB_ADMIN_EMAIL}" PB_ADMIN_PASSWORD: "${PB_ADMIN_PASSWORD}" - AUTH_SECRET: "${AUTH_SECRET:?set AUTH_SECRET in .env}" - # Same-origin requests go through nginx, so CORS is only needed if the - # browser ever calls the API Server directly. Default to the web origin. - CORS_ORIGINS: "${CORS_ORIGINS:-http://localhost:8081}" + # Same-origin requests go through the Web App BFF, so CORS is only needed + # if the browser ever calls the API Server directly. Default to the web origin. + CORS_ALLOW_ORIGINS: "${CORS_ALLOW_ORIGINS:-http://localhost:8081}" AUTH_USERS_COLLECTION: "${AUTH_USERS_COLLECTION:-users}" ports: - # Optional direct access to the API Server; the Web App uses the internal - # network, not this host port. + # Optional direct access to the API Server (and its panel at /); the Web + # App reaches it over the internal network, not this host port. - "${API_PORT:-8080}:8080" web-app: build: context: ../Web App args: - # Empty -> bundle uses same-origin "/api", which nginx proxies below. + # Empty -> bundle uses same-origin "/api", which the BFF proxies below. VITE_API_BASE: "${VITE_API_BASE:-}" image: drivervault-web container_name: drivervault-web @@ -64,10 +63,10 @@ services: depends_on: - api-server environment: - # nginx proxies /api/ to the API Server over the internal network. - API_TARGET: "http://api-server:8080" + # The BFF reverse-proxies /api/* to the API Server over the internal network. + API_BASE: "http://api-server:8080" ports: - - "${WEB_PORT:-8081}:80" + - "${WEB_PORT:-8081}:8090" volumes: pb_data: diff --git a/Phone App/README.md b/Phone App/README.md index fa718fe..d3815cd 100644 --- a/Phone App/README.md +++ b/Phone App/README.md @@ -89,7 +89,7 @@ in Android Keystore–backed secure storage (`flutter_secure_storage`). On success the stored credentials are replayed against the normal login API, so each biometric sign-in mints a fresh session. Stale credentials (e.g. after a password change) auto-disable biometric login. -- **App lock** — the JWT persists, so a valid session normally restores silently. +- **App lock** — the token persists, so a valid session normally restores silently. When biometric login is enabled the app instead starts **locked** (and re-locks when backgrounded) and shows a lock screen requiring a biometric unlock. A **30-second grace period** means quick app-switches don't re-lock; a full app diff --git a/README.md b/README.md index c61600c..aaf35b3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -# Car Control Project +# DriverVault -A car control & service tracking system. Built incrementally — starting with a -**car maintenance tracker** (modeled on `Car Service.xlsx`) and growing toward -live integration with the car via an ESP32 device. +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 @@ -14,7 +15,7 @@ nothing talks to PocketBase directly. Web App (Vue) ─────▶│ │ Phone App (Flutter)▶│ API Server │────▶ PocketBase Home Assistant ────▶│ (Go, stdlib) │ (10.2.1.10:8027) - ESP32 device ──────▶│ │ + Car Agent (ESP32) ─▶│ │ └──────────────────┘ ``` @@ -24,8 +25,9 @@ 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) | +| **Car Agent Device** | ESP32 + SIM7600 (LILYGO TTGO) | 🚧 firmware in progress | [Car Agent Device](Car%20Agent%20Device) | | **Home Assistant Plugin** | — | ⬜ later | — | -| **Car Agent Device** | ESP32 | ⬜ later | — | The Web and Phone apps are at feature parity (the phone omits only data export/import). @@ -33,35 +35,60 @@ export/import). ## Features - **Maintenance tracking** — cars, service history (date/odometer + which parts - were changed), and a per-car parts catalog, with next-due date/km status. -- **Accounts & sessions** — JWT login, per-device active sessions with remote - logout, profile + appearance preferences (theme/locale/date/font), email - verification, and account deletion. -- **Translated UI** — the interface reads its text from per-language files - (English, Polish, Danish today), driven by the language half of the user's - locale, with English as the fallback for any untranslated string. See - [TRANSLATIONS.md](TRANSLATIONS.md) for the format and how to add a language. + 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` / `superadmin` + roles; admins manage users within their own organization, superadmins span all. - **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. -- **Admin** — role-gated user management (create / role / reset password / delete). +- **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. +- **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](TRANSLATIONS.md). - **Phone biometric login & app lock** — fingerprint / face sign-in with an - app-lock that requires an unlock on relaunch (with a short grace period for - quick app-switches). See the Phone App README. + app-lock that requires an unlock on relaunch. See the Phone App README. ## Auth model -All three apps share one auth model: login via `POST /api/auth/login` returns a -JWT issued by the API Server (after verifying against PocketBase `users`), and -every other endpoint requires `Authorization: Bearer `. Each login also -creates a server-side session whose id is embedded in the token, so sessions can -be listed and revoked. Access to cars/records/parts is gated by per-user -ownership and shares; admin endpoints require the admin role. +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: ` (both +`Bearer ` 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. ## Domain (from `Car Service.xlsx`) - **Cars** — one per vehicle (was: one spreadsheet sheet), with spec fields - (engine / transmission / differential oil, brake fluid, coolant, VIN, …) and - configurable service intervals. + (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. @@ -86,13 +113,18 @@ 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). + ## Layout ``` -Car Control Project/ +DriverVault/ ├── API Server/ # Go gateway to PocketBase (the only DB client) -├── Web App/ # Vue 3 + Vite + Tailwind v4 +├── 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 -└── Car Agent Device/ # ESP32, later phase +├── Docker/ # Compose deployment (API Server + Web App) +└── Docker AIO/ # single all-in-one image ``` diff --git a/Web App/.dockerignore b/Web App/.dockerignore new file mode 100644 index 0000000..0d095d8 --- /dev/null +++ b/Web App/.dockerignore @@ -0,0 +1,17 @@ +# Keep the build context small and avoid leaking local artifacts/secrets. +.env +*.log +*.exe +*.exe~ + +# Frontend deps + generated output — rebuilt inside the image. +web/node_modules/ +web/dist/ +server/dist/ + +# VCS / editor / tooling noise +.git/ +.gitignore +.claude/ +.vscode/ +.idea/ diff --git a/Web App/Dockerfile b/Web App/Dockerfile new file mode 100644 index 0000000..8141cfd --- /dev/null +++ b/Web App/Dockerfile @@ -0,0 +1,51 @@ +# syntax=docker/dockerfile:1 +# +# Web App image: the Vue SPA is built and embedded into the Go backend-for- +# frontend (BFF), which serves it and reverse-proxies /api/* to the API Server +# (API_BASE). This mirrors the production Run-WebApp.ps1 flow, so the browser is +# always same-origin and all data access still flows through the API Server. +# +# Build context is the "Web App" directory (see Docker/docker-compose.yml). + +# --- Stage 1: build the Vue SPA --------------------------------------------- +FROM node:22-alpine AS web-build +WORKDIR /web +COPY web/package.json web/package-lock.json ./ +RUN npm ci +COPY web/index.html web/vite.config.js ./ +COPY web/src ./src +COPY web/public ./public +# Empty -> bundle uses same-origin "/api", which the BFF proxies to API_BASE. +ARG VITE_API_BASE="" +ENV VITE_API_BASE=${VITE_API_BASE} +# vite.config writes to ../server/dist by default; emit into ./dist here so the +# next stage can embed it. +RUN npm run build -- --outDir dist --emptyOutDir + +# --- Stage 2: build the Go BFF, embedding the SPA --------------------------- +FROM golang:1.26-alpine AS server-build +WORKDIR /src +COPY server/go.mod ./ +# go.sum is optional (stdlib-only module today); copy it if present. +COPY server/go.su[m] ./ +RUN go mod download +COPY server/ ./ +# Embed the freshly built SPA (main.go uses //go:embed all:dist). +COPY --from=web-build /web/dist ./dist +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/web-bff . + +# --- Runtime stage ---------------------------------------------------------- +FROM alpine:latest +RUN apk add --no-cache ca-certificates tzdata \ + && addgroup -S app && adduser -S -G app app + +WORKDIR /app +COPY --from=server-build /out/web-bff /app/web-bff + +# Config comes from environment variables (see server/.env.example). +ENV WEB_ADDR=:8090 \ + API_BASE=http://api-server:8080 +EXPOSE 8090 + +USER app +ENTRYPOINT ["/app/web-bff"] diff --git a/Web App/README.md b/Web App/README.md index cd3394d..567c002 100644 --- a/Web App/README.md +++ b/Web App/README.md @@ -15,20 +15,25 @@ Browser ─► Web App BFF (:8090) ──/api/*──► API Server (:8080) ─ ``` 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), /cars/:id, /settings, /admin - api.js the only place that calls the API Server (base URL resolution) - auth.js session/profile state, isAdmin - prefs.js theme/locale/date/font preferences -> - lib/format.js date/km formatting + next-service status badges - style.css Tailwind v4 entry (+ dark custom-variant) - App.vue layout shell + nav (Admin link when admin) - components/ Modal, CarFormModal, ServiceFormModal, PartFormModal, ShareModal, Logo - views/ Login, Dashboard, CarDetail, Settings, AdminUsers + 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 -> + 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 @@ -42,7 +47,7 @@ Two terminals: ```powershell # terminal 1 — API Server (see ../API Server/README.md) -cd "../API Server"; ./api-server.exe +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 @@ -78,16 +83,30 @@ Config (`server/.env`, copy from `.env.example`): - **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; shared cars are labelled and gated by your access level. -- **Car detail** — full service history (date, km, computed next date/km, and the - changed-parts flags) plus the per-car parts catalog and all car spec fields - (engine / transmission / differential oil, brake fluid, coolant, VIN, …). - Add/edit/delete service records, parts, and the car; **share** the car with - other users (read/write, owner only). Edit/delete controls are hidden for - read-only shares. -- **Settings** — account (name / email verification / password), appearance - (theme light/dark/system, locale, date format, font size), profile (avatar, - bio), data **export/import**, active sessions with remote logout, and the - account-deletion state machine. +- **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: + - **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` @@ -95,7 +114,9 @@ Config (`server/.env`, copy from `.env.example`): ## Auth & access -Login gets a JWT from the API Server (stored client-side) and creates a server -session. `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. +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.