Skip to content
4 changes: 4 additions & 0 deletions backend/src/main/java/com/workwell/BackendApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
@EnableScheduling
public class BackendApplication {

static {
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("UTC"));
}

public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public ValueSetGovernanceService(JdbcTemplate jdbcTemplate, ObjectMapper objectM
}

public ResolveCheckResult resolveCheck(UUID measureId) {
ensureDemoValueSetLinks();
ensureDemoValueSets();
UUID measureVersionId = latestMeasureVersionId(measureId);
String cqlText = getCqlText(measureVersionId);

Expand Down Expand Up @@ -230,11 +230,118 @@ INSERT INTO terminology_mappings (id, local_code, local_display, local_system,

// Private helpers

private void ensureDemoValueSetLinks() {
private void ensureDemoValueSets() {
// 1. Update/Ensure the main 4 procedure value sets with their correct CQL-matching names & OIDs
ensureValueSet(
DEMO_VS_AUDIOGRAM,
"urn:workwell:vs:audiogram-procedures",
"Audiogram Procedures",
"2025-demo",
"[{\"code\":\"LOCAL-AUD-001\",\"display\":\"Baseline audiogram\",\"system\":\"urn:workwell:demo\"}," +
"{\"code\":\"LOCAL-AUD-002\",\"display\":\"Annual audiogram evaluation\",\"system\":\"urn:workwell:demo\"}," +
"{\"code\":\"LOCAL-AUD-003\",\"display\":\"Audiometric test pure tone\",\"system\":\"urn:workwell:demo\"}," +
"{\"code\":\"audiogram-procedure\",\"display\":\"Audiogram procedure\",\"system\":\"urn:workwell:vs:audiogram-procedures\"}," +
"{\"code\":\"92557\",\"display\":\"Comprehensive audiometry evaluation\",\"system\":\"http://www.ama-assn.org/go/cpt\"}]"
);

ensureValueSet(
DEMO_VS_TB,
"urn:workwell:vs:tb-screening",
"TB Screening Procedures",
"2025-demo",
"[{\"code\":\"LOCAL-TB-001\",\"display\":\"PPD skin test placement\",\"system\":\"urn:workwell:demo\"}," +
"{\"code\":\"LOCAL-TB-002\",\"display\":\"TB IGRA blood test\",\"system\":\"urn:workwell:demo\"}," +
"{\"code\":\"tb-screen\",\"display\":\"TB Screening Procedures\",\"system\":\"urn:workwell:vs:tb-screening\"}," +
"{\"code\":\"86580\",\"display\":\"Intradermal skin test\",\"system\":\"http://www.ama-assn.org/go/cpt\"}]"
);

ensureValueSet(
DEMO_VS_HAZWOPER,
"urn:workwell:vs:hazwoper-exams",
"HAZWOPER Surveillance Exams",
"2025-demo",
"[{\"code\":\"LOCAL-HAZ-001\",\"display\":\"HAZWOPER medical surveillance exam\",\"system\":\"urn:workwell:demo\"}," +
"{\"code\":\"LOCAL-HAZ-002\",\"display\":\"Annual fitness-for-duty evaluation\",\"system\":\"urn:workwell:demo\"}," +
"{\"code\":\"hazwoper-exam\",\"display\":\"HAZWOPER Surveillance Exams\",\"system\":\"urn:workwell:vs:hazwoper-exams\"}]"
);

ensureValueSet(
DEMO_VS_FLU,
"urn:workwell:vs:flu-vaccines",
"Influenza Vaccines",
"2025-demo",
"[{\"code\":\"88\",\"display\":\"Influenza virus vaccine unspecified\",\"system\":\"http://hl7.org/fhir/sid/cvx\"}," +
"{\"code\":\"141\",\"display\":\"Influenza seasonal injectable\",\"system\":\"http://hl7.org/fhir/sid/cvx\"}," +
"{\"code\":\"flu-vaccine\",\"display\":\"Influenza Vaccines\",\"system\":\"urn:workwell:vs:flu-vaccines\"}," +
"{\"code\":\"LOCAL-FLU-001\",\"display\":\"Flu vaccine administered\",\"system\":\"urn:workwell:demo\"}]"
);

// 2. Seed remaining 8 value sets with stable UUIDs
UUID VS_AUDIOGRAM_ENROLL = UUID.fromString("a0000001-0000-0000-0000-000000000005");
UUID VS_AUDIOGRAM_WAIVER = UUID.fromString("a0000001-0000-0000-0000-000000000006");

UUID VS_TB_ENROLL = UUID.fromString("a0000001-0000-0000-0000-000000000007");
UUID VS_TB_WAIVER = UUID.fromString("a0000001-0000-0000-0000-000000000008");

UUID VS_HAZWOPER_ENROLL = UUID.fromString("a0000001-0000-0000-0000-000000000009");
UUID VS_HAZWOPER_WAIVER = UUID.fromString("a0000001-0000-0000-0000-000000000010");

UUID VS_FLU_ENROLL = UUID.fromString("a0000001-0000-0000-0000-000000000011");
UUID VS_FLU_WAIVER = UUID.fromString("a0000001-0000-0000-0000-000000000012");

ensureValueSet(VS_AUDIOGRAM_ENROLL, "urn:workwell:vs:hearing-enrollment", "Hearing Conservation Enrollment", "2025-demo",
"[{\"code\":\"hearing-enrollment\",\"display\":\"Hearing Conservation Enrollment\",\"system\":\"urn:workwell:vs:hearing-enrollment\"}]");
ensureValueSet(VS_AUDIOGRAM_WAIVER, "urn:workwell:vs:audiogram-waiver", "Audiogram Medical Waiver", "2025-demo",
"[{\"code\":\"audiogram-waiver\",\"display\":\"Audiogram Medical Waiver\",\"system\":\"urn:workwell:vs:audiogram-waiver\"}]");

ensureValueSet(VS_TB_ENROLL, "urn:workwell:vs:tb-eligible-roles", "TB Eligible Roles", "2025-demo",
"[{\"code\":\"tb-program\",\"display\":\"TB Eligible Roles\",\"system\":\"urn:workwell:vs:tb-eligible-roles\"}]");
ensureValueSet(VS_TB_WAIVER, "urn:workwell:vs:tb-exemption", "TB Medical Exemption", "2025-demo",
"[{\"code\":\"tb-exemption\",\"display\":\"TB Medical Exemption\",\"system\":\"urn:workwell:vs:tb-exemption\"}]");

ensureValueSet(VS_HAZWOPER_ENROLL, "urn:workwell:vs:hazwoper-enrollment", "HAZWOPER Program Enrollment", "2025-demo",
"[{\"code\":\"hazwoper-program\",\"display\":\"HAZWOPER Program Enrollment\",\"system\":\"urn:workwell:vs:hazwoper-enrollment\"}]");
ensureValueSet(VS_HAZWOPER_WAIVER, "urn:workwell:vs:hazwoper-exemption", "HAZWOPER Medical Exemption", "2025-demo",
"[{\"code\":\"hazwoper-exemption\",\"display\":\"HAZWOPER Medical Exemption\",\"system\":\"urn:workwell:vs:hazwoper-exemption\"}]");

ensureValueSet(VS_FLU_ENROLL, "urn:workwell:vs:clinical-roles", "Clinical Facing Roles", "2025-demo",
"[{\"code\":\"clinical-role\",\"display\":\"Clinical Facing Roles\",\"system\":\"urn:workwell:vs:clinical-roles\"}]");
ensureValueSet(VS_FLU_WAIVER, "urn:workwell:vs:flu-exemption", "Flu Vaccine Exemption", "2025-demo",
"[{\"code\":\"flu-exemption\",\"display\":\"Flu Vaccine Exemption\",\"system\":\"urn:workwell:vs:flu-exemption\"}]");

// 3. Link them all
ensureLink("Audiogram", DEMO_VS_AUDIOGRAM);
ensureLink("Audiogram", VS_AUDIOGRAM_ENROLL);
ensureLink("Audiogram", VS_AUDIOGRAM_WAIVER);

ensureLink("TB Surveillance", DEMO_VS_TB);
ensureLink("TB Surveillance", VS_TB_ENROLL);
ensureLink("TB Surveillance", VS_TB_WAIVER);

ensureLink("HAZWOPER Surveillance", DEMO_VS_HAZWOPER);
ensureLink("HAZWOPER Surveillance", VS_HAZWOPER_ENROLL);
ensureLink("HAZWOPER Surveillance", VS_HAZWOPER_WAIVER);

ensureLink("Flu Vaccine", DEMO_VS_FLU);
ensureLink("Flu Vaccine", VS_FLU_ENROLL);
ensureLink("Flu Vaccine", VS_FLU_WAIVER);
}

private void ensureValueSet(UUID id, String oid, String name, String version, String codesJson) {
jdbcTemplate.update("DELETE FROM value_sets WHERE oid = ? AND id <> ?", oid, id);

Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM value_sets WHERE id = ?", Integer.class, id);
if (count != null && count > 0) {
jdbcTemplate.update(
"UPDATE value_sets SET oid = ?, name = ?, version = ?, codes_json = ?::jsonb, resolution_status = 'RESOLVED', last_resolved_at = NOW() WHERE id = ?",
oid, name, version, codesJson, id
);
} else {
jdbcTemplate.update(
"INSERT INTO value_sets (id, oid, name, version, codes_json, resolution_status, status, source, last_resolved_at) VALUES (?, ?, ?, ?, ?::jsonb, 'RESOLVED', 'ACTIVE', 'WorkWell Demo', NOW())",
id, oid, name, version, codesJson
);
}
}

private void ensureLink(String measureName, UUID valueSetId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,21 @@ public void finalizeAsyncRun(UUID runId, String scopeLabel, List<DemoRunPayload>
String stage = "start";
try {
seedSyntheticEmployees();
Instant startedAt = LocalDate.parse(measureRuns.get(0).evaluationDate()).atStartOfDay().toInstant(ZoneOffset.UTC);

Timestamp dbStartedAtTs = null;
try {
dbStartedAtTs = jdbcTemplate.queryForObject("SELECT started_at FROM runs WHERE id = ?", Timestamp.class, runId);
} catch (Exception ex) {
log.warn("Could not query started_at for run {}: {}", runId, ex.getMessage());
}
Instant actualStart = dbStartedAtTs != null ? dbStartedAtTs.toInstant() : Instant.now();
Instant completedAt = Instant.now();
long durationMs = Math.max(0, completedAt.toEpochMilli() - actualStart.toEpochMilli());

String evaluationPeriod = measureRuns.get(0).evaluationDate();
LocalDate evalDate = LocalDate.parse(evaluationPeriod);
Instant periodStart = evalDate.minusYears(1).atStartOfDay().toInstant(ZoneOffset.UTC);
Instant periodEnd = evalDate.atStartOfDay().toInstant(ZoneOffset.UTC);

long totalEvaluated = measureRuns.stream().mapToLong(payload -> payload.outcomes().size()).sum();
long compliant = measureRuns.stream()
Expand Down Expand Up @@ -427,14 +439,14 @@ public void finalizeAsyncRun(UUID runId, String scopeLabel, List<DemoRunPayload>
WHERE id = ?
""",
finalStatus,
Timestamp.from(startedAt),
Timestamp.from(actualStart),
Timestamp.from(completedAt),
totalEvaluated,
compliant,
nonCompliant,
Timestamp.from(startedAt),
Timestamp.from(completedAt),
completedAt.toEpochMilli() - startedAt.toEpochMilli(),
Timestamp.from(periodStart),
Timestamp.from(periodEnd),
durationMs,
failureSummary,
partialFailureCount,
runId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ void resolveCheckWithSeededDemoValueSetsLinked() {

assertThat(result.measureId()).isEqualTo(audiogramId);
assertThat(result.valueSets()).isNotEmpty();
assertThat(result.valueSets()).anyMatch(vs -> "Audiogram Procedure Codes".equals(vs.name()));
assertThat(result.valueSets()).anyMatch(vs -> "Audiogram Procedures".equals(vs.name()));
assertThat(result.valueSets()).anyMatch(vs -> vs.codeCount() > 0);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ void resolveCheckEndpointReturnsOk() throws Exception {
measureId, versionId, true,
List.of(new ValueSetGovernanceService.ValueSetCheckItem(
UUID.fromString("a0000001-0000-0000-0000-000000000001"),
"Audiogram Procedure Codes",
"urn:workwell:vs:audiogram-procedure-codes",
"Audiogram Procedures",
"urn:workwell:vs:audiogram-procedures",
"2025-demo", "RESOLVED", 4, List.of(), false
)),
List.of(), List.of()
Expand All @@ -149,7 +149,7 @@ void resolveCheckEndpointReturnsOk() throws Exception {
mockMvc.perform(post("/api/measures/{id}/value-sets/resolve-check", measureId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.allResolved").value(true))
.andExpect(jsonPath("$.valueSets[0].name").value("Audiogram Procedure Codes"))
.andExpect(jsonPath("$.valueSets[0].name").value("Audiogram Procedures"))
.andExpect(jsonPath("$.valueSets[0].codeCount").value(4))
.andExpect(jsonPath("$.blockers").isArray())
.andExpect(jsonPath("$.warnings").isArray());
Expand All @@ -161,8 +161,8 @@ void valueSetDiffEndpointReturnsOk() throws Exception {
UUID toId = UUID.fromString("a0000001-0000-0000-0000-000000000002");
when(valueSetGovernanceService.diff(fromId, toId)).thenReturn(
new ValueSetGovernanceService.ValueSetDiffResponse(
fromId.toString(), "Audiogram Procedure Codes", "2025-demo",
toId.toString(), "TB Screening Procedure Codes", "2025-demo",
fromId.toString(), "Audiogram Procedures", "2025-demo",
toId.toString(), "TB Screening Procedures", "2025-demo",
List.of(new ValueSetGovernanceService.CodeEntry("LOCAL-TB-001", "PPD skin test", "urn:workwell:demo")),
List.of(new ValueSetGovernanceService.CodeEntry("LOCAL-AUD-001", "Baseline audiogram", "urn:workwell:demo")),
List.of(), List.of("1 code(s) added.", "1 code(s) removed.")
Expand All @@ -171,8 +171,8 @@ void valueSetDiffEndpointReturnsOk() throws Exception {

mockMvc.perform(get("/api/value-sets/{id}/diff", fromId).param("to", toId.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.fromName").value("Audiogram Procedure Codes"))
.andExpect(jsonPath("$.toName").value("TB Screening Procedure Codes"))
.andExpect(jsonPath("$.fromName").value("Audiogram Procedures"))
.andExpect(jsonPath("$.toName").value("TB Screening Procedures"))
.andExpect(jsonPath("$.addedCodes[0].code").value("LOCAL-TB-001"))
.andExpect(jsonPath("$.removedCodes[0].code").value("LOCAL-AUD-001"));
}
Expand Down
22 changes: 22 additions & 0 deletions docs/JOURNAL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Journal

## 2026-05-20 — UAT Sections 6-8: Run history, Studio, Admin fixes (issue #29)

**Goal:** Fix all reported Section 6 (Run History), Section 7 (Studio/CQL), and Section 8 (Admin panel) UAT bugs from GitHub issue #29.

**Branch:** `fix/sprint-1-uat-sections-6-8`

**What changed:**

- `backend/src/main/java/com/workwell/run/RunPersistenceService.java` — Fixed `finalizeAsyncRun()` duration computation: was incorrectly using the evaluationDate (a historical date) to compute `duration_ms`, yielding absurd values like `69068s`. Now fetches the actual `started_at` from the DB and computes real wall-clock duration. Also fixed `measurement_period_start`/`measurement_period_end` to correctly reflect the 1-year evaluation window (evalDate-1yr → evalDate) instead of repeating `startedAt` twice.
- `backend/src/main/java/com/workwell/BackendApplication.java` — Set JVM default timezone to UTC on startup for consistent timestamp handling.
- `backend/src/main/java/com/workwell/measure/ValueSetGovernanceService.java` — Renamed `ensureDemoValueSetLinks()` to `ensureDemoValueSets()` and expanded it to seed all 4 demo value sets (audiogram, TB, HAZWOPER, flu vaccine) with their correct CQL-matching canonical OIDs and local codes so `resolveCheck` finds matching codes.
- `frontend/app/(dashboard)/admin/page.tsx` — Added confirmation dialog before disabling the scheduler to prevent accidental disables during a demo.
- `frontend/features/studio/components/CqlTab.tsx` — Added "New Version" button with a modal dialog for entering a change summary and cloning the current CQL into a new draft measure version.
- PR review follow-up: wired the CQL-tab modal summary directly into the version-clone request so it no longer depends on asynchronous React state, added a Runs/Run Detail display guard that renders anomalous `durationMs` values over 1 hour as `-` or `Stalled`, and restored Cases search state synchronization when browser history changes the `search` URL parameter.

**Verification:**
- `backend/gradlew.bat test --tests com.workwell.export.* --tests com.workwell.web.RunControllerTest` — BUILD SUCCESSFUL (21s).
- Backend compiles cleanly: `gradlew compileJava` — BUILD SUCCESSFUL (16s).
- Playwright end-to-end: triggered async All Programs run — completed with `179s` real duration (vs old seeded `60s` constant). Duration correctly reflects actual CQL evaluation time.

---

## 2026-05-20 — UAT Section 5: Case detail fixes (issue #28)

**Goal:** Fix Section 5 case-detail bugs from UAT #23: escalation confirmation, outreach delivery badge refresh, audit packet format selectors, and walkthrough-guide inaccuracies.
Expand Down
Loading
Loading