mirror of
https://github.com/feder-cr/Jobs_Applier_AI_Agent.git
synced 2026-11-07 10:36:05 -10:00
web: the health answer is a context of its own
The shell's context held the health with everything else, so its poll every 30 seconds gave the whole value a new identity and re-rendered every user of the shell. useHealth reads it from its own context; only the rail's status, the offline banner, the setup checklist, the key form and the host settings follow the poll.
This commit is contained in:
1 parent
bd1b5866cc
commit
3d0da4e090
8 files changed
+32
-21
No files matched your search
@@ -6,7 +6,7 @@ import type { ThemePreference } from "../../lib/theme";
|
||||
import { useThemePreference } from "../../lib/use-theme";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { ErrorAlert } from "../ErrorAlert";
|
||||
import { useShell } from "../shell/attention";
|
||||
import { useHealth } from "../shell/attention";
|
||||
import { CheckAgainButton, CheckList, CheckListSkeleton } from "../setup/CheckList";
|
||||
import { KeyForm } from "../setup/KeyForm";
|
||||
import { Section } from "../setup/Section";
|
||||
@@ -84,7 +84,7 @@ function Appearance() {
|
||||
}
|
||||
|
||||
function About() {
|
||||
const { health } = useShell();
|
||||
const health = useHealth();
|
||||
return (
|
||||
<Section id="about" title="About" busy={health.loading && health.data === undefined}>
|
||||
{health.error !== null && health.data === undefined ? <ErrorAlert error={health.error} title="The control plane does not answer" /> : null}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { api } from "../../lib/api";
|
||||
import { keySavedMessage } from "../../lib/host-settings";
|
||||
import { ErrorAlert } from "../ErrorAlert";
|
||||
import { Field } from "../new-dot/Field";
|
||||
import { useShell } from "../shell/attention";
|
||||
import { useHealth } from "../shell/attention";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { useAction } from "../ui";
|
||||
@@ -18,7 +18,7 @@ import { useAction } from "../ui";
|
||||
* answer, and the answer to saving one says how many running Dots were given it at once.
|
||||
*/
|
||||
export function KeyForm() {
|
||||
const { health } = useShell();
|
||||
const health = useHealth();
|
||||
const [value, setValue] = useState("");
|
||||
const [problem, setProblem] = useState<string | null>(null);
|
||||
const [saved, setSaved] = useState<string | null>(null);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { isReady, notReadyCount } from "../../lib/preflight";
|
||||
import { useShell } from "../shell/attention";
|
||||
import { useHealth } from "../shell/attention";
|
||||
import { CheckAgainButton, CheckList } from "./CheckList";
|
||||
import { KeyForm } from "./KeyForm";
|
||||
import { Section } from "./Section";
|
||||
@@ -16,7 +16,7 @@ import { useHostChecks } from "./use-host-checks";
|
||||
*/
|
||||
export function SetupChecklist() {
|
||||
const checks = useHostChecks();
|
||||
const { health } = useShell();
|
||||
const health = useHealth();
|
||||
const [needed, setNeeded] = useState<{ key: boolean } | null>(null);
|
||||
const items = checks.items;
|
||||
const ready = items !== null && isReady(items);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef } from "react";
|
||||
import { api } from "../../lib/api";
|
||||
import { isReady, preflightItems, type HealthResult, type HostResult, type PreflightItem } from "../../lib/preflight";
|
||||
import { useResource } from "../ui";
|
||||
import { useShell } from "../shell/attention";
|
||||
import { useHealth } from "../shell/attention";
|
||||
|
||||
export interface HostChecks {
|
||||
/** Null until both answers are in (or have failed). */
|
||||
@@ -27,7 +27,7 @@ function reason(error: unknown): string {
|
||||
* still not ready, the checks run again by themselves.
|
||||
*/
|
||||
export function useHostChecks(): HostChecks {
|
||||
const { health } = useShell();
|
||||
const health = useHealth();
|
||||
const doctor = useResource(() => api.doctor(), "host:doctor");
|
||||
|
||||
const healthResult: HealthResult | null = health.error !== null ? { error: reason(health.error) } : health.data ? { health: health.data } : null;
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
import { ServerIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { useShell } from "./attention";
|
||||
import { useHealth } from "./attention";
|
||||
import { StatusIcon } from "./StatusIcon";
|
||||
|
||||
type ApiState = "checking" | "ok" | "down";
|
||||
|
||||
function useApiState(): ApiState {
|
||||
const { health } = useShell();
|
||||
const health = useHealth();
|
||||
return health.error !== null ? "down" : health.data ? "ok" : "checking";
|
||||
}
|
||||
|
||||
/** Whether the control plane answers through the proxy, with its version (the shell asks every 30 seconds): an icon, said in full on hover. */
|
||||
export function ApiStatus() {
|
||||
const { health } = useShell();
|
||||
const health = useHealth();
|
||||
const state = useApiState();
|
||||
const label = state === "checking" ? "API: checking" : state === "ok" ? "API: ok" : "API: unreachable";
|
||||
const version = state === "ok" && health.data ? `v${health.data.version}` : null;
|
||||
@@ -30,7 +30,7 @@ export function ApiStatus() {
|
||||
|
||||
/** While the control plane has no OpenRouter key: a link to where it is entered, on a line of its own under the statuses. */
|
||||
export function MissingKeyNotice({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const { health } = useShell();
|
||||
const health = useHealth();
|
||||
const state = useApiState();
|
||||
if (state !== "ok" || !health.data || health.data.openrouter_configured) return null;
|
||||
return (
|
||||
|
||||
@@ -26,8 +26,6 @@ const HEALTH_INTERVAL_MS = 30_000;
|
||||
interface ShellData {
|
||||
dots: Resource<Dot[]>;
|
||||
approvals: Resource<Approval[]>;
|
||||
/** The control plane's answer, asked every 30 seconds and again on `health.reload()` (after a key is saved). */
|
||||
health: Resource<HealthResponse>;
|
||||
attention: ReadonlyMap<string, DotAttention>;
|
||||
/** What needs the person, across every Dot: waiting approvals, Dots in ERROR, failed tasks and channels to link again. The number on the rail's Inbox entry. */
|
||||
needsYou: number;
|
||||
@@ -42,6 +40,7 @@ interface ShellData {
|
||||
}
|
||||
|
||||
const Context = createContext<ShellData | null>(null);
|
||||
const HealthContext = createContext<Resource<HealthResponse> | null>(null);
|
||||
|
||||
/**
|
||||
* What every page of the app shares: the Dots, the approvals that wait, the tasks that failed lately, the channels that
|
||||
@@ -118,10 +117,22 @@ export function AttentionProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const dismiss = useCallback((dotId: string) => setLive((current) => dismissRestart(current, dotId)), []);
|
||||
const value = useMemo<ShellData>(
|
||||
() => ({ dots, approvals, health, attention, needsYou, failedTasks, relinks, dismissFailedTask, live, dismissRestart: dismiss }),
|
||||
[dots, approvals, health, attention, needsYou, failedTasks, relinks, dismissFailedTask, live, dismiss],
|
||||
() => ({ dots, approvals, attention, needsYou, failedTasks, relinks, dismissFailedTask, live, dismissRestart: dismiss }),
|
||||
[dots, approvals, attention, needsYou, failedTasks, relinks, dismissFailedTask, live, dismiss],
|
||||
);
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
// The health is a context of its own: its poll every 30 seconds re-renders what reads it, not every user of the shell.
|
||||
return (
|
||||
<HealthContext.Provider value={health}>
|
||||
<Context.Provider value={value}>{children}</Context.Provider>
|
||||
</HealthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/** The control plane's answer, asked every 30 seconds and again on `reload()` (after a key is saved). */
|
||||
export function useHealth(): Resource<HealthResponse> {
|
||||
const value = useContext(HealthContext);
|
||||
if (!value) throw new Error("useHealth must be used inside AttentionProvider");
|
||||
return value;
|
||||
}
|
||||
|
||||
export function useShell(): ShellData {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { WifiOffIcon } from "lucide-react";
|
||||
import { offlineOf } from "../../lib/offline";
|
||||
import { useStreamStatus } from "../events";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
import { useShell } from "./attention";
|
||||
import { useHealth } from "./attention";
|
||||
|
||||
/**
|
||||
* Says so, at the top of every page, when the control plane does not answer: the live stream is down or the health
|
||||
@@ -12,7 +12,7 @@ import { useShell } from "./attention";
|
||||
*/
|
||||
export function OfflineBanner() {
|
||||
const stream = useStreamStatus();
|
||||
const { health } = useShell();
|
||||
const health = useHealth();
|
||||
const offline = offlineOf({ stream: stream.status, streamDetail: stream.detail, apiError: health.error });
|
||||
if (offline === null) return null;
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { EventStreamProvider } from "../src/components/events";
|
||||
import { HostSettings } from "../src/components/settings/HostSettings";
|
||||
import { AttentionProvider, useShell } from "../src/components/shell/attention";
|
||||
import { AttentionProvider, useHealth } from "../src/components/shell/attention";
|
||||
import { THEME_KEY } from "../src/lib/theme";
|
||||
import { stubMatchMedia } from "./support/browser";
|
||||
import { FakeControlPlane } from "./support/control-plane";
|
||||
@@ -29,7 +29,7 @@ afterEach(() => {
|
||||
|
||||
/** What the shell's rail reads from the same health answer, to see that the page and the rail agree. */
|
||||
function RailKey() {
|
||||
const { health } = useShell();
|
||||
const health = useHealth();
|
||||
return <p data-testid="rail-key">{health.data ? String(health.data.openrouter_configured) : "unknown"}</p>;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user