Same arrangement the provider panel gives its readings, applied to the four charging cards: drag one and the column reorders live under the pointer, the card being dragged goes half-transparent, the one it is over takes a ring, and the rail's lock holds the lot still. Two things differ from the readings row, both because these are four different things rather than four of one. A card is placed with the CSS order property instead of by moving markup, so each keeps its own template and its own v-if. And the handle is the card's heading rather than the whole card — a card that was draggable everywhere would fight the current-limit slider and the address fields for the pointer. Saved on the profile as charger_card_order, beside the tab order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
321 lines
13 KiB
Go
321 lines
13 KiB
Go
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),
|
|
// ISO 8601 reduced precision: "2015", "2015-03" or "2015-03-10". A build
|
|
// date is often only half known; see normalizeBuildDate in api/cars.go.
|
|
fText("build_date", false),
|
|
fText("first_registration_date", false), // ISO YYYY-MM-DD
|
|
// Link to the manufacturer service this car came from: the plugin name plus
|
|
// that plugin's own id for the vehicle (the VIN, for Toyota). See
|
|
// internal/api/vehicleproviders.go. Blank for a hand-entered car.
|
|
fText("provider", false),
|
|
fText("provider_vehicle_id", false),
|
|
// What this car's page shows: the tabs switched off (["fuel"] on an EV)
|
|
// and the Information fields switched off (["differentialOil"] on a car
|
|
// without one). Properties of the car, so everyone it is shared with sees
|
|
// the same page. Stored as the hidden sets, so anything added in a later
|
|
// release is on by default. Keys are validated in internal/api/cars.go.
|
|
fJSON("hidden_tabs", 2000),
|
|
fJSON("hidden_fields", 2000),
|
|
// And the columns of the Service history table (["parts"] for a reader who
|
|
// never records what was changed). Date is not hideable and so never
|
|
// appears here.
|
|
fJSON("hidden_service_columns", 2000),
|
|
// And the parts its services never change (["oil"] on an EV), which come
|
|
// off the service form and out of that column together.
|
|
fJSON("hidden_service_parts", 2000),
|
|
// The order the tabs are laid out in, as tab keys, the same for the
|
|
// Information rows, the Service history columns, and the connected
|
|
// service's headline readings. Empty means the page's own default order.
|
|
fJSON("tab_order", 2000),
|
|
fJSON("field_order", 2000),
|
|
fJSON("service_column_order", 2000),
|
|
fJSON("metric_order", 2000),
|
|
// 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
|
|
},
|
|
// Charging sessions for an electric car — the EV counterpart of fuel_entries,
|
|
// same shape so the two logs behave alike. Consumption is derived on read.
|
|
"charging_sessions": {
|
|
fRelation("car", "cars", true, true),
|
|
fDate("date", true),
|
|
fNumber("km"), // odometer when plugging in
|
|
fNumber("kwh"),
|
|
fNumber("cost"),
|
|
fBool("full_charge"),
|
|
fBool("missed_session"),
|
|
fText("location", false), // "Home", "Ionity Køge"
|
|
fText("notes", false),
|
|
attachment(), // the charge point's 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),
|
|
},
|
|
// Server-wide settings as a single record, keyed "global". Today it holds
|
|
// pluginSettings: the top (L1) layer of the integration cascade — every
|
|
// plugin's enable state, its global config, and the registration of any
|
|
// external HTTP plugin. The org (L2) and user (L3) layers keep their own
|
|
// plugin config in a field of the same name below, so the global layer is
|
|
// stored the way they are instead of in a file beside the binary.
|
|
"app_settings": {
|
|
fText("key", true),
|
|
fJSON("pluginSettings", 200000),
|
|
},
|
|
// 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),
|
|
},
|
|
// The chargers a user owns — their own wallbox, not the public network. A
|
|
// charger belongs to a person rather than to a car: it charges whichever car
|
|
// is plugged into it, and it outlives any of them.
|
|
"home_chargers": {
|
|
fText("name", true),
|
|
fText("serial", false),
|
|
fText("vendor", false), // "Anker Solix", "Greencell" — who makes it
|
|
fText("model", false), // "A5191"
|
|
fText("site_name", false),
|
|
fNumber("power_kw"),
|
|
fText("connector", false),
|
|
// Where this charger came from: the charger-provider id plus that
|
|
// provider's own id for it (the serial, for both providers we speak to).
|
|
// Blank for one added by hand. See internal/api/chargerproviders.go.
|
|
fText("provider", false),
|
|
fText("provider_charger_id", false),
|
|
// Owner. Non-cascading, like a car's: deleting a user must not silently
|
|
// wipe the records they own.
|
|
fRelation("owner", "users", false, false),
|
|
// A charger carries no date of its own, so the import order is the only
|
|
// order there is to list them in. PocketBase adds no created field to a
|
|
// collection defined through the API, so it is declared here like the
|
|
// audit trail's.
|
|
fAutodate("created", true, false),
|
|
},
|
|
// 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),
|
|
// Holds every arrangement on this user's pages still — the garage, a
|
|
// car's tabs and Information rows, the provider's readings — so reading a
|
|
// page cannot nudge its layout. Per user, like the garage order.
|
|
fBool("drag_locked"),
|
|
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),
|
|
// The garage order: car ids in the order this user arranged them. Per
|
|
// user rather than per car, so it also covers cars shared with them and
|
|
// never reorders somebody else's garage.
|
|
fJSON("car_order", 20000),
|
|
// The charging page's tab order. Per user like the garage order, and for
|
|
// the same reason: it is this person's arrangement of their own page.
|
|
fJSON("charger_tab_order", 2000),
|
|
// The charging page's card order, stored the same way as its tab order.
|
|
fJSON("charger_card_order", 2000),
|
|
},
|
|
}
|
|
|
|
// 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{
|
|
"app_settings",
|
|
"organizations",
|
|
"cars",
|
|
"service_records",
|
|
"technical_checks",
|
|
"parts",
|
|
"car_shares",
|
|
"fuel_entries",
|
|
"charging_sessions",
|
|
"maintenance_entries",
|
|
"car_documents",
|
|
"reminders",
|
|
"control_audit",
|
|
"home_chargers",
|
|
}
|
|
|
|
// reconcileOrder additionally includes "users" so its custom fields (role,
|
|
// organization, preferences) are added to the built-in collection.
|
|
var reconcileOrder = []string{
|
|
"app_settings",
|
|
"organizations",
|
|
"users",
|
|
"cars",
|
|
"service_records",
|
|
"technical_checks",
|
|
"parts",
|
|
"car_shares",
|
|
"fuel_entries",
|
|
"charging_sessions",
|
|
"maintenance_entries",
|
|
"car_documents",
|
|
"reminders",
|
|
"control_audit",
|
|
"home_chargers",
|
|
}
|
|
|
|
// indexes are extra SQL indexes applied at collection-create time.
|
|
var indexes = map[string][]string{
|
|
// One settings record per key, so the global singleton cannot be duplicated.
|
|
"app_settings": {"CREATE UNIQUE INDEX `idx_app_settings_key` ON `app_settings` (`key`)"},
|
|
"organizations": {"CREATE UNIQUE INDEX `idx_organizations_name` ON `organizations` (`name`)"},
|
|
"fuel_entries": {"CREATE INDEX `idx_fuel_entries_car_km` ON `fuel_entries` (`car`, `km`)"},
|
|
"charging_sessions": {"CREATE INDEX `idx_charging_sessions_car_km` ON `charging_sessions` (`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`)"},
|
|
// A charger is looked up by its owner, and by serial when checking whether
|
|
// the account it came from has already been imported.
|
|
"home_chargers": {"CREATE INDEX `idx_home_chargers_owner_serial` ON `home_chargers` (`owner`, `serial`)"},
|
|
"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`)",
|
|
},
|
|
}
|