Repository navigation
feat(ui): move experimental metrics to separate pages - #440
Merged
wpfleger96 merged 3 commits intoNov 1, 2026
Merged
wpfleger96 merged 3 commits into
wpfleger96 merged 3 commits into
Conversation
Owner
Author
ScreenshotsDay detailThe flow-limitation and RERA group is gone, replaced by one plain link to the experimental page. Experimental day pageNew page at Analysis pageEffectiveness and AHI trend now sit in the Therapy Assessment card. The sidebar group reads Experimental. DashboardThe effectiveness and trend badges are gone. StatsThe effectiveness and trend badges are gone. |
wpfleger96
added a commit
that referenced
this pull request
Oct 31, 2026
This was referenced Oct 31, 2026
wpfleger96
force-pushed
the
wpfleger96/ui-experimental-pages
branch
from
November 1, 2026 19:08
552ec5a to
0efd518
Compare
wpfleger96
force-pushed
the
wpfleger96/mcp-experimental-gate
branch
from
November 1, 2026 19:08
aaf2a2d to
02825f1
Compare
Day detail, the Dashboard, Stats and reports showed FL, RERA, effectiveness
and trend judgements beside device data, and GET /days/{date} computed them on
every load. These are SNORE's own inventions, so they move behind
/days/:date/experimental and the Analysis page, and the sidebar group is
renamed Experimental.
REST URLs stay; operations that return experimental data carry an experimental
tag so a test can pin the list and reject experimental fields anywhere else.
Effectiveness and trend direction are retagged Experimental, since they are
judgements rather than arithmetic on device data.
A large days_limit overflowed date arithmetic into a 500, and the all-time experimental stats never needed the parameter, so it is gone there and bounded elsewhere. The provenance gate saw only numbers and booleans, which let an untagged string judgement through; it now covers enum and Literal fields. DayDetailView kept showing the old date when the router reused it, so it now reloads on date change through useApiLoad, which also drops stale responses.
wpfleger96
force-pushed
the
wpfleger96/ui-experimental-pages
branch
from
November 1, 2026 20:09
0efd518 to
d829a38
Compare
`GET /sessions/{id}/events/match` reports 0 matched. It adds the stored
`timestamp_start` to each SNORE event's relative time and compares the
result with absolute device times, but `timestamp_start` is `0.0` on
stored analysis results, so nothing lines up. On all 205 analyzed
sessions I checked locally it matched nothing, which is the 0 matched
and 0% sensitivity on Event Explorer, and it also made every device
event a false negative and every SNORE event a false positive.
`/waveforms/compare` has the right time base but disagrees with it in
other ways: it uses any-match false negatives, counts every device event
type in `machine_event_count`, has no matched count, and hardcodes the 5
s tolerance.
This PR makes `compare_events` the one implementation and deletes
`/events/match`. It now pools apneas and hypopneas on both sides and
uses `match_events_by_start_time` with `EVENT_MATCH_TOLERANCE_SECONDS`,
the batch-validation matcher, so each event is in at most one pair. The
result gains `matched_events`, the name `EventValidationResult` already
uses. Matched and Sensitivity move to the Event Comparison card on the
Analysis page, and Event Explorer drops its match section, so it's now a
device-only page. The card names its detection mode, follows the mode
toggle, loads after a run from the page, and hides when the selected
mode has no comparison. `compare_events` now finds its row through
`latest_analysis_row`.
**Removed:** `/events/match`, `EventMatchResult`,
`EventService.match_events`, `get_machine_apnea_hypopnea_times`,
`MATCHABLE_MACHINE_EVENT_TYPES`, `split_by_tolerance_match`,
`HasStartTime` and `getEventMatch`. `convert_machine_events` already
keeps only the matchable types.
**Changes that show up in numbers:**
- Sensitivity goes from 0% to its real value. Across 200 analyzed local
sessions that's 347 of 1,132 device apneas and hypopneas, about 31%.
- `machine_event_count` on the card and in `snore waveform compare` now
counts only apneas and hypopneas, so it drops (1,149 to 1,132 on those
sessions). The card says so, because the summary card on the same page
uses the same label for all device event types.
- False-negative and false-positive totals can shift under one-to-one
matching. On those sessions the totals didn't move (785 and 5,289), and
new counts equal the old matching logic with the time base corrected on
all 205 sessions.
- A missed device hypopnea now reports type `H`. It used to be
`unknown`, because `HypopneaEvent` has no `event_type`.
- `snore waveform compare` prints the matched count, and its Machine
line says "(apneas and hypopneas only)".
- `snore analysis` Missed and Extra lists are now one-to-one and
time-ordered, so they can be longer than before. It still includes RERAs
when the device scored them and `snore waveform compare` doesn't, so the
two can differ on those sessions.
- False-positive and false-negative tables now page at 25 rows through a
shared `TablePager`, which Events by Mode uses too.
These local analysis rows are `STALE_VERSION`, so the exact percentages
will move after re-analysis. The sensitivity number is still SNORE's
experimental detection scored against device events, not a clinical
measurement.
`ui/src/tests/experimentalBoundary.test.ts` closes the gap the REST test
can't see: a default page calling an experimental route. It calls every
`ui/src/api` export against a fake adapter to find the wrappers that hit
experimental operations, then scans all of `ui/src` except `api`,
`tests` and `types`. The operation list lives in
`ui/src/tests/experimentalOperations.json`, and
`EXPERIMENTAL_OPERATIONS` reads the same file, so a new tagged route is
covered on both sides. `cancelAnalysisJob` is the one exception, since
`ImportView` needs it and it reads no metrics.
No migration, re-analysis, re-parse or `recompute-days` is needed.
Stack: #439 → #440 → this PR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.






