Missing chrome: no logout, no upload UI, manual evidence unreachable #101

Closed
opened 2026-07-28 06:03:09 +00:00 by claude-bot · 2 comments

Severity: MEDIUM

The problems

Three pieces of basic chrome are missing, each with the backend already in place.

No logout button. frontend/src/components/AppShell.tsx:21-27 shows the user but offers no
sign-out, while POST /api/auth/logout exists at backend/app/api/routes/auth.py:76. On a shared
family machine you cannot sign out.

No upload UI. frontend/src/api/client.ts:61 has api.upload with no caller. Ingest requires
curl. This is legitimately deferrable if a watch-directory worker is the real plan — but the README
should say so, and the bulk ingest issue covers the larger version.

Manual evidence is unreachable. frontend/src/api/photos.ts:51-66 implements
createManualEvidence and supersedeEvidence; no UI calls either. So the evidence model's
"manual" source — how a reviewer records "Mom wrote 1972 on the back" as evidence rather than
burying it in a decision rationale — has plumbing on both sides and no interface.

Scope

  • Logout control in the nav rail footer.
  • An "Add evidence" row at the end of the evidence list, opening a small form (source, date range,
    precision, note).
  • A supersede affordance on existing evidence rows, once the backend issue that fixes
    superseded_by has landed.
  • A minimal upload page, or a README note that ingest is command-line/watch-directory only.

Done when

  • A user can sign out
  • A reviewer can add manual evidence without using the API directly
  • Evidence can be superseded from the UI
  • The ingest story is either in the UI or documented

References

  • frontend/src/components/AppShell.tsx:21-27
  • frontend/src/api/photos.ts:51-66; client.ts:61
  • backend/app/api/routes/auth.py:76
## Severity: MEDIUM ## The problems Three pieces of basic chrome are missing, each with the backend already in place. **No logout button.** `frontend/src/components/AppShell.tsx:21-27` shows the user but offers no sign-out, while `POST /api/auth/logout` exists at `backend/app/api/routes/auth.py:76`. On a shared family machine you cannot sign out. **No upload UI.** `frontend/src/api/client.ts:61` has `api.upload` with no caller. Ingest requires curl. This is legitimately deferrable if a watch-directory worker is the real plan — but the README should say so, and the bulk ingest issue covers the larger version. **Manual evidence is unreachable.** `frontend/src/api/photos.ts:51-66` implements `createManualEvidence` and `supersedeEvidence`; no UI calls either. So the evidence model's "manual" source — how a reviewer records "Mom wrote 1972 on the back" as *evidence* rather than burying it in a decision rationale — has plumbing on both sides and no interface. ## Scope - Logout control in the nav rail footer. - An "Add evidence" row at the end of the evidence list, opening a small form (source, date range, precision, note). - A supersede affordance on existing evidence rows, once the backend issue that fixes `superseded_by` has landed. - A minimal upload page, or a README note that ingest is command-line/watch-directory only. ## Done when - [ ] A user can sign out - [ ] A reviewer can add manual evidence without using the API directly - [ ] Evidence can be superseded from the UI - [ ] The ingest story is either in the UI or documented ## References - `frontend/src/components/AppShell.tsx:21-27` - `frontend/src/api/photos.ts:51-66`; `client.ts:61` - `backend/app/api/routes/auth.py:76`
claude-bot added this to the v0.3.0 milestone 2026-07-28 06:03:09 +00:00
Author

Picking this up.

Probe first. All three claims hold, which is worth saying given how many issues turned out to
be stale last session:

  • AppShell.tsx shows the user and offers no sign-out. POST /api/auth/logout is live (it has
    moved to auth.py:158 since this was filed) and revokes the session server-side, not just the
    cookie.
  • createManualEvidence and supersedeEvidence in api/photos.ts have zero callers in
    frontend/src.
  • api.upload in api/client.ts:92 likewise has zero callers.

