Plan: craig-intake Portal — Phase 5 (Remove craig-web /report)
On this page
In one line: delete craig-web’s now-redundant public /report* flow; production routes /report to craig-intake
via path-based ingress (no redirect — constraint L6). This is the last phase of epic &60 — finishing it closes the
epic.
|
Erratum (2026-06-27) — gated on a parity remediation
A pre-deletion audit (the edge-gap rule) found L5 was not fully met: Phase 3 scoped "parity" as i18n + a11y + wizard
+ attachments, but the edge’s NONE/cases public form is a reduced field subset of craig-web’s — missing the 911
safety banner, child Update (2026-06-27): the remediation is COMPLETE — R0 #742, R1 #741, R2 #739, R3 #740 all landed (the edge None/cases
form now captures craig-web’s full public field set, POST-body-asserted by the |
|
Constraint legend (the consolidation program’s locks; all satisfied)
L5 — i18n + a11y parity (met by Phase 3). L6 — |
Context — the gap
Phases 1–4 made craig-intake (the edge) the single themed public portal:
-
P1 — per-jurisdiction theming reaches the edge.
-
P2 — integration aids (debug panel, view-keys).
-
P3 — full i18n + a11y + wizard + attachments parity, with the UI mounted in integrated mode (P3.6 / #721).
-
P4 — server-side mandated signing.
craig-web still serves the original public /report* flow — now a duplicate. ADR-043 (Decisions 4–5) says: once
parity (L5) and the integrated mount (L7) land, craig-web’s /report* is deleted outright, and /report reaches
the edge by ingress path-routing (L6). Those gates are met, so Phase 5 is unblocked. (Integrated individual signing
stays a deferred global-L4 follow-up, #731 — explicitly not a Phase-5 blocker.)
Scope
Delete (all verified public-report-only)
-
Routes:
public_report_routes()(craig-web/src/main.rs:305-334, merged l.944) +src/routes/report.rs(10 handlers). -
Templates:
templates/report/{form,confirmation,status_form,status,not_found,keys}.html+templates/report_base.html. -
Static JS:
static/js/{report-wizard,key-registration,craig-sign}.js. -
The public-report-only i18n keys (a set-difference — see the procedure in Design).
Critical rewrites — do these BEFORE deleting (a naive delete breaks the build)
-
Repoint
http().BffClients::http()(src/clients/mod.rs:136-140) returns the shared transport viaself.intake.http(), and the OIDC callback (src/auth.rs:312) callsstate.clients.http()— so deleting theintakefield first breaks login. Repointhttp()toself.cases.http()(the cases client always survives; every client clones the sameTransport), then remove theintakefield +intake_url+CRAIG_WEB__INTAKE_URL. -
i18n is a set-difference, not a prefix-wipe. Many
report-/status-keys incraig-web/locales/en/web.ftlare used by the surviving caseworker intake-review templates (templates/intake/*). See the exact procedure in Design.
Status
| Step | Description | Status |
|---|---|---|
P5.1 strip |
Delete craig-web’s public |
Done (2026-06-28) — craig-web |
P5.2 final |
Archive both plans (Phase 5 + the consolidation program); flip P5 → Done; close epic &60; Plan Completion Audit |
Done (2026-06-28) — this MR: the consolidation program + Phase 5 + the parity-remediation plans |
Epic: &60 · Sequencing: P5.0 plan MR → P5.1 strip (atomic) → P5.2 final. P5.1 is atomic — the
public-report* e2e specs 404 the moment public_report_routes() is removed, so the strip and the e2e retarget cannot
be split across MRs. One feature/{desc} branch + MR per issue; full pre-push validate + a fresh J1–J8 pass; #676
keepalive on push.
Design
|
P5.1 as-built — preflight-verification corrections (2026-06-28)
A preflight verification pass against current
|
P5.1 — Strip the surface (atomic: code + e2e + operational docs)
Code deletion.
-
src/main.rs— removepublic_report_routes()+ its.merge(…);src/routes/mod.rs— drop thereportmodule. -
Delete
src/routes/report.rs,templates/report/(6),templates/report_base.html, and the 3 static JS files. -
static/css/components.css— delete the dead "PUBLIC REPORT WIZARD" block (.public-wizard-*/.public-footer, ~l.1129-1148) — it backedreport_base.htmlonly and is dead once the templates go.
Critical rewrites (before deletion).
-
src/clients/mod.rs— repointhttp()toself.cases.http(); then remove the whole intake client: thepub(crate) mod intake;(l.39), thepub(crate) use intake::IntakeClient;re-export (l.57), theintake: IntakeClientfield + its// ── intake ──comment (l.102-103), and the constructor wiring (l.181). Delete the filesrc/clients/intake.rs(87 lines). -
Config (3 sites — all of them): remove
intake_urlfromsrc/config.rs(WebSettings) and its env bindings:.env.example(l.228CRAIG_WEB__INTAKE_URL=),docker-compose.yml(l.709, the craig-web service env — not the e2eCRAIG_INTAKE_URLat l.838), and thedeployment-guide.adocenv table row (l.350). -
Cargo.toml— removecraig-intake-contracts(verified: its only use is the deletedreport.rs). Ifcargo build -p craig-webflags another use, revert that line and file a follow-up rather than chase it here.
Stale comments. Update the two comments that name the removed surface: tests/i18n_coverage.rs (l.6-11, describes the
craig-web /report wizard — reframe to the edge's public form, which still uses the embedded public catalog) and
src/middleware.rs (l.39, names public_report_routes).
i18n set-difference (the exact procedure). For each key in the report- and status- namespaces of
craig-web/locales/en/web.ftl, search the surviving templates (services/craig-web/templates/ minus the deleted
report/ + report_base.html — primarily templates/intake/) for that exact key — e.g.
grep -rn 'report-kv-suggestion' services/craig-web/templates/intake/. Keep the key if any surviving template
references it; delete it from web.ftl otherwise. (Match whole keys, not prefixes — a key may be report-form-only even
when a sibling in its namespace is caseworker-shared.) The caseworker-shared keep-set is known to include report-kv-,
report-detail-, report-children-col-, report-adults-col-, report-attachments-col-, report-suggest-,
report-priority-, and the status-{open,closed,pending,pending-review,screened-in,screened-out} lifecycle values.
*Do NOT touch crates/craig-i18n/catalog/en/public.ftl — that shared catalog is what the edge renders the public
form from, and it survives. Guard: cargo nextest run -p craig-web (the Askama compile fails on a deleted-but-still-
referenced key; i18n_coverage.rs proves every surviving template key resolves) + a grep proving no surviving template
references a removed key.
E2E — functional (public project).
-
Audit first: read
tests/e2e/specs/intake-embedded-ui.spec.ts(the edgeintake-uiproject — same form binary) and list which of these behaviors it already asserts: 911-warning visibility, concurrent submissions get distinct ids, the max-10 children/adults UI cap, add/remove adults. The craig-web source assertions live intests/e2e/specs/public-report.spec.ts(911 warning; concurrent-distinct-ids) andpublic-report-form.spec.ts(max-10; add/remove) — locate them there. -
Port the genuine gaps (only the behaviors the edge spec lacks) into
intake-embedded-ui.spec.ts. Edge-gap rule (the deletion gate, NOT "file and move on"): if porting reveals the edge does not implement a behavior craig-web had, P5.1 is blocked — deleting craig-web would regress it and break L5 parity. Resolve it FIRST, one of two ways: (a) close the gap on the edge in a preceding MR (filed as its ownfix:issue) and verify green, then proceed; or (b) make an explicit, recorded decision (in this plan + the MR) that the behavior is no longer required. Never delete the only working implementation while leaving the gap open. -
Then delete:
tests/e2e/specs/public-report.spec.ts+public-report-form.spec.ts; remove the entireprojects[name='public']object fromtests/e2e/playwright.config.ts; drop craig-web public-report screenshots from the screenshot spec/corpus.
E2E — accessibility (accessibility project). tests/e2e/specs/accessibility-audit.spec.ts audits both
craig-web’s public form (the BFF baseURL — /report l.97, the active/done hover axe runs l.107/114, /report/status
l.118, /report/keys l.122) and the edge ({NONE_URL}/report*, l.157-164). Delete the craig-web public audit
blocks (they 404 after the route removal); the edge audits already cover the public form’s a11y, so this is a removal,
not a port. (#725’s hover-contrast axe runs at l.107/114 target the deleted wizard — they go too.)
E2E — security/ZAP. tests/security/zap-public-web.yaml includes http://craig-web:8080/report. (l.8) in the
craig-web public-web scan. craig-web no longer serves it → *remove that include, and retarget the public-portal DAST
coverage to craig-intake (the edge is the public portal now) so the public surface stays scanned. Add the relevant
security verification to the MR.
Operational docs (ship with the deletion).
-
CHANGELOG.adoc— a== Unreleasedentry in the existing type-prefixed heading style (=== chore(web): remove the public /report flow (#NNN) (YYYY-MM-DD)); the body states the pre-1.0 breaking removal (craig-web no longer serves/report*; the edge does, via ingress path-routing). -
deployment-guide.adoc— a new "Public report routing" section: one public hostname; the ingress path-routes/report+/report/→ craig-intake (integrated, container port 8008), everything else → craig-web; an illustrative nginx *and k8s-Ingress snippet; explicitly no redirect (L6). -
architecture.adoc(thePUBLIC -→|/report| …edge),services.adoc(craig-web BFF no longer serves the public form),intake-standalone-architecture.adoc("removed later, in Phase 5" → "removed in Phase 5"). -
ADR-043 — append a "Phase 5 as-built" annotation block (do not reuse the existing §5 heading): craig-web
/report*deleted; ingress-routing posture realized; L6 satisfied. -
roadmap.adoc(l.358) — minor: the historical e2e checklist still namespublic-report*.spec.ts; reconcile to reflect the edge specs (a stale-reference cleanup, not load-bearing).
P5.2 — Final MR (program completion: archive + close epic &60)
-
Archive both plans via
git mv→plans/archive/: this Phase 5 plan and the consolidation program plan craig-intake-portal-consolidation.adoc (the program is complete once P5 lands). Repoint every inbound xref to the archived paths (ADR-043,nav.adoc, cross-plan links); drop both Active nav entries; add bothplans/archive.adocFeatures rows.cargo xtask check-docs(doc-pointer-integrity) is the gate. -
Flip the consolidation P5 row →
Done (YYYY-MM-DD)and past-tense the program intro ("Phase 5 complete; epic &60 done"). -
.claude/CLAUDE.md§ Project status — epic &60 complete (all 5 phases); #731 (integrated individual signing) + #723 (partner-key home) remain as tracked non-children. -
Close epic &60 (
state_event=close; the epic-notes-404 workaround = PUT the description); ensure every P5 child is ticked. -
Plan Completion Audit — a fresh Explore subagent over both archived plans (catch stale "this MR" / forward-tense / Design↔as-built drift) before the archive merges.
Definition of done — the acceptance bar for closing epic &60
L6 routing is operator-owned (no in-repo ingress; ADR-017/L1 keep that a deployment concern), so production
path-routing cannot be a repo gate. Epic &60 closes on the in-repo deliverables being complete + verified:
(1) craig-web’s public /report* deleted and all gates green (craig-web compiles, auth/accessibility/edge e2e
pass, ZAP retargeted); (2) the edge serves /report in integrated mode (already true, L7); (3) the deployment-guide
documents the ingress path-routing as a required operator cutover step, and the CHANGELOG flags the breaking change
(an operator who upgrades without adding the ingress rule will see /report 404 until they do). The operator’s actual
ingress wiring is a deployment-time activity, explicitly out of repo scope — surfaced as a release note, not gated by CI.
(If a stronger bar is wanted, the only repo-side option is a smoke test in the deployment runbook, not an automated gate.)
Files touched (quick index)
| File | Step |
|---|---|
Code |
P5.1 |
Templates/assets |
P5.1 |
Config |
P5.1 |
Tests |
P5.1 |
Docs |
P5.1 |
both plans → |
P5.2 |
Untouched: crates/craig-i18n/catalog/en/public.ftl (the edge’s catalog). No new dependency.
Risks & mitigations
| Risk | Mitigation |
|---|---|
Deleting the |
Repoint |
A |
Per-key grep over surviving templates (set-difference), not a prefix-wipe; Askama compile + grep guard both directions |
The edge lacks a craig-web behavior a ported assertion needs (would silently regress L5 parity) |
Blocks P5.1 — close the edge gap in a preceding |
a11y / DAST coverage of the public form is silently lost when craig-web |
The |
Expected (not risks): devstack has no ingress, so "`/report` → edge" path-routing is a production doc concern, not
e2e-testable (the harness reaches the edge by port). Verified safe: no surviving craig-web page links to a deleted
/report* route (the surviving base.html links to /reporting/, a different module).
Delivery sequence
-
P5.0 plan MR — commit this plan (
plans/craig-intake-portal-phase5-remove-craig-web-report.adoc, nav Active, beside the consolidation program plan) + the ADR-043 Phase-5 annotation stub; file the P5.1 + P5.2 issues under epic &60 (type-prefixed, weighted, epic-linked). -
P5.1 strip MR (atomic) — the deletion + the two rewrites + the e2e port/retarget + the operational docs. Full pre-push validate + J1–J8 must pass;
Closes #<P5.1-issue>+ a closing comment. -
P5.2 final MR (depends on P5.1 merged) — archive both plans; flip P5 → Done; close epic &60; Plan Completion Audit.
Verification (whole phase)
cargo nextest run -p craig-web # Askama orphan/dangling-key guard + i18n_coverage
cargo build -p craig-web # clients/config/contracts removal compiles clean
cargo xtask dev reload && cargo xtask e2e --no-refresh -- \
--project=intake-ui --project=intake-ui-integrated \ # edge public form + integrated mount (ported gaps green)
--project=auth \ # OIDC login still works after the http() repoint
--project=accessibility # edge public a11y passes; no craig-web /report audit 404s
cargo xtask check-docs && cargo xtask plan-lint
# + the public-portal ZAP scan (retargeted to craig-intake) still covers the public surface
The load-bearing guardrails — all pre-push/CI-gated: (a) the surviving caseworker UI still compiles + resolves every
i18n key after the set-difference deletion; (b) the auth project still passes — the http() repoint didn’t break
OIDC login; (c) the accessibility project still passes — the edge public audits cover a11y and no craig-web
/report audit 404s; (d) the public-portal DAST coverage moved to the edge, not lost.