fix: clean up runtime UI and harden session expiry handling
Remove redundant "X/X LIVE" badge from runtime page, breadcrumb trail and routes section from agent detail page (pills moved into Process Information card). Fix session expiry: guard against concurrent 401 refresh races and skip re-entrant triggers on auth endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { useAuthStore } from '../auth/auth-store';
|
||||
let getAccessToken: () => string | null = () =>
|
||||
useAuthStore.getState().accessToken;
|
||||
let onUnauthorized: () => void = () => {};
|
||||
let refreshing: Promise<void> | null = null;
|
||||
|
||||
export function configureAuth(opts: {
|
||||
getAccessToken?: () => string | null;
|
||||
@@ -24,9 +25,19 @@ const authMiddleware: Middleware = {
|
||||
request.headers.set('X-Cameleer-Protocol-Version', '1');
|
||||
return request;
|
||||
},
|
||||
async onResponse({ response }) {
|
||||
async onResponse({ request, response }) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
onUnauthorized();
|
||||
// Don't re-trigger for auth endpoints (refresh/login) to avoid loops
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname.endsWith('/auth/refresh') || url.pathname.endsWith('/auth/login')) {
|
||||
return response;
|
||||
}
|
||||
// Coalesce concurrent 401s into a single refresh attempt
|
||||
if (!refreshing) {
|
||||
refreshing = (async () => {
|
||||
try { onUnauthorized(); } finally { refreshing = null; }
|
||||
})();
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAuthStore } from './auth-store';
|
||||
import { configureAuth } from '../api/client';
|
||||
import { useNavigate } from 'react-router';
|
||||
@@ -6,14 +6,21 @@ import { useNavigate } from 'react-router';
|
||||
export function useAuth() {
|
||||
const { accessToken, isAuthenticated, refresh, logout } = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
const refreshingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
configureAuth({
|
||||
onUnauthorized: async () => {
|
||||
const ok = await useAuthStore.getState().refresh();
|
||||
if (!ok) {
|
||||
useAuthStore.getState().logout();
|
||||
navigate('/login', { replace: true });
|
||||
if (refreshingRef.current) return;
|
||||
refreshingRef.current = true;
|
||||
try {
|
||||
const ok = await useAuthStore.getState().refresh();
|
||||
if (!ok) {
|
||||
useAuthStore.getState().logout();
|
||||
navigate('/login', { replace: true });
|
||||
}
|
||||
} finally {
|
||||
refreshingRef.current = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -400,14 +400,6 @@ export default function AgentHealth() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Badge
|
||||
label={`${liveCount}/${totalInstances} live`}
|
||||
color={deadCount > 0 ? 'error' : staleCount > 0 ? 'warning' : 'success'}
|
||||
variant="filled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Application config bar */}
|
||||
{appId && appConfig && (
|
||||
<div className={`${sectionStyles.section} ${styles.configBar}`}>
|
||||
|
||||
@@ -14,42 +14,20 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Scope trail — matches /agents */
|
||||
.scopeTrail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.scopeLink {
|
||||
color: var(--amber);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.scopeLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.scopeSep {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.scopeCurrent {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Process info card — card styling via sectionStyles.section */
|
||||
.processCard {
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.processBadges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.processGrid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr;
|
||||
@@ -70,14 +48,6 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Route badges */
|
||||
.routeBadges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Charts 3x2 grid */
|
||||
.chartsGrid {
|
||||
display: grid;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router';
|
||||
import { RefreshCw, ChevronRight } from 'lucide-react';
|
||||
import { useParams } from 'react-router';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
StatCard, StatusDot, Badge, ThemedChart, Line, Area, ReferenceLine, CHART_COLORS,
|
||||
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
|
||||
@@ -213,36 +213,25 @@ export default function AgentInstance() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Scope trail + badges */}
|
||||
{agent && (
|
||||
<>
|
||||
<div className={styles.scopeTrail}>
|
||||
<Link to="/agents" className={styles.scopeLink}>
|
||||
All Agents
|
||||
</Link>
|
||||
<span className={styles.scopeSep}><ChevronRight size={12} /></span>
|
||||
<Link to={`/agents/${appId}`} className={styles.scopeLink}>
|
||||
{appId}
|
||||
</Link>
|
||||
<span className={styles.scopeSep}><ChevronRight size={12} /></span>
|
||||
<span className={styles.scopeCurrent}>{agent.displayName}</span>
|
||||
<StatusDot variant={statusVariant} />
|
||||
<Badge label={agent.status} color={statusColor} />
|
||||
{agent.containerStatus && agent.containerStatus !== 'UNKNOWN' && (
|
||||
<Badge label={`Container: ${agent.containerStatus}`} variant="outlined" color="auto" />
|
||||
)}
|
||||
{agent.version && <Badge label={agent.version} variant="outlined" color="auto" />}
|
||||
<Badge
|
||||
label={`${agent.activeRoutes ?? (agent.routeIds?.length ?? 0)}/${agent.totalRoutes ?? (agent.routeIds?.length ?? 0)} routes`}
|
||||
color={
|
||||
(agent.activeRoutes ?? 0) < (agent.totalRoutes ?? 0) ? 'warning' : 'success'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Process info card */}
|
||||
<div className={`${sectionStyles.section} ${styles.processCard}`}>
|
||||
<SectionHeader>Process Information</SectionHeader>
|
||||
<div className={styles.processBadges}>
|
||||
<StatusDot variant={statusVariant} />
|
||||
<Badge label={agent.status} color={statusColor} />
|
||||
{agent.containerStatus && agent.containerStatus !== 'UNKNOWN' && (
|
||||
<Badge label={`Container: ${agent.containerStatus}`} variant="outlined" color="auto" />
|
||||
)}
|
||||
{agent.version && <Badge label={agent.version} variant="outlined" color="auto" />}
|
||||
<Badge
|
||||
label={`${agent.activeRoutes ?? (agent.routeIds?.length ?? 0)}/${agent.totalRoutes ?? (agent.routeIds?.length ?? 0)} routes`}
|
||||
color={
|
||||
(agent.activeRoutes ?? 0) < (agent.totalRoutes ?? 0) ? 'warning' : 'success'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.processGrid}>
|
||||
{agent.capabilities?.jvmVersion && (
|
||||
<>
|
||||
@@ -281,17 +270,7 @@ export default function AgentInstance() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Routes */}
|
||||
{(agent.routeIds?.length ?? 0) > 0 && (
|
||||
<>
|
||||
<SectionHeader>Routes</SectionHeader>
|
||||
<div className={styles.routeBadges}>
|
||||
{(agent.routeIds || []).map((r: string) => (
|
||||
<Badge key={r} label={r} color="auto" />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user