- One settings-scoped checkbox rule (accent, size, focus ring) so the Advanced-settings toggle, SettingsToggleRow, ntfy/webhook card headers, and MCP toggle stop falling back to the browser-default accent. - Fix the empty/off-screen help bubble on mobile: .notification-provider-header and .settings-field-label-row are now positioned ancestors for SettingsHelpTip. - Migrate every remaining inline <small> description across settings sections to the shared SettingsHelpTip "?" affordance (validation errors and live status stay inline). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
82 lines
4.9 KiB
TypeScript
82 lines
4.9 KiB
TypeScript
import type { NodeInfo } from "../../../api";
|
|
import { NodeHealthDot } from "../../NodeHealthDot";
|
|
import { SettingsSelectRow } from "../SettingsSelectRow";
|
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
|
import type { SettingsFormState, SetSettingsForm } from "./context";
|
|
import { useTranslation } from "react-i18next";
|
|
function getNodeStatusLabel(status: "online" | "offline" | "connecting" | "error", t: ReturnType<typeof useTranslation<"app">>["t"]): string {
|
|
if (status === "online")
|
|
return t("settings.nodeRouting.statusOnline", "Online");
|
|
if (status === "connecting")
|
|
return t("settings.nodeRouting.statusConnecting", "Connecting");
|
|
if (status === "error")
|
|
return t("settings.nodeRouting.statusError", "Error");
|
|
return t("settings.nodeRouting.statusOffline", "Offline");
|
|
}
|
|
export interface NodeRoutingSectionProps {
|
|
form: SettingsFormState;
|
|
setForm: SetSettingsForm;
|
|
nodes: NodeInfo[];
|
|
}
|
|
/*
|
|
FNXC:SettingsScope 2026-07-15-17:35:
|
|
Both routing settings are project-scoped (DEFAULT_PROJECT_SETTINGS), which is what the note below already tells the operator in prose; the per-row scope badge is the machine-readable form of that same claim.
|
|
*/
|
|
export function NodeRoutingSection({ form, setForm, nodes }: NodeRoutingSectionProps) {
|
|
const { t } = useTranslation("app");
|
|
return (<>
|
|
<h4 className="settings-section-heading">{t("settings.nodeRouting.nodeRouting", "Node Routing")}</h4>
|
|
<p className="settings-section-description">{t("settings.nodeRouting.configureHowTasksAreRoutedToExecutionNodes", "Configure how tasks are routed to execution nodes.")}</p>
|
|
<p className="settings-node-routing-note">{t("settings.nodeRouting.theseSettingsApplyAtTheProjectLevel", "These settings apply at the project level.")}</p>
|
|
{/*
|
|
FNXC:SettingsStyling 2026-07-15-17:35:
|
|
This row stays hand-rolled: routing safety requires the live NodeHealthDot for the selected node to render right under the control, and the shared select row renders only label/control/help with no slot for an adjacent status widget. Forcing it onto the primitive would move or drop the health readout, so the dot wins over row uniformity here.
|
|
|
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
|
Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. The tip is a SIBLING of the label (a button inside a label is invalid), so both sit in a `.settings-field-label-row`. The live NodeHealthDot status stays inline: it is state feedback, not help copy.
|
|
*/}
|
|
<div className="form-group">
|
|
<div className="settings-field-label-row">
|
|
<label htmlFor="defaultNodeId">{t("settings.nodeRouting.defaultExecutionNode", "Default Execution Node")}</label>
|
|
<SettingsHelpTip settingKey="defaultNodeId">{t("settings.nodeRouting.usedWhenATaskHasNoNodeOverride", "Used when a task has no node override. Node status is shown for safer routing selection. No default — unset (local execution).")}</SettingsHelpTip>
|
|
</div>
|
|
<select id="defaultNodeId" className="select" value={typeof form.defaultNodeId === "string" ? form.defaultNodeId : ""} onChange={(e) => {
|
|
const val = e.target.value;
|
|
setForm((f) => ({ ...f, defaultNodeId: val || undefined } as SettingsFormState));
|
|
}}>
|
|
<option value="">{t("settings.nodeRouting.localExecutionNoDefaultNode", "Local execution (no default node)")}</option>
|
|
{nodes.map((node) => (<option key={node.id} value={node.id}>
|
|
{node.name} ({getNodeStatusLabel(node.status, t)})
|
|
</option>))}
|
|
</select>
|
|
{(() => {
|
|
const selectedNode = nodes.find((node) => node.id === form.defaultNodeId);
|
|
if (!selectedNode)
|
|
return null;
|
|
return (<div className="settings-node-status">
|
|
<span>{t("settings.nodeRouting.selectedNode", "Selected node:")}</span>
|
|
<NodeHealthDot status={selectedNode.status} showLabel/>
|
|
</div>);
|
|
})()}
|
|
</div>
|
|
<SettingsSelectRow
|
|
descriptor={{
|
|
key: "unavailableNodePolicy",
|
|
label: t("settings.nodeRouting.unavailableNodePolicy", "Unavailable Node Policy"),
|
|
help: t("settings.nodeRouting.unavailableNodePolicyHint", "Default: block execution."),
|
|
scope: "project",
|
|
options: [
|
|
{ value: "block", label: t("settings.nodeRouting.blockExecution", "Block execution") },
|
|
{ value: "fallback-local", label: t("settings.nodeRouting.fallBackToLocal", "Fall back to local") },
|
|
],
|
|
}}
|
|
value={form.unavailableNodePolicy === "fallback-local" ? "fallback-local" : "block"}
|
|
onChange={(v) => setForm((f) => ({
|
|
...f,
|
|
unavailableNodePolicy: v as "block" | "fallback-local",
|
|
} as SettingsFormState))}
|
|
/>
|
|
</>);
|
|
}
|
|
export default NodeRoutingSection;
|