From 96755ac5ba44af11c03a19efa0651f22c3512335 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 16:33:20 -0400
Subject: [PATCH 01/21] Move security submenu to client page
---
.../(submenus)/security/page-client.tsx | 22 ++++++++++++++++++
.../app/settings/(submenus)/security/page.tsx | 23 +++----------------
2 files changed, 25 insertions(+), 20 deletions(-)
create mode 100644 frontend/src/app/settings/(submenus)/security/page-client.tsx
diff --git a/frontend/src/app/settings/(submenus)/security/page-client.tsx b/frontend/src/app/settings/(submenus)/security/page-client.tsx
new file mode 100644
index 000000000..a4ad05fdd
--- /dev/null
+++ b/frontend/src/app/settings/(submenus)/security/page-client.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import ChangePasswordDialog from "@/features/account/setting-dialogs/change-password/main-dialog";
+
+export default function ClientPage() {
+ return (
+
+
+
+
Password
+
+ Update your password to keep your account secure.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/settings/(submenus)/security/page.tsx b/frontend/src/app/settings/(submenus)/security/page.tsx
index 8a0ad22e2..16c11e8cf 100644
--- a/frontend/src/app/settings/(submenus)/security/page.tsx
+++ b/frontend/src/app/settings/(submenus)/security/page.tsx
@@ -1,22 +1,5 @@
-"use client";
+import ClientPage from "@/app/settings/(submenus)/security/page-client";
-import ChangePasswordDialog from "@/features/account/setting-dialogs/change-password/main-dialog";
-
-export default function Page() {
- return (
-
-
-
-
Password
-
- Update your password to keep your account secure.
-
-
-
-
-
-
-
-
- );
+export default async function Page() {
+ return ;
}
From 0045c36e7f56ac3e8902c53c28113552522e04db Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 17:18:17 -0400
Subject: [PATCH 02/21] Add created_at to session data
---
backend/api/account/serializers.py | 1 +
backend/api/account/views.py | 1 +
frontend/src/lib/utils/api/types.ts | 1 +
3 files changed, 3 insertions(+)
diff --git a/backend/api/account/serializers.py b/backend/api/account/serializers.py
index 186b87ce4..7eccff07c 100644
--- a/backend/api/account/serializers.py
+++ b/backend/api/account/serializers.py
@@ -24,6 +24,7 @@ class ActiveSessionSerializer(serializers.Serializer):
client_version = serializers.CharField(
required=False, allow_null=True, default=None
)
+ created_at = serializers.DateTimeField(required=True)
last_used = serializers.DateTimeField(required=True)
is_current = serializers.BooleanField(required=True)
diff --git a/backend/api/account/views.py b/backend/api/account/views.py
index 5a18c7e15..b24fdf6d6 100644
--- a/backend/api/account/views.py
+++ b/backend/api/account/views.py
@@ -112,6 +112,7 @@ def get_active_sessions(request):
for session in sessions:
session_data = {
"public_id": session.public_id,
+ "created_at": session.created_at,
"last_used": session.last_used,
"is_current": session.session_token
== request.COOKIES.get(ACCOUNT_COOKIE_NAME),
diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts
index e87613620..24229277f 100644
--- a/frontend/src/lib/utils/api/types.ts
+++ b/frontend/src/lib/utils/api/types.ts
@@ -141,6 +141,7 @@ export type ActiveSession = {
os_version: string | null;
client_name: string | null;
client_version: string | null;
+ created_at: string;
last_used: string;
is_current: boolean;
}
From 2a8341523213450cea16c94a3212d7dafeb85355 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 18:09:31 -0400
Subject: [PATCH 03/21] Create formatTimeAgo function
---
frontend/src/lib/utils/date-time-format.ts | 32 ++++++++++++++++++----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/frontend/src/lib/utils/date-time-format.ts b/frontend/src/lib/utils/date-time-format.ts
index d8e61ad68..ae9265619 100644
--- a/frontend/src/lib/utils/date-time-format.ts
+++ b/frontend/src/lib/utils/date-time-format.ts
@@ -72,19 +72,18 @@ export function timeslotToISOString(
}
}
-
/**
* Checks if two timezones are equivalent even if they represent different locations.
- *
+ *
* For example, "America/New_York" and "America/Detroit" are equal because they are both
* in Eastern Time.
- *
+ *
* IMPORTANT: This function also checks if the timezones have the same DST rules by
* comparing offsets in January and July.
- *
+ *
* For example, "America/New_York" and "America/Caracas" are NOT equal because Caracas
* does not observe DST, despite both having the same offset during part of the year.
- *
+ *
* @param tz1 The first timezone to compare
* @param tz2 The second timezone to compare
* @returns `true` if the timezones are equivalent, `false` otherwise
@@ -207,3 +206,26 @@ export function convert12To24(time12: string): string {
const date = parse(time12, "hh:mm a", new Date());
return format(date, "HH:mm");
}
+
+/* TIME AGO UTILS */
+
+/**
+ * Formats a time difference in seconds into a simplified readable string.
+ * @param secondsAgo The number of seconds ago.
+ * @param largestUnit The largest time unit to display.
+ * @returns A string representing the time difference.
+ */
+export function formatTimeAgo(secondsAgo: number): string {
+ if (secondsAgo < 60) {
+ return "just now";
+ } else if (secondsAgo < 60 * 60) {
+ const minutes = Math.floor(secondsAgo / 60);
+ return `${minutes} minute${minutes !== 1 ? "s" : ""} ago`;
+ } else if (secondsAgo < 60 * 60 * 24) {
+ const hours = Math.floor(secondsAgo / (60 * 60));
+ return `${hours} hour${hours !== 1 ? "s" : ""} ago`;
+ } else {
+ const days = Math.floor(secondsAgo / (60 * 60 * 24));
+ return `${days} day${days !== 1 ? "s" : ""} ago`;
+ }
+}
From 6bf3c2d1ea8c9106da1fa2e025426bfa2e721a52 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 18:09:58 -0400
Subject: [PATCH 04/21] Add basic session data to security submenu
---
.../(submenus)/security/page-client.tsx | 71 ++++++++++++++++++-
.../app/settings/(submenus)/security/page.tsx | 6 +-
2 files changed, 75 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/settings/(submenus)/security/page-client.tsx b/frontend/src/app/settings/(submenus)/security/page-client.tsx
index a4ad05fdd..c5a7bbd3e 100644
--- a/frontend/src/app/settings/(submenus)/security/page-client.tsx
+++ b/frontend/src/app/settings/(submenus)/security/page-client.tsx
@@ -1,8 +1,20 @@
"use client";
+import { toZonedTime } from "date-fns-tz";
+import { CircleQuestionMark, Laptop2Icon, SmartphoneIcon } from "lucide-react";
+
import ChangePasswordDialog from "@/features/account/setting-dialogs/change-password/main-dialog";
+import { ActiveSession } from "@/lib/utils/api/types";
+import { cn } from "@/lib/utils/classname";
+import { formatTimeAgo } from "@/lib/utils/date-time-format";
+
+export default function ClientPage({
+ sessions,
+}: {
+ sessions: ActiveSession[];
+}) {
+ const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
-export default function ClientPage() {
return (
@@ -17,6 +29,63 @@ export default function ClientPage() {
+
+
+
+
Active Devices
+
+ If there are any devices you don{"'"}t recognize, remove it and
+ change your password.
+
+
+
+ {sessions.map((session) => {
+ const lastUsedLocal = toZonedTime(
+ new Date(session.last_used + "Z"),
+ userTimeZone,
+ );
+ const lastUsedSecondsAgo =
+ (Date.now() - lastUsedLocal.getTime()) / 1000;
+
+ return (
+
+
+
+ {session.device_type === "desktop" ? (
+
+ ) : session.device_type === "smartphone" ? (
+
+ ) : (
+
+ )}
+
+
+
+ {!session.os_name && !session.client_name
+ ? "Unknown"
+ : (session.os_name || "Unknown Device") +
+ " • " +
+ (session.client_name || "Unknown Browser")}
+
+
+ {session.is_current
+ ? "This Session"
+ : `Last used ${formatTimeAgo(lastUsedSecondsAgo)}`}
+
+
+
+
+ );
+ })}
+
);
}
diff --git a/frontend/src/app/settings/(submenus)/security/page.tsx b/frontend/src/app/settings/(submenus)/security/page.tsx
index 16c11e8cf..e572e15a3 100644
--- a/frontend/src/app/settings/(submenus)/security/page.tsx
+++ b/frontend/src/app/settings/(submenus)/security/page.tsx
@@ -1,5 +1,9 @@
import ClientPage from "@/app/settings/(submenus)/security/page-client";
+import { ROUTES } from "@/lib/utils/api/endpoints";
+import { serverGet } from "@/lib/utils/api/server-fetch";
export default async function Page() {
- return ;
+ const activeSessions = await serverGet(ROUTES.account.getActiveSessions);
+
+ return ;
}
From 83ffb9098c3c2af11f7b64a8076d38f72da94fa5 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 18:18:08 -0400
Subject: [PATCH 05/21] Move security submenu back to server page
---
.../(submenus)/security/page-client.tsx | 91 -------------------
.../app/settings/(submenus)/security/page.tsx | 22 ++++-
.../security/components/session-manager.tsx | 75 +++++++++++++++
3 files changed, 95 insertions(+), 93 deletions(-)
delete mode 100644 frontend/src/app/settings/(submenus)/security/page-client.tsx
create mode 100644 frontend/src/features/account/settings/security/components/session-manager.tsx
diff --git a/frontend/src/app/settings/(submenus)/security/page-client.tsx b/frontend/src/app/settings/(submenus)/security/page-client.tsx
deleted file mode 100644
index c5a7bbd3e..000000000
--- a/frontend/src/app/settings/(submenus)/security/page-client.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-"use client";
-
-import { toZonedTime } from "date-fns-tz";
-import { CircleQuestionMark, Laptop2Icon, SmartphoneIcon } from "lucide-react";
-
-import ChangePasswordDialog from "@/features/account/setting-dialogs/change-password/main-dialog";
-import { ActiveSession } from "@/lib/utils/api/types";
-import { cn } from "@/lib/utils/classname";
-import { formatTimeAgo } from "@/lib/utils/date-time-format";
-
-export default function ClientPage({
- sessions,
-}: {
- sessions: ActiveSession[];
-}) {
- const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
-
- return (
-
-
-
-
Password
-
- Update your password to keep your account secure.
-
-
-
-
-
-
-
-
-
-
-
Active Devices
-
- If there are any devices you don{"'"}t recognize, remove it and
- change your password.
-
-
-
- {sessions.map((session) => {
- const lastUsedLocal = toZonedTime(
- new Date(session.last_used + "Z"),
- userTimeZone,
- );
- const lastUsedSecondsAgo =
- (Date.now() - lastUsedLocal.getTime()) / 1000;
-
- return (
-
-
-
- {session.device_type === "desktop" ? (
-
- ) : session.device_type === "smartphone" ? (
-
- ) : (
-
- )}
-
-
-
- {!session.os_name && !session.client_name
- ? "Unknown"
- : (session.os_name || "Unknown Device") +
- " • " +
- (session.client_name || "Unknown Browser")}
-
-
- {session.is_current
- ? "This Session"
- : `Last used ${formatTimeAgo(lastUsedSecondsAgo)}`}
-
-
-
-
- );
- })}
-
-
- );
-}
diff --git a/frontend/src/app/settings/(submenus)/security/page.tsx b/frontend/src/app/settings/(submenus)/security/page.tsx
index e572e15a3..97b539a90 100644
--- a/frontend/src/app/settings/(submenus)/security/page.tsx
+++ b/frontend/src/app/settings/(submenus)/security/page.tsx
@@ -1,9 +1,27 @@
-import ClientPage from "@/app/settings/(submenus)/security/page-client";
+import ChangePasswordDialog from "@/features/account/setting-dialogs/change-password/main-dialog";
+import SessionManager from "@/features/account/settings/security/components/session-manager";
import { ROUTES } from "@/lib/utils/api/endpoints";
import { serverGet } from "@/lib/utils/api/server-fetch";
export default async function Page() {
const activeSessions = await serverGet(ROUTES.account.getActiveSessions);
- return ;
+ return (
+
+
+
+
Password
+
+ Update your password to keep your account secure.
+
+
+
+
+
+
+
+
+
+
+ );
}
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
new file mode 100644
index 000000000..056b27d64
--- /dev/null
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { toZonedTime } from "date-fns-tz";
+import { CircleQuestionMark, Laptop2Icon, SmartphoneIcon } from "lucide-react";
+
+import { type ActiveSession } from "@/lib/utils/api/types";
+import { cn } from "@/lib/utils/classname";
+import { formatTimeAgo } from "@/lib/utils/date-time-format";
+
+export default function SessionManager({
+ sessions,
+}: {
+ sessions: ActiveSession[];
+}) {
+ const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ return (
+
+
+
Active Devices
+
+ If there are any devices you don{"'"}t recognize, remove it and change
+ your password.
+
+
+
+ {sessions.map((session) => {
+ const lastUsedLocal = toZonedTime(
+ new Date(session.last_used + "Z"),
+ userTimeZone,
+ );
+ const lastUsedSecondsAgo =
+ (Date.now() - lastUsedLocal.getTime()) / 1000;
+
+ return (
+
+
+
+ {session.device_type === "desktop" ? (
+
+ ) : session.device_type === "smartphone" ? (
+
+ ) : (
+
+ )}
+
+
+
+ {!session.os_name && !session.client_name
+ ? "Unknown"
+ : (session.os_name || "Unknown Device") +
+ " • " +
+ (session.client_name || "Unknown Browser")}
+
+
+ {session.is_current
+ ? "This Session"
+ : `Last used ${formatTimeAgo(lastUsedSecondsAgo)}`}
+
+
+
+
+ );
+ })}
+
+ );
+}
From be64815f90300fac2ae3855acc68de9d186def31 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 18:18:49 -0400
Subject: [PATCH 06/21] Update subtitle
---
.../account/settings/security/components/session-manager.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index 056b27d64..cd0234232 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -19,8 +19,8 @@ export default function SessionManager({
Active Devices
- If there are any devices you don{"'"}t recognize, remove it and change
- your password.
+ If there are any devices you don{"'"}t recognize, remove them and
+ change your password.
From e292563c8a216a81ead015f2ae8fb7a2d697288b Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 18:34:49 -0400
Subject: [PATCH 07/21] Update active sessions endpoint return type
---
backend/api/account/serializers.py | 5 +++--
backend/api/account/views.py | 25 ++++++++++++++++++++++---
frontend/src/lib/utils/api/types.ts | 3 ++-
3 files changed, 27 insertions(+), 6 deletions(-)
diff --git a/backend/api/account/serializers.py b/backend/api/account/serializers.py
index 7eccff07c..9515969f6 100644
--- a/backend/api/account/serializers.py
+++ b/backend/api/account/serializers.py
@@ -30,8 +30,9 @@ class ActiveSessionSerializer(serializers.Serializer):
class ActiveSessionListSerializer(serializers.Serializer):
- sessions = serializers.ListField(
- child=ActiveSessionSerializer(), required=True, allow_empty=False
+ current_session = ActiveSessionSerializer(required=True)
+ other_sessions = serializers.ListField(
+ child=ActiveSessionSerializer(), required=True, allow_empty=True
)
diff --git a/backend/api/account/views.py b/backend/api/account/views.py
index b24fdf6d6..6af3fdbba 100644
--- a/backend/api/account/views.py
+++ b/backend/api/account/views.py
@@ -29,6 +29,7 @@
from api.settings import (
ACCOUNT_COOKIE_NAME,
AUTHED_PWD_RESET_EXP_SECONDS,
+ GENERIC_ERR_RESPONSE,
LONG_SESS_EXP_SECONDS,
SEND_EMAILS,
SESS_EXP_SECONDS,
@@ -107,7 +108,8 @@ def get_active_sessions(request):
user_account=user,
).order_by("-last_used")
- active_sessions = []
+ current_session = None
+ other_sessions = []
for session in sessions:
session_data = {
@@ -124,9 +126,26 @@ def get_active_sessions(request):
session_data["os_version"] = device.os_version() or None
session_data["client_name"] = device.client_name() or None
session_data["client_version"] = device.client_version() or None
- active_sessions.append(session_data)
- return Response({"sessions": active_sessions}, status=200)
+ if session_data["is_current"]:
+ if current_session is not None:
+ logger.error(
+ f"Multiple sessions matching session token for user: {user.id}."
+ )
+ current_session = session_data
+ else:
+ other_sessions.append(session_data)
+
+ if current_session is None:
+ logger.error(
+ f"No session matching session token for user, despite being authenticated: {user.id}."
+ )
+ return GENERIC_ERR_RESPONSE
+
+ return Response(
+ {"current_session": current_session, "other_sessions": other_sessions},
+ status=200,
+ )
@api_endpoint("POST")
diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts
index 24229277f..ad5b8e825 100644
--- a/frontend/src/lib/utils/api/types.ts
+++ b/frontend/src/lib/utils/api/types.ts
@@ -147,7 +147,8 @@ export type ActiveSession = {
}
export type ActiveSessionList = {
- sessions: ActiveSession[];
+ current_session: ActiveSession;
+ other_sessions: ActiveSession[];
}
export type SessionId = {
From ce2c5a99f3d053c5ebd0da197f5ad3ea39e534ba Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 18:37:59 -0400
Subject: [PATCH 08/21] Update session data and display
---
.../app/settings/(submenus)/security/page.tsx | 2 +-
.../security/components/session-manager.tsx | 108 ++++++++++--------
2 files changed, 63 insertions(+), 47 deletions(-)
diff --git a/frontend/src/app/settings/(submenus)/security/page.tsx b/frontend/src/app/settings/(submenus)/security/page.tsx
index 97b539a90..a434192fe 100644
--- a/frontend/src/app/settings/(submenus)/security/page.tsx
+++ b/frontend/src/app/settings/(submenus)/security/page.tsx
@@ -21,7 +21,7 @@ export default async function Page() {
-
+
);
}
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index cd0234232..ab6b38aff 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -3,14 +3,14 @@
import { toZonedTime } from "date-fns-tz";
import { CircleQuestionMark, Laptop2Icon, SmartphoneIcon } from "lucide-react";
-import { type ActiveSession } from "@/lib/utils/api/types";
+import { ActiveSessionList, type ActiveSession } from "@/lib/utils/api/types";
import { cn } from "@/lib/utils/classname";
import { formatTimeAgo } from "@/lib/utils/date-time-format";
export default function SessionManager({
sessions,
}: {
- sessions: ActiveSession[];
+ sessions: ActiveSessionList;
}) {
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -24,52 +24,68 @@ export default function SessionManager({
- {sessions.map((session) => {
- const lastUsedLocal = toZonedTime(
- new Date(session.last_used + "Z"),
- userTimeZone,
- );
- const lastUsedSecondsAgo =
- (Date.now() - lastUsedLocal.getTime()) / 1000;
+
+
- return (
-
-
-
- {session.device_type === "desktop" ? (
-
- ) : session.device_type === "smartphone" ? (
-
- ) : (
-
- )}
-
-
-
- {!session.os_name && !session.client_name
- ? "Unknown"
- : (session.os_name || "Unknown Device") +
- " • " +
- (session.client_name || "Unknown Browser")}
-
-
- {session.is_current
- ? "This Session"
- : `Last used ${formatTimeAgo(lastUsedSecondsAgo)}`}
-
-
-
+
+
+
+ {sessions.other_sessions.map((session) => (
+
+ ))}
+
+
+
+ );
+}
+
+function Session({
+ session,
+ userTz,
+}: {
+ session: ActiveSession;
+ userTz: string;
+}) {
+ const lastUsedLocal = toZonedTime(new Date(session.last_used + "Z"), userTz);
+ const lastUsedSecondsAgo = (Date.now() - lastUsedLocal.getTime()) / 1000;
+
+ return (
+
+
+
+ {session.device_type === "desktop" ? (
+
+ ) : session.device_type === "smartphone" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {!session.os_name && !session.client_name
+ ? "Unknown"
+ : (session.os_name || "Unknown Device") +
+ " • " +
+ (session.client_name || "Unknown Browser")}
+
+
+ {session.is_current
+ ? "This Session"
+ : `Last used ${formatTimeAgo(lastUsedSecondsAgo)}`}
- );
- })}
+
+
);
}
From b067a8c030ef797f25aadb47c6cf9c01c249359b Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 18:38:48 -0400
Subject: [PATCH 09/21] Lighten accent on current session
---
.../account/settings/security/components/session-manager.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index ab6b38aff..7d5ca1f20 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -59,7 +59,7 @@ function Session({
{session.device_type === "desktop" ? (
From b1a69061f034ee55bbcc9f0bf069b589a145f29e Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 21:19:14 -0400
Subject: [PATCH 10/21] Add device type value typing
---
frontend/src/lib/utils/api/types.ts | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts
index ad5b8e825..c36f3ecc7 100644
--- a/frontend/src/lib/utils/api/types.ts
+++ b/frontend/src/lib/utils/api/types.ts
@@ -136,7 +136,23 @@ export type DisplayName = {
export type ActiveSession = {
public_id: string;
- device_type: string | null;
+ device_type:
+ | "desktop"
+ | "smartphone"
+ | "tablet"
+ | "feature phone"
+ | "console"
+ | "tv"
+ | "car browser"
+ | "smart display "
+ | "camera"
+ | "portable media player"
+ | "phablet"
+ | "smart speaker"
+ | "wearable"
+ | "peripheral"
+ | ""
+ | null;
os_name: string | null;
os_version: string | null;
client_name: string | null;
From 2e1b6751fd587d328c81c84b596c751426d0b0d4 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 21:19:50 -0400
Subject: [PATCH 11/21] Add collapsible session information
---
.../security/components/session-manager.tsx | 125 +++++++++++++-----
1 file changed, 95 insertions(+), 30 deletions(-)
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index 7d5ca1f20..ec020de0c 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -1,11 +1,20 @@
"use client";
+import { useState } from "react";
+
+import * as Collapsible from "@radix-ui/react-collapsible";
import { toZonedTime } from "date-fns-tz";
-import { CircleQuestionMark, Laptop2Icon, SmartphoneIcon } from "lucide-react";
+import {
+ ChevronDownIcon,
+ CircleQuestionMark,
+ Laptop2Icon,
+ SmartphoneIcon,
+} from "lucide-react";
import { ActiveSessionList, type ActiveSession } from "@/lib/utils/api/types";
import { cn } from "@/lib/utils/classname";
import { formatTimeAgo } from "@/lib/utils/date-time-format";
+import EmptyButton from "@/features/button/components/empty";
export default function SessionManager({
sessions,
@@ -50,42 +59,98 @@ function Session({
session: ActiveSession;
userTz: string;
}) {
+ const [isOpen, setIsOpen] = useState(false);
+
const lastUsedLocal = toZonedTime(new Date(session.last_used + "Z"), userTz);
const lastUsedSecondsAgo = (Date.now() - lastUsedLocal.getTime()) / 1000;
return (
-
-
-
- {session.device_type === "desktop" ? (
-
- ) : session.device_type === "smartphone" ? (
-
- ) : (
-
- )}
-
+
+
+
+
+
+ {session.device_type === "desktop" ? (
+
+ ) : session.device_type === "smartphone" ? (
+
+ ) : (
+
+ )}
+
-
-
- {!session.os_name && !session.client_name
- ? "Unknown"
- : (session.os_name || "Unknown Device") +
- " • " +
- (session.client_name || "Unknown Browser")}
+
+
+ {!session.os_name && !session.client_name
+ ? "Unknown"
+ : (session.os_name || "Unknown Device") +
+ " • " +
+ (session.client_name || "Unknown Browser")}
+
+
+ {session.is_current
+ ? "This Session"
+ : `Last used ${formatTimeAgo(lastUsedSecondsAgo)}`}
+
+
-
- {session.is_current
- ? "This Session"
- : `Last used ${formatTimeAgo(lastUsedSecondsAgo)}`}
+
-
-
+
+
+
+
+ {session.os_name && session.os_version && (
+
+ {session.os_name} {session.os_version}
+
+ )}
+ {session.client_name && session.client_version && (
+
+ {session.client_name} {session.client_version}
+
+ )}
+
+ Logged in on{" "}
+ {toZonedTime(
+ new Date(session.created_at + "Z"),
+ userTz,
+ ).toLocaleString(undefined, {
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ })}
+
+
+ {!session.is_current && (
+
+ )}
+
+
+
);
}
From 28cb62fec8e8a06fdcf3db8f0949b62c7d2f14ff Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 21:33:23 -0400
Subject: [PATCH 12/21] Add tablet device icon
---
.../account/settings/security/components/session-manager.tsx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index ec020de0c..fd2331a03 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -7,8 +7,10 @@ import { toZonedTime } from "date-fns-tz";
import {
ChevronDownIcon,
CircleQuestionMark,
+ EllipsisIcon,
Laptop2Icon,
SmartphoneIcon,
+ TabletIcon,
} from "lucide-react";
import { ActiveSessionList, type ActiveSession } from "@/lib/utils/api/types";
@@ -79,6 +81,8 @@ function Session({
) : session.device_type === "smartphone" ? (
+ ) : session.device_type === "tablet" ? (
+
) : (
)}
From 845652caeccbfb2d2a0a05704795ac400012229f Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 21:37:22 -0400
Subject: [PATCH 13/21] Clean up layout and imports
---
.../security/components/session-manager.tsx | 27 ++++++++-----------
1 file changed, 11 insertions(+), 16 deletions(-)
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index fd2331a03..7b89f89cb 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -7,16 +7,15 @@ import { toZonedTime } from "date-fns-tz";
import {
ChevronDownIcon,
CircleQuestionMark,
- EllipsisIcon,
Laptop2Icon,
SmartphoneIcon,
TabletIcon,
} from "lucide-react";
+import EmptyButton from "@/features/button/components/empty";
import { ActiveSessionList, type ActiveSession } from "@/lib/utils/api/types";
import { cn } from "@/lib/utils/classname";
import { formatTimeAgo } from "@/lib/utils/date-time-format";
-import EmptyButton from "@/features/button/components/empty";
export default function SessionManager({
sessions,
@@ -35,20 +34,16 @@ export default function SessionManager({
-
-
-
-
-
-
- {sessions.other_sessions.map((session) => (
-
- ))}
-
+
+
+
+ {sessions.other_sessions.map((session) => (
+
+ ))}
);
From 574bfe591518aad10156a28f613b72c3b09cb3f3 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 21:39:10 -0400
Subject: [PATCH 14/21] Add cursor pointer to collapsible trigger
---
.../settings/security/components/session-manager.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index 7b89f89cb..822317547 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -64,7 +64,11 @@ function Session({
return (
-
+
Date: Thu, 13 Aug 2026 21:41:42 -0400
Subject: [PATCH 15/21] Move created at conversion logic
---
.../settings/security/components/session-manager.tsx | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index 822317547..4415f3dee 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -61,6 +61,11 @@ function Session({
const lastUsedLocal = toZonedTime(new Date(session.last_used + "Z"), userTz);
const lastUsedSecondsAgo = (Date.now() - lastUsedLocal.getTime()) / 1000;
+ const createdAtLocal = toZonedTime(
+ new Date(session.created_at + "Z"),
+ userTz,
+ );
+
return (
@@ -133,10 +138,7 @@ function Session({
)}
Logged in on{" "}
- {toZonedTime(
- new Date(session.created_at + "Z"),
- userTz,
- ).toLocaleString(undefined, {
+ {createdAtLocal.toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
From 0b5db4ac0fd020b21162fb355bcfad099dc0d99c Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 22:27:00 -0400
Subject: [PATCH 16/21] Add session removal
---
.../security/components/session-manager.tsx | 85 ++++++++++++++++---
.../settings/security/remove-session.tsx | 22 +++++
frontend/src/lib/messages.ts | 1 +
3 files changed, 97 insertions(+), 11 deletions(-)
create mode 100644 frontend/src/features/account/settings/security/remove-session.tsx
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index 4415f3dee..b7649c3f0 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState } from "react";
+import { startTransition, useOptimistic, useRef, useState } from "react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { toZonedTime } from "date-fns-tz";
@@ -12,7 +12,10 @@ import {
TabletIcon,
} from "lucide-react";
-import EmptyButton from "@/features/button/components/empty";
+import { removeSession } from "@/features/account/settings/security/remove-session";
+import ActionButton from "@/features/button/components/action";
+import { ConfirmationDialog, useToast } from "@/features/system-feedback";
+import { MESSAGES } from "@/lib/messages";
import { ActiveSessionList, type ActiveSession } from "@/lib/utils/api/types";
import { cn } from "@/lib/utils/classname";
import { formatTimeAgo } from "@/lib/utils/date-time-format";
@@ -24,27 +27,84 @@ export default function SessionManager({
}) {
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ const [optimisticSessions, removeOptimisticSession] = useOptimistic(
+ sessions,
+ (state, sessionToRemove: string) => {
+ return {
+ current_session: state.current_session,
+ other_sessions: state.other_sessions.filter(
+ (s) => s.public_id !== sessionToRemove,
+ ),
+ };
+ },
+ );
+
+ const [confirmationOpen, setConfirmationOpen] = useState(false);
+ const sessionToRemove = useRef(null);
+ const { addToast } = useToast();
+
+ const handleRemoveSession = async (publicId: string) => {
+ // Immediate UI update
+ startTransition(() => {
+ removeOptimisticSession(publicId);
+ });
+
+ // Server Action
+ const result = await removeSession(publicId);
+
+ if (!result.success) {
+ addToast("error", result.error || MESSAGES.ERROR_GENERIC);
+ } else {
+ addToast("success", MESSAGES.SUCCESS_SESSION_REMOVE);
+ }
+ };
+
+ const onRemoveSession = (publicId: string) => {
+ sessionToRemove.current = publicId;
+ setConfirmationOpen(true);
+ };
+
return (
-
Active Devices
+
Active Sessions
- If there are any devices you don{"'"}t recognize, remove them and
- change your password.
+ These devices have access to your account. If there are any you don
+ {"'"}t recognize, remove them and change your password.
-
+
- {sessions.other_sessions.map((session) => (
+ {optimisticSessions.other_sessions.map((session) => (
{
+ onRemoveSession(session.public_id);
+ }}
/>
))}
+
+
{
+ if (!sessionToRemove.current) return false;
+ handleRemoveSession(sessionToRemove.current);
+ return true;
+ }}
+ />
);
}
@@ -52,9 +112,11 @@ export default function SessionManager({
function Session({
session,
userTz,
+ onRemove,
}: {
session: ActiveSession;
userTz: string;
+ onRemove?: () => void;
}) {
const [isOpen, setIsOpen] = useState(false);
@@ -146,11 +208,12 @@ function Session({
})}
- {!session.is_current && (
-
)}
diff --git a/frontend/src/features/account/settings/security/remove-session.tsx b/frontend/src/features/account/settings/security/remove-session.tsx
new file mode 100644
index 000000000..dcd89cb8b
--- /dev/null
+++ b/frontend/src/features/account/settings/security/remove-session.tsx
@@ -0,0 +1,22 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+
+import { ROUTES } from "@/lib/utils/api/endpoints";
+import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper";
+import { serverPost } from "@/lib/utils/api/server-fetch";
+
+export async function removeSession(sessionId: string) {
+ try {
+ await serverPost(
+ ROUTES.account.terminateSession,
+ { session_id: sessionId },
+ { cache: "no-store" },
+ );
+ revalidatePath("/settings/security");
+ return { success: true };
+ } catch (e) {
+ const error = e as ApiErrorResponse;
+ return { success: false, error: error.formattedMessage };
+ }
+}
diff --git a/frontend/src/lib/messages.ts b/frontend/src/lib/messages.ts
index e298bfb1d..b706d17a7 100644
--- a/frontend/src/lib/messages.ts
+++ b/frontend/src/lib/messages.ts
@@ -49,6 +49,7 @@ export const MESSAGES = {
SUCCESS_DEFAULT_NAME_SAVED: "Nickname saved successfully.",
SUCCESS_DEFAULT_NAME_REMOVED: "Nickname removed successfully.",
SUCCESS_EVENT_DELETE: "Event deleted successfully.",
+ SUCCESS_SESSION_REMOVE: "Session removed successfully.",
SUCCESS_ACCOUNT_DELETE: "Account deleted successfully. Have a nice day!",
// copy link messages
From 73e7ee6fbe7a13eea56e6676460e3e5cde019209 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 22:36:15 -0400
Subject: [PATCH 17/21] Add updating last used timestamps
---
.../security/components/session-manager.tsx | 23 +++++++++++++++++--
1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index b7649c3f0..2c43796ef 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -1,6 +1,12 @@
"use client";
-import { startTransition, useOptimistic, useRef, useState } from "react";
+import {
+ startTransition,
+ useEffect,
+ useOptimistic,
+ useRef,
+ useState,
+} from "react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { toZonedTime } from "date-fns-tz";
@@ -25,6 +31,15 @@ export default function SessionManager({
}: {
sessions: ActiveSessionList;
}) {
+ const [now, setNow] = useState(Date.now());
+ // This updates the "last used" timestamps every minute
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setNow(Date.now());
+ }, 60000); // Update every minute
+ return () => clearInterval(interval);
+ }, []);
+
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const [optimisticSessions, removeOptimisticSession] = useOptimistic(
@@ -76,6 +91,7 @@ export default function SessionManager({
@@ -84,6 +100,7 @@ export default function SessionManager({
{
onRemoveSession(session.public_id);
@@ -111,17 +128,19 @@ export default function SessionManager({
function Session({
session,
+ now,
userTz,
onRemove,
}: {
session: ActiveSession;
+ now: number;
userTz: string;
onRemove?: () => void;
}) {
const [isOpen, setIsOpen] = useState(false);
const lastUsedLocal = toZonedTime(new Date(session.last_used + "Z"), userTz);
- const lastUsedSecondsAgo = (Date.now() - lastUsedLocal.getTime()) / 1000;
+ const lastUsedSecondsAgo = (now - lastUsedLocal.getTime()) / 1000;
const createdAtLocal = toZonedTime(
new Date(session.created_at + "Z"),
From 0812706dfb830b9fd745277d0b33f1bf45332995 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 22:41:06 -0400
Subject: [PATCH 18/21] Change remove session to TypeScript file
---
.../settings/security/{remove-session.tsx => remove-session.ts} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename frontend/src/features/account/settings/security/{remove-session.tsx => remove-session.ts} (100%)
diff --git a/frontend/src/features/account/settings/security/remove-session.tsx b/frontend/src/features/account/settings/security/remove-session.ts
similarity index 100%
rename from frontend/src/features/account/settings/security/remove-session.tsx
rename to frontend/src/features/account/settings/security/remove-session.ts
From fbd752040a7694650ca2649ef6861e060a05f151 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 22:49:09 -0400
Subject: [PATCH 19/21] Add session pruning
---
.../security/components/session-manager.tsx | 77 +++++++++++++++----
.../settings/security/prune-sessions.ts | 20 +++++
frontend/src/lib/messages.ts | 1 +
3 files changed, 85 insertions(+), 13 deletions(-)
create mode 100644 frontend/src/features/account/settings/security/prune-sessions.ts
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index 2c43796ef..19a44548e 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -25,6 +25,9 @@ import { MESSAGES } from "@/lib/messages";
import { ActiveSessionList, type ActiveSession } from "@/lib/utils/api/types";
import { cn } from "@/lib/utils/classname";
import { formatTimeAgo } from "@/lib/utils/date-time-format";
+import { pruneSessions } from "@/features/account/settings/security/prune-sessions";
+
+type SessionAction = { type: "remove"; publicId: string } | { type: "prune" };
export default function SessionManager({
sessions,
@@ -42,26 +45,53 @@ export default function SessionManager({
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
- const [optimisticSessions, removeOptimisticSession] = useOptimistic(
+ const [optimisticSessions, setOptimisticSessions] = useOptimistic(
sessions,
- (state, sessionToRemove: string) => {
- return {
- current_session: state.current_session,
- other_sessions: state.other_sessions.filter(
- (s) => s.public_id !== sessionToRemove,
- ),
- };
+ (state, action: SessionAction) => {
+ switch (action.type) {
+ case "remove":
+ return {
+ current_session: state.current_session,
+ other_sessions: state.other_sessions.filter(
+ (s) => s.public_id !== action.publicId,
+ ),
+ };
+ case "prune":
+ return {
+ current_session: state.current_session,
+ other_sessions: [],
+ };
+ default:
+ return state;
+ }
},
);
- const [confirmationOpen, setConfirmationOpen] = useState(false);
+ const [pruneConfirmationOpen, setPruneConfirmationOpen] = useState(false);
+ const [removeConfirmationOpen, setRemoveConfirmationOpen] = useState(false);
const sessionToRemove = useRef(null);
const { addToast } = useToast();
+ const handlePruneSessions = async () => {
+ // Immediate UI update
+ startTransition(() => {
+ setOptimisticSessions({ type: "prune" });
+ });
+
+ // Server Action
+ const result = await pruneSessions();
+
+ if (!result.success) {
+ addToast("error", result.error || MESSAGES.ERROR_GENERIC);
+ } else {
+ addToast("success", MESSAGES.SUCCESS_SESSION_PRUNE);
+ }
+ };
+
const handleRemoveSession = async (publicId: string) => {
// Immediate UI update
startTransition(() => {
- removeOptimisticSession(publicId);
+ setOptimisticSessions({ type: "remove", publicId });
});
// Server Action
@@ -76,7 +106,7 @@ export default function SessionManager({
const onRemoveSession = (publicId: string) => {
sessionToRemove.current = publicId;
- setConfirmationOpen(true);
+ setRemoveConfirmationOpen(true);
};
return (
@@ -109,19 +139,40 @@ export default function SessionManager({
))}
+ {
+ setPruneConfirmationOpen(true);
+ }}
+ />
+
{
if (!sessionToRemove.current) return false;
handleRemoveSession(sessionToRemove.current);
return true;
}}
/>
+ {
+ handlePruneSessions();
+ return true;
+ }}
+ />
);
}
diff --git a/frontend/src/features/account/settings/security/prune-sessions.ts b/frontend/src/features/account/settings/security/prune-sessions.ts
new file mode 100644
index 000000000..e5c7eaee3
--- /dev/null
+++ b/frontend/src/features/account/settings/security/prune-sessions.ts
@@ -0,0 +1,20 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+
+import { ROUTES } from "@/lib/utils/api/endpoints";
+import { ApiErrorResponse } from "@/lib/utils/api/fetch-wrapper";
+import { serverPost } from "@/lib/utils/api/server-fetch";
+
+export async function pruneSessions() {
+ try {
+ await serverPost(ROUTES.account.pruneSessions, undefined, {
+ cache: "no-store",
+ });
+ revalidatePath("/settings/security");
+ return { success: true };
+ } catch (e) {
+ const error = e as ApiErrorResponse;
+ return { success: false, error: error.formattedMessage };
+ }
+}
diff --git a/frontend/src/lib/messages.ts b/frontend/src/lib/messages.ts
index b706d17a7..4fe72ebee 100644
--- a/frontend/src/lib/messages.ts
+++ b/frontend/src/lib/messages.ts
@@ -50,6 +50,7 @@ export const MESSAGES = {
SUCCESS_DEFAULT_NAME_REMOVED: "Nickname removed successfully.",
SUCCESS_EVENT_DELETE: "Event deleted successfully.",
SUCCESS_SESSION_REMOVE: "Session removed successfully.",
+ SUCCESS_SESSION_PRUNE: "All other sessions removed successfully.",
SUCCESS_ACCOUNT_DELETE: "Account deleted successfully. Have a nice day!",
// copy link messages
From 0155de9162be4b0a3af8311ad36be35066508448 Mon Sep 17 00:00:00 2001
From: jzgom067
Date: Thu, 13 Aug 2026 22:53:01 -0400
Subject: [PATCH 20/21] Add empty other sessions layout
---
.../security/components/session-manager.tsx | 50 +++++++++++--------
1 file changed, 29 insertions(+), 21 deletions(-)
diff --git a/frontend/src/features/account/settings/security/components/session-manager.tsx b/frontend/src/features/account/settings/security/components/session-manager.tsx
index 19a44548e..49c6905e4 100644
--- a/frontend/src/features/account/settings/security/components/session-manager.tsx
+++ b/frontend/src/features/account/settings/security/components/session-manager.tsx
@@ -18,6 +18,7 @@ import {
TabletIcon,
} from "lucide-react";
+import { pruneSessions } from "@/features/account/settings/security/prune-sessions";
import { removeSession } from "@/features/account/settings/security/remove-session";
import ActionButton from "@/features/button/components/action";
import { ConfirmationDialog, useToast } from "@/features/system-feedback";
@@ -25,7 +26,6 @@ import { MESSAGES } from "@/lib/messages";
import { ActiveSessionList, type ActiveSession } from "@/lib/utils/api/types";
import { cn } from "@/lib/utils/classname";
import { formatTimeAgo } from "@/lib/utils/date-time-format";
-import { pruneSessions } from "@/features/account/settings/security/prune-sessions";
type SessionAction = { type: "remove"; publicId: string } | { type: "prune" };
@@ -125,28 +125,36 @@ export default function SessionManager({
userTz={userTimeZone}
/>
-
- {optimisticSessions.other_sessions.map((session) => (
-
{
- onRemoveSession(session.public_id);
+ {optimisticSessions.other_sessions.length > 0 ? (
+ <>
+
+ {optimisticSessions.other_sessions.map((session) => (
+ {
+ onRemoveSession(session.public_id);
+ }}
+ />
+ ))}
+
+
+ {
+ setPruneConfirmationOpen(true);
}}
/>
- ))}
-
-
- {
- setPruneConfirmationOpen(true);
- }}
- />
+ >
+ ) : (
+
+ No other active sessions.
+
+ )}
Date: Fri, 14 Aug 2026 00:15:39 -0400
Subject: [PATCH 21/21] Remove accidental whitespace
---
frontend/src/lib/utils/api/types.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/lib/utils/api/types.ts b/frontend/src/lib/utils/api/types.ts
index c36f3ecc7..77e82e1a9 100644
--- a/frontend/src/lib/utils/api/types.ts
+++ b/frontend/src/lib/utils/api/types.ts
@@ -144,7 +144,7 @@ export type ActiveSession = {
| "console"
| "tv"
| "car browser"
- | "smart display "
+ | "smart display"
| "camera"
| "portable media player"
| "phablet"