diff --git a/apps/server/src/auth/SessionStore.test.ts b/apps/server/src/auth/SessionStore.test.ts
index fa87c5ce4e84..fa1769061a0c 100644
--- a/apps/server/src/auth/SessionStore.test.ts
+++ b/apps/server/src/auth/SessionStore.test.ts
@@ -379,7 +379,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
}).pipe(Effect.provide(makeSessionStoreLayer())),
);
- it.effect("persists lastConnectedAt on first connect and updates it after reconnect", () =>
+ it.effect("tracks the connection start and final disconnect time", () =>
Effect.gen(function* () {
const sessions = yield* SessionStore.SessionStore;
const issued = yield* sessions.issue({
@@ -405,11 +405,20 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
expect(stillConnected[0]?.lastConnectedAt?.toString()).toBe(firstConnectedAt?.toString());
yield* sessions.markDisconnected(issued.sessionId);
+ const afterFirstDisconnect = yield* sessions.listActive();
+
+ expect(afterFirstDisconnect[0]?.connected).toBe(true);
+ expect(afterFirstDisconnect[0]?.lastConnectedAt?.toString()).toBe(
+ firstConnectedAt?.toString(),
+ );
+
yield* sessions.markDisconnected(issued.sessionId);
const afterDisconnect = yield* sessions.listActive();
+ const disconnectedAt = afterDisconnect[0]?.lastConnectedAt;
expect(afterDisconnect[0]?.connected).toBe(false);
- expect(afterDisconnect[0]?.lastConnectedAt?.toString()).toBe(firstConnectedAt?.toString());
+ expect(disconnectedAt).not.toBeNull();
+ expect(disconnectedAt?.toString()).not.toBe(firstConnectedAt?.toString());
yield* TestClock.adjust(Duration.seconds(1));
yield* sessions.markConnected(issued.sessionId);
@@ -417,7 +426,7 @@ it.layer(NodeServices.layer)("SessionStore.layer", (it) => {
expect(afterReconnect[0]?.connected).toBe(true);
expect(afterReconnect[0]?.lastConnectedAt).not.toBeNull();
- expect(afterReconnect[0]?.lastConnectedAt?.toString()).not.toBe(firstConnectedAt?.toString());
+ expect(afterReconnect[0]?.lastConnectedAt?.toString()).not.toBe(disconnectedAt?.toString());
}).pipe(Effect.provide(Layer.merge(makeSessionStoreLayer(), TestClock.layer()))),
);
it.effect("records client connection metadata without clearing prior values", () =>
diff --git a/apps/server/src/auth/SessionStore.ts b/apps/server/src/auth/SessionStore.ts
index b315bdf87c7f..053e2b295d02 100644
--- a/apps/server/src/auth/SessionStore.ts
+++ b/apps/server/src/auth/SessionStore.ts
@@ -530,6 +530,16 @@ export const make = Effect.gen(function* () {
);
});
+ const setLastConnectedAtNow = (sessionId: AuthSessionId) =>
+ DateTime.now.pipe(
+ Effect.flatMap((lastConnectedAt) =>
+ authSessions.setLastConnectedAt({
+ sessionId,
+ lastConnectedAt,
+ }),
+ ),
+ );
+
const markConnected: SessionStore["Service"]["markConnected"] = (sessionId) =>
Ref.modify(connectedSessionsRef, (current) => {
const next = new Map(current);
@@ -538,16 +548,7 @@ export const make = Effect.gen(function* () {
return [wasDisconnected, next] as const;
}).pipe(
Effect.flatMap((wasDisconnected) =>
- wasDisconnected
- ? DateTime.now.pipe(
- Effect.flatMap((lastConnectedAt) =>
- authSessions.setLastConnectedAt({
- sessionId,
- lastConnectedAt,
- }),
- ),
- )
- : Effect.void,
+ wasDisconnected ? setLastConnectedAtNow(sessionId) : Effect.void,
),
Effect.flatMap(() => loadActiveSession(sessionId)),
Effect.flatMap((session) =>
@@ -587,7 +588,7 @@ export const make = Effect.gen(function* () {
);
const markDisconnected: SessionStore["Service"]["markDisconnected"] = (sessionId) =>
- Ref.update(connectedSessionsRef, (current) => {
+ Ref.modify(connectedSessionsRef, (current) => {
const next = new Map(current);
const remaining = (next.get(sessionId) ?? 0) - 1;
if (remaining > 0) {
@@ -595,8 +596,11 @@ export const make = Effect.gen(function* () {
} else {
next.delete(sessionId);
}
- return next;
+ return [remaining === 0, next] as const;
}).pipe(
+ Effect.flatMap((becameDisconnected) =>
+ becameDisconnected ? setLastConnectedAtNow(sessionId) : Effect.void,
+ ),
Effect.flatMap(() => loadActiveSession(sessionId)),
Effect.flatMap((session) =>
Option.isSome(session) ? emitUpsert(session.value) : Effect.void,
diff --git a/apps/web/src/components/settings/ConnectionsSettings.tsx b/apps/web/src/components/settings/ConnectionsSettings.tsx
index 1c4e034cf6a1..0886e05ee7d8 100644
--- a/apps/web/src/components/settings/ConnectionsSettings.tsx
+++ b/apps/web/src/components/settings/ConnectionsSettings.tsx
@@ -44,7 +44,11 @@ import * as Option from "effect/Option";
import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
import { cn } from "../../lib/utils";
-import { formatElapsedDurationLabel, formatExpiresInLabel } from "../../timestampFormat";
+import {
+ formatElapsedDurationLabel,
+ formatExpiresInLabel,
+ formatRelativeTimeLabel,
+} from "../../timestampFormat";
import { resolveDesktopPairingUrl, resolveHostedPairingUrl } from "./pairingUrls";
import {
applyWslEnableSelection,
@@ -952,6 +956,11 @@ const ConnectedClientListRow = memo(function ConnectedClientListRow({
: lastConnectedAt
? `Last connected at ${formatAccessTimestamp(lastConnectedAt)}`
: "Not connected yet.";
+ const lastSeenLabel = isLive
+ ? "Last seen now"
+ : lastConnectedAt
+ ? `Last seen ${formatRelativeTimeLabel(lastConnectedAt)}`
+ : "Never seen";
const deviceInfoBits = [
clientSession.client.deviceType !== "unknown"
? clientSession.client.deviceType[0]?.toUpperCase() + clientSession.client.deviceType.slice(1)
@@ -991,6 +1000,7 @@ const ConnectedClientListRow = memo(function ConnectedClientListRow({
) : null}
{lastSeenLabel}