Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -110,6 +114,15 @@ export default function App() {
<Route path="review/learned" element={<Aliases />} />
<Route path="record" element={<Record />} />
<Route path="security" element={<Security />} />
{/* 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. */}
<Route path="*" element={<NotFound />} />
</Route>

<Route path="*" element={<Navigate to="/" replace />} />
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/screens/Aliases.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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.
</p>
{/* 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. */}
<Link className="btn btn--primary" to="/app/review">Go to the review queue</Link>
</div>
) : (
<ul className="aliases">
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/screens/NotFound.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="empty">
<h2 className="empty__title">That page is not here</h2>
<p className="empty__body">
The address is wrong, or whatever was here has moved. Your session is
fine — nothing has signed you out.
</p>
<Link className="btn btn--primary" to="/app">See your results</Link>
</div>
);
}
Loading