WIP: feat(auth): automatic logoff after user inactivity (UI API 1.12.0) #211

Draft
james.maes wants to merge 1 commits from feat/session-user-activity into main
pull from: feat/session-user-activity
Owner

Why

Automatic logoff: a session must end after a set time without a user action. This app polls (jobs, alerts, presence), so its requests are not its user. QQQ core gets a user inactivity timeout in QRun-IO/qqq PR 1361 (UI API 1.12.0); this is the Next UI's side of it.

What

Nothing changes, and no header is sent, unless the server publishes values.userInactivityTimeoutSeconds in GET /metaData/authentication.

  • src/lib/auth/user-activity.ts: the time of the last key, pointer (down or move), wheel or touch; page load counts as one. Shared between the tabs of one browser through localStorage (they share the session), written at most once a second.
  • src/lib/api/client.ts: every request states X-QQQ-User-Activity-Age (whole seconds since the last user action). The three fetch call sites that bypass the client (assistant stream, realtime stream, presence beacon) state it too.
  • src/components/layout/InactivityLogoff.tsx, mounted in providers.tsx: warns 60 seconds before ("Are you still there? You will be signed out in N seconds because of inactivity." with "Stay signed in"), and signs out through the normal logout() when the time runs out (backend logout, then the identity provider's end-session). Any user action dismisses the warning. For an active user on a page that sent nothing for four minutes it resumes the session once, so the activity reaches the server and the session is renewed.
  • Login page: after an automatic logoff it says "You were signed out after a period of inactivity."
  • QAuthenticationMetaData.values.userInactivityTimeoutSeconds typed. The bundled UI API document is not re-vendored: ui-api:check accepts a newer server.

The server enforces the same timeout by itself at the next request (401). A hidden tab's timers are throttled by the browser; the server and the tab's next tick still end the session.

Verified

  • pnpm typecheck: clean. pnpm lint: 0 errors (6 warnings, none in these files), license headers pass. pnpm test: 366 files, 3834 passed, 1 skipped; the new tests are user-activity.test.ts, InactivityLogoff.test.tsx, one each in client.test.ts and the login page test.
  • Gitea build on d9f2baa0 (typecheck, lint, test, build:export, Maven): success (run 7367).
  • NOT verified: in a browser (none was allowed for this work). No Playwright test was added; the warning dialog and the sign-out redirect have only been exercised in jsdom.

Release

After QRun-IO/qqq PR 1361 is released (the header is only sent to a server that publishes the timeout, so the order is not critical): tag the next RC (v1.0.0-RC.40), then hosts pin it.

🤖 Generated with Claude Code

## Why Automatic logoff: a session must end after a set time without a **user action**. This app polls (jobs, alerts, presence), so its requests are not its user. QQQ core gets a user inactivity timeout in QRun-IO/qqq PR 1361 (UI API 1.12.0); this is the Next UI's side of it. ## What Nothing changes, and no header is sent, unless the server publishes `values.userInactivityTimeoutSeconds` in `GET /metaData/authentication`. - `src/lib/auth/user-activity.ts`: the time of the last key, pointer (down or move), wheel or touch; page load counts as one. Shared between the tabs of one browser through `localStorage` (they share the session), written at most once a second. - `src/lib/api/client.ts`: every request states `X-QQQ-User-Activity-Age` (whole seconds since the last user action). The three `fetch` call sites that bypass the client (assistant stream, realtime stream, presence beacon) state it too. - `src/components/layout/InactivityLogoff.tsx`, mounted in `providers.tsx`: warns 60 seconds before ("Are you still there? You will be signed out in N seconds because of inactivity." with "Stay signed in"), and signs out through the normal `logout()` when the time runs out (backend logout, then the identity provider's end-session). Any user action dismisses the warning. For an active user on a page that sent nothing for four minutes it resumes the session once, so the activity reaches the server and the session is renewed. - Login page: after an automatic logoff it says "You were signed out after a period of inactivity." - `QAuthenticationMetaData.values.userInactivityTimeoutSeconds` typed. The bundled UI API document is not re-vendored: `ui-api:check` accepts a newer server. The server enforces the same timeout by itself at the next request (401). A hidden tab's timers are throttled by the browser; the server and the tab's next tick still end the session. ## Verified - `pnpm typecheck`: clean. `pnpm lint`: 0 errors (6 warnings, none in these files), license headers pass. `pnpm test`: 366 files, 3834 passed, 1 skipped; the new tests are `user-activity.test.ts`, `InactivityLogoff.test.tsx`, one each in `client.test.ts` and the login page test. - Gitea build on d9f2baa0 (typecheck, lint, test, `build:export`, Maven): success (run 7367). - NOT verified: in a browser (none was allowed for this work). No Playwright test was added; the warning dialog and the sign-out redirect have only been exercised in jsdom. ## Release After QRun-IO/qqq PR 1361 is released (the header is only sent to a server that publishes the timeout, so the order is not critical): tag the next RC (`v1.0.0-RC.40`), then hosts pin it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 1 commit 2026-11-10 18:45:17 +00:00
feat(auth): automatic logoff after user inactivity (UI API 1.12.0)
maven / build (push) Successful in 4m26s
build / maven (push) Successful in 4m26s
d9f2baa079
When the server publishes userInactivityTimeoutSeconds, the app states the age
of the last user action on every request (X-QQQ-User-Activity-Age), since its
own polling is not the user. It warns a minute before the timeout, signs out
when it runs out, shares activity between the tabs of one browser, and renews
the session for an active user whose page sent nothing for four minutes. The
login page says when the sign-out was automatic. Without the server value
nothing changes and no header is sent.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
All checks were successful
maven / build (push) Successful in 4m26s
build / maven (push) Successful in 4m26s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
This pull request is marked as a work in progress.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/session-user-activity:feat/session-user-activity
git checkout feat/session-user-activity
Sign in to join this conversation.