Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(dashboard):** surface durable exclusive managed leases in the existing Sessions view, keeping leased clients visible across idle gaps while marking connections with in-flight work as active ([#11389](https://github.com/diegosouzapw/OmniRoute/pull/11389)) — thanks @KaspaPulse
115 changes: 84 additions & 31 deletions src/app/(dashboard)/dashboard/usage/components/SessionsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,47 @@

import { useTranslations } from "next-intl";

import { useState, useEffect, useCallback } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
mergeDashboardSessions,
type DashboardSession,
type ExclusiveDashboardSession,
type RecentSessionForDashboard,
} from "@/lib/sessionObservability";
import { Card } from "@/shared/components";

type SessionsResponse = {
sessions: RecentSessionForDashboard[];
exclusiveSessions: ExclusiveDashboardSession[];
};

const EMPTY_DATA: SessionsResponse = {
sessions: [],
exclusiveSessions: [],
};

function isLeaseBackedSession(session: DashboardSession): session is ExclusiveDashboardSession {
return "leaseBacked" in session && session.leaseBacked;
}

export default function SessionsTab() {
const t = useTranslations("usage");
const [data, setData] = useState({ count: 0, sessions: [] });
const tCommon = useTranslations("common");
const [data, setData] = useState<SessionsResponse>(EMPTY_DATA);
const [loading, setLoading] = useState(true);

const loadSessions = useCallback(async () => {
try {
const res = await fetch("/api/sessions");
if (res.ok) setData(await res.json());
if (res.ok) {
const next = await res.json();
setData({
sessions: Array.isArray(next.sessions) ? next.sessions : [],
exclusiveSessions: Array.isArray(next.exclusiveSessions) ? next.exclusiveSessions : [],
});
}
} catch {
// A failed background poll leaves the last successful Sessions snapshot visible.
} finally {
setLoading(false);
}
Expand All @@ -26,7 +54,12 @@ export default function SessionsTab() {
return () => clearInterval(interval);
}, [loadSessions]);

const formatAge = (ms) => {
const displaySessions = useMemo(() => {
return mergeDashboardSessions(data.exclusiveSessions, data.sessions);
}, [data.exclusiveSessions, data.sessions]);

const formatAge = (ms: number | null) => {
if (ms == null) return t("notAvailableSymbol");
if (ms < 60000) return t("durationSecondsShort", { value: Math.floor(ms / 1000) });
if (ms < 3600000) return t("durationMinutesShort", { value: Math.floor(ms / 60000) });
return t("durationHoursShort", { value: Math.floor(ms / 3600000) });
Expand All @@ -47,12 +80,17 @@ export default function SessionsTab() {
<div className="flex items-center gap-2">
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/20">
<span className="w-2 h-2 rounded-full bg-cyan-500 animate-pulse" />
<span className="text-sm font-semibold tabular-nums text-cyan-400">{data.count}</span>
<span
className="text-sm font-semibold tabular-nums text-cyan-400"
data-testid="session-count"
>
{displaySessions.length}
</span>
</span>
</div>
</div>

{data.sessions.length === 0 ? (
{displaySessions.length === 0 ? (
<div className="text-center py-8 text-text-muted">
<span
className="material-symbols-outlined text-[40px] mb-2 block opacity-40"
Expand Down Expand Up @@ -83,31 +121,46 @@ export default function SessionsTab() {
</tr>
</thead>
<tbody>
{data.sessions.map((s) => (
<tr
key={s.sessionId}
className="border-b border-border/10 hover:bg-surface/20 transition-colors"
>
<td className="py-2.5 px-3">
<span className="font-mono text-xs px-2 py-1 rounded bg-surface/40 text-text-muted">
{s.sessionId.slice(0, 12)}…
</span>
</td>
<td className="py-2.5 px-3 text-text-muted tabular-nums">{formatAge(s.ageMs)}</td>
<td className="py-2.5 px-3 text-right">
<span className="font-semibold tabular-nums">{s.requestCount}</span>
</td>
<td className="py-2.5 px-3">
{s.connectionId ? (
<span className="text-xs font-mono text-cyan-400">
{s.connectionId.slice(0, 10)}
</span>
) : (
<span className="text-text-muted/40">{t("notAvailableSymbol")}</span>
)}
</td>
</tr>
))}
{displaySessions.map((s) => {
const leaseBacked = isLeaseBackedSession(s);
return (
<tr
key={s.sessionId}
className="border-b border-border/10 hover:bg-surface/20 transition-colors"
>
<td className="py-2.5 px-3">
<div className="flex items-center gap-2">
<span
className="font-mono text-xs px-2 py-1 rounded bg-surface/40 text-text-muted"
title={s.sessionId}
>
{s.sessionId.slice(0, 12)}…
</span>
{leaseBacked && s.active && (
<span className="text-[10px] font-semibold tracking-wide px-2 py-0.5 rounded-full border text-green-400 border-green-500/30 bg-green-500/10">
{tCommon("active")}
</span>
)}
</div>
</td>
<td className="py-2.5 px-3 text-text-muted tabular-nums">
{formatAge(s.ageMs)}
</td>
<td className="py-2.5 px-3 text-right">
<span className="font-semibold tabular-nums">{s.requestCount}</span>
</td>
<td className="py-2.5 px-3">
{s.connectionId ? (
<span className="text-xs font-mono text-cyan-400" title={s.connectionId}>
{(leaseBacked && s.connectionName) || s.connectionId.slice(0, 10)}
</span>
) : (
<span className="text-text-muted/40">{t("notAvailableSymbol")}</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
Expand Down
33 changes: 32 additions & 1 deletion src/app/api/sessions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,44 @@ import {
getAllActiveSessionCountsByKey,
} from "@omniroute/open-sse/services/sessionManager.ts";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { getExclusiveLeaseConnectionIds } from "@/lib/db/apiKeys";
import { getExclusiveLeaseOccupancy } from "@/lib/db/exclusiveConnectionLeases";
import { getProviderConnectionDisplayMetadata } from "@/lib/db/providers";
import { getAccountDisplayName } from "@/lib/display/names";
import { getPendingRequests } from "@/lib/usage/usageHistory";
import { buildExclusiveDashboardSessions } from "@/lib/sessionObservability";

export async function GET() {
try {
const sessions = getActiveSessions();
const count = getActiveSessionCount();
const byApiKey = getAllActiveSessionCountsByKey();
return NextResponse.json({ count, sessions, byApiKey });

// Reuse the hard-lease authority added by #10362. The API-key policy derives
// the managed candidate set; SQLite occupancy is the source of truth for
// which of those connections are actually leased right now.
const managedConnectionIds = Array.from(await getExclusiveLeaseConnectionIds());
const occupancy = getExclusiveLeaseOccupancy(managedConnectionIds);
const leasedConnectionIds = new Set(occupancy.keys());
const connectionNames = new Map(
getProviderConnectionDisplayMetadata([...leasedConnectionIds]).map((connection) => [
connection.id,
getAccountDisplayName(connection),
])
);
const exclusiveSessions = buildExclusiveDashboardSessions(
leasedConnectionIds,
getPendingRequests().byAccount,
sessions,
connectionNames
);

return NextResponse.json({
count,
sessions,
byApiKey,
exclusiveSessions,
});
} catch (error) {
return NextResponse.json({ error: sanitizeErrorMessage(error) }, { status: 500 });
}
Expand Down
37 changes: 37 additions & 0 deletions src/lib/db/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,43 @@ export async function getProviderConnectionById(id: string) {
);
}

export interface ProviderConnectionDisplayMetadata {
id: string;
name: string | null;
displayName: string | null;
email: string | null;
}

/**
* Reads only the non-credential fields needed by account display-name resolvers.
*
* This avoids decrypting provider credentials when a dashboard only needs labels.
*/
export function getProviderConnectionDisplayMetadata(
connectionIds: readonly string[]
): ProviderConnectionDisplayMetadata[] {
const ids = [...new Set(connectionIds.filter((id) => id.length > 0))];
if (ids.length === 0) return [];

const db = getDbInstance() as unknown as DbLike;
const rows = db
.prepare(
`SELECT id, name, display_name, email FROM provider_connections
WHERE id IN (${ids.map(() => "?").join(", ")})`
)
.all(...ids);

return rows.map((row) => {
const view = rowToCamel(row) as JsonRecord;
return {
id: toStringOrNull(view.id) || "",
name: toStringOrNull(view.name),
displayName: toStringOrNull(view.displayName),
email: toStringOrNull(view.email),
};
});
}

// #3368 PR6 — dedup web-session cookie/token credentials on connection create.
// Re-importing the same session (e.g. via bulk web-session import) under a
// different or blank name must update the existing connection instead of
Expand Down
93 changes: 93 additions & 0 deletions src/lib/sessionObservability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
export type RecentSessionForDashboard = {
sessionId: string;
ageMs: number;
requestCount: number;
connectionId: string | null;
};

export type PendingRequestsByAccount = Record<string, Record<string, number>>;

export type ExclusiveDashboardSession = {
sessionId: string;
ageMs: null;
requestCount: number;
connectionId: string;
connectionName: string | null;
leaseBacked: true;
active: boolean;
};

export type DashboardSession = RecentSessionForDashboard | ExclusiveDashboardSession;

function positiveCount(value: unknown): number {
const count = Number(value);
return Number.isFinite(count) && count > 0 ? count : 0;
}

function countInFlightRequests(
pendingByAccount: PendingRequestsByAccount,
connectionId: string
): number {
return Object.values(pendingByAccount[connectionId] ?? {}).reduce(
(total, count) => total + positiveCount(count),
0
);
}

/**
* Build the dashboard-only view of durable exclusive leases.
*
* The lease table remains the lifecycle authority. The request tracker is used
* only to flag work currently in flight for an already-held lease; it never
* creates, extends, or removes lease ownership.
*
* Deliberately does not expose the persisted owner hash, API-key id, or lease
* generation. The dashboard needs occupancy, connection binding, and activity
* state — not fencing material.
*/
export function buildExclusiveDashboardSessions(
leasedConnectionIds: ReadonlySet<string>,
pendingByAccount: PendingRequestsByAccount,
recentSessions: readonly RecentSessionForDashboard[],
connectionNames: ReadonlyMap<string, string> = new Map()
): ExclusiveDashboardSession[] {
const recentRequestsByConnection = new Map<string, number>();
for (const session of recentSessions) {
if (!session.connectionId) continue;
recentRequestsByConnection.set(
session.connectionId,
(recentRequestsByConnection.get(session.connectionId) ?? 0) +
positiveCount(session.requestCount)
);
}

return Array.from(leasedConnectionIds)
.map((connectionId) => ({
sessionId: `lease:${connectionId}`,
ageMs: null,
requestCount: recentRequestsByConnection.get(connectionId) ?? 0,
connectionId,
connectionName: connectionNames.get(connectionId) ?? null,
leaseBacked: true as const,
active: countInFlightRequests(pendingByAccount, connectionId) > 0,
}))
.sort((left, right) => {
if (left.active !== right.active) return left.active ? -1 : 1;
return left.connectionId.localeCompare(right.connectionId);
});
}

/**
* Lease-backed rows replace request-derived rows for the same connection.
* Sessions without a connection binding remain untouched.
*/
export function mergeDashboardSessions(
leaseSessions: readonly ExclusiveDashboardSession[],
recentSessions: readonly RecentSessionForDashboard[]
): DashboardSession[] {
const leasedConnectionIds = new Set(leaseSessions.map((session) => session.connectionId));
const unleasedRecentSessions = recentSessions.filter(
(session) => !session.connectionId || !leasedConnectionIds.has(session.connectionId)
);
return [...leaseSessions, ...unleasedRecentSessions];
}
Loading
Loading