diff --git a/HANDOFF.md b/HANDOFF.md index b23e79c..4cdf7d8 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -544,11 +544,12 @@ structurally sound and in the right unit. ### 6. Polish that is worth the time -- **Empty states with a next action.** "0 learned mappings" explains itself well - but offers nothing to do. -- **The upload screen does not say what happens next.** A dropped file goes - `queued → extracting → mapping → needs_review`, and only the last one is - actionable. Say so on the screen rather than in status labels. +- ~~**Empty states with a next action.**~~ Done. The last one without a door + was `/app/review/learned`, which explained itself and offered nothing; it now + links to the queue the rules come from. +- ~~**The upload screen does not say what happens next.**~~ Already done when + this was re-read. Each file carries its own stage, and once anything resolves + the screen offers "See your results" and "Confirm N uncertain results". - ~~**Keyboard path through the review queue.**~~ Done. `j`/`k` move a cursor, `1`–`9` choose a candidate, `Enter` confirms, `x` rejects, `/` reaches the search field and `Escape` leaves it. There is no skip key: moving the cursor @@ -579,8 +580,21 @@ structurally sound and in the right unit. `alias_written: false` and means it — the one counter whose job is to make convergence visible rather than claimed was overstating it. And a queue of one read "1 result need you". -- **`prefers-reduced-motion`.** The cascade scene animates unconditionally. -- **A real 404 route.** Unknown `/app/*` paths currently render the shell empty. +- ~~**`prefers-reduced-motion`.**~~ Already handled when this was re-read: + `CascadeScene.jsx` reads `matchMedia` before it animates, and `tokens.css` + carries the global rule. The entry was stale, not outstanding. +- ~~**A real 404 route.**~~ Done — **and the diagnosis here was wrong**, which + is worth keeping. Unknown `/app/*` paths did not render the shell empty. They + never reached the shell at all: with no child matching, `/app` does not match + either, so the *top-level* `*` caught them and redirected to the marketing + page. A signed-in user who mistyped a URL was dropped out of the app onto the + landing page, which is indistinguishable from having been signed out — on a + screen holding clinical data, the one thing it must not look like. + + Measured both ways: before, `/app/typo` ended at `/` showing the hero; after, + it stays at `/app/typo` inside the shell and says so. The catch-all is a + child of `/app` and inside the guard, deliberately — bouncing anywhere would + reproduce the bug it fixes. ### Deliberately still not in scope diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4691924..2de5f03 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,6 +2,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Suspense, lazy } from "react"; import { Navigate, Route, BrowserRouter as Router, Routes, useLocation } from "react-router-dom"; import AppShell from "./app/AppShell.jsx"; +// Static, unlike the screens below: this one is a heading and a link, and +// making it a chunk would mean a network round trip to tell somebody their +// URL is wrong — on the one path where the network may be why they are here. +import NotFound from "./screens/NotFound.jsx"; import { AuthProvider, useAuth } from "./auth/AuthContext.jsx"; import { PatientProvider } from "./patients/PatientContext.jsx"; import Hero from "./sections/Hero.jsx"; @@ -110,6 +114,15 @@ export default function App() { } /> } /> } /> + {/* Inside the shell, not outside it. The top-level `*` below + never sees `/app/typo`: that path matches this route, whose + outlet then has no child to render, so the shell painted its + nav and its patient switcher around an empty column and + called it a page. Signed in, apparently fine, nothing there. + A wrong URL should say so — and it must stay inside the + guard, because bouncing to the marketing page would read as + having been signed out. */} + } /> } /> diff --git a/frontend/src/screens/Aliases.jsx b/frontend/src/screens/Aliases.jsx index 882ea42..80c925a 100644 --- a/frontend/src/screens/Aliases.jsx +++ b/frontend/src/screens/Aliases.jsx @@ -1,5 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; import { api, messageFor } from "../api/client"; import Modal from "../components/Modal"; import "./Aliases.css"; @@ -142,6 +143,10 @@ export default function Aliases() { Confirming a mapping in the review queue teaches the system how this lab prints that test. Those rules will appear here.

+ {/* An empty state that explains itself and offers nothing to do + leaves the reader where they started. The queue is where these + come from, so that is the door. */} + Go to the review queue ) : (
    diff --git a/frontend/src/screens/NotFound.jsx b/frontend/src/screens/NotFound.jsx new file mode 100644 index 0000000..51e2697 --- /dev/null +++ b/frontend/src/screens/NotFound.jsx @@ -0,0 +1,25 @@ +import { Link } from "react-router-dom"; + +/** + * A wrong path inside the app. + * + * `/app/typo` matches the `/app` route, and with no child to render the shell + * drew its navigation and patient switcher around an empty column — signed in, + * apparently working, and nothing on the page. This says what happened instead. + * + * It stays inside the guard on purpose. Redirecting to the marketing page would + * be indistinguishable from being signed out, which is the one thing a + * mistyped URL must not look like on a screen that holds clinical data. + */ +export default function NotFound() { + return ( +
    +

    That page is not here

    +

    + The address is wrong, or whatever was here has moved. Your session is + fine — nothing has signed you out. +

    + See your results +
    + ); +}