Initial commit: Agentic OS troubleshooting platform
Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend + LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini -> DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials, RBAC, audit log, project-memory + Obsidian integrations, and editable troubleshooting decision rules tuned for the GeneseasX vessel stack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
134
frontend/app/rules/page.tsx
Normal file
134
frontend/app/rules/page.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import AppShell from "@/components/AppShell";
|
||||
import { PageHeader } from "@/components/ui";
|
||||
import { api, fetcher } from "@/lib/api";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
type RulesResponse = {
|
||||
yaml: string;
|
||||
parsed: { rules?: { id: string; name: string; priority?: number }[] };
|
||||
path_hint: string;
|
||||
};
|
||||
|
||||
export default function RulesPage() {
|
||||
const { user } = useAuth();
|
||||
const isAdmin = user?.role === "admin";
|
||||
const { data, mutate } = useSWR<RulesResponse>("/api/rules/troubleshooting", fetcher);
|
||||
const [yaml, setYaml] = useState<string | null>(null);
|
||||
const [previewTitle, setPreviewTitle] = useState("Asterisk health status");
|
||||
const [previewIssue, setPreviewIssue] = useState("Check voip pjsip registration on GeneseasX");
|
||||
const [preview, setPreview] = useState<{ matched: Record<string, unknown> | null } | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
|
||||
const content = yaml ?? data?.yaml ?? "";
|
||||
|
||||
const [msgKind, setMsgKind] = useState<"ok" | "err">("ok");
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
setMsg(null);
|
||||
try {
|
||||
await api("/api/rules/troubleshooting", { method: "PUT", body: JSON.stringify({ yaml: content }) });
|
||||
setMsgKind("ok");
|
||||
setMsg("Rules saved — next task run will use them.");
|
||||
setYaml(null);
|
||||
mutate();
|
||||
} catch (err: unknown) {
|
||||
setMsgKind("err");
|
||||
setMsg(err instanceof Error ? err.message : "Save failed");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runPreview = async () => {
|
||||
setPreview(null);
|
||||
try {
|
||||
const res = await api<{ matched: Record<string, unknown> | null }>("/api/rules/troubleshooting/preview", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ title: previewTitle, issue: previewIssue }),
|
||||
});
|
||||
setPreview(res);
|
||||
} catch (err: unknown) {
|
||||
setMsg(err instanceof Error ? err.message : "Preview failed");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<PageHeader
|
||||
title="Troubleshooting rules"
|
||||
subtitle="Standardize which devices to check and investigation order — YAML editable by admins"
|
||||
/>
|
||||
|
||||
<div className="mb-6 grid gap-4 lg:grid-cols-2">
|
||||
<div className="card">
|
||||
<h3 className="mb-2 text-sm font-semibold">Rule preview</h3>
|
||||
<p className="mb-3 text-xs text-muted">Test which rule matches a task title + issue.</p>
|
||||
<input
|
||||
className="input mb-2 w-full"
|
||||
value={previewTitle}
|
||||
onChange={(e) => setPreviewTitle(e.target.value)}
|
||||
placeholder="Task title"
|
||||
/>
|
||||
<textarea
|
||||
className="input mb-2 min-h-[80px] w-full"
|
||||
value={previewIssue}
|
||||
onChange={(e) => setPreviewIssue(e.target.value)}
|
||||
placeholder="Issue description"
|
||||
/>
|
||||
<button type="button" className="btn-secondary" onClick={runPreview}>
|
||||
Preview match
|
||||
</button>
|
||||
{preview?.matched && (
|
||||
<pre className="mt-3 overflow-x-auto rounded-lg bg-panel2 p-3 text-xs">
|
||||
{JSON.stringify(preview.matched, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
{preview && !preview.matched && (
|
||||
<p className="mt-3 text-sm text-muted">No specific rule — keyword fallback / all devices.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h3 className="mb-2 text-sm font-semibold">Defined rules</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{(data?.parsed?.rules || []).map((r) => (
|
||||
<li key={r.id} className="flex justify-between gap-2 border-b border-edge/50 py-2">
|
||||
<span>{r.name}</span>
|
||||
<span className="text-xs text-muted">priority {r.priority ?? 0}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-3 text-xs text-muted">File: {data?.path_hint}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">YAML editor</h3>
|
||||
{!isAdmin && <span className="text-xs text-muted">Read-only (admin can edit)</span>}
|
||||
</div>
|
||||
<textarea
|
||||
className="input min-h-[420px] w-full font-mono text-xs leading-relaxed"
|
||||
value={content}
|
||||
onChange={(e) => isAdmin && setYaml(e.target.value)}
|
||||
readOnly={!isAdmin}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{msg && (
|
||||
<p className={`mt-2 text-sm ${msgKind === "ok" ? "text-good" : "text-bad"}`}>{msg}</p>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<button type="button" className="btn-primary mt-4" disabled={saving} onClick={save}>
|
||||
{saving ? "Saving…" : "Save rules"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user