Fleet management — Developer Guide
How the fleet module (epic #1388) is laid out: pages, services, files, the daily expiries digest and the one-off iDrv4 history import.
The fleet module brings Onyx to parity with the iDrv4 (Nicola/Ceres) fleet suite. It was built in phases #1381–#1387 under epic #1388. Operator guides are in the Admin Guide under Fleet and Compliance → NCR / CAR. This page covers the code.
Layout
Section titled “Layout”| Where | What |
|---|---|
portals/operations/fleet_routes.py | Fleet pages on the operations_portal blueprint: overview, vehicle detail (/fleet/vehicles/<id>/view), maintenance, assets, expiries, KMs, fuel, fuel cards, infringements, capacity |
api/fleet_api.py | /api/fleet/* JSON endpoints the pages call |
services/fleet/ | One module per area: vehicles, odometer, maintenance, assets, expiries, fuel, fuel_documents, fuel_cards, infringements, ncr, costs, documents, digest, cli, plus columns (absent-column probes) |
templates/portals/operations/ | The pages. Add and edit happen in right-hand offcanvas-end drawers, not modals |
utils/user_permissions.py | FLEET_WRITE_ROLE_IDS (Operations + Admin) and FLEET_ADMIN_ROLE_IDS (Admin: unarchive, delete documents, NCR vocabulary) |
Fleet tables are read and written with plain SQL, mostly through pooled_connection(). The ORM is used only to read the compliance and document modules’ own models (defects, submissions, documents). Don’t import models/asset_management.py or models/asset_sqlalchemy.py: they declare columns the database doesn’t have.
Rules worth knowing
Section titled “Rules worth knowing”- One expiry rule.
services/fleet/expiries.py::list_expiriesdrives the Expiries page, the dashboard Fleet expiries widget and the email digest. It judges dates in Melbourne time. Don’t compute expiry status anywhere else. vehiclesstays the source dispatch reads. Saving a registration recomputesvehicles.registration_expiry, and a DG certificate updatesdg_licence_*. Both happen in the same transaction and lock the vehicle row first. They are only recomputed when records exist, never cleared.- Odometer.
fleet_odometer_readings_vunions manual readings, fuel records, maintenance records and vehicle compliance submissions. The latest dated reading wins, and readings before a reset are ignored. - Files. Fleet attachments live in the private bucket
fleet-docs(config.fleet_docs_bucket) and are streamed through role-checked Flask routes. Never hand out a public URL. - Fuel cards never store a full number, only a label and the last four digits. A migration CHECK rejects 8+ digit values.
- Schema changes are migrations under
supabase/migrations/. Promote them a step ahead of code that reads new columns.services/fleet/columns.pylets pages show a “not migrated yet” state instead of a 500.
Daily expiries digest (#1384)
Section titled “Daily expiries digest (#1384)”flask fleet digest sends each enabled subscriber the day’s fleet expiries. The command is registered in both app_factory.py and main.py.
flask fleet digest --dry-run # print it; send and write nothingflask fleet digest --to me@example.com # one copy to one address, no send logflask fleet digest --date 2026-11-03 # judge expiries on that Melbourne dateflask fleet digest --force # skip the local-hour gateflask fleet digest-subscribe --email ADDR [--user-id N] [--lead-days 30]- Users manage their own subscription in Operations → Settings → Fleet digest. See the operator guide.
digest_sendsholds one row per Melbourne date and address. That row makes repeat runs no-ops and lets a failed send be retried safely.- It is deployed as a separate Railway cron service using
railway.fleet-digest.toml, which runs hourly. The command’s ownFLEET_DIGEST_LOCAL_HOURgate (Melbourne, default 7) decides when it sends. - The service needs
FLEET_DIGEST_ENABLED=true,DATABASE_URL, its ownSESSION_SECRETandJWT_SECRET_KEY, andPORTAL_BASE_URL. In production it also needs the web service’sMAIL_*variables. - Without the
MAIL_*variables, production refuses to send (exit 2) rather than drop mail into the Mailosaur sandbox. - The comments at the top of
railway.fleet-digest.tomlare the setup runbook.
iDrv4 history import (#1387)
Section titled “iDrv4 history import (#1387)”scripts/import_ceres_fleet_records.py loads iDrv4 fleet history from live Ceres, read-only. Run it after scripts/import_ceres_fleet.py (the vehicles themselves), and only once every fleet migration is on the target tier.
- Rule: import current state regardless of date: certificates and expiries, latest registration and insurance, fuel cards, odometer, service intervals, NCR lists and open NCRs. Import events only since
--since(default2026-07-01): refuels, other costs, renewals, NCRs and fines. - Datasets (
--only):vocabulary,fuel_cards,certificates,registrations,insurances,odometer,service_intervals,fuel_records,other_costs,ncr_reports,infringements. - Flags:
--dry-runuses a READ ONLY transaction and rolls back.--report FILEwrites a Markdown reconciliation.--source JAT|JATTpicks the Ceres database.- Production also needs
--allow-production.
- Idempotent. Every row carries
legacy_sourceandlegacy_id, and a second run must report onlyunchanged. Fields operators own are insert-only. - Privacy. It never selects card or account numbers; MySQL returns only the last four digits. Drivers are matched on licence number, never by name.
- Ceres credentials are temporary. Pass them via
--mysql-defaults-file, never commit or store them, and delete the file after the run.
It ran on dev, staging and production in October 2026. It is kept for re-runs, which are safe, and for the JATT source.