import { useEffect, useState, type CSSProperties } from "react"; import { useSearchParams } from "react-router-dom"; import { api, type CurrentUser, type IntegrationSummary, type IntegrationType } from "../api/client"; import IntegrationForm from "../components/IntegrationForm"; import IntegrationEditForm from "../components/IntegrationEditForm"; import { useSortable } from "../hooks/useSortable"; import SortableTh from "../components/SortableTh"; import { downloadCsv } from "../utils/csv"; const TYPE_LABELS: Record = { tailscale: "Tailscale", proxmox: "Proxmox", synology: "Synology", semaphore: "Semaphore", gitea: "Gitea", dockhand: "Dockhand", uptimekuma: "Uptime Kuma", }; function typeBadgeStyle(colors: Record, type: IntegrationType): CSSProperties { const color = colors[type]; if (!color) return {}; return { backgroundColor: `${color}22`, color, border: `1px solid ${color}55` }; } export default function Integrations({ user }: { user: CurrentUser }) { const isAdmin = user.role === "admin"; const [searchParams] = useSearchParams(); const [integrations, setIntegrations] = useState(null); const [adding, setAdding] = useState(false); const [editingIntegration, setEditingIntegration] = useState(null); const [error, setError] = useState(null); const [integrationColors, setIntegrationColors] = useState>({}); const [testingId, setTestingId] = useState(null); const [testResults, setTestResults] = useState>({}); useEffect(() => { api.settings.integrationColors().then((res) => setIntegrationColors(res.integrationColors)).catch(() => {}); }, []); function loadIntegrations() { api.integrations .list() .then((res) => { setIntegrations(res.integrations); // Deep-link from global search (?editId=) — jump straight to that integration's edit form. const editId = Number(searchParams.get("editId")); const target = res.integrations.find((i) => i.id === editId); if (target && isAdmin) setEditingIntegration(target); }) .catch((err) => setError(String(err))); } useEffect(loadIntegrations, []); async function toggleEnabled(i: IntegrationSummary) { try { await api.integrations.update(i.id, { enabled: !i.enabled }); loadIntegrations(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } async function testIntegration(i: IntegrationSummary) { setTestingId(i.id); try { const result = await api.integrations.testExisting(i.id, {}); setTestResults((prev) => ({ ...prev, [i.id]: result })); } catch (err) { setTestResults((prev) => ({ ...prev, [i.id]: { ok: false, error: err instanceof Error ? err.message : String(err) } })); } finally { setTestingId(null); } } async function removeIntegration(i: IntegrationSummary) { if (!confirm(`Delete integration "${i.name}"?`)) return; try { await api.integrations.remove(i.id); loadIntegrations(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } const { sorted: sortedIntegrations, sortKey, sortDir, requestSort } = useSortable(integrations); function exportCsv() { if (!sortedIntegrations) return; downloadCsv( "integrations.csv", ["Name", "Type", "Status"], sortedIntegrations.map((i) => [i.name, TYPE_LABELS[i.type], i.enabled ? "Enabled" : "Disabled"]), ); } return ( <>

Integrations

Tailscale, Proxmox, Synology, Semaphore, Gitea, Dockhand, and Uptime Kuma each get their own page once connected — add and manage credentials here.
{isAdmin && !adding && !editingIntegration && ( )}
{error &&
{error}
} {editingIntegration ? ( { setEditingIntegration(null); loadIntegrations(); }} onCancel={() => setEditingIntegration(null)} /> ) : ( adding && ( { setAdding(false); loadIntegrations(); }} onCancel={() => setAdding(false)} /> ) )}
label="Name" sortKeyName="name" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Type" sortKeyName="type" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Status" sortKeyName="enabled" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> {isAdmin && } {sortedIntegrations?.map((i) => ( {isAdmin && ( )} ))} {sortedIntegrations?.length === 0 && ( )}
Actions
{i.name} {TYPE_LABELS[i.type]} {isAdmin ? ( ) : ( {i.enabled ? "Enabled" : "Disabled"} )}
{testResults[i.id] && testingId !== i.id && ( {testResults[i.id].ok ? `OK${testResults[i.id].latencyMs !== undefined ? ` (${testResults[i.id].latencyMs}ms)` : ""}` : "Failed"} )}
{testResults[i.id] && !testResults[i.id].ok && testingId !== i.id && (
{testResults[i.id].error}
)}
No integrations configured yet. {isAdmin && ( <> {" "} . )}
); }