File size: 24,822 Bytes
038574d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | const state = {
view: location.hash.slice(1) || "overview",
apiKey: sessionStorage.getItem("maester_api_key") || "",
workspaceId: "",
campaigns: [], variants: [], jobs: [], assets: [], calendar: [], workspaces: [],
accounts: [], integrations: [], experiments: [], rules: [], audit: [], overview: {}, selectedVariantId: null,
};
const $ = (query, root = document) => root.querySelector(query);
const $$ = (query, root = document) => [...root.querySelectorAll(query)];
const escapeHtml = (value = "") => String(value).replace(/[&<>'"]/g, char => ({"&":"&","<":"<",">":">","'":"'",'"':"""}[char]));
const badge = value => `<span class="badge ${escapeHtml(value)}">${escapeHtml(String(value).replaceAll("_", " "))}</span>`;
const date = value => value ? new Date(value).toLocaleString() : "-";
const tags = values => `<div class="platforms">${(values || []).map(value => `<span class="platform">${escapeHtml(String(value).replaceAll("_", " "))}</span>`).join("")}</div>`;
const jsonPost = body => ({method: "POST", body: JSON.stringify(body)});
function toast(message, error = false) {
const node = document.createElement("div");
node.className = `toast${error ? " error" : ""}`;
node.textContent = message;
$("#toast-region").append(node);
setTimeout(() => node.remove(), 4000);
}
async function api(path, options = {}) {
const headers = new Headers(options.headers || {});
if (state.apiKey) headers.set("X-API-Key", state.apiKey);
if (options.body && !(options.body instanceof FormData)) headers.set("Content-Type", "application/json");
const response = await fetch(path, {...options, headers, credentials: "same-origin"});
if (response.status === 401) {
$("#settings-dialog").showModal();
throw new Error("API key required");
}
if (!response.ok) {
const body = await response.json().catch(() => ({detail: response.statusText}));
throw new Error(typeof body.detail === "string" ? body.detail : JSON.stringify(body.detail));
}
return response.status === 204 ? null : response.json();
}
async function loadData() {
const scope = state.workspaceId ? `?workspace_id=${encodeURIComponent(state.workspaceId)}` : "";
const values = await Promise.all([
api(`/automation/overview${scope}`), api("/automation/campaigns"), api("/automation/variants"), api("/automation/jobs"),
api("/automation/assets"), api("/automation/calendar"), api("/automation/workspaces"), api("/automation/accounts"),
api("/automation/integrations"), api("/automation/experiments"), api("/automation/rules"), api("/automation/audit?limit=100"),
]);
[state.overview, state.campaigns, state.variants, state.jobs, state.assets, state.calendar, state.workspaces, state.accounts, state.integrations, state.experiments, state.rules, state.audit] = values;
$(".status-dot").classList.add("online");
$("#connection-label").textContent = "Gateway online";
fillSelectors();
}
function fillSelectors() {
$("#workspace-filter").innerHTML = `<option value="">All workspaces</option>${state.workspaces.map(item => `<option value="${item.id}">${escapeHtml(item.name)}</option>`).join("")}`;
$("#workspace-filter").value = state.workspaceId;
const campaignOptions = state.campaigns.map(item => `<option value="${item.id}">${escapeHtml(item.name)}</option>`).join("");
const editorValue = $("#editor-campaign-filter").value;
$("#editor-campaign-filter").innerHTML = `<option value="">All campaigns</option>${campaignOptions}`;
$("#editor-campaign-filter").value = editorValue;
$("#calendar-campaign").innerHTML = campaignOptions;
const variantOptions = state.variants.map(item => `<option value="${item.id}">${escapeHtml(item.title)} · ${escapeHtml(item.platform)}</option>`).join("");
$("#analytics-variant").innerHTML = variantOptions;
$("#review-variants").innerHTML = variantOptions;
}
function metric(label, value, detail = "") {
return `<div class="metric"><div class="metric-label">${label}</div><div class="metric-value">${value}</div><div class="metric-detail">${escapeHtml(detail)}</div></div>`;
}
function renderOverview() {
const data = state.overview;
$("#overview-metrics").innerHTML = metric("Campaigns", data.campaigns || 0, "Production briefs") + metric("Variants", data.variants || 0, "Platform cuts") + metric("Queued", data.queue?.states?.queued || 0, `${data.queue?.total || 0} jobs`) + metric("Published", data.approvals?.published || 0, "Live variants");
const queue = data.queue?.states || {};
$("#overview-queue").innerHTML = Object.keys(queue).length ? Object.entries(queue).map(([name, count]) => `<div class="progress-row"><span>${escapeHtml(name)}</span><div class="progress"><span style="width:${count / Math.max(data.queue.total, 1) * 100}%"></span></div><strong>${count}</strong></div>`).join("") : `<div class="empty-state">Queue is clear</div>`;
const scores = data.analytics?.platform_average_scores || {};
$("#overview-performance").innerHTML = Object.keys(scores).length ? Object.entries(scores).map(([name, score]) => `<div class="progress-row"><span>${escapeHtml(name)}</span><div class="progress"><span style="width:${Math.min(score * 4, 100)}%"></span></div><strong>${score}</strong></div>`).join("") : `<div class="empty-state">No performance data</div>`;
$("#overview-approvals").innerHTML = `<div class="metric-grid">${Object.entries(data.approvals || {}).map(([name, count]) => metric(name.replaceAll("_", " "), count)).join("")}</div>`;
}
function renderCampaigns() {
const counts = state.variants.reduce((result, item) => ({...result, [item.campaign_id]: (result[item.campaign_id] || 0) + 1}), {});
$("#campaign-table").innerHTML = state.campaigns.map(item => `<tr><td><strong>${escapeHtml(item.name)}</strong><div class="muted">${date(item.updated_at)}</div></td><td>${escapeHtml(item.topic)}</td><td>${tags(item.platforms)}</td><td>${counts[item.id] || 0}</td><td>${badge(item.status)}</td><td><div class="table-actions"><button class="command" data-action="open-campaign" data-id="${item.id}">Edit</button><button class="command" data-action="generate-ai" data-id="${item.id}">AI generate</button><button class="command" data-action="schedule-campaign" data-id="${item.id}">Schedule</button></div></td></tr>`).join("") || `<tr><td colspan="6"><div class="empty-state">No campaigns</div></td></tr>`;
}
function renderEditor() {
const campaignId = $("#editor-campaign-filter").value;
const variants = state.variants.filter(item => !campaignId || item.campaign_id === campaignId);
$("#variant-list").innerHTML = variants.map(item => `<button class="selection-item ${item.id === state.selectedVariantId ? "active" : ""}" data-action="select-variant" data-id="${item.id}"><span class="selection-title">${escapeHtml(item.title)}</span><span class="selection-meta"><span>${escapeHtml(item.platform)}</span>${badge(item.approval_state)}</span></button>`).join("") || `<div class="empty-state">No variants</div>`;
const item = state.variants.find(variant => variant.id === state.selectedVariantId);
if (!item) return;
$("#variant-editor").innerHTML = `<form id="variant-form"><div class="section-head"><h2>${escapeHtml(item.title)}</h2>${badge(item.approval_state)}</div><div class="form-grid"><label class="span-2">Title<input name="title" value="${escapeHtml(item.title)}"></label><label class="span-2">Hook<textarea name="hook" rows="3">${escapeHtml(item.hook)}</textarea></label><label class="span-2">Script<textarea name="script" rows="10">${escapeHtml(item.script)}</textarea></label><label class="span-2">Caption<textarea name="caption" rows="4">${escapeHtml(item.caption)}</textarea></label><label>Hashtags<input name="hashtags" value="${escapeHtml(item.hashtags.join(", "))}"></label><label>Call to action<input name="cta" value="${escapeHtml(item.cta)}"></label><label>Template<input name="template" value="${escapeHtml(item.template)}"></label><label>Style<input name="creative_style" value="${escapeHtml(item.creative_style)}"></label><label>Duration<input name="duration_seconds" type="number" min="3" max="600" value="${item.duration_seconds}"></label></div><div class="editor-actions"><button class="command primary" type="submit">Save changes</button><button class="command" type="button" data-action="approve-variant" data-id="${item.id}">✓ Approve</button><button class="command" type="button" data-action="render-variant" data-id="${item.id}">▶ Render</button><button class="command" type="button" data-action="publish-variant" data-id="${item.id}">Publish draft</button><button class="command" type="button" data-action="rewrite-variant" data-id="${item.id}">Rewrite</button></div></form>`;
$("#variant-form").addEventListener("submit", saveVariant);
renderInspector(item);
}
async function renderInspector(item) {
try {
const quality = await api(`/automation/quality/variants/${item.id}`);
$("#variant-inspector").innerHTML = `<div class="section-head"><h2>Inspector</h2>${badge(item.job_state)}</div><div class="quality-grid"><div class="quality"><strong>${quality.hook.score}</strong><span>Hook</span></div><div class="quality"><strong>${quality.script.score}</strong><span>Script</span></div><div class="quality"><strong>${quality.caption.score}</strong><span>Caption</span></div><div class="quality"><strong>${Math.round(quality.retention.predicted_completion_rate * 100)}%</strong><span>Completion</span></div></div><h3>Render payload</h3><div class="json-box">${escapeHtml(JSON.stringify(item.render_payload, null, 2))}</div>`;
} catch (error) { $("#variant-inspector").innerHTML = `<div class="empty-state">${escapeHtml(error.message)}</div>`; }
}
function renderCalendar() {
const variants = Object.fromEntries(state.variants.map(item => [item.id, item]));
$("#calendar-table").innerHTML = state.calendar.sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at)).map(item => `<tr><td>${date(item.scheduled_at)}</td><td>${tags([item.platform])}</td><td>${escapeHtml(variants[item.variant_id]?.title || item.variant_id)}</td><td>${badge(item.status)}</td></tr>`).join("") || `<tr><td colspan="4"><div class="empty-state">No scheduled content</div></td></tr>`;
}
function renderJobs() {
const filter = $("#job-filter").value;
$("#job-table").innerHTML = state.jobs.filter(item => !filter || item.state === filter).map(item => `<tr><td><strong>${escapeHtml(item.id)}</strong><div class="muted">${escapeHtml(item.idempotency_key || "")}</div></td><td>${escapeHtml(item.kind)}</td><td>${badge(item.state)}</td><td>${item.attempts}/${item.max_attempts}</td><td>${date(item.updated_at)}</td><td><div class="table-actions">${["failed", "cancelled"].includes(item.state) ? `<button class="command" data-action="retry-job" data-id="${item.id}">↻</button>` : ""}${!["published", "ready", "failed", "cancelled"].includes(item.state) ? `<button class="command danger" data-action="cancel-job" data-id="${item.id}">×</button>` : ""}</div></td></tr>`).join("") || `<tr><td colspan="6"><div class="empty-state">No matching jobs</div></td></tr>`;
}
function renderAnalytics() {
const data = state.overview.analytics || {}, winners = data.winners || [];
$("#analytics-metrics").innerHTML = metric("Winners", winners.length, "Viral score ≥ 20") + metric("Platforms", Object.keys(data.platform_average_scores || {}).length, "Reporting sources") + metric("Experiments", state.experiments.length, "Controlled tests") + metric("Top score", winners[0]?.score || 0, winners[0]?.platform || "No data");
$("#winner-board").innerHTML = winners.length ? `<div class="stack-list">${winners.map(item => `<div class="stack-row"><div class="stack-row-main"><strong>${escapeHtml(item.title)}</strong><small>${escapeHtml(item.platform)}</small></div>${badge(String(item.score))}</div>`).join("")}</div>` : `<div class="empty-state">No winners yet</div>`;
$("#experiment-list").innerHTML = state.experiments.length ? `<div class="stack-list">${state.experiments.map(item => `<div class="stack-row"><div class="stack-row-main"><strong>${escapeHtml(item.name)}</strong><small>${escapeHtml(item.variable)} · ${item.variant_ids.length} variants</small></div>${badge(item.status)}<button class="command" data-action="evaluate-experiment" data-id="${item.id}">Evaluate</button></div>`).join("")}</div>` : `<div class="empty-state">No experiments</div>`;
}
function renderAssets() {
$("#asset-table").innerHTML = state.assets.map(item => `<tr><td><strong>${escapeHtml(item.filename)}</strong><div class="muted">${escapeHtml(item.checksum.slice(0, 12))}</div></td><td>${badge(item.kind)}</td><td>${Math.ceil(item.size_bytes / 1024)} KB</td><td>${escapeHtml(item.license)}<div class="muted">${escapeHtml(item.attribution)}</div></td><td>${tags(item.tags)}</td><td><button class="command danger" data-action="delete-asset" data-id="${item.id}">Delete</button></td></tr>`).join("") || `<tr><td colspan="6"><div class="empty-state">No assets</div></td></tr>`;
}
function renderReviews() {
const pending = state.variants.filter(item => ["draft", "needs_review"].includes(item.approval_state));
$("#pending-reviews").innerHTML = pending.length ? `<div class="stack-list">${pending.map(item => `<div class="stack-row"><div class="stack-row-main"><strong>${escapeHtml(item.title)}</strong><small>${escapeHtml(item.platform)}</small></div>${badge(item.approval_state)}</div>`).join("")}</div>` : `<div class="empty-state">Nothing awaiting review</div>`;
}
function renderIntegrations() {
$("#account-list").innerHTML = state.accounts.length ? `<div class="stack-list">${state.accounts.map(item => `<div class="stack-row"><div class="stack-row-main"><strong>${escapeHtml(item.display_name || item.handle)}</strong><small>${escapeHtml(item.platform)} · ${escapeHtml(item.auth_status)}</small></div><button class="command" data-action="sync-account" data-id="${item.id}">Sync</button></div>`).join("")}</div>` : `<div class="empty-state">No connected accounts</div>`;
$("#integration-list").innerHTML = state.integrations.length ? `<div class="stack-list">${state.integrations.map(item => `<div class="stack-row"><div class="stack-row-main"><strong>${escapeHtml(item.name)}</strong><small>${escapeHtml(item.kind)}</small></div>${badge(item.active ? "ready" : "disabled")}</div>`).join("")}</div>` : `<div class="empty-state">No delivery integrations</div>`;
$("#rule-list").innerHTML = state.rules.length ? `<div class="stack-list">${state.rules.map(item => `<div class="stack-row"><div class="stack-row-main"><strong>${escapeHtml(item.name)}</strong><small>${escapeHtml(item.event)} · ${item.actions.length} actions</small></div>${badge(item.active ? "ready" : "disabled")}</div>`).join("")}</div>` : `<div class="empty-state">No automation rules</div>`;
}
async function renderOperations() {
const [providers, services] = await Promise.all([api("/automation/providers"), api("/services")]);
$("#ops-metrics").innerHTML = metric("Workspaces", state.workspaces.length, "Tenant boundaries") + metric("Accounts", state.accounts.length, "Publishing targets") + metric("Assets", state.assets.length, "Managed media") + metric("Audit events", state.audit.length, "Recent operations");
$("#provider-status").innerHTML = `<div class="stack-list">${Object.entries(providers.providers).map(([name, item]) => `<div class="stack-row"><div class="stack-row-main"><strong>${escapeHtml(name)}</strong><small>${escapeHtml(item.model)}</small></div>${badge(item.configured ? "ready" : "not_configured")}</div>`).join("")}</div>`;
$("#service-status").innerHTML = `<div class="stack-list">${services.services.map(item => `<div class="stack-row"><div class="stack-row-main"><strong>${escapeHtml(item.title)}</strong><small>${escapeHtml(item.path)}</small></div>${badge(item.mounted ? "ready" : item.enabled ? "failed" : "disabled")}</div>`).join("")}</div>`;
$("#audit-table").innerHTML = state.audit.map(item => `<tr><td>${date(item.created_at)}</td><td>${escapeHtml(item.actor)}</td><td>${escapeHtml(item.action)}</td><td>${escapeHtml(item.resource_type)} · ${escapeHtml(item.resource_id)}</td></tr>`).join("") || `<tr><td colspan="4"><div class="empty-state">No audit events</div></td></tr>`;
}
const renderers = {overview: renderOverview, campaigns: renderCampaigns, editor: renderEditor, calendar: renderCalendar, jobs: renderJobs, analytics: renderAnalytics, assets: renderAssets, reviews: renderReviews, integrations: renderIntegrations, operations: renderOperations};
async function refresh() { try { await loadData(); await renderers[state.view]?.(); } catch (error) { $(".status-dot").classList.remove("online"); $("#connection-label").textContent = "Gateway unavailable"; toast(error.message, true); } }
function switchView(view) { state.view = view; location.hash = view; $$(".view").forEach(item => item.classList.toggle("active", item.id === `view-${view}`)); $$(".nav-item").forEach(item => item.classList.toggle("active", item.dataset.view === view)); $("#view-title").textContent = view[0].toUpperCase() + view.slice(1); $("#sidebar").classList.remove("open"); renderers[view]?.(); }
async function saveVariant(event) {
event.preventDefault();
const data = Object.fromEntries(new FormData(event.currentTarget));
data.hashtags = data.hashtags.split(",").map(item => item.trim()).filter(Boolean);
data.duration_seconds = Number(data.duration_seconds);
try { await api(`/automation/variants/${state.selectedVariantId}`, {method: "PATCH", body: JSON.stringify(data)}); toast("Variant saved"); await refresh(); } catch (error) { toast(error.message, true); }
}
async function action(name, id) {
try {
if (name === "process-queue") { const result = await api("/automation/queue/process", {method: "POST"}); toast(`${result.count} job processed`); }
if (name === "select-variant") { state.selectedVariantId = id; renderEditor(); return; }
if (name === "open-campaign") { $("#editor-campaign-filter").value = id; state.selectedVariantId = state.variants.find(item => item.campaign_id === id)?.id; switchView("editor"); return; }
if (name === "generate-ai") await api(`/automation/campaigns/${id}/generate-ai`, jsonPost({}));
if (name === "schedule-campaign") await api(`/automation/schedule/apply?campaign_id=${id}`, {method: "POST"});
if (name === "apply-schedule") { const start = $("#calendar-start").value; await api(`/automation/schedule/apply?campaign_id=${$("#calendar-campaign").value}${start ? `&start_at=${encodeURIComponent(new Date(start).toISOString())}` : ""}`, {method: "POST"}); }
if (name === "approve-variant") await api(`/automation/variants/${id}/approval`, jsonPost({state: "approved", reviewer: "Studio operator", notes: "Approved in Content Studio"}));
if (name === "render-variant") await api(`/automation/variants/${id}/render-job`, {method: "POST"});
if (name === "publish-variant") await api("/automation/publish", jsonPost({variant_id: id, draft: true}));
if (name === "rewrite-variant") await api(`/automation/variants/${id}/rewrite`, {method: "POST"});
if (name === "retry-job") await api(`/automation/jobs/${id}/retry`, {method: "POST"});
if (name === "cancel-job") await api(`/automation/jobs/${id}/cancel`, {method: "POST"});
if (name === "delete-asset") await api(`/automation/assets/${id}`, {method: "DELETE"});
if (name === "sync-account") await api(`/automation/accounts/${id}/analytics-sync`, {method: "POST"});
if (name === "evaluate-experiment") await api(`/automation/experiments/${id}/evaluate`, {method: "POST"});
if (name === "new-experiment") { const campaign = state.campaigns[0]; if (!campaign) throw new Error("Create a campaign first"); await api("/automation/experiments", jsonPost({campaign_id: campaign.id, name: `${campaign.name} hook test`, variable: "hook", variant_ids: state.variants.filter(item => item.campaign_id === campaign.id).map(item => item.id)})); }
if (name === "new-rule") await api("/automation/rules", jsonPost({name: "Render approved variants", event: "variant_approved", actions: [{type: "render"}], active: true}));
toast("Operation completed"); await refresh();
} catch (error) { toast(error.message, true); }
}
document.addEventListener("click", event => { const nav = event.target.closest("[data-view]"); if (nav) switchView(nav.dataset.view); const dialog = event.target.closest("[data-dialog]"); if (dialog) $(`#${dialog.dataset.dialog}`).showModal(); const button = event.target.closest("[data-action]"); if (button) action(button.dataset.action, button.dataset.id); });
$("#menu-button").addEventListener("click", () => $("#sidebar").classList.toggle("open"));
$("#refresh-button").addEventListener("click", refresh);
$("#settings-button").addEventListener("click", () => $("#settings-dialog").showModal());
$("#settings-form").addEventListener("submit", event => { event.preventDefault(); state.apiKey = $("#api-key").value.trim(); sessionStorage.setItem("maester_api_key", state.apiKey); $("#settings-dialog").close(); refresh(); });
$("#workspace-filter").addEventListener("change", event => { state.workspaceId = event.target.value; refresh(); });
$("#editor-campaign-filter").addEventListener("change", renderEditor);
$("#job-filter").addEventListener("change", renderJobs);
$("#campaign-form").addEventListener("submit", async event => { event.preventDefault(); const form = new FormData(event.currentTarget); const data = Object.fromEntries(form); data.platforms = form.getAll("platforms"); data.quantity = Number(data.quantity); data.workspace_id = state.workspaceId || null; data.brand_kit_id ||= null; try { await api("/automation/campaigns/generate", jsonPost(data)); event.currentTarget.reset(); $("#campaign-dialog").close(); await refresh(); } catch (error) { toast(error.message, true); } });
$("#repurpose-form").addEventListener("submit", async event => { event.preventDefault(); const data = Object.fromEntries(new FormData(event.currentTarget)); data.quantity = Number(data.quantity); data.workspace_id = state.workspaceId || null; data.platforms = ["tiktok", "instagram_reels", "youtube_shorts"]; try { await api("/automation/repurpose", jsonPost(data)); event.currentTarget.reset(); $("#repurpose-dialog").close(); await refresh(); } catch (error) { toast(error.message, true); } });
$("#analytics-form").addEventListener("submit", async event => { event.preventDefault(); const data = Object.fromEntries(new FormData(event.currentTarget)); ["views", "likes", "comments", "shares"].forEach(key => data[key] = Number(data[key])); data.completion_rate = Number(data.completion_rate); try { await api("/automation/analytics/events", jsonPost(data)); await refresh(); } catch (error) { toast(error.message, true); } });
$("#asset-form").addEventListener("submit", async event => { event.preventDefault(); const data = new FormData(event.currentTarget); if (state.workspaceId) data.set("workspace_id", state.workspaceId); try { await api("/automation/assets", {method: "POST", body: data}); event.currentTarget.reset(); await refresh(); } catch (error) { toast(error.message, true); } });
$("#review-form").addEventListener("submit", async event => { event.preventDefault(); const form = new FormData(event.currentTarget); const data = {variant_ids: form.getAll("variant_ids"), expires_in_days: Number(form.get("expires_in_days")), created_by: form.get("created_by"), workspace_id: state.workspaceId || null}; try { const result = await api("/automation/reviews", jsonPost(data)); const url = new URL(result.review_url, location.origin).href; $("#review-result").innerHTML = `<div class="link-output"><strong>Review link</strong><br><a href="${escapeHtml(url)}" target="_blank" rel="noopener">${escapeHtml(url)}</a></div>`; await refresh(); } catch (error) { toast(error.message, true); } });
$("#account-form").addEventListener("submit", async event => { event.preventDefault(); const data = Object.fromEntries(new FormData(event.currentTarget)); data.workspace_id = state.workspaceId || null; data.metadata = {token_env: data.token_env, publish_endpoint: data.publish_endpoint, analytics_endpoint: data.analytics_endpoint}; delete data.token_env; delete data.publish_endpoint; delete data.analytics_endpoint; try { await api("/automation/accounts", jsonPost(data)); $("#account-dialog").close(); await refresh(); } catch (error) { toast(error.message, true); } });
$("#integration-form").addEventListener("submit", async event => { event.preventDefault(); const data = Object.fromEntries(new FormData(event.currentTarget)); data.workspace_id = state.workspaceId || null; try { await api("/automation/integrations", jsonPost(data)); $("#integration-dialog").close(); await refresh(); } catch (error) { toast(error.message, true); } });
$("#api-key").value = state.apiKey;
switchView(state.view in renderers ? state.view : "overview");
refresh();
|