Add production Docker compose + on-startup PocketBase bootstrap
Introduce registry-pull production stacks (docker-compose.prod.yml) for both the multi-container Docker setup and the all-in-one Docker AIO image, with everything an operator needs (superuser, super-admin, ports, volumes) driven from .env. The API Server now bootstraps PocketBase on startup: a new internal/bootstrap package (Go port of setup-pocketbase.mjs) creates missing collections, reconciles existing ones, and creates the DriverVault super-admin from DRIVERVAULT_SUPERADMIN_* when absent. Idempotent and gated by PB_BOOTSTRAP. The PocketBase superuser is still upserted by the PocketBase container, since the REST API cannot bootstrap the first superuser. Move PocketBase to port 8070 (internal + published) and the web app to 8090 across both stacks, with matching CORS defaults. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e76c2b7f9
commit
30c9cdebe9
17 files changed
+1042
-23
No files matched your search
@@ -0,0 +1,218 @@
|
||||
package bootstrap
|
||||
|
||||
// This file is the Go mirror of the DESIRED schema, create order, reconcile
|
||||
// order and INDEXES in scripts/setup-pocketbase.mjs. Keep the two in sync: edit
|
||||
// here and re-run the server (bootstrap applies on startup), or run the script.
|
||||
//
|
||||
// Access rules are left null on every collection on purpose — every client goes
|
||||
// through the API Server, which authenticates as a superuser, so the database is
|
||||
// never exposed directly (including attachments, proxied by the API).
|
||||
|
||||
// collectionsSchema is the desired field set per collection.
|
||||
var collectionsSchema = map[string][]fieldDef{
|
||||
"cars": {
|
||||
fText("name", true),
|
||||
fText("make", false),
|
||||
fText("model", false),
|
||||
fNumber("year"),
|
||||
fText("registration", false),
|
||||
fText("registration_country", false),
|
||||
fText("vin", false),
|
||||
fNumber("service_interval_days"),
|
||||
fNumber("service_interval_km"),
|
||||
// Roadworthiness inspection cycle. Only prefills a check's next-due date.
|
||||
fNumber("technical_check_interval_days"),
|
||||
fText("oil_spec", false),
|
||||
fText("transmission_oil_spec", false),
|
||||
fText("differential_oil_spec", false),
|
||||
fText("brake_fluid_spec", false),
|
||||
fText("coolant_spec", false),
|
||||
fNumber("current_km"),
|
||||
// Bi-fuel LPG conversions are their own choice: the car runs on either tank.
|
||||
fSelect("fuel_type", []string{
|
||||
"petrol", "petrol_lpg", "diesel", "diesel_lpg", "hybrid", "electric", "hydrogen",
|
||||
}, false),
|
||||
fText("build_date", false), // ISO YYYY-MM-DD (date-only)
|
||||
fText("first_registration_date", false), // ISO YYYY-MM-DD
|
||||
// Owner of this car. Non-cascading: deleting a user must not wipe their cars.
|
||||
fRelation("owner", "users", false, false),
|
||||
},
|
||||
"service_records": {
|
||||
fRelation("car", "cars", true, true),
|
||||
fDate("date", true),
|
||||
fNumber("km"),
|
||||
fBool("changed_oil"),
|
||||
fBool("changed_engine_air_filter"),
|
||||
fBool("changed_cabin_air_filter"),
|
||||
fText("notes", false),
|
||||
attachment(), // the workshop receipt / stamped service-book page
|
||||
},
|
||||
// Mandatory roadworthiness inspections (przegląd techniczny / MOT / TÜV).
|
||||
"technical_checks": {
|
||||
fRelation("car", "cars", true, true),
|
||||
fDate("date", true),
|
||||
fSelect("result", []string{"passed", "failed"}, false),
|
||||
fNumber("cost"),
|
||||
fText("station", false),
|
||||
fDate("valid_until", false),
|
||||
fText("notes", false),
|
||||
attachment(), // the certificate
|
||||
},
|
||||
"parts": {
|
||||
fRelation("car", "cars", true, true),
|
||||
fText("name", true),
|
||||
fText("part_number", false),
|
||||
fText("category", false),
|
||||
fText("notes", false),
|
||||
attachment(), // a photo of the box, or the part's spec sheet
|
||||
},
|
||||
// Fuel refills. Consumption is derived on read, not stored.
|
||||
"fuel_entries": {
|
||||
fRelation("car", "cars", true, true),
|
||||
fDate("date", true),
|
||||
fNumber("km"), // odometer at the pump
|
||||
fNumber("liters"),
|
||||
fNumber("cost"),
|
||||
fBool("full_tank"),
|
||||
fBool("missed_fill"),
|
||||
fText("station", false),
|
||||
fText("notes", false),
|
||||
attachment(), // the pump receipt
|
||||
},
|
||||
// Workshop visits and repairs (unplanned/one-off garage work with a labour bill).
|
||||
"maintenance_entries": {
|
||||
fRelation("car", "cars", true, true),
|
||||
fDate("date", true),
|
||||
fNumber("km"),
|
||||
fSelect("type", []string{"repair", "inspection", "bodywork", "tyres", "diagnostics", "recall", "warranty", "other"}, false),
|
||||
fSelect("status", []string{"scheduled", "in_progress", "completed"}, false),
|
||||
fText("workshop", false),
|
||||
fText("location", false),
|
||||
fText("description", false),
|
||||
fText("parts_used", false),
|
||||
fNumber("labor_cost"),
|
||||
fNumber("parts_cost"),
|
||||
fText("invoice_number", false),
|
||||
fDate("warranty_until", false),
|
||||
fText("notes", false),
|
||||
attachment(), // the workshop's invoice
|
||||
},
|
||||
// Insurance, pollution certificates, registration papers … expiry drives reminders.
|
||||
"car_documents": {
|
||||
fRelation("car", "cars", true, true),
|
||||
fSelect("type", []string{"insurance", "pollution", "registration", "inspection", "roadTax", "warranty", "other"}, false),
|
||||
fText("title", true),
|
||||
fText("provider", false),
|
||||
fText("reference", false),
|
||||
fDate("issue_date", false),
|
||||
fDate("expiry_date", false),
|
||||
fNumber("cost"),
|
||||
fText("notes", false),
|
||||
attachment(), // the scan/PDF of the paperwork
|
||||
},
|
||||
// User-set reminders (derived document/service ones are computed on read).
|
||||
"reminders": {
|
||||
fRelation("car", "cars", true, true),
|
||||
fText("title", true),
|
||||
fSelect("type", []string{"maintenance", "document", "service", "inspection", "other"}, false),
|
||||
fDate("due_date", false),
|
||||
fNumber("due_km"),
|
||||
fNumber("repeat_days"),
|
||||
fNumber("repeat_km"),
|
||||
fBool("done"),
|
||||
fDate("done_at", false),
|
||||
fText("notes", false),
|
||||
},
|
||||
// Per-car sharing grants. Cascades on both relations.
|
||||
"car_shares": {
|
||||
fRelation("car", "cars", true, true),
|
||||
fRelation("user", "users", true, true),
|
||||
fSelect("permission", []string{"read", "write"}, true),
|
||||
fAutodate("created", true, false),
|
||||
},
|
||||
// Append-only audit trail for OCPP charger control. Actor/org stored as plain
|
||||
// text ids (not relations) so the trail survives user or org deletion.
|
||||
"control_audit": {
|
||||
fText("user_id", false),
|
||||
fText("org_id", false),
|
||||
fText("serial", false),
|
||||
fText("action", true),
|
||||
fText("result", false),
|
||||
fJSON("params", 10000),
|
||||
fAutodate("created", true, false),
|
||||
},
|
||||
// Tenants that users belong to.
|
||||
"organizations": {
|
||||
fText("name", true),
|
||||
fAutodate("created", true, false),
|
||||
// Per-organization plugin/integration config (middle layer of the cascade).
|
||||
fJSON("pluginSettings", 100000),
|
||||
},
|
||||
// Custom fields layered onto the built-in "users" auth collection.
|
||||
"users": {
|
||||
fText("bio", false),
|
||||
fSelect("theme", []string{"light", "dark", "system"}, false),
|
||||
fText("locale", false),
|
||||
fSelect("date_format", []string{"YMD", "DMY_NUM", "DMY", "MDY"}, false),
|
||||
fSelect("currency", []string{
|
||||
"EUR", "GBP", "CHF", "PLN", "CZK", "HUF", "RON", "BGN", "DKK", "SEK", "NOK",
|
||||
"ISK", "ALL", "AMD", "AZN", "BAM", "BYN", "GEL", "MDL", "MKD", "RSD", "RUB",
|
||||
"TRY", "UAH", "USD", "CAD", "AUD", "JPY",
|
||||
}, false),
|
||||
fSelect("font_size", []string{"small", "medium", "large"}, false),
|
||||
fDate("deletion_requested_at", false),
|
||||
// Access role. Empty value is treated as "user" by the API.
|
||||
fSelect("role", []string{"user", "admin", "superadmin"}, false),
|
||||
// Organization membership. Non-cascading: deleting an org keeps its people.
|
||||
fRelation("organization", "organizations", false, false),
|
||||
// Per-user plugin/integration config (bottom layer of the cascade).
|
||||
fJSON("pluginSettings", 100000),
|
||||
},
|
||||
}
|
||||
|
||||
// createOrder is the dependency order for creating missing collections.
|
||||
// "users" is PocketBase's built-in auth collection and is never created here.
|
||||
var createOrder = []string{
|
||||
"organizations",
|
||||
"cars",
|
||||
"service_records",
|
||||
"technical_checks",
|
||||
"parts",
|
||||
"car_shares",
|
||||
"fuel_entries",
|
||||
"maintenance_entries",
|
||||
"car_documents",
|
||||
"reminders",
|
||||
"control_audit",
|
||||
}
|
||||
|
||||
// reconcileOrder additionally includes "users" so its custom fields (role,
|
||||
// organization, preferences) are added to the built-in collection.
|
||||
var reconcileOrder = []string{
|
||||
"organizations",
|
||||
"users",
|
||||
"cars",
|
||||
"service_records",
|
||||
"technical_checks",
|
||||
"parts",
|
||||
"car_shares",
|
||||
"fuel_entries",
|
||||
"maintenance_entries",
|
||||
"car_documents",
|
||||
"reminders",
|
||||
"control_audit",
|
||||
}
|
||||
|
||||
// indexes are extra SQL indexes applied at collection-create time.
|
||||
var indexes = map[string][]string{
|
||||
"organizations": {"CREATE UNIQUE INDEX `idx_organizations_name` ON `organizations` (`name`)"},
|
||||
"fuel_entries": {"CREATE INDEX `idx_fuel_entries_car_km` ON `fuel_entries` (`car`, `km`)"},
|
||||
"maintenance_entries": {"CREATE INDEX `idx_maintenance_entries_car_date` ON `maintenance_entries` (`car`, `date`)"},
|
||||
"car_documents": {"CREATE INDEX `idx_car_documents_car_expiry` ON `car_documents` (`car`, `expiry_date`)"},
|
||||
"reminders": {"CREATE INDEX `idx_reminders_car_due` ON `reminders` (`car`, `due_date`)"},
|
||||
"technical_checks": {"CREATE INDEX `idx_technical_checks_car_date` ON `technical_checks` (`car`, `date`)"},
|
||||
"control_audit": {
|
||||
"CREATE INDEX `idx_control_audit_serial_created` ON `control_audit` (`serial`, `created`)",
|
||||
"CREATE INDEX `idx_control_audit_user_created` ON `control_audit` (`user_id`, `created`)",
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user