Projects¶
URL: https://projects.heezy.info — LAN only
Internal: http://192.168.1.15:30862
k8s: heezy namespace, deployment heezy-projects
Source: heezy-containers/dockerfiles/heezy-projects/
Manifests: heezy-k8s/apps/heezy-projects/
Last Updated: 2026-08-14
What It Does¶
Tracks planned and in-flight projects: what it is, what it needs, what vendors quoted, what it actually cost. Flask, two server-rendered pages plus a JSON API.
| Route | Page |
|---|---|
/ |
All projects, with rollup counters |
/project/<id> |
One project: requirements, quotes, attachments |
The index computes four figures in Python, not SQL: total projects, in-progress count, completions
this calendar year, and the sums of actual_cost and estimated_cost across every project
regardless of status. The spend figure is therefore lifetime, not year to date.
Deployed 2026-08-02.
Vocabulary¶
Three fixed lists in app.py, passed to the templates as dropdown options:
| List | Values |
|---|---|
CATEGORIES |
home-improvement, tech-lab, vehicle, yard, other |
STATUSES |
planning, in-progress, on-hold, completed |
PRIORITIES |
low, medium, high |
The database does not enforce any of them — the columns are plain TEXT with defaults other,
planning and medium. An API client sending an unlisted value gets it stored.
Ordering on the index is priority DESC, updated_at DESC, which is a lexical sort on the priority
text: medium > low > high. High-priority projects sort last.
Database¶
Its own projects database on big-boi, separate from the shared heezy database, using the same
heezy_app credentials from OpenBao production/heezy/postgres/heezy-credentials.
| Table | Holds |
|---|---|
projects |
Name, category, status, priority, description, notes, estimated/actual cost, three dates |
project_requirements |
Free-text requirement lines |
project_quotes |
Vendor, rep, amount, contact details, notes, optional attachment |
project_tasks |
Checklist items |
project_log |
Timestamped entries |
Every child table is ON DELETE CASCADE against projects, so deleting a project takes its quotes
and requirements with it.
project_tasks and project_log have no routes yet
Both tables ship in schema.sql and neither is read or written by app.py. They are schema laid
down ahead of the feature, not something the UI exposes.
Quote attachments¶
POST /api/projects/<id>/quotes is multipart/form-data, not JSON — it is the only write endpoint
that is. The optional attachment file is validated by extension against ALLOWED_EXTENSIONS
(pdf, jpg, jpeg, png, gif, webp, heic, heif, bmp, tiff, tif) and rejected with
a 400 otherwise.
Two names are stored: attachment_filename is the sanitised original for display,
attachment_stored is a UUID plus extension and is the actual file on disk. Serving goes through
GET /api/quotes/<id>/attachment, which looks the mimetype back up from the stored extension.
Deleting a quote removes the file before the row.
| Property | Value |
|---|---|
| PVC | heezy-projects-data (5Gi, longhorn, RWO) |
| Mount | /data |
| Attachments | /data/attachments/<uuid>.<ext> (UPLOAD_DIR) |
RWO plus strategy: Recreate caps this at one replica. Scaling up would leave the second pod unable
to attach the volume.
API¶
| Route | Purpose |
|---|---|
POST /api/projects |
Create, returns the new id |
PUT /api/projects/<id> |
Full replace, bumps updated_at |
DELETE /api/projects/<id> |
Deletes the project and everything cascaded from it |
POST /api/projects/<id>/requirements, DELETE /api/requirements/<id> |
Requirements |
POST /api/projects/<id>/quotes |
Multipart, optional attachment |
DELETE /api/quotes/<id> |
Deletes the quote and its file |
GET /api/quotes/<id>/attachment |
Serves the stored file |
GET /api/health |
Liveness |
PUT /api/projects/<id> writes every column from the payload. A partial body blanks the fields it
omits; there is no PATCH.
No authentication. Same as Maintenance: the only control is that the name does not resolve off-LAN.
Access¶
projects.heezy.info is a dnsmasq override to the SWAG VIP and nothing else. No public DNS record,
no Cloudflare tunnel hostname, no Access application. LAN clients need 192.168.1.29 as their
resolver. See DNS Architecture.
CI¶
test-heezy-projects.yml runs pytest with --cov-fail-under=62.
deploy-heezy-projects.yml builds, pushes to ECR and commits a .deploy-trigger to heezy-k8s.
Related¶
- Maintenance — sibling service, same deployment pattern
- New Service runbook