Day detail, the Dashboard, Stats and reports show SNORE's own FL and RERA numbers and its effectiveness and trend judgements next to device data.
GET /days/{date}also runs the breath lookup on every load to produce them. This PR moves that data to separate pages and endpoints, so a person only sees it by going there. Nothing is deleted, and analysis still runs on import.DayDetail:fl_class_ge4_pct,rera_index,rera_countand their reasons/days/:date/experimentalandGET /days/{date}/experimentalTherapySummary, report templates:effectiveness,ahi_trend_directionGET /stats/experimentalREST URLs stay put. Operations that return experimental data now carry a FastAPI
experimentaltag: the analysis and validation routers,/events/match,/waveforms/compareand the two new endpoints.tests/unit/test_provenance_tags.pyrejects any Experimental field reachable from an untagged operation, and pins the tagged list inEXPERIMENTAL_OPERATIONSso a route can't be tagged to escape the check. Tagging is now required on enum andLiteralresponse fields too, so a judgement likeeffectivenesscan't go untagged. Plain string fields still aren't checked. The sidebar group "Analysis" is renamed "Experimental".Changes that show up:
effectivenessandahi_trend_directionare retagged from Derived to Experimental, and their API types are now enums. They're SNORE's judgements, not arithmetic on device data.GET /days/{date}no longer touches breath rows, so it answers the same whether or not analysis has run.snore statsdrops the Effectiveness line and the trend direction in the chart title./stats/experimentaltakes nodays_limitand always returns 200. An empty range gives{"effectiveness": "unknown", "ahi_trend_direction": null}./stats/summary,/stats/periods,/stats/trendsand/stats/recordsreturn 422 fordays_limitoutside 0 to 36500. Very large values used to return 500 and negative ones an empty result. No client sends either.ApneaEvent.event_typeis now tagged Experimental.DayDetailViewreloads when the date changes and drops stale responses, like the new page.Left as is:
DayExperimentalMetricshas nodevice_id, andDayDetailViewhas no device query, so on a two-device date the page shows the lowest device id, the same rowGET /days/{date}picks by default.snore stats(well controlledorelevated) is also a SNORE judgement. I left it for now since it wasn't part of this split./events/match. The next PR in the stack removes that.ImportViewstill callsDELETE /analysis/jobs/{job_id}, which is taggedexperimentalwith the rest of the analysis router. It only cancels a job and reads no metrics.Apple Health stays on the therapy pages, and Records and Rx best/worst stay default.
No migration, re-analysis, re-parse or
recompute-daysis needed.Stack: #439 → this PR → #441