Initial commit: SIC harness (backend, web, pi-adapter, configs, docs)
- pnpm monorepo: apps/api (Fastify + SQLite + SSE), apps/web (React+Vite), packages/shared, packages/pi-adapter - Local auth (admin/webhook-runner roles) + Keycloak JWT ready - Multi-session chat with reliable history (user persisted before LLM, assistant persisted after stream) - Markdown knowledge base with /api/docs/search + /api/docs/:id - YAML webhook catalog with backend-only execution, retry/backoff, audit (webhook_runs), and per-user rate limit - Skills config (sre-on-call, blameless-postmortem, security-incident) injected into LLM system prompt - LLM provider failover chain (config/models.yml fallback + LLM_FALLBACK_CHAIN override) - Context-aware webhooks panel + backend id-mention safety net - Per-message stats (time/duration/tokens/model), Markdown+GFM render, code & table copy/download buttons - Vitest suite, end-to-end smoke test (scripts/smoke.mjs), per-session system prompt override - /metrics Prometheus endpoint + /api/metrics JSON, request-id correlation - dotenv with explicit repo-root path; envString/envNumber helpers (handles empty-string env) - Runbooks + SOPs under knowledge/ in English; README, docs, and INDEX.md in English
This commit is contained in:
231
apps/web/src/WebhookFormTab.tsx
Normal file
231
apps/web/src/WebhookFormTab.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import { authorizedHeaders, authTokenFromStorage, jsonHeaders, api } from "./api";
|
||||
|
||||
type PublicWebhook = {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
method: string;
|
||||
required_roles: string[];
|
||||
confirmation_required: boolean;
|
||||
};
|
||||
|
||||
type RunResult =
|
||||
| { kind: "idle" }
|
||||
| { kind: "running" }
|
||||
| { kind: "success"; responseStatus: number | null; runId: string }
|
||||
| { kind: "error"; message: string };
|
||||
|
||||
type WebhookFormTabProps = {
|
||||
webhookId: string;
|
||||
sessionId: string;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
const labelsEn = {
|
||||
title: "Run webhook",
|
||||
description: "Description",
|
||||
requiredRoles: "Required roles",
|
||||
confirmation: "Requires confirmation",
|
||||
method: "Method",
|
||||
payload: "Payload (optional JSON)",
|
||||
payloadHelp: "These fields are merged with the backend template. Available variables: {user}, {session}, {message}.",
|
||||
run: "Run",
|
||||
running: "Running...",
|
||||
resultOk: "Webhook executed",
|
||||
resultErr: "Failed to execute",
|
||||
httpStatus: "HTTP",
|
||||
runId: "Audit ID",
|
||||
back: "Back to chat",
|
||||
notFound: "Webhook not found or insufficient permissions",
|
||||
loading: "Loading webhook...",
|
||||
user: "User",
|
||||
session: "Session",
|
||||
};
|
||||
|
||||
const detectLanguage = (): "en" => "en";
|
||||
|
||||
const WebhookFormTabInner = ({ webhookId, sessionId, onBack }: WebhookFormTabProps) => {
|
||||
const [labels] = useState(() => labelsEn);
|
||||
const [webhook, setWebhook] = useState<PublicWebhook | null>(null);
|
||||
const [payload, setPayload] = useState("{}");
|
||||
const [result, setResult] = useState<RunResult>({ kind: "idle" });
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const data = await api<{ items: PublicWebhook[] }>("/api/webhooks");
|
||||
const found = data.items.find((item) => item.id === webhookId);
|
||||
if (!found) {
|
||||
setError(labels.notFound);
|
||||
return;
|
||||
}
|
||||
setWebhook(found);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(labels.notFound);
|
||||
}
|
||||
})();
|
||||
}, [webhookId, labels.notFound]);
|
||||
|
||||
const submit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!webhook) return;
|
||||
|
||||
if (webhook.confirmation_required) {
|
||||
const ok = window.confirm(`Run ${webhook.label}?`);
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
let parsed: Record<string, unknown> = {};
|
||||
if (payload.trim().length > 0) {
|
||||
try {
|
||||
const value = JSON.parse(payload);
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
parsed = value as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
setResult({ kind: "error", message: "Payload is not valid JSON" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setResult({ kind: "running" });
|
||||
try {
|
||||
const response = await fetch(`/api/webhooks/${webhook.id}/run`, {
|
||||
method: "POST",
|
||||
headers: jsonHeaders(),
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
confirmed: true,
|
||||
lastUserMessage: undefined,
|
||||
payload: parsed,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = await response.text().catch(() => "");
|
||||
throw new Error(`http_${response.status}: ${detail.slice(0, 200)}`);
|
||||
}
|
||||
const body = (await response.json()) as { id: string; response_status: number | null };
|
||||
setResult({ kind: "success", responseStatus: body.response_status, runId: body.id });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const message = err instanceof Error ? err.message : "error";
|
||||
setResult({ kind: "error", message });
|
||||
}
|
||||
};
|
||||
|
||||
const tokenInfo = useMemo(() => {
|
||||
const t = authTokenFromStorage();
|
||||
return t ? `${t.slice(0, 12)}…` : labels.notFound;
|
||||
}, [labels.notFound]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<main className="formTab error">
|
||||
<h1>{labels.title}</h1>
|
||||
<p className="muted">{error}</p>
|
||||
<button type="button" onClick={onBack}>{labels.back}</button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!webhook) {
|
||||
return (
|
||||
<main className="formTab loading">
|
||||
<h1>{labels.title}</h1>
|
||||
<p className="muted">{labels.loading}</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="formTab">
|
||||
<header className="formTabHeader">
|
||||
<div>
|
||||
<small>SIC</small>
|
||||
<h1>{webhook.label}</h1>
|
||||
</div>
|
||||
<button type="button" onClick={onBack}>{labels.back}</button>
|
||||
</header>
|
||||
|
||||
<dl className="formTabMeta">
|
||||
{webhook.description ? (
|
||||
<div>
|
||||
<dt>{labels.description}</dt>
|
||||
<dd>{webhook.description}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<dt>{labels.method}</dt>
|
||||
<dd><code>{webhook.method}</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{labels.requiredRoles}</dt>
|
||||
<dd>{webhook.required_roles.join(", ")}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{labels.confirmation}</dt>
|
||||
<dd>{webhook.confirmation_required ? "Yes" : "No"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{labels.session}</dt>
|
||||
<dd><code>{sessionId.slice(0, 8)}…</code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{labels.user}</dt>
|
||||
<dd><code>{tokenInfo}</code></dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<form onSubmit={submit} className="formTabForm">
|
||||
<label>
|
||||
<span>{labels.payload}</span>
|
||||
<textarea
|
||||
onChange={(e) => setPayload(e.target.value)}
|
||||
rows={8}
|
||||
spellCheck={false}
|
||||
value={payload}
|
||||
/>
|
||||
</label>
|
||||
<small className="muted">{labels.payloadHelp}</small>
|
||||
<button
|
||||
className="formTabRun"
|
||||
disabled={result.kind === "running"}
|
||||
type="submit"
|
||||
>
|
||||
{result.kind === "running" ? labels.running : labels.run}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{result.kind === "success" ? (
|
||||
<section className="formTabResult success">
|
||||
<strong>{labels.resultOk}</strong>
|
||||
<small>{labels.httpStatus}: {result.responseStatus ?? "—"}</small>
|
||||
<small>{labels.runId}: <code>{result.runId}</code></small>
|
||||
</section>
|
||||
) : null}
|
||||
{result.kind === "error" ? (
|
||||
<section className="formTabResult error">
|
||||
<strong>{labels.resultErr}</strong>
|
||||
<small>{result.message}</small>
|
||||
</section>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
// Read query params helper for the main App.
|
||||
export const getWebhookFormTabParams = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const webhook = params.get("webhook");
|
||||
const session = params.get("session");
|
||||
if (!webhook || !session) return null;
|
||||
return { webhookId: webhook, sessionId: session };
|
||||
};
|
||||
|
||||
const WebhookFormTab = WebhookFormTabInner;
|
||||
export default WebhookFormTab;
|
||||
export type { WebhookFormTabProps };
|
||||
Reference in New Issue
Block a user