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
39 changes: 25 additions & 14 deletions frontend/src/components/FleetRightNow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ function Constellation({ data, flips }: { data: RightNowData; flips: Set<string>
}).filter(c => c.dots.length > 0);

const n = data.now;
const pct = n.machines ? Math.round((n.running / n.machines) * 100) : 0;
const running = data.projects.running;
const totalRunning = Object.values(running).reduce((s, v) => s + v, 0) || 1;
const topProject = order.reduce((a, b) => (running[b] > running[a] ? b : a), order[0]);
const share = (k: RightNowProject) => (running[k] / totalRunning) * 100;
const ranked = [...order].sort((a, b) => running[b] - running[a]);
const [topProject, runnerUp] = ranked;

return (
<div className="card p-6 relative overflow-hidden">
Expand All @@ -61,14 +62,24 @@ function Constellation({ data, flips }: { data: RightNowData; flips: Set<string>
<div className="flex items-center gap-2 text-[11px] text-gray-500 uppercase tracking-wider">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" /> Right now
</div>
{/* The Fleet Load card above owns the running/pending counts; this panel's
headline says the one thing only it knows: whose work the fleet is doing. */}
<p className="text-2xl md:text-3xl font-light text-white leading-snug mt-2 max-w-3xl">
<span className="font-semibold bg-clip-text text-transparent" style={{ backgroundImage: FF_GRADIENT }}>
<AnimatedNumber value={n.running} format={v => Math.round(v).toLocaleString()} />
</span>{" "}
of {n.machines.toLocaleString()} machines are building and testing Firefox
<span className="text-gray-500"> ({pct}%)</span>, with{" "}
<span className="font-semibold text-white"><AnimatedNumber value={n.pending} format={v => Math.round(v).toLocaleString()} /></span>{" "}
tasks waiting in line.
{running[topProject] > 0 ? (
<>
<span className="font-medium">{data.projects.labels[topProject]}</span> is the fleet's biggest customer, with{" "}
<span className="font-semibold bg-clip-text text-transparent" style={{ backgroundImage: FF_GRADIENT }}>
<AnimatedNumber value={share(topProject)} format={v => `${Math.round(v)}%`} />
</span>{" "}
of running work
{runnerUp && running[runnerUp] > 0 && (
<span className="text-gray-500">, ahead of {data.projects.labels[runnerUp]} at {Math.round(share(runnerUp))}%</span>
)}
.
</>
) : (
<span className="text-gray-400">No Firefox work running right now.</span>
)}
</p>

<div className="mt-6 flex flex-wrap gap-x-8 gap-y-6">
Expand Down Expand Up @@ -118,12 +129,12 @@ function Constellation({ data, flips }: { data: RightNowData; flips: Set<string>

{/* whose work it is */}
<div className="xl:w-72 flex-shrink-0">
<div className="pb-5 mb-5 border-b border-gray-800/60">
<div className="text-3xl font-bold text-white tabular-nums leading-none"><AnimatedNumber value={n.machines} format={v => Math.round(v).toLocaleString()} /></div>
<div className="text-[11px] text-gray-500 uppercase tracking-wider mt-1.5">machines</div>
</div>
<div className="text-[11px] text-gray-500 uppercase tracking-wider">Whose work</div>
<p className="text-sm text-gray-300 mt-2 leading-relaxed">
<span className="text-white font-medium">{data.projects.labels[topProject]}</span> is the biggest customer right now, with{" "}
{Math.round((running[topProject] / totalRunning) * 100)}% of running tasks.
</p>
<div className="mt-4 space-y-2.5">
<div className="mt-3 space-y-2.5">
{[...order, "other" as const].map(k => {
const v = running[k];
const color = k === "other" ? OTHER_WORK : PROJECT_COLOR[k];
Expand Down
14 changes: 9 additions & 5 deletions frontend/src/lib/kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,14 @@ function median(xs: number[]): number {
}

const n = (v: number) => Math.round(v).toLocaleString();
/** Percent difference between a and b, relative to `base` (defaults to b). */
const pct = (a: number, b: number, base = b) => Math.round(((a - b) / base) * 100);

/** How Kit reads the fleet right now. Fleet-wide pending is never small (a couple of
* pools are always backed up), so every judgement is against this fleet's own last
* 48 hours rather than a fixed number. Checked in priority order: the first match wins. */
* 48 hours rather than a fixed number. Checked in priority order: the first match wins.
* The Fleet Load card already shows the running/pending counts, so Kit never repeats
* them; it says how today compares instead. */
export function kitMood(r: FleetReading): KitMood {
if (r.shipsToday) return { pose: "proud", says: `Firefox ${r.shipsToday} ships today.` };
if (r.pending == null || r.running == null) return { pose: r.idlePose, says: "Checking on the fleet…" };
Expand All @@ -73,16 +77,16 @@ export function kitMood(r: FleetReading): KitMood {
const usualRunning = median(r.history.map(h => h.running));

if (enoughHistory && r.pending >= 500 && r.pending >= usualPending * 1.5) {
return { pose: "alert", says: `The queue's long: ${n(r.pending)} waiting, ${(r.pending / usualPending).toFixed(1)}× the usual.` };
return { pose: "alert", says: `The queue's ${(r.pending / usualPending).toFixed(1)}× its usual length.` };
}
if (r.macAttention >= 10 && r.macTotal > 0 && r.macAttention / r.macTotal >= 0.05) {
return { pose: "inquisitive", says: `${n(r.macAttention)} Macs need a look.` };
}
if (enoughHistory && usualRunning > 0 && r.running >= usualRunning * 1.1) {
return { pose: "juggling", says: `Juggling ${n(r.running)} tasks, busier than usual.` };
return { pose: "juggling", says: `Busier than usual: ${pct(r.running, usualRunning)}% above the last two days.` };
}
if (enoughHistory && usualRunning > 0 && r.running <= usualRunning * 0.6) {
return { pose: "meditating", says: `Quiet hours. ${n(r.running)} tasks running.` };
return { pose: "meditating", says: `Quiet hours: ${pct(usualRunning, r.running, usualRunning)}% below the usual.` };
}
return { pose: r.idlePose, says: `Keeping watch: ${n(r.running)} running, ${n(r.pending)} waiting.` };
return { pose: r.idlePose, says: enoughHistory ? "All steady. Nothing unusual in the fleet." : "Keeping watch while the history fills in." };
}
91 changes: 4 additions & 87 deletions frontend/src/pages/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const yardstickAll = `${YARDSTICK_BASE}&var-provisioner=$__all&var-workerType=$_
// health ring / sparkline accents and leaves emerald free for "healthy"/production.
// Android gets its own iconic robot-green — it sits outside the gradient (its devices are
// Taskcluster-only, not in the worker DB) and the label disambiguates it from health greens.
const PLATFORM_COLORS = { macOS: "#FF9400", Linux: "#FF1AD9", Windows: "#9059FF", Android: "#3DDC84" } as const;

function timeAgo(iso: string | null, now = Date.now()) {
if (!iso) return "never";
Expand Down Expand Up @@ -93,27 +92,6 @@ function Sparkline({ points, className = "w-full h-16" }: { points: number[]; cl
}

/** Fleet-health ring with a gradient arc; children render in the center. */
function HealthRing({ pct, children }: { pct: number; children: React.ReactNode }) {
const r = 54, c = 2 * Math.PI * r, dash = (Math.max(0, Math.min(100, pct)) / 100) * c;
return (
<div className="relative w-32 h-32 flex-shrink-0">
<svg viewBox="0 0 128 128" className="w-full h-full -rotate-90">
<defs>
<linearGradient id="ringGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stopColor="#FF9400" />
<stop offset="50%" stopColor="#FF1AD9" />
<stop offset="100%" stopColor="#9059FF" />
</linearGradient>
</defs>
<circle cx="64" cy="64" r={r} fill="none" stroke="#1f2937" strokeWidth="8" />
<circle cx="64" cy="64" r={r} fill="none" stroke="url(#ringGrad)" strokeWidth="8" strokeLinecap="round"
strokeDasharray={`${dash} ${c}`} style={{ transition: "stroke-dasharray 0.6s ease" }} />
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">{children}</div>
</div>
);
}

// ── Monitored pools (user-pinned; follows the IAP-authenticated user across devices) ──
const MONITOR_KEY = "hangar.monitoredPools"; // legacy per-browser store, migrated once into the backend
const MONITOR_MAX = 8;
Expand Down Expand Up @@ -1105,27 +1083,19 @@ export function Overview() {
const [data, setData] = useState<FleetSummary | null>(null);
const [load, setLoad] = useState<LoadHistory | null>(null);
const [scale, setScale] = useState<ShowcaseData["scale"] | null>(null);
// Android device pools are Taskcluster-only (no worker DB rows), so pull their live
// device total separately to fold into the platform breakdown below.
const [androidTotal, setAndroidTotal] = useState<number | null>(null);
const [error, setError] = useState("");

const loadAndroid = () =>
api.fleet.androidPools().then(d => setAndroidTotal(d.pools.reduce((s, p) => s + p.total, 0)));

useEffect(() => {
api.fleet.summary().then(setData).catch(e => setError(e.message));
api.fleet.loadHistory(48).then(setLoad).catch(() => {});
api.fleet.showcase().then(d => setScale(d.scale)).catch(() => {});
loadAndroid().catch(() => {});
}, []);

// Keep the dashboard live: silently refresh everything while the tab is visible.
usePoll(() => {
api.fleet.summary().then(setData).catch(() => {});
api.fleet.loadHistory(48).then(setLoad).catch(() => {});
api.fleet.showcase().then(d => setScale(d.scale)).catch(() => {});
loadAndroid().catch(() => {});
}, 60_000);

if (error) return <div className="p-8 text-red-400 text-sm">{error}</div>;
Expand All @@ -1136,26 +1106,9 @@ export function Overview() {
</div>
);

const platforms = (() => {
let mac = 0, linux = 0, windows = 0;
Object.entries(data.by_pool).forEach(([name, count]) => {
if (name.includes("osx")) mac += count;
else if (name.includes("linux")) linux += count;
else if (name.includes("win") || name.includes("nuc")) windows += count;
});
return [
{ name: "macOS" as const, value: mac, color: PLATFORM_COLORS.macOS },
{ name: "Linux" as const, value: linux, color: PLATFORM_COLORS.Linux },
{ name: "Windows" as const, value: windows, color: PLATFORM_COLORS.Windows },
{ name: "Android" as const, value: androidTotal ?? 0, color: PLATFORM_COLORS.Android },
].filter(d => d.value > 0);
})();
const platformTotal = platforms.reduce((s, p) => s + p.value, 0) || 1;

// Health ring is scoped to macOS hardware — the fleet RelOps actually owns — to cut noise.
// macOS hardware is the fleet RelOps actually owns; Kit reads it.
const attention = data.attention_mac.quarantined + data.attention_mac.missing_from_tc;
const macTotal = platforms.find(p => p.name === "macOS")?.value ?? 0;
const healthPct = macTotal > 0 ? Math.round((1 - attention / macTotal) * 100) : 100;
const macTotal = Object.entries(data.by_pool).reduce((sum, [name, count]) => sum + (name.includes("osx") ? count : 0), 0);

// Greeting (client-local time)
const now = new Date();
Expand Down Expand Up @@ -1272,7 +1225,8 @@ export function Overview() {
</div>
</div>

{/* The fleet right now — every machine, lit by whose work it's running */}
{/* The fleet right now — every machine, lit by whose work it's running, with
fleet size (this card is the one source for fleet counts) */}
<FleetRightNow />

{/* Monitored pools — user-pinned */}
Expand All @@ -1282,43 +1236,6 @@ export function Overview() {
{pinned.try && <TryPushCard pushes={work.pushes ?? []} thUrl={work.thUrl} onUnpin={() => togglePin("try")} />}
{pinned.needinfo && <NeedinfoCard bugs={work.bugs ?? []} listUrl={work.listUrl} onUnpin={() => togglePin("needinfo")} />}

{/* Total workers breakdown */}
<div className="card p-6">
<div className="flex flex-col md:flex-row items-center gap-8">
<HealthRing pct={healthPct}>
<span className="text-2xl font-bold text-white tabular-nums leading-none">{healthPct}%</span>
<span className="text-[10px] uppercase tracking-wider text-gray-500 mt-1">healthy</span>
</HealthRing>

<div className="flex-1 w-full space-y-5">
<div className="flex flex-wrap items-end gap-x-10 gap-y-4">
<div>
<div className="text-4xl font-bold text-white tabular-nums leading-none"><AnimatedNumber value={data.total_workers + (androidTotal ?? 0)} /></div>
<div className="text-[11px] text-gray-500 uppercase tracking-wider mt-1.5">Total workers</div>
</div>
</div>

<div className="pt-4 border-t border-gray-800/60">
<div className="flex w-full h-2.5 rounded-full overflow-hidden gap-px">
{platforms.map(p => (
<div key={p.name} style={{ width: `${(p.value / platformTotal) * 100}%`, backgroundColor: p.color }} title={`${p.name}: ${p.value}`} />
))}
</div>
<div className="flex flex-wrap gap-x-6 gap-y-1.5 mt-3">
{platforms.map(p => (
<div key={p.name} className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: p.color }} />
<span className="text-xs text-gray-300">{p.name}</span>
<span className="text-xs font-mono text-gray-400 tabular-nums">{p.value.toLocaleString()}</span>
<span className="text-[10px] text-gray-600 tabular-nums">{Math.round((p.value / platformTotal) * 100)}%</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>

</div>
);
}
Loading