Repository navigation
Description
Phase 1.5 — Egg product hardening
Make the egg product safer and more operationally complete.
Includes
-
Discount visibility and control on sales — own epic EPIC: Discount visibility and control on sales #719 (9 slices, Sales: snapshot the list price on the order line (discount foundation) #720–Alerts: raise an alert-centre entry for a large discount at confirm #728). Discounts were being given without the owner knowing; the system has no discount concept at all. Tracked there, listed here so it is navigable from the phase. Note Alerts: raise an alert-centre entry for a large discount at confirm #728 depends on the Alert center item above.
-
Legacy import wizard
-
Inventory reconciliation
-
Alert center (Alert centre: durable per-farm alerts with an SPA surface (specs §17.1) #803) — filed 2026-09-13 during the issue cleanup, because it is the only unbuilt Phase 1.5 item something already-scheduled waits on (Alerts: raise an alert-centre entry for a large discount at confirm #728). The other bullets in this list stay bullets until one of them blocks scheduled work.
-
Email digest for critical alerts
-
Packaging inventory
-
Additives/supplements
-
Vaccination records
-
Flock profitability allocation rules
-
Installable PWA baseline — manifest, icons, SW app-shell cache (Installable PWA baseline — manifest, icons, service worker app-shell cache (split from #50) #142, PR F142: installable PWA baseline — manifest, icons, app-shell service worker #198, merged; split from Offline data capture (PWA) — tech spec KD-4/§6 #50)
-
Offline data capture (PWA) — tech spec KD-4/§6 (Offline data capture (PWA) — tech spec KD-4/§6 #50; split from the 1.1 responsive pass F9: Responsive/mobile pass — usable on phones and tablets #49, layered on Installable PWA baseline — manifest, icons, service worker app-shell cache (split from #50) #142)
-
UI design revamp — distinctive visual identity (UI design revamp — distinctive visual identity #52; after 1.1 dashboard/reports anchor the visual language)
-
Backup/recovery workflow
-
Farm-local dates for withdrawal restriction + allocation boundaries (Use farm-local dates for withdrawal restriction and allocation boundaries #35, PR Farm-local date boundary for withdrawal restriction and allocation (#35) #156, merged)
-
Sweep the remaining UTC date boundaries onto the farm clock (Sweep the remaining UTC date boundaries onto the farm-local clock (follow-up to #35) #155, PR Sweep the remaining date boundaries onto the farm clock (#155) #158, merged)
-
First non-English UI translation (language pack on the 1.1 i18n infrastructure) — machine-drafted es + tl packs already ship app-wide via i18n infrastructure (SPA foundation): react-i18next + resolution + string sweep (tracker) #182's translate-now switch (Spanish/Tagalog render across every screen today, marked pending native review); this item was to complete when a native-speaker review reconciled those drafts into a shippable pack. Declined by the owner, 2026-09-13: the native-speaker pass will not be done. The machine-drafted es/tl packs are what ships, and the
// machine-drafted (#182) — pending native reviewcomments now describe a permanent state rather than a scheduled one. i18n: two es/tl wording items raised in #720 review, for the native-speaker pass #738 (two es/tl wording items held for that pass) has no pass to wait for. -
Release rehearsal: day-in-the-life simulation of 10 users (all roles) with container-resource + HTTP-traffic monitoring (Release rehearsal: day-in-the-life simulation of 10 users (all roles) with container-resource + HTTP-traffic monitoring #243)
- SPA UI verification track: Playwright E2E smoke + canary-under-load over the Release rehearsal: day-in-the-life simulation of 10 users (all roles) with container-resource + HTTP-traffic monitoring #243 sim fixture (SPA UI verification track: Playwright E2E smoke + canary-under-load over the #243 sim fixture #277, sibling — shares the seeder/cast fixture)
- E2E smoke: Playwright harness + five persona flows over the sim fixture (SPA E2E smoke: Playwright harness + five persona flows over the #243 sim fixture #385)
- Canary-under-load: Core Web Vitals from browsers concurrent with the k6 baseline (SPA canary-under-load: Core Web Vitals from browsers running concurrently with the k6 baseline #386)
- CI: workflow_dispatch job for the Playwright E2E smoke suite (CI: workflow_dispatch job for the Playwright E2E smoke suite #387)
- SPA UI verification track: Playwright E2E smoke + canary-under-load over the Release rehearsal: day-in-the-life simulation of 10 users (all roles) with container-resource + HTTP-traffic monitoring #243 sim fixture (SPA UI verification track: Playwright E2E smoke + canary-under-load over the #243 sim fixture #277, sibling — shares the seeder/cast fixture)
-
Developer experience: add an Aspire AppHost for local orchestration and observability (Developer experience: add an Aspire AppHost for local orchestration and observability #565)
- CI: add Aspire-backed system smoke testing and evaluate pipeline/deployment orchestration (ci: add Aspire-backed system smoke testing and evaluate pipeline orchestration #571)
-
Deployment readiness audit: production go-live scan before first real users, host undecided (Deployment readiness audit: production go-live scan before first real users (host undecided) #244) — the 2026-07-28 review-fleet pass spawned 15 gap issues Deploy: production proxy-trust chain unsolved — silently disables #144 HSTS and the #143 per-IP login limiter #260–PWA never calls registration.update() — long-lived sessions run a stale shell #274 (7 blockers); full list + severity tracked in Deployment readiness audit: production go-live scan before first real users (host undecided) #244
- Observability: emit compact JSON to stdout in Production so logs are machine-parseable (obs(api): emit compact JSON to stdout in Production so logs are machine-parseable (#273 prerequisite) #404) — narrow prerequisite of Production log redaction + retention + cost + log sink #273's redaction/retention/sink work; stdout stays the only destination, OTLP log export is a later additive step
- Production log redaction + retention + cost + log sink (Production log redaction + retention + cost + log sink #273; PR feat(api): redact sensitive log content and emit stable security events (#273) #349, PR feat(api): a two-stage Serilog pipeline so every sink can be wrapped #426, merged)
- Background worker has no single-runner guarantee — double-runs if scaled >1 instance (Background worker has no single-runner guarantee — double-runs if scaled >1 instance #271)
- JWT signing-key rotation needs a multi-key validation ring + kid (JWT signing-key rotation needs a multi-key validation ring + kid #268)
- chore(schema): generate PostgreSQL schema documentation (chore(schema): generate PostgreSQL schema documentation #417)
-
Program.cs maintainability: group DI registration into extension methods (Group Program.cs DI/service registration into extension methods #291); extract run-then-exit CLI commands into a testable command layer (feat(api): migrate command + prod migration split (#263) + extract CLI commands into a testable layer #288)
-
idempotency_records unbounded growth: purge sweep for cached write responses (idempotency_records grows unbounded: full response body stored per successful write, no purge job #259, found by the Release rehearsal: day-in-the-life simulation of 10 users (all roles) with container-resource + HTTP-traffic monitoring #243 spec review; feeds Deployment readiness audit: production go-live scan before first real users (host undecided) #244)
-
Squash the 30 EF migrations into a single InitialCreate before first production deploy (Squash the 30 EF migrations into a single InitialCreate before first production deploy #245, split from Deployment readiness audit: production go-live scan before first real users (host undecided) #244)
-
Security: ReadOnly can read customer PII + sales financials via ungated /customers + /sales reads (Security: ReadOnly role can read customer PII + sales financials via ungated /customers and /sales reads #127)
-
Security: worker flock assignment is enforced on writes but not on reads — a restricted worker can enumerate/read unassigned flocks (Worker flock assignment is enforced on writes but not on reads — a restricted worker can enumerate and read unassigned flocks #388, found by the SPA E2E smoke: Playwright harness + five persona flows over the #243 sim fixture #385 E2E probe; same shape as Security: ReadOnly role can read customer PII + sales financials via ungated /customers and /sales reads #127)— moved to Phase 1.6, epic EPIC: Phase 1.6 — Multi-farm tenancy #530 as slice T8a (2026-08-18). Not dropped: it sits on the authorization surface 1.6 is reworking, and its premise (is read-side flock scoping intended?) is still undecided. -
Auth: configured account lockout is inert (Auth: configured account lockout is inert (CheckPasswordAsync never counts failures) #128)
-
SPA error boundary: a render-time throw is a silent blank page (SPA has no error boundary: any render-time throw is a silent blank page #140, PR Error boundary: a render throw degrades to a usable screen, not a blank page (#140) #153, merged)
-
Static asset Cache-Control headers + CDN-in-front strategy (Static asset Cache-Control headers + Cloudflare-in-front CDN strategy #141)
-
Security: rate limiting on anonymous auth endpoints (Security: rate limiting on anonymous auth endpoints (login/refresh) #143, PR F143: per-client-IP rate limiting on anonymous auth endpoints #152, merged)
-
Security: forwarded headers, HSTS, CSP + security headers, pinned AllowedHosts (Security: forwarded headers, HSTS, CSP + security headers, pinned AllowedHosts #144)
-
Security: refresh token → HttpOnly cookie, access token in memory only (Security: move refresh token to HttpOnly cookie, access token in memory only (CSRF posture included) #145)
-
Auth: concurrent multi-tab refresh trips reuse-detection (Auth: concurrent multi-tab refresh can trip reuse-detection and log both tabs out #169)
-
Auth: idempotent refresh to close the tab-death cross-tab residual (Auth: idempotent refresh to close the tab-death cross-tab residual (#169 follow-up) #176)
-
CI: dependency-vulnerability and SAST gates (CI: dependency-vulnerability and SAST gates #146, PR F146: CI dependency-vulnerability + SAST gates #184, merged)
-
CI: auto-regenerate dependent packages.lock.json on Dependabot NuGet PRs (CI: auto-regenerate dependent packages.lock.json on Dependabot NuGet PRs #203, PR feat(ci): auto-regenerate downstream packages.lock.json on Dependabot NuGet PRs (#203) #230, merged)
-
CI/build: migrate Node 22 → 24 across CI and the SPA image build (chore: migrate Node 22 → 24 (CI, security-audit, SPA image build) #275, PR chore(build): migrate Node 22 → 24 in CI and the SPA image build #276, merged)
-
Security: react-router GHSA-qwww-vcr4-c8h2 — fixed by react-router 8 migration (react-router GHSA-qwww-vcr4-c8h2 (high): replace temporary gate exception with real fix #199, PR feat(web): migrate to react-router 8.3.0 (fixes GHSA-qwww-vcr4-c8h2) #202, merged)
Spec: §6 (Phase 1.5).
Migrated from Gitea issue #15 (https://gitea.mforcelabs.com/mforce/cluckwork/issues/15).
- UX: Sales quantity + integer-count inputs adopt the NumberField −/+ stepper with a min floor (Sales quantity fields: adopt the NumberField −/+ stepper (and roll it out to the other integer-count inputs) #250, PR feat(web): NumberField min prop + stepper rollout to integer-count inputs (#250) #256, merged)
Phase 1.1 carryover — moved from #14 on epic close (2026-08-11)
#14 (Phase 1.1) closed complete — every item in specs.md §6's Phase 1.1 scope shipped. These are follow-on work discovered while shipping it, not part of that original scope; three (#438, #511, #512) were open but untracked in any epic before this cleanup.
- Audit: entity-scoped "View history" — who created/changed a specific record, and when (Audit: entity-scoped "View history" — who created/changed a specific record, and when #493)
- Seeded audit events carry "(unresolved)" as the actor, visible on the Show "created by / when" and "last changed by / when" inline on record pages #494 History columns (fix(seed): seeded audit events carry "(unresolved)" as the actor, now visible on record History columns #500)
- Order same-instant audit events by a durable monotonic key, not a random Guid (fix(api): order same-instant audit events by a durable monotonic key, not a random Guid #508) — shipped in PR fix(api): order same-instant audit events by a durable monotonic key #700
- Do not partition
AuditEventsby time — decision recorded on the issue (no partitioning); only live follow-up is keyset pagination on the Audit page's ownOFFSETpaging, if it ever shows up in practice (chore(infra): do not partition AuditEvents by time — record why, and what to do instead #505) - Users: change a user's email address — follows Users: disable / re-enable a user #356 (shipped) (Users: change a user's email address #357; PR feat: let owners change user email addresses #605, merged)
- Step-up (Require recent step-up authentication for privileged user administration #308) gates only Owner creation — a stolen Owner token still buys durable access; needs a design decision among 4 documented options (Step-up (#308) gates only Owner creation — a stolen Owner token still buys durable access #360; PR fix(auth): require step-up for durable user access (#360) #607, merged)
- Require step-up for durable flock-scope changes — flock-scope boundary deliberately deferred from Step-up (#308) gates only Owner creation — a stolen Owner token still buys durable access #360 (Require step-up for durable flock-scope changes #606; PR fix: require step-up for flock scope changes #609, merged)
- Sales: an abandoned attempt's success hijacks the dialog session that replaced it (Sales: an abandoned attempt's success hijacks the dialog session that replaced it #477) — shipped in PR fix(web): an abandoned order attempt's success no longer hijacks the dialog that replaced it #702
- SPA: generalise the abandoned-success guard — shared action hook for all dialog screens (SPA: generalise the abandoned-success guard — shared action hook for all dialog screens (follow-up to #477) #703) — follow-up to Sales: an abandoned attempt's success hijacks the dialog session that replaced it #477; PR 1 (hook + Sales) shipped in refactor(web): extract SalesPage's dialog-write wrapper into a shared useDialogAction hook (#703) #704; PR 2 (Customers/Daily Entry/Flocks/Grades/Products) shipped in fix(web): an abandoned dialog attempt's success no longer hijacks the replacement on Customers, Daily Entry, Flocks, Grades and Products (#703) #705; PR 3 (Inventory/Expenses/History/Stock) shipped in fix(web): an abandoned dialog attempt's success no longer hijacks the replacement on Inventory, Expenses, History and Stock (#703) #706; PR 4 (the hook's
{ dialog }+startLoad, Users) shipped in fix(web): an abandoned edit's success no longer hijacks the dialog that replaced it on Users (#703) #710; PR 5 (Sales panel callers) shipped in fix(web): keep Sales order panels closed after pending writes #711; PR 6 (type session-edge helpers toS+ fail-open decision record) shipped in fix(web): constrain dialog session helpers to declared scopes #715; residual guards test(web): pin Stock write-off's clearKey-before-refresh ordering (#703 PR 3 residual) #708/test(web): pin History void→adjust cross-scope close on the success path (#703 PR 3 residual) #709 shipped in test(web): pin the two #703 PR 3 residual guards (Stock key rotation, History void→adjust close) #714 — COMPLETE - SPA: pin Stock write-off's clearKey-before-refresh ordering (test(web): pin Stock write-off's clearKey-before-refresh ordering (#703 PR 3 residual) #708) — SPA: generalise the abandoned-success guard — shared action hook for all dialog screens (follow-up to #477) #703 PR 3 test residual
- SPA: pin History void→adjust cross-scope close on the success path (test(web): pin History void→adjust cross-scope close on the success path (#703 PR 3 residual) #709) — SPA: generalise the abandoned-success guard — shared action hook for all dialog screens (follow-up to #477) #703 PR 3 test residual
- Sales: Close can lose to the primary Open read (Sales: Close loses to an in-flight order Open read #712) — pre-existing read race, separate from SPA: generalise the abandoned-success guard — shared action hook for all dialog screens (follow-up to #477) #703 PR5 writes
- Sales: reconcile live line-editor state after a refetch or order-status change (Sales: reconcile the live line editor after refetch and status changes #713) — pre-existing behavior found during SPA: generalise the abandoned-success guard — shared action hook for all dialog screens (follow-up to #477) #703 PR5 review
- PWA/shell: verify the inert-swallowed announcements with a real screen reader — needs a human with NVDA/JAWS/VoiceOver, not agent-doable (PWA/shell: verify the inert-swallowed announcements with a real screen reader #501)
- Five screens silently truncate the flock list at 100, ordered by name — closed 2026-09-01 as covered by Add a reusable searchable paged entity picker for flock and customer selectors #512/PR feat: add searchable entity pickers #642: reachability, row-owned names and row-owned editability all land there; the residual capped-list DEFAULTS are Three screens pick a default flock from the capped flock list #646 (Five screens silently truncate the flock list at 100, ordered by name #509)
- Cross-tab login/refresh race can still restore the wrong session — fix(spa): a superseded refresh can land its Set-Cookie over a newer login (#310 follow-up) #393 follow-up (Cross-tab login/refresh race can still restore the wrong session (#393 follow-up) #438)
- Page truncated customer and movement tables with the shared usePagedList hook (Page truncated customer and movement tables with the shared usePagedList hook #511) — SPA half merged 2026-08-31 (PR fix(web): page truncated customer and movement tables with usePagedList #628,
7cfe4d6f); fixture half is Simulation fixture: over-cap bands for customers, bird movements and inventory movements (split from #511) #627 - Simulation fixture: over-cap bands for customers, bird movements and inventory movements — split from Page truncated customer and movement tables with the shared usePagedList hook #511 on 2026-08-30; carries Page truncated customer and movement tables with the shared usePagedList hook #511's fixture + Playwright half (Simulation fixture: over-cap bands for customers, bird movements and inventory movements (split from #511) #627) — merged 2026-08-31 (PR feat: certify over-cap simulation fixture bands #633,
a67b2e1f) - Add a reusable searchable paged entity picker for flock and customer selectors — merged 2026-09-02 (PR feat: add searchable entity pickers #642,
60d2053c); closed Five screens silently truncate the flock list at 100, ordered by name #509 too (Add a reusable searchable paged entity picker for flock and customer selectors #512) - Three screens pick a DEFAULT flock from the capped flock list — Water/Users/DailyEntry; split from Five screens silently truncate the flock list at 100, ordered by name #509 on close, nothing unreachable (Three screens pick a default flock from the capped flock list #646)
- usePagedList: a write issued during an in-flight load-more can shrink the loaded window — merged 2026-09-02 (PR fix(web): keep a paged window and an item panel on the user's newest intent #645,
d81bccf7); the same PR also retired the previous filter's window depth at replacement START, a pre-existing defect the CodeRabbit round found (usePagedList: a write issued during an in-flight load-more can shrink the loaded window #629) - InventoryPage: a write's refresh re-activates its own item, yanking a user who has navigated elsewhere — merged 2026-09-02 (PR fix(web): keep a paged window and an item panel on the user's newest intent #645,
d81bccf7);refreshAllreads the open item at refresh time and the capturedopenItemIdis gone fromrun(InventoryPage: a write's refresh re-activates its own item, yanking a user who has navigated elsewhere #630) - InventoryPage: the lots-read ticket has no test — disabling it reddens nothing — merged 2026-09-02 (PR test(web): pin the lots-read ticket and aim the row-lookup lint at each screen's real page size #647,
3870acd0); the catch arm turned out to be pinned already, so it was one test, not two (InventoryPage: the lots-read ticket has no test — disabling it reddens nothing #631) - Paged-row-lookup lint detects only a literal 100-row fixture — merged 2026-09-02 (PR test(web): pin the lots-read ticket and aim the row-lookup lint at each screen's real page size #647,
3870acd0); the lint was blind on all five 50-row screens, not latent as filed, and found two live instances on the way in (Paged-row-lookup lint detects only a literal 100-row fixture #637) - Generated screenshots of the running SPA in the README — captured from the
tools/simulation/ui/fixture, no CI gate (docs: generated screenshots of the running SPA in the README #549, follow-on from docs: split the README into audience-scoped docs and adopt repo-template scaffolding #548) -
Replace(chore(docs): replace toc.py's hand-rolled markdown scanner with a real parser #553 — declined 2026-09-05: the fix costs the repo's first unscanned dependency ecosystem, pip, to remove a cost that only comes due when the file is edited; reopen triggers recorded on the issue)tools/docs/toc.py's hand-rolled markdown scanner with a real CommonMark parser -
seed --profile simulationcannot validate aboveHistoryDays≈107 — the lock sweep is batched at 200 and the seeder runs it once (fix(seed): simulation profile cannot validate above HistoryDays≈107 — lock sweep runs once, batched at 200 #638, found while writing the dev-database fixture runbook) - SPA: route-based code splitting — all 25 pages ship eagerly in one chunk (SPA: route-based code splitting — all 25 pages ship eagerly in one chunk #595, found while scoping SPA: self-guided product tour / first-run tutorial #596)
-
SPA: per-locale i18n splitting — all 3 language catalogs ship eagerly— deferred, not current priority (SPA: per-locale i18n splitting — all 3 language catalogs ship eagerly, conflicts with the no-flash init invariant #597, split from SPA: route-based code splitting — all 25 pages ship eagerly in one chunk #595; needs a design pass on the no-flash init invariant before pickup) - SPA: self-guided product tour / first-run tutorial (SPA: self-guided product tour / first-run tutorial #596, feature idea; depends on SPA: route-based code splitting — all 25 pages ship eagerly in one chunk #595)
- SPA look-and-feel pass (design review 2026-09-02) — eight slices, do in this order:
- Typeset numbers as numbers — right-aligned tabular columns, grouping, locale money/date (SPA: typeset numbers as numbers — right-aligned tabular columns, digit grouping, locale currency and date formatting #650; PR feat(web): typeset numbers as numbers and refresh the Help glossary (#650, #657) #658, merged 2026-09-02)
- Retire tracked all-caps labels outside the nav group dividers (SPA: retire tracked all-caps labels outside the nav group dividers #652)
- Elevation hierarchy — one card treatment no longer shared by stat/panel/toolbar/pane (SPA: elevation hierarchy — stop using one card treatment for stat cards, panels, toolbars and entry panes #651)
- Table layout — provenance to one line, date-range filters into a bounded toolbar (SPA: table layout — provenance column to one line, date-range filters into a bounded toolbar #653)
- Dashboard rework — capture-status tiles, 14-day sparkline, stock stacked bar (SPA: dashboard rework — capture-status tiles, 14-day sparkline, stock as a stacked bar #654)
- Empty states as invitations (SPA: empty states as invitations — icon, one sentence, the primary action #655)
- Help page + glossary refresh — grouped, searchable, deep-linkable (SPA: Help page + glossary refresh — grouped, searchable, deep-linkable definitions; guide reordered around the tasks people come for #657; folded into PR feat(web): typeset numbers as numbers and refresh the Help glossary (#650, #657) #658, merged 2026-09-02)
- Visual identity — brand-derived links, display optical size, two-colour mark; owner decisions first (SPA: visual identity — brand-derived link colour, display optical size, two-colour mark (owner decisions) #656)
- Customers are create-only — no way to fix a typo'd phone or a changed address; needs
Version+ a migration first (Edit an existing customer (name, phone, email, address, note) #625; PR feat(customers): edit existing customer details (#625) #626, merged)
Security hardening follow-up — 2026-07-31
- Public-deployment security follow-up from Deployment readiness audit: production go-live scan before first real users (host undecided) #244
- Atomic multi-replica HTTP idempotency (Make API write idempotency atomic across replicas #307)
- Regression: IdempotencyMiddleware 500s on every 204 response under Kestrel (fix(api): IdempotencyMiddleware 500s on every 204 response under Kestrel (regression from #307) #340; Make API write idempotency atomic across replicas #307 swapped an empty-buffer
CopyToAsyncforWriteAsync(""), which TestServer permits and Kestrel rejects)- Close the transport gap fix(api): IdempotencyMiddleware 500s on every 204 response under Kestrel (regression from #307) #340 exposed — the suite is TestServer-only, so Kestrel-enforced behaviour is structurally invisible (PR fix(api): never write a body on 204/205/304 in IdempotencyMiddleware (#340) #341 added the first Kestrel-backed factory)
- Exercise request-body caps at the Kestrel transport layer (test(api): exercise request-body caps at the Kestrel transport layer (follow-up to #340) #342;
IHttpMaxRequestBodySizeFeatureis absent under TestServer, andRequestBodyLimit's!HasStarted413 recovery rests on the same assumption that hid fix(api): IdempotencyMiddleware 500s on every 204 response under Kestrel (regression from #307) #340)- IdempotencyMiddleware buffers the full body before FarmLogoEndpoints/ClientErrorEndpoints can lower Kestrel's transport cap (IdempotencyMiddleware buffers the full body before FarmLogoEndpoints/ClientErrorEndpoints can lower Kestrel's transport cap #442; found while writing test(api): exercise request-body caps at the Kestrel transport layer (follow-up to #340) #342's tests — the farm logo upload's own cap-lowering silently no-ops behind idempotency's buffering read)
- Verify forwarded-header trust + per-IP limiting against a real socket peer (test(api): verify forwarded-header trust and per-IP limiting against a real socket peer (follow-up to #340) #343; TestServer has no peer, so the untrusted-
X-Forwarded-Fornegative — a limiter bypass — is never demonstrated) - Verify HSTS + HTTPS redirection over real TLS, or record what is unverifiable (test(api): verify HSTS and HTTPS redirection over real TLS, or record what is unverifiable (follow-up to #340) #344)
- Exercise request-body caps at the Kestrel transport layer (test(api): exercise request-body caps at the Kestrel transport layer (follow-up to #340) #342;
- Close the transport gap fix(api): IdempotencyMiddleware 500s on every 204 response under Kestrel (regression from #307) #340 exposed — the suite is TestServer-only, so Kestrel-enforced behaviour is structurally invisible (PR fix(api): never write a body on 204/205/304 in IdempotencyMiddleware (#340) #341 added the first Kestrel-backed factory)
- Regression: IdempotencyMiddleware 500s on every 204 response under Kestrel (fix(api): IdempotencyMiddleware 500s on every 204 response under Kestrel (regression from #307) #340; Make API write idempotency atomic across replicas #307 swapped an empty-buffer
- Recent step-up authentication for privileged user administration (Require recent step-up authentication for privileged user administration #308)
- Add TOTP + WebAuthn/passkey step-up factors, follow-up to Require recent step-up authentication for privileged user administration #308 (Passkeys: sign in without a password #320)
- Step-up grant registry to a shared store before >1 replica, follow-up to Require recent step-up authentication for privileged user administration #308 (Move the step-up grant registry to a shared store before running >1 replica (follow-up to #308) #338)
- Authentication body and credential input bounds (Bound authentication request bodies and credential inputs before password hashing #309)
- Browser login/logout/refresh session-generation safety (Prevent late refresh responses from resurrecting or overwriting SPA sessions #310)
- Bounded production report workload and concurrency (Bound production report workload, query size, and concurrency #311)
- Origin private/no-store policy for API and exports (Set private no-store cache policy on authenticated API and export responses #312)
- Tenant-scoped pessimistic row locks (Tenant-scope pessimistic lock reads before acquiring database row locks #313)
- Clear Create User plaintext password state on close (Clear Create User plaintext password on every dialog close path #314)
- Deterministic locked NuGet restore inside Docker (Restore NuGet packages in Docker with committed lock files and locked mode #315)
- Production OTLP HTTPS and endpoint redaction guard (Require secure and redacted OTLP endpoint configuration in Production #316)
- Ephemeral test JWT keys and sensitive-artifact ignore hygiene (Generate test JWT keys at runtime and exclude sensitive artifacts from Git and Docker contexts #317)
- Fail-closed design-time migration connection configuration (Fail closed when the design-time migration connection is unset or insecure #318)
- Production AllowedHosts boot guard (Fail Production boot when AllowedHosts is wildcard or unset #319)
- Atomic multi-replica HTTP idempotency (Make API write idempotency atomic across replicas #307)
Any host-specific companion work is tracked separately in a private deploy/infra tracker.
- PostgreSQL 18 container baseline: reference/dev/CI/Testcontainers, PG18 volume-path safety, and patched digest (Upgrade pre-launch PostgreSQL baseline from 16 to 18 #321, PR Update PostgreSQL container baseline to 18.4 #325, merged)
Pre-launch schema freeze — sequencing for #245
#245 squashes the 30 EF migrations into a single InitialCreate. It only pays off once the
schema stops moving, so every schema-affecting issue that is a first-launch requirement lands
first, then a short freeze, then #245 as the final squash.
Schema-affecting work scheduled before the freeze:
- Base provisioning: static ref-data via
InsertData+MustChangePasswordfirst-run admin (Base provisioning: static ref-data into schema + secure first-run admin (generated secret + MustChangePassword) — retire Seed:* config #283)- First-run discoverability: a sign-in with no admin says so, from the login attempt (Report first-run 'no accounts yet' from the login endpoint instead of a polled endpoint #361)
- Refresh-token retention sweep + covering index for the delete predicate (Purge expired refresh_tokens (retention sweep; sibling of #259) #270)
- Atomic multi-replica HTTP idempotency — claim/lease state (Make API write idempotency atomic across replicas #307, tracked above)
Deferred past the freeze (schema-affecting but not first-launch): TOTP/WebAuthn credential
storage (#320, blocked by #308), and personal-data retention/erasure (#272, market-dependent).
- Personal-data export / erasure / retention policy — GDPR + PH DPA (Personal-data export / erasure / retention policy (GDPR + PH DPA) #272; deferred past the freeze, market-dependent)
#318 is deliberately sequenced before this batch: it secures the EF design-time connection
factory and changes no schema, so it should land before more migrations are generated.
- Declare the schema freeze once the batch above is merged
- Squash to a single
InitialCreate(Squash the 30 EF migrations into a single InitialCreate before first production deploy #245)
Sequenced AFTER the squash
-
Extract
Cluckwork.Clilibrary + explicitProcessRolefor boot guards (refactor(api): make process role explicit for boot guards #347) — one binary/one image kept deliberately; a separate console app was considered and rejected (image coupling with theHEALTHCHECK/migrateentrypoints, and host/config duplication riskingmigrateinterpreting a connection string differently from the serving process). Motivated by the recurring "serving-process guard aborts a one-shot verb" class: fix(api): require secure and redacted OTLP endpoint configuration in Production (#316) #331, Deploy: production proxy-trust chain unsolved — silently disables #144 HSTS and the #143 per-IP login limiter #260, Require secure and redacted OTLP endpoint configuration in Production #316. -
AGENTS.md: three conventions earned while shipping #651 + #652 #662 — AGENTS.md: three conventions earned while shipping SPA: elevation hierarchy — stop using one card treatment for stat cards, panels, toolbars and entry panes #651 + SPA: retire tracked all-caps labels outside the nav group dividers #652
-
docs/images/dashboard.png is generated by the screenshot spec but never committed #663 — docs/images/dashboard.png is generated but never committed
-
SPA: visual check of #651/#652 in night mode and the three non-default farm palettes #664 — visual check of SPA: elevation hierarchy — stop using one card treatment for stat cards, panels, toolbars and entry panes #651/SPA: retire tracked all-caps labels outside the nav group dividers #652 in night mode and the non-default farm palettes
-
SPA: Audit has no date-range filter, and an audit trail is the screen that most needs one #666 — Audit has no date-range filter
-
SPA: Expenses filters by a single month, where every sibling screen filters by a date range #667 — Expenses filters by month where siblings filter by range
-
SPA revamp: implement the chosen direction — field-first phone (B), ledger desktop (A), one attention line (C) #674 — SPA revamp: field-first phone (B) + ledger desktop (A) + one attention line (C); direction chosen on SPA: visual identity — brand-derived link colour, display optical size, two-colour mark (owner decisions) #656 (2026-09-02); design doc → grill → slices, each with a 1:1 before/after render
-
Entity picker input indistinguishable from its options; opening does not focus or select #735 — Entity picker input indistinguishable from its options; opening does not focus or select (11 instances / 7 screens)
-
Sales: the Orders list cannot show which orders are unpaid #769 — Sales: the Orders list cannot show which orders are unpaid; F21: Customer payments + balances — order-attached payments, outstanding per order/customer (admin-only) #89 shipped outstanding on the order panel and the Customers page but never on the list, so "who still owes me" needs opening each order. Sales tier only, Workers excluded; unpaid means outstanding > 0
-
Sales: an old order's missing list price reads as "the product had no price" #773 — Sales: an old order's missing list price reads as "the product had no price";
ListPriceBasisalready separatesPreDatingfromProductUnpriced/NotComparable, but the basis never reaches the JSON API so the screen renders all three alike -
Backend: measure test coverage, report before gating #776 — Backend: measure test coverage, report before gating. Nothing measures the 2584 backend tests today, so "what is untested" has no data. Report only first; a floor picked before anyone has seen a number is a guess. Blocks CI: measure where Build and test's 10 minutes go, and what the backend suite covers #775
-
CI: measure where Build and test's 10 minutes go, and what the backend suite covers #775 — CI: measure where
Build and test's 10 minutes go, and what the backend suite covers. Blocked by Backend: measure test coverage, report before gating #776 -
Production report cannot distinguish a day with no entry from a day that produced zero eggs #780 — Dashboard: the production report cannot tell a day with no entry from a day that produced zero eggs, so the 14-day strip drew both as an empty slot. Split out of Dashboard: Recent sales rows do not form columns, and the trend and stock charts are hard to read #777
-
E2E suite has no phone viewport, so nothing automated has ever rendered the app at phone width #814 — E2E suite has no phone viewport, so nothing automated has ever rendered the app at phone width; the product's primary context is a phone in a hen house (SPA revamp: implement the chosen direction — field-first phone (B), ledger desktop (A), one attention line (C) #674's direction B). Gate only, not the layout fix
-
Dashboard recent-sales row overflows the viewport at phone width #816 — Dashboard recent-sales row overflows at phone width— WITHDRAWN, not a defect: measured against a 28-hour-old sim image built before fix(dashboard): give recent sales real columns and make both charts readable #781 (7193ebe), which had already fixed it. Rebuilt it measures 390/390./is now walked by E2E suite has no phone viewport, so nothing automated has ever rendered the app at phone width #814's phone overflow guard instead.
Activity
Spec: multi-language support phasing — i18n infra in 1.1, first trans…
test(api): fix logo test's race claim; correct it's deterministic, no…
test(api): exercise request-body caps at the Kestrel transport layer (#…
docs(agents): update phase context now that epic #14 is closed (#515)
mforce commentedon Sep 13, 2026
Owner instruction, 2026-09-13: no new slices go on this epic. Phase 1.5 is considered done for the purpose of taking on new work.
Recording it here so agents planning future work do not attach to this phase by default —
AGENTS.mdsays slice issues go on the phase epic's checklist, and without this note the obvious reading is that 1.5 is the current phase and therefore the right home.Concretely, the MCP server support epic (#789) and the OAuth authorization server epic (#788), designed 2026-09-12/13, are deliberately not attached to this epic. They are navigated by their own
epic-789/epic-788labels. Attaching them was considered and rejected: this phase is "egg product hardening" and neither is that.What is still open here, unresolved on purpose
14 issues still carry
epic-1.5, four of thempriority:tier1:kidPlus #182 (tier2, i18n infrastructure) and eight tier3/tier4 items: #50, #320, #501, #571, #596, #597, #656, #738.
The disposition of these is explicitly deferred, not decided. The owner was asked whether to re-home them and chose to decide later. So this epic is in a deliberate in-between state: closed to new work, with a backlog still attached.
Anyone picking up deployment-readiness work should look here rather than assuming it is finished — "done with 1.5" means done adding to it, not that everything under it shipped. The four tier1 items above are the ones most likely to be missed, because the phase reads as complete.
mforce commentedon Sep 13, 2026
Closing Phase 1.5 — parked, not finished
Closed by the owner on 2026-09-13 during the issue cleanup. This is a deliberate park, not a
completion claim. The epic has been open for two months, 96 of its 125 checklist items are done,
and the remaining 29 are a mix of shipped-but-unticked, work that moved elsewhere, and scope
nobody has scheduled. Leaving it open made all three look like the same thing — work in flight.
Per the repo's own rule: a criterion nobody will implement is either a follow-up issue or a
recorded won't-fix, never a bullet quietly left to rot in a closed issue. So every unchecked
item is dispositioned below.
The milestone
Phase 1.5 — Hardeningstays open and is now the tracker. Closing this issueremoves the navigation surface, not the work.
Already shipped — four stale checkboxes
These read as open above and are not:
AGENTS.mdrecords it.AuditEventstime partitioning — closed as a recorded decision (no partitioning).Unbuilt and unissued — the nine core hardening bullets
Legacy import wizard · Inventory reconciliation · Email digest for critical alerts · Packaging
inventory · Additives/supplements · Vaccination records · Flock profitability allocation rules ·
Backup/recovery workflow.
None of these has ever had an issue. They are
specs/product/specs.md§6 scope written asbullets, and a bullet cannot be picked up, sized or routed. Filing eight issues for work nobody
intends to start this quarter would trade one kind of noise for another, so they stay in the spec
and stay unfiled. Read this as: not scheduled, and not being worked. When one of them becomes
real, file it against the milestone.
The ninth, the alert centre, was filed as #803 — the only one of the nine that something
already-scheduled (#728) was blocked on.
Moved or superseded
AuditEventskeyset pagination (chore(infra): do not partition AuditEvents by time — record why, and what to do instead #505's residual) — noted as a follow-up only ifOFFSETpaging shows up in practice. It has not.Still open, tracked by the milestone and their tier labels
tier1 — production readiness, reviewed next: #244 (go-live audit, 15 spawned gap issues) ·
#268 (JWT key-rotation ring) · #272 (GDPR + PH DPA) · #344 (HSTS over real TLS).
Unlabelled, pending the standalone review: #803 (alert centre) · #775 (CI wall clock) ·
#729 (Owner-only farm config) · #674 (SPA revamp) · #656 (visual identity) · #738 (es/tl wording).
Epic #719 (discounts) stays open on its own: 7 of 9 slices shipped, #725 and #726 unscoped,
#728 waiting on #803, #730 deferred to tier4.
tier4 — deferred or speculative: #50 · #320 · #501 · #571 · #596 · #597.
Nothing above is closed by this. Reopen this epic only if Phase 1.5 is resumed as a coordinated
phase rather than as individual issues.