Two corrections to the issue text:

  1. The form has no "source" field to offer. The scope here asks for "source, date range,
    precision, note", but create_manual_evidence hard-codes source=EvidenceSource.manual and
    reliability=EvidenceReliability.high. A source picker would be a control whose value the
    server ignores. The form is date low, date high, precision, note.

  2. The superseded_by precondition has already landed. This issue defers the supersede
    affordance until "the backend issue that fixes superseded_by" is done — that is #80, and it
    is in: EvidenceRepository.supersede is called from both routes, and
    POST /photos/{id}/evidence/manual accepts a supersedes id so that retiring one row and
    recording its replacement happen in one transaction. The frontend wrappers do not expose
    either parameter yet, so that atomicity is currently unreachable — worth fixing here, since the
    route's own comment says splitting it into two requests "leaves a window where the photo has
    neither or both".

Splitting the upload UI out as #142, per the plan for this milestone. It is the browser's first
write path into the archive, its failures are per-file rather than per-request, and there is a real
undecided question behind it (browser upload vs. a watch-directory worker) that should not be
settled as the third bullet of a chrome ticket.

So this issue is now: a logout control, and manual evidence made reachable — including
supersede/replace.
Remaining scope box:

  • A minimal upload page, or a README note#142
Picking this up. **Probe first.** All three claims hold, which is worth saying given how many issues turned out to be stale last session: - `AppShell.tsx` shows the user and offers no sign-out. `POST /api/auth/logout` is live (it has moved to `auth.py:158` since this was filed) and revokes the session server-side, not just the cookie. - `createManualEvidence` and `supersedeEvidence` in `api/photos.ts` have **zero callers** in `frontend/src`. - `api.upload` in `api/client.ts:92` likewise has zero callers. Two corrections to the issue text: 1. **The form has no "source" field to offer.** The scope here asks for "source, date range, precision, note", but `create_manual_evidence` hard-codes `source=EvidenceSource.manual` and `reliability=EvidenceReliability.high`. A source picker would be a control whose value the server ignores. The form is date low, date high, precision, note. 2. **The `superseded_by` precondition has already landed.** This issue defers the supersede affordance until "the backend issue that fixes `superseded_by`" is done — that is #80, and it is in: `EvidenceRepository.supersede` is called from both routes, and `POST /photos/{id}/evidence/manual` accepts a `supersedes` id so that retiring one row and recording its replacement happen in **one transaction**. The frontend wrappers do not expose either parameter yet, so that atomicity is currently unreachable — worth fixing here, since the route's own comment says splitting it into two requests "leaves a window where the photo has neither or both". **Splitting the upload UI out** as #142, per the plan for this milestone. It is the browser's first write path into the archive, its failures are per-file rather than per-request, and there is a real undecided question behind it (browser upload vs. a watch-directory worker) that should not be settled as the third bullet of a chrome ticket. So this issue is now: **a logout control, and manual evidence made reachable — including supersede/replace.** Remaining scope box: - [x] ~~A minimal upload page, or a README note~~ → #142
Author

Done in 4849694, CI green (run 64).

Logout. In the nav rail footer. Three things beyond issuing the request: it empties the entire query cache rather than just the identity — this issue's own scenario is a shared family machine, and invalidating ["auth","me"] alone would return the next person to the sign-in page with every photograph, comment and note the previous reviewer opened still in memory behind it; it treats a 401 as success, because the session had already ended and reporting that as a failure would tell someone standing up from a shared machine that they are still signed in; and it says so loudly when the server refuses, because the dangerous failure is the silent one.

