;
}
function Card({ children, style, pad=18, onClick, className }){
const float = !!className && className.split(' ').includes('clk'); // .clk owns the shadow; drop the inline one so the :hover lift works
return
{children}
;
}
function CardHead({ title, sub, right, icon }){
return
{icon&&}
{title}
{sub&&
{sub}
}
{right}
;
}
// Live freshness indicator (PM-167): "Live . updated Ns ago" + manual refresh.
// Self-contained 1s tick so only this pill re-renders (no app-wide flicker). The
// 30s auto-poll lives in OpsApp; this shows the age of the last successful load.
function fmtAgoSec(s){ if(s<5) return 'just now'; if(s<60) return s+'s ago'; const m=Math.floor(s/60); if(m<60) return m+'m ago'; const h=Math.floor(m/60); return h+'h ago'; }
function LiveIndicator({ lastUpdated, refreshing, onRefresh }){
const [,tick]=React.useState(0);
React.useEffect(()=>{ const id=setInterval(()=>tick(t=>t+1),1000); return ()=>clearInterval(id); },[]);
const ageSec = lastUpdated!=null ? Math.max(0,Math.floor((Date.now()-lastUpdated)/1000)) : null;
const stale = ageSec!=null && ageSec>=600; // 10 min since the last successful fetch = data delayed (green->red; raises a Critical alert)
const dot = refreshing ? O.azure : stale ? O.crit : O.leaf;
const txt = refreshing ? 'Updating...' : ageSec==null ? 'Connecting...' : (stale?'Delayed':'Live')+' . updated '+fmtAgoSec(ageSec);
return
{/* vNext canon ribbon: prototype-only chrome, hidden when served as production (PROD_PORTAL). */}
{!PROD_PORTAL &&
Prototype - vNext CanonPlanned next version of portal.tbdcloud.com - the sign-off surface. Live fleet via /api/fleet-snapshot, auto-refreshed every 30s. Not the production portal.
}
{children}
;
}
function PageTitle({ title, sub, right }){
return
{title}
{sub&&
{sub}
}
{right}
;
}
Object.assign(window,{ O, STATUS, SEV, KIND, OIcon, StatusPill, Battery, Spark, Donut, Bars, Card, CardHead, Shell, PageTitle, NAV });