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 = () =>
|
let getAccessToken: () => string | null = () =>
|
||||||
useAuthStore.getState().accessToken;
|
useAuthStore.getState().accessToken;
|
||||||
let onUnauthorized: () => void = () => {};
|
let onUnauthorized: () => void = () => {};
|
||||||
|
let refreshing: Promise<void> | null = null;
|
||||||
|
|
||||||
export function configureAuth(opts: {
|
export function configureAuth(opts: {
|
||||||
getAccessToken?: () => string | null;
|
getAccessToken?: () => string | null;
|
||||||
@@ -24,9 +25,19 @@ const authMiddleware: Middleware = {
|
|||||||
request.headers.set('X-Cameleer-Protocol-Version', '1');
|
request.headers.set('X-Cameleer-Protocol-Version', '1');
|
||||||
return request;
|
return request;
|
||||||
},
|
},
|
||||||
async onResponse({ response }) {
|
async onResponse({ request, response }) {
|
||||||
if (response.status === 401 || response.status === 403) {
|
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;
|
return response;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useAuthStore } from './auth-store';
|
import { useAuthStore } from './auth-store';
|
||||||
import { configureAuth } from '../api/client';
|
import { configureAuth } from '../api/client';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
@@ -6,14 +6,21 @@ import { useNavigate } from 'react-router';
|
|||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const { accessToken, isAuthenticated, refresh, logout } = useAuthStore();
|
const { accessToken, isAuthenticated, refresh, logout } = useAuthStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const refreshingRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
configureAuth({
|
configureAuth({
|
||||||
onUnauthorized: async () => {
|
onUnauthorized: async () => {
|
||||||
const ok = await useAuthStore.getState().refresh();
|
if (refreshingRef.current) return;
|
||||||
if (!ok) {
|
refreshingRef.current = true;
|
||||||
useAuthStore.getState().logout();
|
try {
|
||||||
navigate('/login', { replace: true });
|
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>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
<Badge
|
|
||||||
label={`${liveCount}/${totalInstances} live`}
|
|
||||||
color={deadCount > 0 ? 'error' : staleCount > 0 ? 'warning' : 'success'}
|
|
||||||
variant="filled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Application config bar */}
|
{/* Application config bar */}
|
||||||
{appId && appConfig && (
|
{appId && appConfig && (
|
||||||
<div className={`${sectionStyles.section} ${styles.configBar}`}>
|
<div className={`${sectionStyles.section} ${styles.configBar}`}>
|
||||||
|
|||||||
@@ -14,42 +14,20 @@
|
|||||||
margin-bottom: 16px;
|
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 */
|
/* Process info card — card styling via sectionStyles.section */
|
||||||
.processCard {
|
.processCard {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.processBadges {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.processGrid {
|
.processGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto 1fr;
|
grid-template-columns: auto 1fr auto 1fr;
|
||||||
@@ -70,14 +48,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Route badges */
|
|
||||||
.routeBadges {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Charts 3x2 grid */
|
/* Charts 3x2 grid */
|
||||||
.chartsGrid {
|
.chartsGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams, Link } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { RefreshCw, ChevronRight } from 'lucide-react';
|
import { RefreshCw } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
StatCard, StatusDot, Badge, ThemedChart, Line, Area, ReferenceLine, CHART_COLORS,
|
StatCard, StatusDot, Badge, ThemedChart, Line, Area, ReferenceLine, CHART_COLORS,
|
||||||
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
|
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
|
||||||
@@ -213,36 +213,25 @@ export default function AgentInstance() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scope trail + badges */}
|
|
||||||
{agent && (
|
{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 */}
|
{/* Process info card */}
|
||||||
<div className={`${sectionStyles.section} ${styles.processCard}`}>
|
<div className={`${sectionStyles.section} ${styles.processCard}`}>
|
||||||
<SectionHeader>Process Information</SectionHeader>
|
<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}>
|
<div className={styles.processGrid}>
|
||||||
{agent.capabilities?.jvmVersion && (
|
{agent.capabilities?.jvmVersion && (
|
||||||
<>
|
<>
|
||||||
@@ -281,17 +270,7 @@ export default function AgentInstance() {
|
|||||||
</div>
|
</div>
|
||||||
</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