Commit graph

2035 commits

Author SHA1 Message Date
Daniel Roth
b896f22bf6
Merge pull request #464 from Hestia-Homes/main
Some checks failed
Test Suite / unit-tests (push) Has been cancelled
woo deploy!
2026-07-27 11:54:57 +01:00
Daniel Roth
8377ef5146
Merge pull request #456 from Hestia-Homes/fix/install-doc-categorisation
fix(live-tracking): stop coordination and design docs counting as install docs
2026-07-27 11:52:19 +01:00
Daniel Roth
0ca52d56d9 fix incorrect merge conflict resolution 2026-07-27 10:48:54 +00:00
Daniel Roth
f6acce9f63
Merge branch 'main' into fix/install-doc-categorisation 2026-07-27 11:39:21 +01:00
Daniel Roth
4859d7df55
Merge pull request #461 from Hestia-Homes/issue-460-design-doc-gates-installation
fix(live-projects): gate Installation in Progress on Retrofit Design Document presence (#460)
2026-07-24 17:10:56 +01:00
Daniel Roth
1db2e7c92c Record ADR-0023: Installation in Progress gated on Retrofit Design Document 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:54:27 +00:00
Daniel Roth
80cf21922e Feed Retrofit Design Document presence into Live projects classification 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:53:50 +00:00
Daniel Roth
854e42080e Exclude Retrofit Design Documents from the install-document bucket 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:52:13 +00:00
Daniel Roth
00a9f22925 Exclude Retrofit Design Documents from the install-document bucket 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:51:58 +00:00
Daniel Roth
259c36e652 Derive the set of deals holding a Retrofit Design Document 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:51:26 +00:00
Daniel Roth
840b6a7992 Derive the set of deals holding a Retrofit Design Document 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:51:13 +00:00
Daniel Roth
939cfcbee5 Classify by design-document presence through the whole pipeline 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:50:30 +00:00
Daniel Roth
0acc9f5211 Gate Installation in Progress on Retrofit Design Document presence 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:49:36 +00:00
Daniel Roth
11a542b973 Gate Installation in Progress on Retrofit Design Document presence 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:47:34 +00:00
Daniel Roth
1039c21c57 Categorise install docs identically in the table badge and the drawer 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:22:26 +00:00
Daniel Roth
8ee27789bc Report no install docs for coordination- and design-only properties 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:21:18 +00:00
Daniel Roth
d666d0e800 Exclude design docs from the Install Docs badge 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:20:16 +00:00
Daniel Roth
a641e95f51 Exclude design docs from the Install Docs badge 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:19:56 +00:00
Daniel Roth
f5fbb64e3c Exclude coordination docs from the Install Docs badge 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:19:38 +00:00
Daniel Roth
24dc2927da Exclude coordination docs from the Install Docs badge 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:19:22 +00:00
Daniel Roth
0d8adad1d9
Merge pull request #453 from Hestia-Homes/issue-413-wizard-contractor-assignment
feat(ara-projects): setup wizard 5/5 — contractor assignment and permissions
2026-07-23 17:01:12 +01:00
Daniel Roth
feee863010 feat(ara-projects): unroll the permission detail instead of popping it (#413)
The sub-row's wrapper is a one-row grid whose track animates 0fr → 1fr over
160ms, so the detail unrolls to whatever height its own content needs — no
measuring, no ref, and no effect to start it: mounting runs the animation.
Rows below slide down with it rather than jumping. motion-reduce:animate-none
respects a reduced-motion preference.

Adds the `disclosure-open` keyframe to tailwind.config.js — namespaced and
additive, affecting nothing else. tailwindcss-animate is installed but not
registered in `plugins`, so the animate-in/slide-in-from-* utilities the
shadcn components reference generate no CSS; enabling that plugin would
change animation behaviour across every dialog and tooltip in the app, which
is not this ticket's call to make.

Collapsing is still immediate — React unmounts the row, so there is no exit
animation without holding it mounted through one.
2026-07-23 15:55:49 +00:00
Daniel Roth
44774febe5
Merge pull request #451 from Hestia-Homes/issue-411-wizard-stage-configuration
feat(ara-projects): setup wizard 3/5 — stage configuration
2026-07-23 16:53:03 +01:00
Daniel Roth
d15eee3958 feat(ara-projects): drop the ladder header's terminal-stage sentence
The last row already says "Work is complete here", right next to the stage
it is talking about. Repeating it in the card header named the same stage a
second place away from itself, which is a place it can go stale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:48:51 +00:00
Daniel Roth
2fe4b5ea17 style(ara-projects): let the permission detail fill the expanded row (#413)
Drop the "What <contractor> may do" heading and the "Set per workstream —
Edit to change them" footnote, and span the detail table across all four
columns with no padding, so it fills the expanded space rather than sitting
inset within it. Its header row and cell padding now line up with the table
above.
2026-07-23 15:48:50 +00:00
Daniel Roth
b396aabb91 feat(ara-projects): workstreams down the left, actions in the header
The workstream list moves from above the ladder into a column beside it, as
the wireframe has it. Above the ladder its height pushed the thing being
edited off the fold; in a column it costs nothing, and a catalogue of forty
scrolls inside that column rather than growing the page.

The Back / primary buttons move up onto the title row, which is where the
wireframe puts them and which saves another row of height — the screen now
fits a laptop without scrolling. The "Next: <workstream>" button goes: with
every workstream visible in the left column, stepping is a click on the next
one, and it was competing with the primary action for attention.

Primary action is now "Submit All", with a note under it saying changes
already save as you go — it moves on, it does not save.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:38:45 +00:00
Daniel Roth
6d662e81bd feat(ara-projects): edit and show permissions per workstream (#413)
The switch was the wrong control. project_workstream_contractor stores both
flags per (workstream, organisation) pair, so one switch stood for n booleans:
it could report a per-workstream split but never author one, and clicking it
destroyed the split to stay decidable. The schema's central capability was
unreachable from the only screen that configures it.

Add/edit dialog is now a grid: one row per workstream the *project* covers —
not only the ones this contractor holds — with a tick to assign and a tick per
permission. Unassigned rows stay visible and greyed, so the shape of the
project and its uncovered workstreams are part of the decision. Un-ticking a
workstream clears its permissions, so re-ticking never silently restores a
grant. Column headers carry an all/none shortcut.

The main table no longer shows permissions at all: contractor, workstreams,
Actions. Clicking a row (or its disclosure button, which carries
aria-expanded/aria-controls for keyboards and screen readers) opens a sub-row
listing each assignment with its own two permissions, read-only. Several may
be open at once. Actions stay per contractor — Edit and Remove act on the
whole set.

Wire shape follows: POST/PATCH take assignments[{projectWorkstreamId,
updateStagesPermission, uploadDocumentsPermission}]. A workstream named twice
is a 400 — two answers to one question. ContractorGroup drops its
per-contractor summary and permissionState is gone; nothing needs the flags
reduced to one answer, and reducing them is how a contractor allowed on one
workstream comes to look allowed everywhere. Updates are grouped by flag pair,
so twenty workstreams still cost at most four UPDATEs.

ADR-0022 records this; ADR-0021's decision 1 is marked superseded, its other
two (removal never cascades; Continue leads to the import) stand.
2026-07-23 15:34:06 +00:00
Daniel Roth
3cad369315
Merge pull request #454 from Hestia-Homes/issue-419-admin-work-orders-table
feat(ara-projects): admin work-orders table (filters + pagination)
2026-07-23 16:27:36 +01:00
Daniel Roth
23c7b1dcdf fix(filters): bucket the oil boiler heating archetypes as Oil, not Unknown
#455 added three main_heating_system values — "Oil boiler, regular", "Oil
boiler, combi" and "Solid fuel boiler" — without a matching needle. The two
oil ones fell through every bucket to Unknown, which the vocabulary-coverage
test caught: an overridden component that displays but cannot be filtered
(ADR-0012).

The gap is specific to these archetypes. Every other oil value carries its
fuel *after* a comma ("Boiler and radiators, oil"), which the ", oil" needle
matches; these lead with it. Bare "oil" is unusable as a needle because it is
a substring of "b(oil)er", so the fix is the narrower "oil boiler" — which
cannot collide with "solid fuel boiler", and Solid Fuel is checked first
regardless. "Solid fuel boiler" already bucketed correctly.

One edit fixes both halves: the portfolio query's SQL bucketing reads the
same DESCRIPTOR_FILTER_CONFIG rather than restating the needles, so the
classifier and the SQL cannot drift here.

Also asserts these three by *bucket* rather than relying on the coverage
test's "not Unknown" — the failure that matters is a wrong bucket, and a
reordered needle list would silently make "Oil boiler, combi" a Gas Boiler
while the coverage test kept passing.
2026-07-23 15:19:28 +00:00
Daniel Roth
d5fee8902f chore(db): migration for work_order.priority text -> boolean (#419)
Generated with drizzle-kit, then hand-edited for two defects in its output.
The snapshot is left exactly as generated, so future diffs stay accurate;
only the SQL is corrected.

  1. drizzle emitted a bare `SET DATA TYPE boolean`, which Postgres refuses
     on a text column ("cannot be cast automatically to type boolean").
     Added the USING clause that performs the cast.
  2. drizzle recorded DEFAULT false in 0280_snapshot.json but left it out of
     the SQL. Applied as generated, the column would be NOT NULL with no
     default while drizzle believed a default existed — so no later diff
     would ever add it, and an insert omitting priority would fail.

Also added a NULL backfill ahead of SET NOT NULL. work_order held 0 rows
when this was written, so both the cast and the backfill are no-ops today;
they matter only if the programme import (#417) lands rows first, in which
case a value that will not cast should fail the migration loudly rather than
be coerced into a wrong answer.

Not applied. Running it against the shared production database is the repo
owner's call.
2026-07-23 15:13:09 +00:00
Daniel Roth
e92f6e91db feat(ara-projects): let stage setup be left at any point, not walked through
Continue is no longer gated on reaching the last workstream. Every
workstream is seeded with the standard ladder on arrival, so the screen
opens in a state that is already correct for a project happy with the
defaults — making someone step through forty workstreams to confirm they
changed nothing is a toll, not a safeguard. The real gate is the import
readiness check (#414), which re-derives from the database anyway.

So the picker becomes the primary navigation (jump to the two ladders you
care about), "Next: Doors" demotes to a secondary convenience for anyone who
does want to review each one, and the summary line says what leaving now
would leave behind — "All 8 on the standard ladder", or a warning naming the
workstreams whose missing stages will block import.

The picker also has to survive a catalogue that size: it is capped at a
fixed height, scrolls, and grows a filter box past ten workstreams so it
can't push the ladder being edited off the screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:11:05 +00:00
Daniel Roth
d3772c3f7f Merge remote-tracking branch 'origin/main' into issue-419-admin-work-orders-table 2026-07-23 15:07:33 +00:00
Daniel Roth
069464dad3
Merge pull request #452 from Hestia-Homes/issue-412-wizard-evidence-checklist
feat(ara-projects): setup wizard 4/5 — evidence checklist + SharePoint fields
2026-07-23 16:02:13 +01:00
Daniel Roth
7b65eae7bd feat(ara-projects): step through workstreams, and drop the per-stage dates
Ladders are no longer stacked. A project with eight workstreams has forty
stages, and one page of them turns configuring the last workstream into a
scroll — so one ladder is on screen at a time, the "Workstream overrides"
panel doubles as the picker, and the footer walks forwards through them
before it offers to leave. The stages *within* a ladder stay listed
together: they are dragged against each other, so they have to be seen
together.

`start_date` / `due_date` come out end-to-end — inputs, request schemas,
payload, insert and update — rather than being hidden in the UI. A stage is
a rung of a workflow, not a piece of scheduled work, so it has no date to
hold; delivery dates already belong to a work order. The columns stay on the
table and stay NULL until their keep-or-drop decision is made. This
supersedes one of #411's acceptance criteria, recorded in ADR-0021.

`updateStage` collapses to `renameStage` — the name is now the only field of
a stage a user owns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:00:14 +00:00
KhalimCK
25bec3de7d
Merge pull request #455 from Hestia-Homes/feat/oil-solid-boiler-archetypes
feat(db): oil & solid-fuel boiler archetypes for main_heating_system
2026-07-23 15:58:47 +01:00
Daniel Roth
0e69c080e1 feat(ara-projects): make work_order.priority a boolean and show it in the table (#419)
priority was one of the "enum values TBC" text columns. It turns out to be a
flag, not a scale — "look at this one first" — so there is no value set left
to agree, and text was modelling a decision nobody needs to make. Now
boolean NOT NULL DEFAULT false: a third "unknown" state has no meaning for a
flag, and the table either renders it or renders nothing.

Deliberately independent of overdue. Overdue is derived from the forecast
date and the stage ladder; priority is stored, because someone marked it. A
flagged work order may be well inside its dates and a late one nobody
flagged is not a priority, so they are separate columns and separate badges.

Read path: selected and grouped in the page query, carried through
RawWorkOrderRow -> WorkOrderRow, rendered as the wireframe's Priority column
between Docs and the row action. Shown only when true; a column of "No"
chips would give the exception the same weight as the rule.

Docs updated where they claimed otherwise: CONTEXT.md gains a Priority entry
and no longer lists the column as untouched, and schema.md no longer groups
it with the TBC-enum text columns.

MIGRATION NOT INCLUDED AND NOT APPLIED. This commit changes the Drizzle
schema only; work_order.priority is still `text` in the shared production
database, so the boolean the code expects is not there yet. Generating and
applying the migration is the repo owner's call. Two notes for whoever does:
the table holds 0 rows today (verified by a read-only SELECT), so the change
is lossless; and Postgres refuses ALTER COLUMN ... TYPE boolean on a text
column without a USING clause, which drizzle-kit will not emit.
2026-07-23 14:55:29 +00:00
Daniel Roth
4bec5b72d2 feat(ara-projects): rename the stages permission and explain "mixed" (#413)
Rename: "Update stages" → "Update stages of workstream(s)", in the table
header and the add/edit dialog. (Requested as "Updaet" — corrected to
"Update"; say the word if the typo was deliberate.)

"Mixed" on its own raised more questions than it answered — mixed how, and
what happens if I click it? It is now explained at three depths, all fed by
one new pure helper (splitByPermission, unit-tested):

- The chip carries the count — "Mixed · 2 of 3" — needing no interaction.
- Its tooltip names which workstreams allow the permission and which don't,
  and states what the switch will do: grant to all N, or revoke from all N.
- The switch's accessible name says the same in a sentence, so a screen
  reader never depends on hover. role="switch" admits no aria-checked="mixed"
  (ARIA 1.2), which is the other reason it has to be in the name.

The edit dialog gets the same treatment: rather than a bare "permissions
differ" note, it names each split before the user overwrites it.

The tooltip states plainly that saving clears the split, because no surface
in v1 writes divergent flags — per ADR-0021 they are readable, not authorable
here. PermissionToggle now takes an explicit testId rather than deriving one
from the label, so copy changes don't move the test hook.
2026-07-23 14:50:51 +00:00
Khalim Conn-Kowlessar
f1d4398fc6 feat(db): add oil & solid-fuel boiler archetypes to main_heating_system
A fuel-agnostic "Boiler" description defaults to a Gas boiler archetype, which is
wrong when the property's main_fuel is oil or a solid fuel — the enum had no
non-gas/non-electric wet-boiler member, so ~43 properties in portfolio 796 sit as
"Gas boiler, regular/combi" on oil / house coal / wood logs / dual fuel.

Adds `Oil boiler, regular`, `Oil boiler, combi`, `Solid fuel boiler` (+ ALTER TYPE
migration 0279). Fuel granularity (coal/wood/dual) is carried by the separate
main_fuel column, applied on top of the archetype (ADR-0041), so one solid-fuel
boiler member suffices.

Inert until the Model backend adds matching members + overlay handling and a
fuel-aware classifier mapping (Model#1676); the 43 existing rows get a
per-property, fuel-joined backfill afterwards.

Open question for the backend grilling: whether solid fuel should split into
biomass/wood vs mineral coal archetypes, or stay one member with the fuel column
driving carbon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:39:19 +00:00
Daniel Roth
235af64b43 feat(ara-projects): drag stages to reorder instead of up/down buttons
Rows are dragged by a grip rather than by their body — the body is full of
text inputs, and a row that dragged from anywhere would fight every attempt
to select a stage name.

@dnd-kit rather than native HTML5 drag events, because it brings a keyboard
path (tab to grip, space to lift, arrows, space to drop) and screen-reader
announcements with it. Without that, removing the up/down buttons would have
left the ladder mouse-only.

The drop writes the new order into the query cache before the request goes
out and rolls back if the server refuses it. Otherwise the row springs back
to where it started for as long as the PATCH takes, which reads as "the drag
didn't work" rather than "the drag is saving".

`moveStage` and its tests go with the buttons — dnd-kit's `arrayMove` does
that job now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:28:07 +00:00
Daniel Roth
df8e0315cb fix(ara-projects): align the add-document composer with the table it feeds (#412)
Three complaints, one cause: the composer was a separate block trying to
look like part of the table. It now *is* part of it — a `<tfoot>`, so its
fields sit in the same columns as the rows by construction, and the
table's own headings label them. No second caption set to drift out of
line, and no unlabelled fields either.

Its controls belong to a form declared below the table and are wired to
it by `form=`, because a `<form>` cannot sit between a `<table>` and its
rows; Enter still submits.

It stays distinct: a 4px rule above it, a tinted band, and its own "Add a
document" caption spanning the width.

Also: selects get `pr-8` so the native chevron stops overlapping the
option text, the Stage column widens to `w-52`, and the last column
widens to fit the Add button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:24:02 +00:00
Daniel Roth
5c94a538ed refactor(ara-projects): make the add-document composer read as a form (#412)
Labelled cells sitting directly under the table's own headings read as a
second header row: two near-identical sets of words that never quite
lined up, worst of all when the table was empty and they were the only
two things on screen.

The composer now announces itself — a thick divider, its own "Add a
document" heading — and lays its controls out as a form rather than as
twelfth-aligned columns: placeholders and aria-labels instead of column
captions, fields free to size themselves, a text "Add" button. The empty
table points at the heading by name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:57:04 +00:00
Daniel Roth
ab29a246a4 fix(ara-projects): key the stages routes on the shared [workstreamId] slug
Next.js refuses two different slug names for one dynamic segment, and #410
already owns `/api/projects/[projectId]/workstreams/[workstreamId]`. Adding
`[projectWorkstreamId]` beside it was a hard boot/build failure that took
the whole app down, not just these routes — `next dev` refused to start.

The URL shape is unchanged; the id in that segment is now the catalogue
`workstream.id` throughout, so `/workstreams/9` and `/workstreams/9/stages`
name the same workstream. `findLadder` translates to the
`project_workstream.id` the writes take, once per request, at the edge —
keeping that id out of URLs entirely. ADR-0021 records the trade-off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:38:41 +00:00
Daniel Roth
feff631582 fix(ara-projects): stop the add-requirement button colliding with the toggle (#412)
The required checkbox and its label sat in one twelfth of the add row —
narrower than their own contents. Grid children default to
`min-width: auto`, so instead of shrinking they spilled over the Add
button beside them.

Every cell is now `min-w-0`, the toggle gets two columns with its label
above it like its siblings, and the button is `size="sm"` so it lines up
with the inputs rather than standing 4px taller. On a narrow screen the
button says "Add" instead of being a bare icon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:26:32 +00:00
Daniel Roth
d75bdf0ac4 Merge origin/main into issue-419-admin-work-orders-table (pull in #450 build fix) 2026-07-23 13:10:11 +00:00
Daniel Roth
a7b1a3982b Merge origin/main into issue-413-wizard-contractor-assignment (pull in #450 build fix) 2026-07-23 13:10:09 +00:00
Daniel Roth
752686b562 Merge origin/main into issue-412-wizard-evidence-checklist (pull in #450 build fix) 2026-07-23 13:10:07 +00:00
Daniel Roth
42c027a5c5 Merge origin/main into issue-411-wizard-stage-configuration (pull in #450 build fix) 2026-07-23 13:10:04 +00:00
Daniel Roth
ac932524fe
Merge pull request #450 from Hestia-Homes/fix/setup-workstreams-authz-import
fix(ara-projects): repoint workstreams setup import to guards (fixes main build)
2026-07-23 14:08:43 +01:00
Daniel Roth
aa505bc84f fix(ara-projects): repoint workstreams setup import to guards
setup/workstreams/page.tsx imported requireProjectAccess from
../../../authz, the stub #406 created and #409 deleted when it moved the
guard into src/app/projects/guards.ts. #410 (workstreams) was built in
parallel against the stub path and merged after #409, so the two landed
cleanly by git yet left a dangling import — the only tsc error in the
repo and a next build failure.

The page calls requireProjectAccess purely for its redirect/notFound
side effect and discards the return, so repointing the import is the
whole fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:58:47 +00:00
Daniel Roth
7d271b06bc test(ara-projects): pin the seam between the toggles and the authz guards (#413)
Acceptance asks that the permission toggles "take effect in the authz lib
guards". Both ends are already tested — the guards' rules in #408's tests,
the endpoints in route.test.ts — but neither notices if they stop describing
the same two booleans.

This starts from a project_workstream_contractor row typed against the real
schema, flattens it the way ./queries does, and asks the real guards what the
contractor may do: a renamed column or wire field fails it, at compile time
or on the assertion. No database is involved; the row is a literal.
2026-07-23 12:53:40 +00:00