Code Quality Review & Refactoring Plan
On this page
Status
COMPLETE (partial — see Residual) — archived 2026-04-17 (plan-hygiene sweep).
| Phase | Description | Status |
|---|---|---|
1 |
Service boot consolidation (extract to |
Deferred (not applied) — |
2 |
Role-check helper centralization |
Done (pre-ADR-030) — helpers are methods on |
3 |
|
Done (pre-ADR-030) — |
4 |
Split oversized files |
Done (2026-03-23) — MR !63 — |
5 |
SecurityAddon consolidation |
Done (pre-ADR-030) — consolidated in |
6 |
Template cleanup ( |
Deferred (out of scope) — polish deferred indefinitely; no active plan |
Residual — moved to follow-up tracking
Phase 1 (bootstrap integration) is the only Phase with genuine remaining work: bootstrap() function exists in crates/craig-api/src/bootstrap.rs but no service consumes it. Retrofitting all 8 services is a mechanical refactor (~350 lines saved) — tracked as #179, referenced from the active Code Quality Remediation plan (#180).
Context
A comprehensive code quality audit of the CRAIG codebase identified recurring patterns of duplication, oversized files, and inconsistent error handling. The shared crates (crates/) are excellent — clean APIs, no god modules, proper separation of concerns. The issues concentrate in two areas: (1) boilerplate duplicated across backend services, and (2) structural issues in craig-web’s route handlers and templates.
The goal is to eliminate spaghetti code, reduce duplication, and improve maintainability without changing any external behavior.
Audit Summary
Strengths (no action needed)
-
Shared crates (craig-common, craig-auth, craig-db, craig-mq, craig-api, craig-store, craig-reference) — production quality
-
EventEnvelope pattern — standardized across all services
-
Test organization — consistent structure across all services
-
Workspace dependency management — no unused deps, justified pins
-
Configuration/settings pattern — consistent env var loading with redaction
Issues Found
| # | Issue | Severity | Instances | Est. Lines Saved |
|---|---|---|---|---|
1 |
Boot sequence boilerplate in every service main.rs |
HIGH |
8 services |
~350 |
2 |
Role-check helpers redefined in 4+ services |
HIGH |
12+ functions |
~60 |
3 |
|
HIGH |
11 |
~70 |
4 |
List handler pagination pattern duplicated |
HIGH |
18 handlers |
~150 |
5 |
Oversized files (800+ lines) |
HIGH |
5 files |
— (reorganize) |
6 |
Silent error swallowing in web list handlers |
MEDIUM |
115 call sites |
— (fix pattern) |
7 |
SecurityAddon duplicated in every service |
MEDIUM |
8 services |
~50 |
8 |
|
MEDIUM |
18 sites |
— (constant) |
9 |
|
MEDIUM |
1 file |
— (split) |
10 |
27 list/count function pairs with near-identical WHERE |
LOW |
27 pairs |
complex |
Phase 1: Service Boot Consolidation
Problem
All 8 service main.rs files contain ~50 identical lines. The shared boot sequence in every service is:
// 1. dotenvy
dotenvy::dotenv().ok();
// 2. settings
let settings = ServiceSettings::load("CRAIG_<SERVICE>"); // or IntakeSettings::load() for intake
// 3. telemetry
craig_common::telemetry::init(&settings.log_level);
// 4. DB connect + options
let db = DbPool::connect_with(
PgConnectOptions::from_str(&settings.database_url)?.log_statements(log::LevelFilter::Debug),
).await?;
// 5. migrations
db.run_migrations(&sqlx::migrate!()).await?;
// 6. JWKS + auth
let jwks = JwksProvider::new(&settings.keycloak_issuer, None).await;
jwks.refresh().await?;
jwks.start_refresh_task();
let auth = AuthLayer::new(jwks);
// 7. RabbitMQ + channels
let rmq = lapin::Connection::connect(&settings.rabbitmq_url, ...).await?;
let pub_channel = rmq.create_channel().await?;
let sub_channel = rmq.create_channel().await?; // except intake: no subscriber
let publisher = Publisher::new(pub_channel);
let subscriber = Subscriber::new(sub_channel);
Each service then adds its own setup after the shared boot:
| Service | Service-Specific Setup After Boot |
|---|---|
craig-rules |
|
craig-cases |
|
craig-placement |
|
craig-exchange |
|
craig-financial |
|
craig-reporting |
|
craig-security |
|
craig-intake |
|
Solution
Add bootstrap() to craig-api crate that encapsulates steps 1-7 above.
File: crates/craig-api/src/bootstrap.rs (NEW)
pub struct BootstrapResult {
pub db: DbPool,
pub auth: AuthLayer,
pub publisher: Publisher,
pub subscriber: Subscriber,
}
pub async fn bootstrap(service_name: &str) -> (ServiceSettings, BootstrapResult) {
dotenvy::dotenv().ok();
let settings = ServiceSettings::load(service_name);
craig_common::telemetry::init(&settings.log_level);
tracing::info!("{service_name} starting — {settings:?}");
let db = DbPool::connect_with(
PgConnectOptions::from_str(&settings.database_url)
.expect("invalid database URL")
.log_statements(log::LevelFilter::Debug),
)
.await
.expect("DB connect");
db.run_migrations(&sqlx::migrate!()).await.expect("migrations");
let jwks = JwksProvider::new(&settings.keycloak_issuer, None).await;
jwks.refresh().await.expect("JWKS refresh");
jwks.start_refresh_task();
let auth = AuthLayer::new(jwks);
let rmq = lapin::Connection::connect(
&settings.rabbitmq_url,
lapin::ConnectionProperties::default(),
)
.await
.expect("RabbitMQ connect");
let pub_channel = rmq.create_channel().await.expect("publisher channel");
let sub_channel = rmq.create_channel().await.expect("subscriber channel");
let publisher = Publisher::new(pub_channel);
let subscriber = Subscriber::new(sub_channel);
(settings, BootstrapResult { db, auth, publisher, subscriber })
}
bootstrap() takes &sqlx::migrate::Migrator as a parameter since each service has its own sqlx::migrate!() macro invocation (compile-time embedded migrations).
|
Each service main.rs reduces to:
let (settings, boot) = craig_api::bootstrap("CRAIG_CASES", &sqlx::migrate!()).await;
// Service-specific setup only:
let rules_client = reqwest::Client::builder().timeout(Duration::from_secs(30)).build()?;
let object_store = Store::from_config(&ObjectStoreConfig::load()?)?;
let router = ApiServer::router(state, routes, opts, Some(api_doc));
ApiServer::serve(router, settings.port, shutdown_signal()).await;
Per-service changes
-
craig-rules (
main.rs, 235 lines → ~80): Extract lines 19-74 intobootstrap(). Keep lines 77-130 (RulesEngine, event subscriptions incl.subscribe_exclusivecache invalidation). -
craig-cases (
main.rs, 186 lines → ~60): Extract lines 19-77 intobootstrap(). Keep lines 80-110 (rules HTTP client, ObjectStore). -
craig-placement (
main.rs, 154 lines → ~50): Extract lines 18-76 intobootstrap(). Keep lines 79-90 (rules URL, subscriptions). -
craig-exchange (
main.rs, 166 lines → ~55): Extract lines 22-76 intobootstrap(). Keep lines 79-98 (ObjectStore, NoopAdapter). -
craig-financial (
main.rs, 396 lines → ~340): Extract lines 19-73 intobootstrap(). Keep lines 76-396 (event handlers with business logic — these are large and service-specific). -
craig-reporting (
main.rs, 357 lines → ~300): Extract lines 18-72 intobootstrap(). Keep lines 75-357 (event handlers with validation logic). -
craig-security (
main.rs, 185 lines → ~60): Extract lines 20-74 intobootstrap(). Keep lines 77-93 (ObjectStore, wildcard subscriber). -
craig-intake (
main.rs, 181 lines → ~120): Extract lines 26-69 intobootstrap(). Keep lines 75-144 (CaptchaVerifier, ApiKeyState, 3-tier routing, manualaxum::serve()). NOTE: intake usesIntakeSettingsnotServiceSettings— either add a generic settings parameter tobootstrap()or have intake loadIntakeSettingsseparately and call abootstrap_with_settings()variant. Also, intake has NO subscriber channel.
Files modified
-
crates/craig-api/src/bootstrap.rs— NEW -
crates/craig-api/src/lib.rs— addpub mod bootstrap; -
crates/craig-api/Cargo.toml— add deps (craig-db,craig-mq,craig-auth,lapin,sqlx) -
services/craig-rules/src/main.rs— replace lines 19-74 withbootstrap()call -
services/craig-cases/src/main.rs— replace lines 19-77 withbootstrap()call -
services/craig-placement/src/main.rs— replace lines 18-76 withbootstrap()call -
services/craig-exchange/src/main.rs— replace lines 22-76 withbootstrap()call -
services/craig-financial/src/main.rs— replace lines 19-73 withbootstrap()call -
services/craig-reporting/src/main.rs— replace lines 18-72 withbootstrap()call -
services/craig-security/src/main.rs— replace lines 20-74 withbootstrap()call -
services/craig-intake/src/main.rs— replace lines 26-69 withbootstrap()call (special handling for IntakeSettings + no subscriber) -
craig-web does NOT use this bootstrap (no DB, no MQ, different auth flow)
Phase 2: Role-Check Helper Consolidation
Problem
Role-check free functions are redefined in multiple services. The current pattern (identical in each service):
// craig-reporting/src/api.rs lines 27-51, craig-security/src/api.rs, craig-placement/src/api/mod.rs
fn require_caseworker_or_above(claims: &Claims) -> Result<(), ApiError> {
if claims.has_role("caseworker") || claims.has_role("supervisor") || claims.has_role("admin") {
Ok(())
} else {
Err(ApiError::Forbidden)
}
}
fn require_supervisor_or_above(claims: &Claims) -> Result<(), ApiError> {
if claims.has_role("supervisor") || claims.has_role("admin") {
Ok(())
} else {
Err(ApiError::Forbidden)
}
}
fn require_admin(claims: &Claims) -> Result<(), ApiError> {
if claims.has_role("admin") {
Ok(())
} else {
Err(ApiError::Forbidden)
}
}
// craig-financial/src/api.rs lines 31-56 — same pattern but also has:
fn require_eligibility_worker_or_above(claims: &Claims) -> Result<(), ApiError> {
if claims.has_role("eligibility_worker") || claims.has_role("supervisor") || claims.has_role("admin") {
Ok(())
} else {
Err(ApiError::Forbidden)
}
}
Locations to delete:
-
services/craig-reporting/src/api.rslines 27-51 —require_caseworker_or_above,require_supervisor_or_above,require_admin -
services/craig-financial/src/api.rslines 31-56 —require_eligibility_worker_or_above,require_supervisor_or_above,require_admin -
services/craig-placement/src/api/mod.rs—require_caseworker_or_above,require_supervisor_or_above,require_admin -
services/craig-security/src/api.rs—require_caseworker_or_above,require_supervisor_or_above,require_admin -
services/craig-cases/src/api/cases.rs— check for local helpers -
services/craig-exchange/src/api/mod.rs— check for local helpers
Call sites change from require_caseworker_or_above(&claims)? to claims.require_caseworker_or_above()?.
Solution
Move to craig-auth crate as methods on Claims:
File: crates/craig-auth/src/claims.rs
impl Claims {
pub fn require_role(&self, role: &str) -> Result<(), ApiError> {
if self.has_role(role) { Ok(()) } else { Err(ApiError::forbidden("Insufficient role")) }
}
pub fn require_any_role(&self, roles: &[&str]) -> Result<(), ApiError> {
if roles.iter().any(|r| self.has_role(r)) {
Ok(())
} else {
Err(ApiError::forbidden("Insufficient role"))
}
}
pub fn require_caseworker_or_above(&self) -> Result<(), ApiError> {
self.require_any_role(&["caseworker", "supervisor", "admin"])
}
pub fn require_supervisor_or_above(&self) -> Result<(), ApiError> {
self.require_any_role(&["supervisor", "admin"])
}
pub fn require_admin(&self) -> Result<(), ApiError> {
self.require_role("admin")
}
pub fn require_eligibility_worker_or_above(&self) -> Result<(), ApiError> {
self.require_any_role(&["eligibility_worker", "supervisor", "admin"])
}
pub fn require_icpc_coordinator_or_above(&self) -> Result<(), ApiError> {
self.require_any_role(&["icpc_coordinator", "supervisor", "admin"])
}
}
The current code uses ApiError::Forbidden (enum variant). Verify whether craig-auth already depends on craig-common (which defines ApiError). If not, add the dependency. The new methods use ApiError::Forbidden to match the existing pattern exactly.
|
Files modified
-
crates/craig-auth/src/claims.rs— add methods -
crates/craig-auth/Cargo.toml— addcraig-commondep (forApiError) if not already present -
services/craig-financial/src/api.rs— delete lines 31-56, replace all call sites -
services/craig-reporting/src/api.rs— delete lines 27-51, replace all call sites -
services/craig-placement/src/api/mod.rs— delete local helpers, replace all call sites -
services/craig-security/src/api.rs— delete local helpers, replace all call sites -
services/craig-cases/src/api/cases.rs— delete local helpers if present, replace call sites -
services/craig-exchange/src/api/mod.rs— delete local helpers if present, replace call sites
Phase 3: craig-web Deduplication
3a. Extract PageResponse<T> to routes/mod.rs
Currently defined identically in 8 files (intake.rs has it twice). Delete from all locations:
| File | Lines to delete |
|---|---|
|
34-39 |
|
67-72 AND 814-820 (duplicated for public reports) |
|
62-67 |
|
72-77 |
|
89-94 |
|
74-79 |
|
30-35 |
report.rs and dashboard.rs do NOT define PageResponse (no list handlers).
|
Move to shared location. File: services/craig-web/src/routes/mod.rs
#[derive(Deserialize, Default)]
pub struct PageResponse<T> {
pub data: Vec<T>,
pub page: u32,
pub per_page: u32,
pub total: i64,
}
Each route file adds use super::PageResponse;.
3b. Extract pagination utility and fix error swallowing
The current pattern in ALL 18 list handlers (identical in every one):
// Current pattern — silent error swallowing, duplicated 18 times:
let resp: PageResponse<SomeView> = state
.api
.get(&state.config.some_url, &path, token)
.await
.ok() // <-- error silently discarded
.and_then(|v| serde_json::from_value(v).ok()) // <-- parse error silently discarded
.unwrap_or_default();
let total_pages = if resp.per_page > 0 {
(resp.total as u32).div_ceil(resp.per_page)
} else {
1
};
Replace with shared utility. File: services/craig-web/src/routes/mod.rs
pub const DEFAULT_PER_PAGE: u32 = 25;
pub fn total_pages(per_page: u32, total: i64) -> u32 {
if per_page > 0 { (total as u32).div_ceil(per_page) } else { 1 }
}
pub async fn fetch_list<T: serde::de::DeserializeOwned + Default>(
api: &ApiClient,
base_url: &str,
path: &str,
token: &str,
) -> PageResponse<T> {
match api.get(base_url, path, token).await {
Ok(v) => serde_json::from_value(v).unwrap_or_default(),
Err(e) => {
tracing::warn!("API call failed: {path}: {e}");
PageResponse::default()
}
}
}
18 list handlers to update
Each handler’s fetch+deserialize+total_pages block (8-12 lines) reduces to 2 lines:
let resp = fetch_list::<CaseView>(&state.api, &state.config.cases_url, &path, token).await;
let total_pages = total_pages(resp.per_page, resp.total);
Complete list of handlers to update:
| File | Handler Function | Lines |
|---|---|---|
|
|
207-220 |
|
|
173-192 |
|
|
235-254 |
|
|
807-826 |
|
|
155-174 |
|
|
220-230 |
|
|
321-340 |
|
|
176-189 |
|
|
217-235 |
|
|
266-287 |
|
|
319-340 |
|
|
167-185 |
|
|
328-341 |
|
|
445-463 |
|
|
143-167 |
|
|
198-211 |
|
|
242-255 |
|
|
286-299 |
|
|
74-87 |
Also replace all per_page=25 hardcodings with DEFAULT_PER_PAGE:
-
cases.rs:182,intake.rs:157,placement.rs:144,209,313,exchange.rs:176,217,266,319,financial.rs:167,328,445,security.rs:143,198,242,286,rules.rs:74
Files modified
-
services/craig-web/src/routes/mod.rs— addPageResponse<T>,fetch_list(),total_pages(),DEFAULT_PER_PAGE -
services/craig-web/src/routes/cases.rs— delete PageResponse (lines 34-39), updatelist()handler -
services/craig-web/src/routes/intake.rs— delete PageResponse (lines 67-72 AND 814-820), update 3 handlers -
services/craig-web/src/routes/placement.rs— delete PageResponse (lines 62-67), update 3 handlers -
services/craig-web/src/routes/exchange.rs— delete PageResponse (lines 72-77), update 4 handlers -
services/craig-web/src/routes/financial.rs— delete PageResponse (lines 89-94), update 3 handlers -
services/craig-web/src/routes/security.rs— delete PageResponse (lines 74-79), update 4 handlers -
services/craig-web/src/routes/rules.rs— delete PageResponse (lines 30-35), update 1 handler -
services/craig-web/src/routes/report.rs— NO changes (no list handlers) -
services/craig-web/src/routes/dashboard.rs— NO changes (no list handlers)
Phase 4: Split Oversized Files
4a. routes/intake.rs (996 lines) → routes/intake/ directory
routes/intake/mod.rs — shared types, re-exports, route registration:
-
View models:
ReferralView,AllegationView,ReferralDetailView,InvestigationView,ChildView,PerpView -
Query params:
WorklistParams,ReferralListParams -
Template structs: re-export from submodules
-
pub usefor all public handler functions
routes/intake/worklist.rs — investigation worklist handlers:
-
worklist()— investigation worklist list (lines 148-210) -
investigation_detail()— investigation detail -
create_investigation()— create investigation -
update_investigation()— update investigation -
safety_assessment_form()— safety assessment form -
submit_safety_assessment()— submit safety assessment
routes/intake/referrals.rs — referral handlers:
-
referral_list()— referral list (lines 212-274) -
new_referral_form()— new referral form -
create_referral()— create referral -
referral_detail()— referral detail -
add_allegation()— add allegation to referral
routes/intake/reports.rs — public report list handlers (authenticated staff view):
-
PublicReportView,PublicReportListParams(view model + params) -
public_reports()— public reports list (lines 785-847) -
public_report_detail()— report detail -
claim_public_report()— claim report -
convert_public_report()— convert to referral -
screen_out_public_report()— screen out report
4b. routes/cases.rs (838 lines) → routes/cases/ directory
routes/cases/mod.rs — shared types, re-exports:
-
View models:
CaseView,HouseholdMemberView,CasePlanView,ContactView,ContactAttachmentView,CourtOrderView -
Query params:
CaseListParams -
pub usefor all handler functions
routes/cases/list.rs — case listing:
-
list()(lines 172-239) -
new_form()— new case form -
create()— create case
routes/cases/detail.rs — case detail:
-
detail()(lines 302-375) — usestokio::join!for 5 parallel API calls -
update()— update case
routes/cases/contacts.rs — contact CRUD + attachments:
-
add_contact()— add contact log entry -
list_contact_attachments()— htmx fragment for attachment list -
upload_contact_attachment()— multipart upload -
download_contact_attachment()— streaming download -
delete_contact_attachment()— delete attachment
routes/cases/household.rs — household members:
-
add_household_member()— add member
routes/cases/plans.rs — case plans + tasks:
-
create_plan()— create case plan -
approve_plan()— approve case plan -
add_task()— add task to plan
routes/cases/court.rs — court orders:
-
add_court_order()— add court order
4c. services/craig-reporting/src/api.rs (830 lines) → api/ directory
api/mod.rs — routes builder, OpenAPI doc, shared types:
-
routes()function (buildsRouter) -
ApiDocstruct (#[derive(OpenApi)]) -
SecurityAddon(until Phase 5 removes it) -
Role-check helpers (until Phase 2 removes them, lines 27-51)
api/quality.rs — data quality:
-
quality_dashboard()(lines 195-231) -
list_issues()(lines 246-285) -
resolve_issue()(lines 301-319)
api/afcars.rs — AFCARS submissions:
-
generate_afcars()(lines 337-362) -
list_afcars()(lines 377-407) -
get_afcars()(lines 423-436) -
review_afcars()(lines 454-484) -
approve_afcars()(lines 501-532) -
transmit_afcars()(lines 550-589)
api/ncands.rs — NCANDS submissions:
-
generate_ncands()(lines 607-632) -
list_ncands()(lines 647-677) -
review_ncands()(lines 695-725) -
approve_ncands()(lines 742-772) -
transmit_ncands()(lines 790-829)
4d. services/craig-financial/src/api.rs (803 lines) → api/ directory
api/mod.rs — routes builder, OpenAPI doc, shared types:
-
routes()function (buildsRouter) -
ApiDocstruct -
SecurityAddon(until Phase 5 removes it) -
Role-check helpers (until Phase 2 removes them, lines 31-56)
api/rates.rs — rate tables:
-
list_rates()(lines 240-275) -
create_rate()(lines 290-311)
api/payments.rs — payments:
-
list_payments()(lines 328-371) -
get_payment()(lines 387-400) -
calculate_payment()(lines 417-457) -
approve_payment()(lines 474-513)
api/adjustments.rs — payment adjustments:
-
create_adjustment()(lines 532-557) -
approve_adjustment()(lines 574-622)
api/claims.rs — claiming records:
-
list_claims()(lines 639-676) -
generate_claim()(lines 691-721) -
get_claim()(lines 737-750) -
submit_claim()(lines 767-802)
Phase 5: SecurityAddon Consolidation
Problem
Every service defines an identical SecurityAddon struct. The current implementation (same in all 8 services):
// e.g., craig-reporting/src/api.rs lines 97-112, craig-financial/src/api.rs lines 137-152
struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.get_or_insert_default();
components.add_security_scheme(
"bearer",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.build(),
),
);
}
}
The current code uses get_or_insert_default() and the scheme name "bearer" (not "bearer_auth"). The consolidated version must match exactly.
|
Locations to delete:
-
services/craig-rules/src/api.rs— SecurityAddon struct + impl -
services/craig-cases/src/api/mod.rs— SecurityAddon struct + impl -
services/craig-placement/src/api/mod.rs— SecurityAddon struct + impl -
services/craig-exchange/src/api/mod.rs— SecurityAddon struct + impl -
services/craig-financial/src/api.rslines 137-152 -
services/craig-reporting/src/api.rslines 97-112 -
services/craig-security/src/api.rs— SecurityAddon struct + impl -
services/craig-intake/src/api.rs— SecurityAddon struct + impl
Solution
Move to craig-api crate.
File: crates/craig-api/src/lib.rs
pub struct SecurityAddon;
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.get_or_insert_default();
components.add_security_scheme(
"bearer",
utoipa::openapi::security::SecurityScheme::Http(
utoipa::openapi::security::HttpBuilder::new()
.scheme(utoipa::openapi::security::HttpAuthScheme::Bearer)
.bearer_format("JWT")
.build(),
),
);
}
}
Each service replaces use of local SecurityAddon with use craig_api::SecurityAddon; and deletes the local definition.
Files modified
-
crates/craig-api/src/lib.rs— addSecurityAddon+impl Modify -
crates/craig-api/Cargo.toml— addutoipadep -
services/craig-rules/src/api.rs— delete local SecurityAddon, adduse craig_api::SecurityAddon; -
services/craig-cases/src/api/mod.rs— same -
services/craig-placement/src/api/mod.rs— same -
services/craig-exchange/src/api/mod.rs— same -
services/craig-financial/src/api.rs— delete lines 137-152, adduse craig_api::SecurityAddon; -
services/craig-reporting/src/api.rs— delete lines 97-112, adduse craig_api::SecurityAddon; -
services/craig-security/src/api.rs— same -
services/craig-intake/src/api.rs— same
Phase 6: Template Cleanup
6a. Split cases/detail.html (472 lines)
Currently contains 5 tab panels controlled by Alpine.js x-data="{ tab: 'Summary' }". The tab strip is at lines 22-30.
Tab content line ranges:
| Tab | Start | End | Description |
|---|---|---|---|
Summary |
33 |
83 |
Case details card, status badge, close case button |
Household |
86 |
159 |
Member table, add member form |
Case Plan |
162 |
282 |
Plans table with nested tasks, create plan form, approve button |
Contacts |
285 |
384 |
Contact log table, expandable narrative + attachment list (htmx), add contact form |
Court |
387 |
469 |
Court orders table, add court order form |
Shared template variables available to all tabs:
-
case— main case object:case_number,status,stage,admin_unit,assigned_worker,supervisor,opened_at,closed_at,closure_reason,id,icwa_flag -
household—Vecof members:person_id,role,primary_caregiver,active,added_at -
plans—Vecof plans:permanency_goal,status,strengths,needs,review_due_at,approved_by,created_by,id(each with nestedtasks) -
contacts—Vecof contacts:occurred_at,contact_type,contact_with,duration_minutes,recorded_by,narrative,id -
court_orders—Vecof orders:order_date,order_type,court_name,judge,next_hearing_date,id -
ctx— page context:user(withcan_write(),is_supervisor()),branding_app_name
Split into Askama includes:
-
templates/cases/detail.html— parent: extendsbase.html, defines Alpinex-data, tab strip (lines 1-30), includes 5 tab files, closing tags (line 470-472) -
templates/cases/_tab_summary.html— lines 33-83 (51 lines) -
templates/cases/_tab_household.html— lines 86-159 (74 lines) -
templates/cases/_tab_plans.html— lines 162-282 (121 lines) -
templates/cases/_tab_contacts.html— lines 285-384 (100 lines) -
templates/cases/_tab_court.html— lines 387-469 (83 lines)
Each include file is wrapped in <div x-show="tab === 'TabName'" x-cloak> which stays in the parent or the include (implementer’s choice — both work with Askama).
6b. Remove dead code markers
Clean up #[allow(dead_code)] annotations (~17 instances). For each one:
-
If the code is truly unused (no callers, no tests) — delete it
-
If the code is planned for a future phase — add a
// TODO(phase-N): …comment explaining the intended use -
Specific known case:
services/craig-placement/src/api/mod.rslines 20-27 —RulesEngineUrlandJurisdictionstructs with#[allow(dead_code)]are declared but never used as Extension types. Delete if unused or annotate with planned use.
Implementation Order
-
Phase 3 (craig-web dedup) — lowest risk, highest frequency of duplication, no API changes
-
Phase 2 (role helpers) — small, contained, improves all services
-
Phase 5 (SecurityAddon) — small, contained
-
Phase 4 (split files) — reorganization only, no behavior changes
-
Phase 1 (bootstrap) — highest impact but most complex, touches all services
-
Phase 6 (templates + dead code) — polish
Each phase is independently testable and can be done on its own feature branch.
Testing Strategy
Every phase:
-
cargo fmt --all+cargo clippy --workspace --locked— lint clean -
cargo test --workspace --lib— unit tests pass -
cargo xtask dev reload— rebuild all services -
cargo test --workspace— integration tests pass -
cargo xtask e2e— E2E tests pass (no behavior changes)
No new tests needed — these are pure refactoring changes. All existing tests serve as regression tests.
Verification
After all phases:
-
Full test battery passes (unit + integration + E2E)
-
cargo clippy --workspace --locked— zero warnings -
git diff --statshows net line reduction of ~600+ lines -
No file exceeds 600 lines
-
No duplicate
PageResponse<T>definitions -
No duplicate role-check functions
-
No duplicate SecurityAddon implementations
-
All error paths in web list handlers log via tracing