From 20495355162dc2a9046e583357e7809f40443e68 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Wed, 19 Aug 2026 10:11:33 +0200 Subject: [PATCH] feat(session): admin UI showing online sessions --- frontend/src/lib/api/types.ts | 15 ++++ .../src/routes/admin/[[tab]]/+page.svelte | 70 +++++++++++++++++++ frontend/static/locales/en.json | 4 ++ frontend/static/locales/fr.json | 4 ++ 4 files changed, 93 insertions(+) diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 253ec029..c99f2909 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -761,12 +761,27 @@ export interface SessionSummary { user_id: string; created_at: string; expires_at: string; + /** Wall-clock (RFC 3339) of the last authenticated request the + * server observed on this session. Trails the true value by at + * most the tracker's flush interval (30 s) on a running server; + * converges after graceful shutdown. Populates the "last seen X + * ago" tooltip on the presence dot. */ + last_seen_at: string; ip_address: string | null; user_agent: string | null; is_bound: boolean; dpop_jkt_prefix: string | null; is_revoked: boolean; is_active: boolean; + /** Presence signal — `true` when the server observed a request on + * this session within the last 5 minutes AND the row is `is_active` + * (never `true` on revoked / expired rows). Renders as a filled + * green dot in the Status column; `false` on an otherwise-active + * row renders as an outlined idle dot with a "last seen X ago" + * tooltip. Distinct from `is_active`: that's a lifecycle signal, + * this is a presence signal. Derived server-side against the + * same window that drives `oxicloud_sessions_online[_users]`. */ + is_online: boolean; /** How this session was minted. `unknown` covers pre-migration * rows and any origin the SPA doesn't yet render. Server enum * is populated at INSERT (see `Session::new`) and copied on diff --git a/frontend/src/routes/admin/[[tab]]/+page.svelte b/frontend/src/routes/admin/[[tab]]/+page.svelte index 037e4636..532f0dcc 100644 --- a/frontend/src/routes/admin/[[tab]]/+page.svelte +++ b/frontend/src/routes/admin/[[tab]]/+page.svelte @@ -3082,6 +3082,36 @@ {t('admin.sessions.expired', 'expired')} {:else} + + {t('admin.sessions.active', 'active')} @@ -4572,6 +4602,46 @@ text-transform: uppercase; } + /* Presence dot in the sessions-table Status column — filled green + when the row is `is_online` (a request landed in the last 5 min), + outlined grey when the row is active-but-idle. Only rendered on + active rows: a revoked-but-recently-seen row must never flash + green post-revocation (see the markup guard `{#if s.is_active}`). + The dot sits BEFORE the `active` badge with a small gap, so the + Status cell reads left-to-right as `● active` when online and + `○ active` when idle. + + Tokens: `--color-success-alt` / `--color-success-border` for the + filled fill is the same green used by `.badge--active`, keeping + the presence signal visually consistent with the lifecycle one + without stealing the badge's own colour treatment. Grey border + for the idle state uses the neutral `--color-border` token so + both themes (light + dark, driven by `light-dark(...)`) get a + readable contrast. Fixed 8px / 8px sizing — the dot is a signal, + not a click target, so relative units would over-scale on + larger UI densities. */ + .presence-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: var(--space-1); + vertical-align: middle; + /* No border on the filled state, so both variants render at + the same 8×8 footprint (the outlined variant's 1px border + is inset via box-sizing: border-box below). */ + box-sizing: border-box; + } + + .presence-dot--online { + background: var(--color-success-alt); + } + + .presence-dot--idle { + background: transparent; + border: 1px solid var(--color-border); + } + /* External / grant-only account marker. Sibling of `.badge--user` in the same cell so the two stack horizontally; the accent colour reuses `--color-warning-*` because "external" is the diff --git a/frontend/static/locales/en.json b/frontend/static/locales/en.json index 304e0b2e..88557131 100644 --- a/frontend/static/locales/en.json +++ b/frontend/static/locales/en.json @@ -1136,6 +1136,10 @@ "revoked": "revoked", "expired": "expired", "active": "active", + "online": "online", + "idle": "idle", + "presence_online_tooltip": "Online — last seen {{ago}}", + "presence_idle_tooltip": "Idle — last seen {{ago}}", "revoke": "Revoke", "empty": "No sessions match the current filter.", "revoke_self_confirm": "⚠️ This is YOUR current session. Revoking it will log YOU out immediately and you'll have to sign back in. Continue?", diff --git a/frontend/static/locales/fr.json b/frontend/static/locales/fr.json index 165f0ad2..998c1e4a 100644 --- a/frontend/static/locales/fr.json +++ b/frontend/static/locales/fr.json @@ -1269,6 +1269,10 @@ "revoked": "révoquée", "expired": "expirée", "active": "actif", + "online": "en ligne", + "idle": "inactif", + "presence_online_tooltip": "En ligne — vue {{ago}}", + "presence_idle_tooltip": "Inactif — vue {{ago}}", "revoke": "Révoquer", "empty": "Aucune session ne correspond au filtre actuel.", "revoke_self_confirm": "⚠️ Il s'agit de VOTRE session actuelle. La révoquer vous déconnectera immédiatement et vous devrez vous reconnecter. Continuer ?",