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 is_victim, the detailed-narrative section, reporter address + relationships, and demographics. Deleting craig-web’s form before those are restored would regress a child-safety surface. Phase 5 was therefore blocked on the edge public-form parity remediation (R1–R3), which makes the edge a verified superset first. P5.1’s "port the e2e gaps" step is satisfied by that remediation’s assertions. The deletion design below is unchanged.

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 intake-ui e2e). Phase 5 is unblocked.

Constraint legend (the consolidation program’s locks; all satisfied)

L5 — i18n + a11y parity (met by Phase 3). L6/report moves by path-based ingress, never an HTTP redirect or compat route. L7 — the edge serves the UI in integrated mode (met by Phase 3.6 / #721).

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)

  1. Repoint http(). BffClients::http() (src/clients/mod.rs:136-140) returns the shared transport via self.intake.http(), and the OIDC callback (src/auth.rs:312) calls state.clients.http() — so deleting the intake field first breaks login. Repoint http() to self.cases.http() (the cases client always survives; every client clones the same Transport), then remove the intake field + intake_url + CRAIG_WEB__INTAKE_URL.

  2. i18n is a set-difference, not a prefix-wipe. Many report- / status- keys in craig-web/locales/en/web.ftl are used by the surviving caseworker intake-review templates (templates/intake/*). See the exact procedure in Design.

Out of scope (deployment-owned)

The ingress/reverse-proxy config is operator infrastructure — there is no nginx/k8s in this repo, and devstack reaches services by port. Phase 5 documents the path-routing requirement; it does not add an in-repo gateway. No redirect, no compat route (L6).

Status

Step Description Status

P5.1 strip

Delete craig-web’s public /report* (code + the two rewrites + the i18n set-difference); retarget the e2e to the edge; ship the operational docs

Done (2026-06-28) — craig-web /report* deleted (routes/report.rs, templates, static JS, intake client, CRAIG_WEB__INTAKE_URL, craig-intake-contracts, 65 orphaned i18n keys); http() repointed to self.cases.transport().http() (login preserved); 6 e2e assertions + 2 #725 hover-contrast a11y runs ported to the edge (verified green) before deletion; ZAP retargeted to span the edge /report*; operational docs (CHANGELOG breaking-change, deployment-guide "Public report routing", ADR-043 as-built) shipped

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 git mv’d to `plans/archive/ (all inbound xrefs repointed; check-docs clean); consolidation P5 row → Done; .claude/CLAUDE.md epic &60 marked complete; Plan Completion Audit passed; epic &60 closed

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 main (the plan predated R1–R3) refined the Design below:

  • CSS: delete ONLY .public-wizard- + .public-footer — *keep the global [x-cloak] utility (used by 10+ surviving internal templates). The original "delete lines ~1129-1148 wholesale" would have broken Alpine cloaking app-wide.

  • http() repoint target: self.cases.transport().http() via the HasTransport trait — not self.cases.http() (only the removed IntakeClient carried a direct http(); CasesClient exposes the transport accessor).

  • i18n set-difference: 65 report- keys deleted, 45 kept (re-derived authoritatively by per-key grep over the surviving templates + src; Askama compile + i18n_coverage are the guard). No status- keys exist in web.ftl.

  • Edge-gap (the go/no-go): the preflight found 6 craig-web behaviors whose only e2e assertion was in the to-be-deleted craig-web specs — max-10 children, max-10 adults, concurrent-distinct ids, the Submit-Another affordance, the report-form nav link, and incident_location round-trip — plus the 2 #725 wizard hover-contrast a11y runs. All exist on the edge; the assertions were ported to intake-embedded-ui.spec.ts / accessibility-audit.spec.ts and verified green BEFORE the craig-web specs were deleted (the edge hover-contrast came back CLEAN, so #745 is moot once craig-web’s wizard is gone — no edge CSS fix needed).

  • Screenshots: the craig-web /report* screenshot captures were dropped from screenshots.spec.ts (the routes 404); the report PNGs are kept (still Antora-embedded, same UI). Regenerating them from the edge + refreshing the Antora embeds is a tracked follow-up (not load-bearing; screenshots are SCREENSHOTS=1-gated, not in the gate).

  • ZAP: one craig-public context spanning both public surfaces — craig-web’s surviving /login + /welcome
    /healthz and the edge craig-intake-standalone:8009/report.*.

P5.1 — Strip the surface (atomic: code + e2e + operational docs)

Code deletion.

  • src/main.rs — remove public_report_routes() + its .merge(…​); src/routes/mod.rs — drop the report module.

  • 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 backed report_base.html only and is dead once the templates go.

Critical rewrites (before deletion).

  • src/clients/mod.rs — repoint http() to self.cases.http(); then remove the whole intake client: the pub(crate) mod intake; (l.39), the pub(crate) use intake::IntakeClient; re-export (l.57), the intake: IntakeClient field + its // ── intake ── comment (l.102-103), and the constructor wiring (l.181). Delete the file src/clients/intake.rs (87 lines).

  • Config (3 sites — all of them): remove intake_url from src/config.rs (WebSettings) and its env bindings: .env.example (l.228 CRAIG_WEB__INTAKE_URL=), docker-compose.yml (l.709, the craig-web service env — not the e2e CRAIG_INTAKE_URL at l.838), and the deployment-guide.adoc env table row (l.350).

  • Cargo.toml — remove craig-intake-contracts (verified: its only use is the deleted report.rs). If cargo build -p craig-web flags 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 edge intake-ui project — 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 in tests/e2e/specs/public-report.spec.ts (911 warning; concurrent-distinct-ids) and public-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 own fix: 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 entire projects[name='public'] object from tests/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 == Unreleased entry 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 (the PUBLIC -→|/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 names public-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 mvplans/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 both plans/archive.adoc Features 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 craig-web/src/main.rs, routes/mod.rs, routes/report.rs (del); clients/mod.rs (repoint+strip), clients/intake.rs (del); src/config.rs, middleware.rs (comment); Cargo.toml

P5.1

Templates/assets templates/report/* + report_base.html (del); static/js/{report-wizard,key-registration,craig-sign}.js (del); static/css/components.css (dead block); locales/en/web.ftl (set-difference)

P5.1

Config .env.example (l.228), docker-compose.yml (l.709), deployment-guide.adoc env table (l.350) — drop CRAIG_WEB__INTAKE_URL

P5.1

Tests tests/e2e/specs/intake-embedded-ui.spec.ts (port gaps); public-report*.spec.ts + playwright.config.ts public project + screenshots (del); accessibility-audit.spec.ts (drop craig-web /report* audits); tests/security/zap-public-web.yaml (retarget to edge); tests/i18n_coverage.rs (comment)

P5.1

Docs CHANGELOG.adoc, deployment-guide.adoc (ingress section), architecture.adoc, services.adoc, intake-standalone-architecture.adoc, ADR-043, roadmap.adoc

P5.1

both plans → plans/archive/, nav.adoc, plans/archive.adoc, .claude/CLAUDE.md

P5.2

Untouched: crates/craig-i18n/catalog/en/public.ftl (the edge’s catalog). No new dependency.

Risks & mitigations

Risk Mitigation

Deleting the intake client breaks the OIDC callback (auth.rs:312clients.http()self.intake.http())

Repoint http() to self.cases.http() FIRST, then strip; the auth e2e project is the gate

A report-/status- key shared with caseworker templates is deleted (or orphaned — the coverage test flags neither)

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 fix: MR, or record an explicit drop decision; never delete the only working impl with the gap open

a11y / DAST coverage of the public form is silently lost when craig-web /report goes

The accessibility project’s edge audits + the retargeted ZAP scan keep coverage on craig-intake; both in the P5.1 verify

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

  1. 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).

  2. 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.

  3. 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.

Edit this page · latest