Live worker counts and an e621 request-history dropdown

- Status polling drops from 30s to 5s, and download start/finish/cancel
  invalidates it immediately, so the footer's Active Workers reflects
  running download tasks in near real time
- The e621 time in the status pill is now a button: it opens a dropdown
  with the session's request history (clock time, endpoint, duration,
  colour-coded) plus totals; closes on outside click or Escape
- Metrics store keeps the last 50 requests
This commit is contained in:
2026-09-17 12:20:27 -05:00
parent 6962e483fc
commit 75b7ed35eb
5 changed files with 123 additions and 22 deletions
+2 -2
View File
@@ -28,8 +28,8 @@ export function AppShell() {
const statusQuery = useQuery({
queryKey: ["status"],
queryFn: () => api<SystemStatus>("/api/status/"),
refetchInterval: 30_000,
staleTime: 15_000,
refetchInterval: 5_000,
staleTime: 4_000,
});
const status = statusQuery.data;
+81 -6
View File
@@ -1,4 +1,5 @@
import { Laptop, Monitor, Terminal, type LucideIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/cn";
import type { SystemStatus } from "@/lib/types";
@@ -16,17 +17,45 @@ function formatMs(value: number): string {
return `${value.toFixed(1)}ms`;
}
function formatClock(at: number): string {
return new Date(at).toLocaleTimeString();
}
export function StatusPill({ status }: { status?: SystemStatus }) {
const requestCount = useE621Metrics((state) => state.requestCount);
const totalMs = useE621Metrics((state) => state.totalMs);
const last = useE621Metrics((state) => state.last);
const history = useE621Metrics((state) => state.history);
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (!open) return;
function handlePointerDown(event: MouseEvent) {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setOpen(false);
}
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") setOpen(false);
}
document.addEventListener("mousedown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("mousedown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [open]);
if (!status) return null;
const OsIcon = osIcons[status.os.icon] ?? Monitor;
return (
<span
className="hidden select-none items-center gap-1.5 rounded-full border border-ctp-surface0 bg-ctp-mantle px-2.5 py-1 font-mono text-[11px] text-ctp-overlay1 lg:flex"
ref={containerRef}
className="relative hidden select-none items-center gap-1.5 rounded-full border border-ctp-surface0 bg-ctp-mantle px-2.5 py-1 font-mono text-[11px] text-ctp-overlay1 lg:flex"
title={`${status.os.name} ${status.os.release} · built from ${status.git_hash}`}
>
<span
@@ -47,14 +76,19 @@ export function StatusPill({ status }: { status?: SystemStatus }) {
{requestCount > 0 ? (
<>
<span className="text-ctp-surface2">·</span>
<span
className="text-ctp-teal"
<button
type="button"
onClick={() => setOpen((value) => !value)}
title={`${requestCount} e621 request${
requestCount === 1 ? "" : "s"
} this session · last ${last ? `${formatMs(last.ms)} (${last.path})` : "—"}`}
} this session — click for details`}
className={cn(
"text-ctp-teal transition hover:underline",
open && "underline",
)}
>
e621: {formatMs(totalMs)}
</span>
</button>
</>
) : null}
{status.e621_time_ms !== null ? (
@@ -65,6 +99,47 @@ export function StatusPill({ status }: { status?: SystemStatus }) {
</span>
</>
) : null}
{open && requestCount > 0 ? (
<span className="absolute right-0 top-full z-50 mt-2 block w-80 cursor-default overflow-hidden rounded-lg border border-ctp-surface1 bg-ctp-surface0 text-left shadow-[0_4px_12px_rgba(17,17,27,0.6)]">
<span className="flex items-center justify-between gap-2 border-b border-ctp-surface1 px-3 py-2 text-[11px] text-ctp-overlay0">
<span className="font-sans">e621 requests this session</span>
<span className="font-mono">
{requestCount} · {formatMs(totalMs)}
</span>
</span>
<span className="block max-h-72 overflow-y-auto py-1">
{history.map((sample, index) => (
<span
key={`${sample.at}-${index}`}
className="flex items-center gap-2 px-3 py-1 font-mono text-[11px]"
>
<span className="shrink-0 text-ctp-overlay0">
{formatClock(sample.at)}
</span>
<span
className="min-w-0 flex-1 truncate text-ctp-subtext0"
title={sample.path}
>
{sample.path}
</span>
<span
className={cn(
"shrink-0",
sample.ms >= 2000
? "text-ctp-peach"
: sample.ms >= 800
? "text-ctp-yellow"
: "text-ctp-teal",
)}
>
{formatMs(sample.ms)}
</span>
</span>
))}
</span>
</span>
) : null}
</span>
);
}
+18 -4
View File
@@ -129,7 +129,10 @@ export function OnlineDetail({ postId }: { postId: number }) {
post: target,
},
}),
onSuccess: (task) => setDownloadTaskId(task.task_id),
onSuccess: (task) => {
setDownloadTaskId(task.task_id);
void queryClient.invalidateQueries({ queryKey: ["status"] });
},
});
const taskQuery = useQuery({
@@ -143,20 +146,31 @@ export function OnlineDetail({ postId }: { postId: number }) {
},
});
const downloadTask = taskQuery.data;
const downloadStatus = downloadTask?.status;
const downloadActive =
downloadTask?.status === "pending" || downloadTask?.status === "downloading";
downloadStatus === "pending" || downloadStatus === "downloading";
useEffect(() => {
if (downloadTask?.status === "complete") {
if (!downloadStatus) return;
if (
downloadStatus === "complete" ||
downloadStatus === "error" ||
downloadStatus === "cancelled"
) {
void queryClient.invalidateQueries({ queryKey: ["status"] });
}
if (downloadStatus === "complete") {
void queryClient.invalidateQueries({ queryKey: ["library-lookup"] });
void queryClient.invalidateQueries({ queryKey: ["files"] });
}
}, [downloadTask?.status, queryClient]);
}, [downloadStatus, queryClient]);
function cancelDownload() {
if (!downloadTaskId) return;
void api(`/api/online/downloads/${downloadTaskId}/cancel/`, {
method: "POST",
}).then(() => {
void queryClient.invalidateQueries({ queryKey: ["status"] });
});
}
+15 -6
View File
@@ -6,10 +6,13 @@ export interface E621RequestSample {
at: number;
}
const HISTORY_LIMIT = 50;
interface E621MetricsState {
requestCount: number;
totalMs: number;
last: E621RequestSample | null;
history: E621RequestSample[];
record: (path: string, ms: number) => void;
reset: () => void;
}
@@ -19,13 +22,19 @@ export const useE621Metrics = create<E621MetricsState>((set) => ({
requestCount: 0,
totalMs: 0,
last: null,
history: [],
record: (path, ms) =>
set((state) => ({
requestCount: state.requestCount + 1,
totalMs: state.totalMs + ms,
last: { path, ms, at: Date.now() },
})),
set((state) => {
const sample: E621RequestSample = { path, ms, at: Date.now() };
return {
requestCount: state.requestCount + 1,
totalMs: state.totalMs + ms,
last: sample,
history: [sample, ...state.history].slice(0, HISTORY_LIMIT),
};
}),
reset: () => set({ requestCount: 0, totalMs: 0, last: null }),
reset: () =>
set({ requestCount: 0, totalMs: 0, last: null, history: [] }),
}));