Manual evidence. A native <dialog> behind "Edit evidence…" in the evidence panel. Add, replace, retire. The shape was chosen rather than defaulted: the panel is 420px and an evidence row is already the control that adopts a date (#96), so hanging Replace and Retire off the same row would put three actions on one target, two destructive, in the width of a date.

The frontend wrappers were missing the two parameters that make the atomic replace reachable — supersedes on the manual route and superseded_by on supersede — so the one-transaction path #80 built had no way to be called. Replacing is now a single request, asserted end to end against the database.

A third thing no issue named: nothing rendered DateEvidence.notes anywhere, so a manual row whose whole content is the sentence had no dates and no raw_value and drew as — an empty answer for evidence that says something. The panel now shows the note.

Modality is held in a real browser. jsdom 30 implements none of <dialog> — not showModal, not close, not inertness — so test/setup.ts stubs the opening with its limits written down and e2e/tests/evidence-dialog.spec.ts asserts the focus trap, Escape, and that Approve behind the backdrop can neither take focus nor be clicked. Writing that corrected a wrong belief: a programmatic element.click() fires regardless of inertness, because the platform blocks user interaction and not script, so the first version of the assertion failed against a correctly modal dialog.

Two tests were worthless until mutation testing said so, which is the strongest argument for the practice this project has produced. The cache-clearing assertions passed whether the code cleared anything or not — renderWithProviders sets gcTime: 0, which collects an unobserved query the instant it is written, so setQueryData vanished before the next line ran. And the keying test passed with key={photo.id} deleted, because without the next photograph in cache the page renders "Loading photo…" for a tick and that early return unmounts the workspace, resetting state by remount rather than by key. Both now reproduce production and both fail when broken.

Frontend tests 158 → 185, end-to-end 7 → 10.

Two side effects worth recording. The upload UI is #142. And this exposed a pre-existing race in signInThroughTheUi (fixed in ffcf069): it returned after the click rather than the response, so a caller's page.goto could cancel the login browser-side — the server logged a 200 and the browser never processed the Set-Cookie, and the test then waited out its full timeout on a page about HTTP caching.

Remaining known gap, not in scope here: nothing gates write controls by role, so a viewer is offered buttons the API will refuse. That is #22's territory.

Done in `4849694`, CI green (run 64). **Logout.** In the nav rail footer. Three things beyond issuing the request: it empties the **entire** query cache rather than just the identity — this issue's own scenario is a shared family machine, and invalidating `["auth","me"]` alone would return the next person to the sign-in page with every photograph, comment and note the previous reviewer opened still in memory behind it; it treats a **401 as success**, because the session had already ended and reporting that as a failure would tell someone standing up from a shared machine that they are still signed in; and it **says so loudly when the server refuses**, because the dangerous failure is the silent one. **Manual evidence.** A native `<dialog>` behind "Edit evidence…" in the evidence panel. Add, replace, retire. The shape was chosen rather than defaulted: the panel is 420px and an evidence row is *already* the control that adopts a date (#96), so hanging Replace and Retire off the same row would put three actions on one target, two destructive, in the width of a date. The frontend wrappers were missing the two parameters that make the atomic replace reachable — `supersedes` on the manual route and `superseded_by` on supersede — so the one-transaction path #80 built had no way to be called. Replacing is now a single request, asserted end to end against the database. **A third thing no issue named:** nothing rendered `DateEvidence.notes` anywhere, so a manual row whose whole content is the sentence had no dates and no `raw_value` and drew as `—` — an empty answer for evidence that says something. The panel now shows the note. **Modality is held in a real browser.** jsdom 30 implements none of `<dialog>` — not `showModal`, not `close`, not inertness — so `test/setup.ts` stubs the opening with its limits written down and `e2e/tests/evidence-dialog.spec.ts` asserts the focus trap, Escape, and that Approve behind the backdrop can neither take focus nor be clicked. Writing that corrected a wrong belief: a programmatic `element.click()` fires regardless of inertness, because the platform blocks *user* interaction and not script, so the first version of the assertion failed against a correctly modal dialog. **Two tests were worthless until mutation testing said so**, which is the strongest argument for the practice this project has produced. The cache-clearing assertions passed whether the code cleared anything or not — `renderWithProviders` sets `gcTime: 0`, which collects an unobserved query the instant it is written, so `setQueryData` vanished before the next line ran. And the keying test passed with `key={photo.id}` deleted, because without the next photograph in cache the page renders "Loading photo…" for a tick and that early return unmounts the workspace, resetting state by remount rather than by key. Both now reproduce production and both fail when broken. Frontend tests 158 → 185, end-to-end 7 → 10. Two side effects worth recording. The upload UI is #142. And this exposed a **pre-existing race in `signInThroughTheUi`** (fixed in `ffcf069`): it returned after the click rather than the response, so a caller's `page.goto` could cancel the login browser-side — the server logged a 200 and the browser never processed the `Set-Cookie`, and the test then waited out its full timeout on a page about HTTP caching. Remaining known gap, not in scope here: nothing gates write controls by role, so a viewer is offered buttons the API will refuse. That is #22's territory.
Sign in to join this conversation.
No description provided.