Refresh docs and fix Docker builds for current layout
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f9b1bcc520
commit
1e76c2b7f9
@@ -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
|
||||
|
||||
+65
-21
@@ -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
|
||||
│ ├── 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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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/<name>/ and blank-import it here, e.g.
|
||||
// Built-in connectors that ship today: toyota and ankersolix (imported below).
|
||||
// Add another under internal/plugins/builtin/<name>/ 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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
+19
-18
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-5
@@ -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=
|
||||
|
||||
+16
-17
@@ -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:
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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 <token>`. 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: <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.
|
||||
|
||||
## 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://<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).
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -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/
|
||||
@@ -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"]
|
||||
+41
-20
@@ -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
|
||||
router.js /login, / (dashboard), /charging, /cars/:id, /settings, /admin
|
||||
api.js the only place that calls the API Server (base URL resolution)
|
||||
auth.js session/profile state, isAdmin
|
||||
auth.js token/profile state, isAdmin
|
||||
prefs.js theme/locale/date/font preferences -> <html>
|
||||
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 (Admin link when admin)
|
||||
components/ Modal, CarFormModal, ServiceFormModal, PartFormModal, ShareModal, Logo
|
||||
views/ Login, Dashboard, CarDetail, Settings, AdminUsers
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user