Plan: Worker Portal Handler Remediation
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Case detail: household tab — fetch members, address, certification from canopy-persons and canopy-renewals |
Done (2026-04-07) |
2 |
Case detail: income tab — fetch income from canopy-persons, IEVS data from canopy-snap |
Done (2026-04-07) |
3 |
Case detail: determination tab — fetch from canopy-eligibility and canopy-snap |
Done (2026-04-07) |
4 |
Case detail: notices tab — fetch from canopy-notices |
Done (2026-04-07) |
5 |
Case detail: appeals tab — fetch from canopy-appeals |
Done (2026-04-07) |
6 |
Case detail: activity tab — fetch from canopy-security |
Done (2026-04-07) |
7 |
Application processing — fetch application + eligibility result, wire POST approve/deny endpoints |
Done (2026-04-07) |
8 |
Renewal queue — fetch certifications due from canopy-renewals |
Done (2026-04-07) |
9 |
Dashboard — wire work queue aggregation and recent activity feed |
Done (2026-04-07) |
10 |
Fix case detail summary bar — dynamic values from upstream data (not hardcoded) |
Done (2026-04-07) |
11 |
Rewrite session_test.rs for session-based auth (not JWT) |
Done (2026-04-07) |
12 |
Add OIDC env vars to docker-compose.yml for canopy-web |
Done (2026-04-07) |
Epic: &43
Issues: TBD
Branch: fix/worker-portal-remediation
Labels: type::bug, priority::critical, program::snap, service::web
Context
An audit of the worker portal revealed that while the UI shell (templates, routing, theme, session auth) is complete and functional, the data-fetching handlers are largely stubbed. The ServiceClients struct with 8 HTTP clients exists but is ignored by most handlers. Templates receive empty data and render graceful "no data available" fallbacks — giving the appearance of working while returning nothing.
This was marked as complete in the worker-portal-snap plan but should not have been. The handlers need to call the upstream services using the existing ServiceClients and populate the templates with real data.
The seed tool (MR !36) has populated the databases with realistic test data. The upstream API services (canopy-persons, canopy-applications, canopy-snap, canopy-eligibility, canopy-enrollment, canopy-renewals, canopy-notices, canopy-appeals, canopy-security) all return this data when queried with a valid JWT. The canopy-web BFF must forward requests to these services to display the data.
Scope
In scope:
-
Wire all 6 case detail tab handlers to call upstream services via
ServiceClients -
Wire application processing handler to fetch real application and eligibility data
-
Implement
POST /applications/{id}/approveandPOST /applications/{id}/deny -
Wire renewal queue to fetch from canopy-renewals
-
Wire dashboard work queue and activity feed
-
Replace all hardcoded values (benefit amounts, cert periods, household sizes) with upstream data
-
Rewrite session_test.rs integration tests for session-based auth
-
Add OIDC env vars to docker-compose.yml
Out of scope:
-
Keycloak OIDC flow (already implemented)
-
New UI pages or templates (existing templates are complete)
-
E2E Playwright tests (separate plan, depends on this)
Design
Each handler currently has this pattern:
fn render_household_tab(..., _clients: ...) -> ... {
// _clients ignored, hardcoded empty data
HouseholdTab { members: Vec::new(), ... }
}
The fix is straightforward: call the upstream service via the clients, parse the JSON response, and populate the template struct. Errors degrade gracefully (the templates already handle empty data).
fn render_household_tab(..., clients: ...) -> ... {
let members = clients.persons.get(&format!("/v1/v1/households/{hh_id}"))
.await
.map(|r| r.json().await.unwrap_or_default())
.unwrap_or_default();
HouseholdTab { members, ... }
}
/v1/v1/persons). The BFF clients must use this path until the API services are fixed to not double-nest.
Steps
Step 1-6: Case detail tabs
Files: services/canopy-web/src/api/case_detail.rs
Each tab renderer calls the appropriate upstream service. Error handling: if the service returns an error, the tab shows its existing "no data" fallback.
Step 7: Application processing
Files: services/canopy-web/src/api/applications.rs, services/canopy-web/src/api/mod.rs
-
Wire
get_process_applicationto fetch from canopy-applications and canopy-eligibility -
Add
POST /applications/{id}/approveandPOST /applications/{id}/denyroutes -
Both POST routes call canopy-applications to record the determination
Step 8: Renewal queue
Files: services/canopy-web/src/api/renewals.rs
Wire to GET /v1/v1/renewals/snap/due?days={filter_days} on canopy-renewals.
Step 9: Dashboard aggregation
Files: services/canopy-web/src/api/dashboard.rs
Work queue: aggregate from applications (pending), renewals (due), appeals (pending). Activity feed: fetch recent audit events from canopy-security.
Step 10: Case detail summary bar
Files: services/canopy-web/src/api/case_detail.rs
Replace hardcoded $975.00/mo, Apr 2026 → Mar 2027, household size 4 with data from canopy-snap and canopy-enrollment.
Step 11: Rewrite session tests
Files: services/canopy-web/tests/session_test.rs
Tests need to use session cookies (not JWT Bearer) since the BFF now uses OIDC session auth. Either mock the OIDC flow or inject session data directly.
Step 12: Docker compose OIDC env vars
Files: docker-compose.yml
Add to canopy-web service:
CANOPY_WEB__KEYCLOAK_EXTERNAL_URL: "http://localhost:8180/realms/canopy"
CANOPY_WEB__KEYCLOAK_INTERNAL_URL: "http://keycloak:8080/realms/canopy"
CANOPY_WEB__KEYCLOAK_CLIENT_ID: "canopy-ui"
CANOPY_WEB__REDIRECT_URL: "http://localhost:8080/auth/callback"
Files Touched
| File | Change |
|---|---|
|
Wire 6 tab renderers to upstream services |
|
Wire data fetching, add POST approve/deny |
|
Add POST routes for approve/deny |
|
Wire to canopy-renewals |
|
Wire work queue and activity feed |
|
Rewrite for session auth |
|
Add OIDC env vars for canopy-web |
Verification
-
cargo clippy --workspace --all-targets — -D warnings -
cargo nextest run -p canopy-web— all tests pass -
cargo xtask dev restart --shared-db→cargo xtask seed --seed 42 --households 50 -
Browser:
http://localhost:8080/→ Keycloak login → dashboard with real stats -
Search for a person → click result → case detail with populated tabs
-
Application processing page shows real data, approve/deny buttons work
-
Renewal queue shows certifications due
Documentation Updates
-
.claude/CLAUDE.md— update canopy-web status to reflect actual state -
CHANGELOG.adoc— entry for handler wiring -
docs/modules/ROOT/pages/plans/worker-portal-snap.adoc— update Step 2 status honestly