Maintenance¶
URL: https://maintenance.heezy.info — LAN only
Internal: http://192.168.1.15:30861
k8s: heezy namespace, deployment heezy-maintenance
Source: heezy-containers/dockerfiles/heezy-maintenance/
Manifests: heezy-k8s/apps/heezy-maintenance/
Last Updated: 2026-08-14
What It Does¶
Service history for vehicles, small engines and the house. Flask app, server-rendered pages plus a JSON API the front end calls.
Four surfaces:
| Route | Page |
|---|---|
/ |
Dashboard, one card per piece of equipment |
/equipment/<slug> |
Service log, fuel log, schedule and SOPs for one item |
/home |
Home service log (lawn, pest) and recurring interior tasks |
/sprinklers |
Irrigation programs and start times by season |
Deployed 2026-08-02.
Equipment¶
Six rows seeded in equipment, keyed by slug. The type column decides whether the odometer field
means miles or hours, and whether a fuel log applies at all.
| Slug | Name | Type |
|---|---|---|
honda-crv |
Honda CRV (2010) | vehicle_miles |
expedition |
Ford Expedition (2014) | vehicle_miles |
lawnmower |
Lawnmower | equipment_hours |
blower |
Leaf Blower | equipment_hours |
generator |
Generator | equipment_hours |
home |
Home | home |
MILES_SLUGS, HOURS_SLUGS and TWO_STROKE in app.py mirror this. blower is the only
two-stroke, which is why the fuel form offers a mix_ratio. Adding equipment means a row and an
edit to those lists.
Database¶
Its own maintenance database on big-boi, not the shared heezy database. Same host, same
heezy_app user, same OpenBao secret (production/heezy/postgres/heezy-credentials), different
DB_NAME. A join against receipts or a finance table is not possible from here.
| Table | Holds |
|---|---|
equipment |
The six rows above |
maintenance_schedule |
What is due and how often (interval_unit = miles / hours / months / years) |
maintenance_log |
What was actually done, with odometer and cost |
maintenance_log_photo |
Photos attached to a log entry, cascade-deleted with it |
fuel_log |
Gallons, price, odometer, mix ratio |
sop |
Per-equipment procedures, stored as markdown, rendered server-side |
home_service_log |
Lawn and pest visits |
home_task / home_task_log |
Recurring interior tasks and when each was last done |
sprinkler_program / sprinkler_start_time |
Stations, durations and starts per season and program |
home_task seeds eleven tasks (HVAC filter, dryer vent, water heater flush, and so on). Next-due is
computed in SQL, not stored: MAX(done_at) + interval_months. A task never logged sorts first with a
NULL due date rather than being silently omitted.
sprinkler_program is uniquely keyed on (season, program, station) and the API upserts, so saving
the grid twice is safe. Seasons are constrained to spring / summer / fall, programs to A / B / C, and
stations to 1 through 10.
Storage¶
| Property | Value |
|---|---|
| PVC | heezy-maintenance-config (1Gi, longhorn, RWX) |
| Mount | /data |
| Photos | /data/uploads/maintenance/<log_id>/<uuid>.<ext> (UPLOAD_DIR) |
Allowed photo types: jpg, jpeg, png, gif, webp. Filenames are replaced with a UUID, so the
original name is not preserved anywhere.
RWX on Longhorn, not NFS
This is the one Longhorn volume in heezy that asks for ReadWriteMany, which Longhorn serves
through a share-manager pod rather than direct block attach. The deployment is
strategy: Recreate with one replica, so nothing currently depends on the shared mode.
Deleting a log entry removes its photo files from disk first, then the row — the cascade on
maintenance_log_photo would otherwise drop the rows and strand the images.
Fuel receipts arrive from the receipts service¶
The receipts scanner is the intended way to log a fill-up. Its POST /api/fuel-log forwards to this
service's POST /api/fuel at MAINTENANCE_URL (http://heezy-maintenance in-cluster), and
GET /api/vehicles reads the equipment list so the receipts UI can offer a vehicle picker.
That means a gas receipt photographed on a phone lands in both systems: the spend in receipts, the
gallons and odometer here. Entering it in both by hand produces a duplicate.
API¶
| Route | Purpose |
|---|---|
GET /api/equipment/<slug> |
Equipment row, last service, last fill-up, schedule |
GET/POST/PUT/DELETE /api/maintenance[/...] |
Service log CRUD |
GET/POST /api/maintenance/<log_id>/photos |
List and upload photos |
PATCH/DELETE /api/maintenance/photos/<id> |
Edit caption, delete photo and file |
GET /uploads/maintenance/<log_id>/<file> |
Serve a photo |
GET/POST/DELETE /api/fuel[/...] |
Fuel log |
GET/POST/DELETE /api/schedule[/...] |
Schedule definitions |
GET/POST/PUT/DELETE /api/sop[/...] |
SOPs. GET /api/sop/<id> returns content_html alongside the markdown |
GET/POST/DELETE /api/home/services[/...] |
Home service log |
GET/POST /api/home/tasks, POST/GET /api/home/tasks/<id>/log |
Recurring tasks and completions |
GET /api/sprinklers/schedule, POST /api/sprinklers/program, POST/DELETE /api/sprinklers/start_time[/...] |
Irrigation |
GET /api/health |
Liveness |
There is no authentication in the app. Access control is entirely that the hostname resolves only on the LAN.
Access¶
maintenance.heezy.info exists only as a dnsmasq override pointing at the SWAG VIP
(ansible-heezy/roles/dnsmasq/templates/heezy-info-overrides.j2). There is no public DNS record, no
Cloudflare tunnel ingress rule and no Access application, so it is unreachable off-LAN. Clients need
192.168.1.29 as their resolver. See DNS Architecture.
Making it public means a Cloudflare tunnel hostname and an Access app — publishing the DNS record alone would expose an unauthenticated CRUD API.
CI¶
heezy-containers/.gitea/workflows/test-heezy-maintenance.yml runs pytest with
--cov-fail-under=90, the highest floor of any app in the repo. deploy-heezy-maintenance.yml
builds, pushes to ECR and commits a .deploy-trigger to heezy-k8s.
Related¶
- Projects — same shape, sibling service
- Receipts Scanner — where fuel entries come from
- New Service runbook