-
-
Notifications
You must be signed in to change notification settings - Fork 22
feat(observatory): surface server rejections from the steer controls #1421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { useState, useEffect, useCallback } from "react"; | ||
| import { Radar, Pause, Play, Loader2, CircleDot, Minus, Plus } from "lucide-react"; | ||
| import { Radar, Pause, Play, Loader2, CircleDot, Minus, Plus, AlertCircle } from "lucide-react"; | ||
| import { Switch } from "@/components/ui"; | ||
|
|
||
| interface HeldCard { | ||
|
|
@@ -94,6 +94,7 @@ export function ObservatoryApp({ windowId: _windowId }: { windowId: string }) { | |
| const [laneCaps, setLaneCaps] = useState<Record<string, number>>({}); | ||
| const [loading, setLoading] = useState(true); | ||
| const [busy, setBusy] = useState<string | null>(null); | ||
| const [steerError, setSteerError] = useState<string | null>(null); | ||
|
|
||
| const load = useCallback(async (opts?: { silent?: boolean }) => { | ||
| if (!opts?.silent) setLoading(true); | ||
|
|
@@ -128,6 +129,27 @@ export function ObservatoryApp({ windowId: _windowId }: { windowId: string }) { | |
| return () => clearInterval(id); | ||
| }, [load]); | ||
|
|
||
| // Shared write path for every steer control. Posts the change, surfaces a | ||
| // visible error if the server rejects it (so an optimistic value is not left | ||
| // standing silently), and always reconciles against the server. | ||
| const postSteer = useCallback( | ||
| async (url: string, body: object, failMsg: string) => { | ||
| try { | ||
| const res = await fetch(url, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| setSteerError(res.ok ? null : failMsg); | ||
| } catch { | ||
| setSteerError(failMsg); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SUGGESTION: The For a |
||
| } finally { | ||
| await load({ silent: true }); | ||
| } | ||
| }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| [load], | ||
| ); | ||
|
|
||
| const setScope = useCallback( | ||
| async (scope: string, paused: boolean) => { | ||
| setBusy(scope); | ||
|
|
@@ -140,40 +162,28 @@ export function ObservatoryApp({ windowId: _windowId }: { windowId: string }) { | |
| lanes: { ...prev.lanes, [scope]: paused }, | ||
| }, | ||
| ); | ||
| try { | ||
| await fetch("/api/observatory/pause", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ scope, paused }), | ||
| }); | ||
| await load({ silent: true }); | ||
| } catch { | ||
| await load({ silent: true }); | ||
| } finally { | ||
| setBusy(null); | ||
| } | ||
| await postSteer( | ||
| "/api/observatory/pause", | ||
| { scope, paused }, | ||
| "Could not update the pause state.", | ||
| ); | ||
| setBusy(null); | ||
| }, | ||
| [load], | ||
| [postSteer], | ||
| ); | ||
|
|
||
| const setGlobalCap = useCallback( | ||
| async (next: number | null) => { | ||
| setBusy("cap"); | ||
| setCap(next); // optimistic; reconciled on the next poll | ||
| try { | ||
| await fetch("/api/observatory/throttle", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ scope: "global", max_concurrent: next }), | ||
| }); | ||
| await load({ silent: true }); | ||
| } catch { | ||
| await load({ silent: true }); | ||
| } finally { | ||
| setBusy(null); | ||
| } | ||
| await postSteer( | ||
| "/api/observatory/throttle", | ||
| { scope: "global", max_concurrent: next }, | ||
| "Could not update the concurrency cap.", | ||
| ); | ||
| setBusy(null); | ||
| }, | ||
| [load], | ||
| [postSteer], | ||
| ); | ||
|
|
||
| const setLaneCap = useCallback( | ||
|
|
@@ -185,20 +195,14 @@ export function ObservatoryApp({ windowId: _windowId }: { windowId: string }) { | |
| else copy[handle] = next; | ||
| return copy; | ||
| }); | ||
| try { | ||
| await fetch("/api/observatory/throttle", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ scope: handle, max_concurrent: next }), | ||
| }); | ||
| await load({ silent: true }); | ||
| } catch { | ||
| await load({ silent: true }); | ||
| } finally { | ||
| setBusy(null); | ||
| } | ||
| await postSteer( | ||
| "/api/observatory/throttle", | ||
| { scope: handle, max_concurrent: next }, | ||
| `Could not update the cap for ${handle}.`, | ||
| ); | ||
| setBusy(null); | ||
| }, | ||
| [load], | ||
| [postSteer], | ||
| ); | ||
|
|
||
| return ( | ||
|
|
@@ -234,6 +238,23 @@ export function ObservatoryApp({ windowId: _windowId }: { windowId: string }) { | |
| </div> | ||
| )} | ||
|
|
||
| {steerError && ( | ||
| <div | ||
| className="flex items-center gap-2 border-b border-red-500/20 bg-red-500/10 px-5 py-2 text-sm text-red-400" | ||
| role="alert" | ||
| > | ||
| <AlertCircle size={14} className="shrink-0" /> | ||
| {steerError} | ||
| <button | ||
| type="button" | ||
| onClick={() => setSteerError(null)} | ||
| className="ml-auto text-xs text-red-400/80 transition-colors hover:text-red-400" | ||
| > | ||
| Dismiss | ||
| </button> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* Steer: global concurrency cap (volume knob alongside the pause switch) */} | ||
| <div className="flex items-center gap-3 border-b border-shell-border px-5 py-2.5"> | ||
| <span className="text-xs font-medium uppercase tracking-wide text-shell-text-tertiary"> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: The server's rejection body is discarded, so the user sees only the generic
failMsgeven when the API returned a specific reason.The PR description explicitly motivates this change on the cases
"forbidden","unknown lane", and"rejected value"— the whole point is that the server can tell the user why the write was refused, and currently that detail is thrown away bypostSteer(nores.json()call, nobody.detailextraction). The new test on line 239 even mocksbody: { detail: "forbidden" }, which would be a useful string to surface — but the production code never reads it.Consider parsing the response body on
!res.okand preferringbody.detail(orbody.message) over the hard-codedfailMsg, with the generic message kept as a fallback when the body is missing or unparseable. This is the highest-value gap in this change.