/** * @file PushIngestSettings.tsx * @description Settings → Data → Push ingest: the bearer token that opens * `POST /api/hooks/ingest-batch`, for a machine this dashboard cannot reach * over SSH, so the machine pushes its hooks instead. Off until a token is * created; the token is shown once, right after it is made (only its hash is * stored), and can be rotated or revoked. Also shows a forwarder script for * the other machine — its token in a header file, never on a command line. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { AlertTriangle, Copy, KeyRound, RefreshCw, Trash2 } from "lucide-react"; import { api } from "../../lib/api"; import type { PushTokenStatus } from "../../lib/api"; import { ConfirmModal } from "../ConfirmModal"; import { SettingsSection } from "./SettingsRow"; const copyText = (text: string) => navigator.clipboard?.writeText(text).catch(() => undefined); /** The forwarder for the pushing machine; `origin` is how it reaches this dashboard. */ function forwarder(origin: string) { return `#!/bin/sh # ~/.claude/agenthub-push.sh — a Claude Code hook command: # "command": "sh ~/.claude/agenthub-push.sh PreToolUse" # ~/.claude/agenthub-push.header (chmod 600) holds one line: # Authorization: Bearer jq -c --arg t "$1" --arg s "$(hostname -s)" \\ '{source: $s, events: [{hook_type: $t, data: .}]}' | curl -s -m 5 -o /dev/null -X POST "${origin}/api/hooks/ingest-batch" \\ -H "@$HOME/.claude/agenthub-push.header" \\ -H "Content-Type: application/json" --data-binary @- exit 0`; } export function PushIngestSettings() { const { t } = useTranslation("settings"); const [status, setStatus] = useState(null); const [token, setToken] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); // Rotating a live token or revoking one asks first. const [confirm, setConfirm] = useState<"rotate" | "revoke" | null>(null); useEffect(() => { api.settings.pushToken .get() .then(setStatus) .catch(() => undefined); }, []); async function run(fn: () => Promise) { setConfirm(null); setBusy(true); setError(null); try { await fn(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } const rotate = () => run(async () => { const r = await api.settings.pushToken.rotate(); setToken(r.token); setStatus({ enabled: r.enabled, env_owned: r.env_owned }); }); const revoke = () => run(async () => { setStatus(await api.settings.pushToken.revoke()); setToken(null); }); const script = forwarder(window.location.origin); return (
{t("pushIngest.title")} } description={t("pushIngest.description")} >
{status?.enabled ? t("pushIngest.on") : t("pushIngest.off")} {status?.enabled && ( )}
{token && (

{t("pushIngest.shownOnce")}

{token}
)} {status?.env_owned && (

{t("pushIngest.envOwned")}

)}

{t("pushIngest.reachHint")}

              {script}
            

{t("pushIngest.limits")}

{error &&

{error}

}
void (confirm === "revoke" ? revoke() : rotate())} onCancel={() => setConfirm(null)} />
); }