add 0022/SESSION-0022.2-TRANSCRIPT-2026-05-28T12-04--2026-05-28T12-18.md
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
# Session 0022.2 — Transcript
|
||||||
|
> Parent: SESSION-0022.0-TRANSCRIPT-…md
|
||||||
|
> Date: 2026-05-28
|
||||||
|
> Goal: implement roadmap #29 (server-side sign-in state resume) as rfc-app v0.23.0 feature code
|
||||||
|
> Outcome: **Done. Branch `feature/v0.23.0-signin-resume` @ 77010ea. Backend 336 passed (332 baseline + 4 new). Frontend build green. Migration 022 auto-applies. Profile-toggle UI deferred (column + default-on behavior ship now).**
|
||||||
|
|
||||||
|
## Pre-session state
|
||||||
|
- Forked subagent of OHM driver Session 0022.0. Isolated worktree at
|
||||||
|
`/Users/benstull/git/rfc-app-v0.23.0-signin-resume`, branch
|
||||||
|
`feature/v0.23.0-signin-resume` based on main @ 2ac20b1.
|
||||||
|
- Concurrent subagent 0022.1 owns propose/PR regions of api.py +
|
||||||
|
several frontend components + migration 021. Strict file ownership.
|
||||||
|
- Baseline backend suite: 332 passed.
|
||||||
|
|
||||||
|
## Turn-by-turn arc
|
||||||
|
1. Read SESSION-PROTOCOL.md and confirmed worktree/branch. Noted the
|
||||||
|
protocol says subagents don't write their own transcript, but the
|
||||||
|
subsession prompt explicitly requires a §5 transcript file
|
||||||
|
(unpublished) — followed the prompt.
|
||||||
|
2. Inspected migrations. `users.id` is `INTEGER PRIMARY KEY
|
||||||
|
AUTOINCREMENT` (001, rebuilt in 012). Timestamp convention is
|
||||||
|
`TEXT NOT NULL DEFAULT (datetime('now'))` (users.last_seen_at,
|
||||||
|
device_trust.last_seen_at). No native JSONB — other JSON stored as
|
||||||
|
TEXT via json.dumps/loads (permission_events.details,
|
||||||
|
actions.details). There is NO `user_sessions` table; sessions are
|
||||||
|
signed cookies (`rfc_session`), so a new `user_session_state` table
|
||||||
|
keyed on user_id was the right shape.
|
||||||
|
3. Read api.py `/me` region: auth surface uses `auth.require_user`
|
||||||
|
(401 for anon), `/api/auth/me` already does a single-round-trip
|
||||||
|
payload (beta + passcode state). db.conn() is `isolation_level=None`
|
||||||
|
(autocommit) — no explicit commit needed, matching every other
|
||||||
|
write endpoint.
|
||||||
|
4. Read auth.py `require_user` / `current_user` / SessionUser.
|
||||||
|
5. Read the frontend: Login.jsx does a hard `window.location.assign`
|
||||||
|
after sign-in, so the app re-boots and App.jsx's `getMe()` reloads
|
||||||
|
`me`. The #21-Part-C `identify` fires in an App.jsx effect when
|
||||||
|
`me.user.id` first appears. Decision: the resume redirect must live
|
||||||
|
in App.jsx (after identify), NOT in Login.jsx (which I don't own and
|
||||||
|
must not reorder).
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
- **Table:** new `user_session_state` (migration 022), one row per user
|
||||||
|
(per-user, not per-device — roadmap safe default). Columns:
|
||||||
|
`user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE`,
|
||||||
|
`last_route TEXT`, `last_route_state TEXT` (JSON-as-TEXT, nullable),
|
||||||
|
`resume_enabled INTEGER NOT NULL DEFAULT 1`,
|
||||||
|
`last_updated_at TEXT NOT NULL DEFAULT (datetime('now'))`.
|
||||||
|
- **/api/auth/me wiring choice:** folded `resume_enabled`, `last_route`,
|
||||||
|
and decoded `last_route_state` onto the EXISTING `/api/auth/me`
|
||||||
|
payload rather than a new GET — smallest/cleanest diff, the frontend
|
||||||
|
already fetches it on boot. When `resume_enabled=0`, `last_route` is
|
||||||
|
handed back as null (stored row preserved so re-enabling resumes).
|
||||||
|
- **PUT /api/me/last-state:** `require_user` (anon → 401). Upsert via
|
||||||
|
`INSERT … ON CONFLICT(user_id) DO UPDATE`. No-ops (stored=False) when
|
||||||
|
an existing row has `resume_enabled=0`.
|
||||||
|
- **Resume redirect ordering:** added `identifyReady` state in App.jsx,
|
||||||
|
flipped true at the end of the identify effect (and immediately on an
|
||||||
|
anonymous `me`). useLastState gates its one-time redirect on it →
|
||||||
|
redirect always after identify. Redirect only fires when the app
|
||||||
|
booted on `/` (the hard sign-in landing), so a deep-link/refresh
|
||||||
|
mid-RFC is never yanked.
|
||||||
|
- **Stale state:** no server special-casing. `navigate(last_route,
|
||||||
|
{replace:true})` lands on whatever renders today; existing routing
|
||||||
|
falls through to catalog/empty-state for missing/unreadable RFCs.
|
||||||
|
Non-resumable prefixes (/login, /welcome, /invites/, /invitations/)
|
||||||
|
and "/" are skipped in the hook.
|
||||||
|
- **Privacy:** route + light view state only, never draft buffers.
|
||||||
|
Documented in migration 022 column comment, LastStateBody comment,
|
||||||
|
and new SPEC §6.8.
|
||||||
|
|
||||||
|
### Deferred
|
||||||
|
- Profile-settings opt-out toggle UI. The `resume_enabled` column +
|
||||||
|
default-on behavior + the disable semantics (PUT no-op, /me hides
|
||||||
|
route) all ship now; only the client UI to flip the flag is deferred.
|
||||||
|
The vertical test flips it via direct SQL to prove the behavior.
|
||||||
|
|
||||||
|
### Test outcomes
|
||||||
|
- New `test_session_resume_vertical.py`: 4 passed (auth-required,
|
||||||
|
upsert/read-back+overwrite+state-JSON, per-user isolation,
|
||||||
|
resume_enabled=0 disable).
|
||||||
|
- Full backend suite: 336 passed (332 baseline + 4).
|
||||||
|
- Frontend `npm run build`: green (needed `VITE_APP_NAME` set for the
|
||||||
|
build — pre-existing requirement, no default shipped; used `VITE_APP_NAME=OHM`
|
||||||
|
for the build check only, not committed). Pre-existing chunk-size
|
||||||
|
warning unchanged.
|
||||||
|
|
||||||
|
## Cut state
|
||||||
|
- Branch `feature/v0.23.0-signin-resume` @ **77010ea**.
|
||||||
|
- Files changed (7, +459):
|
||||||
|
- `backend/migrations/022_user_session_state.sql` (new)
|
||||||
|
- `backend/app/api.py` (LastStateBody model; /api/auth/me payload
|
||||||
|
additions; new PUT /api/me/last-state handler)
|
||||||
|
- `backend/tests/test_session_resume_vertical.py` (new)
|
||||||
|
- `frontend/src/lib/useLastState.js` (new hook)
|
||||||
|
- `frontend/src/api.js` (putLastState client call)
|
||||||
|
- `frontend/src/App.jsx` (import + hook call + identifyReady)
|
||||||
|
- `SPEC.md` (new §6.8)
|
||||||
|
- NOTE: worktree symlinks `backend/.venv` + `frontend/node_modules` are
|
||||||
|
untracked and were NOT committed (they are not gitignored here, so
|
||||||
|
watch for them on any future `git add -A` — add specific paths).
|
||||||
|
|
||||||
|
## What the driver needs to know
|
||||||
|
- **Proposed CHANGELOG `Upgrade steps:` text:**
|
||||||
|
> Migration `022_user_session_state.sql` auto-applies on next boot
|
||||||
|
> (creates the per-user `user_session_state` table). No new env vars.
|
||||||
|
> No operator action required. New behavior: signing in now lands the
|
||||||
|
> user on their most-recently-viewed route instead of the empty-state
|
||||||
|
> home; a brand-new user (no recorded state) still lands on home.
|
||||||
|
> Resume is per-user and on by default; the stored state is route +
|
||||||
|
> light view state only (never draft contents — see SPEC §6.8). The
|
||||||
|
> per-user opt-out flag ships at the column level (`resume_enabled`);
|
||||||
|
> the profile-settings toggle UI to flip it is a follow-up.
|
||||||
|
- **api.py regions edited (merge-overlap awareness):** all additions are
|
||||||
|
in the `/me`/auth region — (1) a new `LastStateBody` Pydantic model
|
||||||
|
inserted just above `class BetaRequestBody` (~line 65); (2) extra
|
||||||
|
fields appended to the `auth_me` return dict (~line 350, the existing
|
||||||
|
`/api/auth/me` handler); (3) a new `put_last_state` handler inserted
|
||||||
|
immediately AFTER `submit_beta_request`'s `return {"ok": True}`
|
||||||
|
(~line 435). All far from the propose/PR endpoints 0022.1 edits — 3-way
|
||||||
|
merge should be clean.
|
||||||
|
- **App.jsx regions edited:** import line (added line ~5); a new
|
||||||
|
`identifyReady` useState in the state block (~line 50); a
|
||||||
|
`setIdentifyReady(true)` line inside the existing identify effect +
|
||||||
|
a tiny new effect right after it (~line 100); the `useLastState({...})`
|
||||||
|
call right after the `getMe()` effect (~line 130). **Header region
|
||||||
|
(lines ~220–235, btn-signin-header / nav Links / <header>) was NOT
|
||||||
|
touched** — Session 0019 fast-follow safe.
|
||||||
|
- **Deferred:** profile opt-out toggle UI (see above).
|
||||||
|
|
||||||
|
## §19.2 candidates surfaced
|
||||||
|
- The frontend debounce interval (~1s) and the NON_RESUMABLE_PREFIXES
|
||||||
|
list in useLastState.js are hard-coded frontend constants; if other
|
||||||
|
deployments want them configurable that's a candidate, but it's not
|
||||||
|
flotilla-shaped (no OHM-specific value).
|
||||||
|
- A generic "remember per-user view state" surface could share substrate
|
||||||
|
with the device-trust `last_seen_at` refresh pattern; if a future
|
||||||
|
flotilla-core extraction wants a session/state module, this table is a
|
||||||
|
natural member alongside device_trust (already flagged §19.2 in 017).
|
||||||
Reference in New Issue
Block a user