diff --git a/web-ui/src/app/operations/page.tsx b/web-ui/src/app/operations/page.tsx index 8a69e17..f3e3725 100644 --- a/web-ui/src/app/operations/page.tsx +++ b/web-ui/src/app/operations/page.tsx @@ -8,6 +8,8 @@ import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { ScrollArea } from "@/components/ui/scroll-area"; import { @@ -16,11 +18,13 @@ import { DialogHeader, DialogTitle, DialogDescription, + DialogFooter, } from "@/components/ui/dialog"; import { useOperations, useServiceAction, useDrainToggle, + useDrainBurst, useAgentRuns, useRunLog, useCancelRun, @@ -116,6 +120,138 @@ const SERVICE_LABELS: Record = { "legal-chat-service": "שירות צ׳אט אימון (גשר ל-claude CLI)", }; +// ── BURST control (halacha drain) — manual "run continuously now until X" ── +function pad2(n: number): string { + return String(n).padStart(2, "0"); +} +/** Format a Date as a datetime-local input value ("YYYY-MM-DDTHH:MM"), local tz. */ +function toLocalInput(d: Date): string { + return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}`; +} +/** The upcoming Saturday at 18:00, local time. */ +function nextSaturday18(): Date { + const d = new Date(); + d.setHours(18, 0, 0, 0); + d.setDate(d.getDate() + ((6 - d.getDay() + 7) % 7)); // Sat = getDay() 6 + if (d.getTime() <= Date.now()) d.setDate(d.getDate() + 7); + return d; +} +const HE_DAYS = ["א׳", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳"]; +function fmtDeadline(iso: string): string { + const d = new Date(iso); + return `${HE_DAYS[d.getDay()]} ${pad2(d.getDate())}/${pad2(d.getMonth() + 1)} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`; +} + +function BurstControl({ s }: { s: OpsService }) { + const burst = useDrainBurst(); + const [open, setOpen] = useState(false); + const [until, setUntil] = useState(() => toLocalInput(nextSaturday18())); + // The backend (supervisor) is the authority on expiry — it NULLs burst_until at + // the deadline (snapshot refetches every 5s), so presence ⇒ active. Avoids an + // impure Date.now() during render. + const active = !!s.burst_until; + + if (active) { + return ( + + + ⚡ BURST · עד {fmtDeadline(s.burst_until!)} + + + + ); + } + + return ( + <> + + + + + ⚡ הפעלת BURST — חילוץ הלכות רצוף + + הדריינר ירוץ ברצף מעכשיו (מתעלם מחלון-הלילה) ויעבד את תור-ההלכות עד המועד שתבחר, + תוך ניצול מכסת-Claude הפנויה. נעצר אוטומטית במועד — או ידנית בכפתור "עצור BURST". + + +
+ + setUntil(e.target.value)} + /> +
+ + + +
+

+ ℹ️ הפעלה ידנית בלבד — המערכת לא תפעיל BURST לבד. נכנס לתוקף תוך ≤15 דק׳ (מחזור המתזמר). +

+
+ + + + +
+
+ + ); +} +function busyBurst(pending: boolean, disabled?: boolean): boolean { + return pending || !!disabled; +} + // ── Process-management panel (the "Windows services" view) ───────────────── function ServiceControls({ s, @@ -143,6 +279,7 @@ function ServiceControls({ > הרץ עכשיו + {s.name === "legal-halacha-drain" && }