Skip to content

Maintenance

URL: https://maintenance.heezy.infoLAN 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.