frontend: Recompute all button on /scenarios + live queue depth
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Header gets a third button: "Recompute all". POSTs /recompute (202-accepted, async background worker drains the queue). Once work is in flight, the existing /healthz query auto-refetches every 3s and the button label switches to "Computing… (queue N)"; flips back to idle when the queue drains. Removes the need to drop to CLI (`python -m fire_planner recompute-all`) for routine refreshes. Bearer auth still applies in prod once API_BEARER_TOKEN is set — frontend will need a token header in that mode (TODO). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2fc92c12f5
commit
95b4b4ddd7
2 changed files with 47 additions and 1 deletions
|
|
@ -40,6 +40,11 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|||
|
||||
export const api = {
|
||||
health: () => request<{ status: string; queue_depth: number }>('/healthz'),
|
||||
recompute: (body?: Record<string, unknown>) =>
|
||||
request<{ status: string; depth: number }>('/recompute', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body ?? {}),
|
||||
}),
|
||||
networth: {
|
||||
current: () =>
|
||||
request<{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* The Cartesian set is whatever the latest /recompute produced (default
|
||||
* 120 scenarios). User scenarios survive recomputes.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
|
||||
|
|
@ -18,12 +18,35 @@ export function Scenarios() {
|
|||
const [filter, setFilter] = useState<Filter>('all');
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||
const navigate = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
|
||||
const scenarios = useQuery({
|
||||
queryKey: ['scenarios', filter],
|
||||
queryFn: () => api.scenarios.list(filter === 'all' ? undefined : filter),
|
||||
});
|
||||
|
||||
// Poll healthz once a recompute is in flight so the user sees the
|
||||
// queue empty in roughly real time.
|
||||
const health = useQuery({
|
||||
queryKey: ['health'],
|
||||
queryFn: api.health,
|
||||
refetchInterval: (q) => {
|
||||
const data = q.state.data as { queue_depth: number } | undefined;
|
||||
return data && data.queue_depth > 0 ? 3000 : false;
|
||||
},
|
||||
});
|
||||
|
||||
const recompute = useMutation({
|
||||
mutationFn: () => api.recompute(),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['health'] });
|
||||
qc.invalidateQueries({ queryKey: ['scenarios'] });
|
||||
},
|
||||
});
|
||||
|
||||
const queueDepth = health.data?.queue_depth ?? 0;
|
||||
const isComputing = queueDepth > 0;
|
||||
|
||||
const toggle = (id: number) =>
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
|
|
@ -57,6 +80,19 @@ export function Scenarios() {
|
|||
Compare {selected.size}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => recompute.mutate()}
|
||||
disabled={recompute.isPending || isComputing}
|
||||
title="Re-run the full Cartesian (default 120 scenarios) and persist."
|
||||
className="rounded-md border border-slate-300 bg-white text-sm font-medium px-4 py-2 hover:bg-slate-50 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isComputing
|
||||
? `Computing… (queue ${queueDepth})`
|
||||
: recompute.isPending
|
||||
? 'Queueing…'
|
||||
: 'Recompute all'}
|
||||
</button>
|
||||
<Link
|
||||
to="/scenarios/new"
|
||||
className="rounded-md bg-slate-900 text-white text-sm font-medium px-4 py-2 hover:bg-slate-800"
|
||||
|
|
@ -65,6 +101,11 @@ export function Scenarios() {
|
|||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
{recompute.isError && (
|
||||
<div className="rounded-md border border-red-200 bg-red-50 p-3 text-red-800 text-sm">
|
||||
{String((recompute.error as Error)?.message ?? recompute.error)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scenarios.isLoading ? (
|
||||
<p className="text-slate-500">Loading…</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue