feat(hutches): split hutches by compartments #11

Merged
cpressland merged 1 commit from feat/hutch-compartments into main 2026-08-25 11:49:57 +01:00
Owner

Model hutch floors as compartments

Hutches split by floor could only be described as fully partitioned or fully open. Hutch N is neither: Smokey, Burt, Coco, Treacle, Toffee and Maple share the bottom and middle floors while Gareth has the top to himself. There was no way to record that.

The root cause is that a Hutch row was already a living space, not a floor — occupants, cleaning date and playtime all hang off it, and all three describe a space rather than a shelf. floor being a single string was the only thing stopping a space from covering more than one floor.

Changes

Hutch.floor: strHutch.floors: list[str] (JSON). A row is now explicitly a compartment: the set of floors rabbits can move freely between. floor_label, floor_rank and is_whole_hutch derive from it. A compartment spanning a complete structure is still named by its letter alone, so a fully open hutch reads as "N", not "N - Top + Middle + Bottom".

Floors editor. A Floors button on each hutch opens a dialog with a structure picker (single / double / triple) and a ramp or divider toggle for each boundary — covering split, join, add floor and remove floor in one place. Previously there was no way to change a hutch's floors at all short of hand-written SQL.

Saving reconciles compartments against the new layout: each one claims the existing row it shares the most floors with, so cleaning history and occupants stay with the space they belong to. Merged or deleted compartments hand their rabbits to the surviving compartment they overlap most, so nothing is orphaned. Splitting N leaves the six rabbits and the existing history on the Middle+Bottom row and creates an empty Top; the odd rabbit out is then moved with a normal rabbit edit.

Rendering. The list shows one row per physical floor with each compartment's cells rowspan-ed across its floors, so the table looks like the hutch. The same structure picker is reused on the New Hutch page.

Fresh-install fixes (found while testing). create_all() now runs before the startup migrations — three unguarded ALTER TABLE statements assumed tables
that don't exist yet, so the app could not boot against an empty database. That surfaced a second gap: a fresh install had no way to create its first account.
Added rabbitdb --create-user (podman exec -it <container> rabbitdb --create-user), which prompts for username, role and password, and a login-page
hint pointing at it when the users table is empty. The password is prompt-only, never a flag, so it stays out of shell history and ps.

Migration

Runs automatically at startup, following the existing in-place pattern in main.py. Partitioned rows backfill to ["Top"] etc.; the open hutches become one compartment spanning their structure — J/N/O all three floors, P/Q ["Top","Bottom"], House and Shed staying floorless. Verified against a Postgres 18 cluster seeded to production's exact shape, and idempotent across repeated boots.

It drops the floor column and is not reversible — take a snapshot before deploying.

Breaking

/api/v1/hutches now returns "floors": ["Top","Middle","Bottom"] instead of "floor": null. Any external consumer needs updating.

Testing

~60 assertions over the real routes covering split, merge, floor removal, floor addition, hutch creation, the rabbit picker, API and delete; 24 pty-driven checks of --create-user; migration verified on production-shaped Postgres data in both the empty-database and legacy-database directions; and the N split driven through the browser end to end.

## Model hutch floors as compartments Hutches split by floor could only be described as *fully partitioned* or *fully open*. Hutch N is neither: Smokey, Burt, Coco, Treacle, Toffee and Maple share the bottom and middle floors while Gareth has the top to himself. There was no way to record that. The root cause is that a `Hutch` row was already a **living space**, not a floor — occupants, cleaning date and playtime all hang off it, and all three describe a space rather than a shelf. `floor` being a single string was the only thing stopping a space from covering more than one floor. ### Changes **`Hutch.floor: str` → `Hutch.floors: list[str]`** (JSON). A row is now explicitly a compartment: the set of floors rabbits can move freely between. `floor_label`, `floor_rank` and `is_whole_hutch` derive from it. A compartment spanning a complete structure is still named by its letter alone, so a fully open hutch reads as "N", not "N - Top + Middle + Bottom". **Floors editor.** A *Floors* button on each hutch opens a dialog with a structure picker (single / double / triple) and a **ramp or divider** toggle for each boundary — covering split, join, add floor and remove floor in one place. Previously there was no way to change a hutch's floors at all short of hand-written SQL. Saving reconciles compartments against the new layout: each one claims the existing row it shares the most floors with, so cleaning history and occupants stay with the space they belong to. Merged or deleted compartments hand their rabbits to the surviving compartment they overlap most, so nothing is orphaned. Splitting N leaves the six rabbits and the existing history on the Middle+Bottom row and creates an empty Top; the odd rabbit out is then moved with a normal rabbit edit. **Rendering.** The list shows one row per physical floor with each compartment's cells `rowspan`-ed across its floors, so the table looks like the hutch. The same structure picker is reused on the New Hutch page. **Fresh-install fixes** (found while testing). `create_all()` now runs *before* the startup migrations — three unguarded `ALTER TABLE` statements assumed tables that don't exist yet, so the app could not boot against an empty database. That surfaced a second gap: a fresh install had no way to create its first account. Added `rabbitdb --create-user` (`podman exec -it <container> rabbitdb --create-user`), which prompts for username, role and password, and a login-page hint pointing at it when the users table is empty. The password is prompt-only, never a flag, so it stays out of shell history and `ps`. ### Migration Runs automatically at startup, following the existing in-place pattern in `main.py`. Partitioned rows backfill to `["Top"]` etc.; the open hutches become one compartment spanning their structure — J/N/O all three floors, P/Q `["Top","Bottom"]`, House and Shed staying floorless. Verified against a Postgres 18 cluster seeded to production's exact shape, and idempotent across repeated boots. **It drops the `floor` column and is not reversible — take a snapshot before deploying.** ### Breaking `/api/v1/hutches` now returns `"floors": ["Top","Middle","Bottom"]` instead of `"floor": null`. Any external consumer needs updating. ### Testing ~60 assertions over the real routes covering split, merge, floor removal, floor addition, hutch creation, the rabbit picker, API and delete; 24 pty-driven checks of `--create-user`; migration verified on production-shaped Postgres data in both the empty-database and legacy-database directions; and the N split driven through the browser end to end.
cpressland deleted branch feat/hutch-compartments 2026-08-25 11:49:57 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
cpressland/rabbitdb!11
No description provided.