fix: clean up runtime UI and harden session expiry handling
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m27s
CI / docker (push) Successful in 1m13s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 42s

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:
hsiegeln
2026-04-12 22:33:44 +02:00
parent ffce3b714f
commit 27f2503640
5 changed files with 50 additions and 91 deletions

View File

@@ -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;
},

View File

@@ -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;
}
},
});

View File

@@ -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}`}>

View File

@@ -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;

View File

@@ -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>
</>
)}
</>
)}