const state = { config: null, fields: new Map(), localStatus: new Map(), modelOptions: [], activeView: "providers", }; const MASKED_SECRET = "********"; const VIEW_GROUPS = [ { id: "providers", label: "Providers", title: "Providers", sections: ["providers", "runtime"], containerId: "providersSections", }, { id: "model_config", label: "Model Config", title: "Model Config", sections: ["models", "thinking", "web_tools"], containerId: "modelConfigSections", }, { id: "messaging", label: "Messaging", title: "Messaging", sections: ["messaging", "voice"], containerId: "messagingSections", }, ]; const byId = (id) => document.getElementById(id); function sourceLabel(source) { const labels = { default: "default", template: "template", repo_env: "repo .env", managed_env: "", explicit_env_file: "FCC_ENV_FILE", process: "process env", }; return Object.prototype.hasOwnProperty.call(labels, source) ? labels[source] : source; } function sourceText(field) { const parts = []; const label = sourceLabel(field.source); if (label) { parts.push(label); } if (field.locked) { parts.push("locked"); } return parts.join(" "); } function providerName(providerId) { const names = { nvidia_nim: "NVIDIA NIM", open_router: "OpenRouter", deepseek: "DeepSeek", lmstudio: "LM Studio", llamacpp: "llama.cpp", ollama: "Ollama", kimi: "Kimi", wafer: "Wafer", opencode: "OpenCode Zen", zai: "Z.ai", }; if (names[providerId]) return names[providerId]; return providerId .split("_") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function statusClass(status) { if (["configured", "reachable", "running"].includes(status)) return "ok"; if (["missing_key", "missing_url", "unknown"].includes(status)) return "warn"; if (["offline", "error"].includes(status)) return "error"; return "neutral"; } async function api(path, options = {}) { const response = await fetch(path, { headers: { "Content-Type": "application/json", ...(options.headers || {}) }, ...options, }); if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`); } return response.json(); } async function load() { showMessage("Loading admin config"); const config = await api("/admin/api/config"); state.config = config; state.fields = new Map(config.fields.map((field) => [field.key, field])); renderNav(); renderProviders(config.provider_status); renderSections(config.sections, config.fields); byId("configPath").textContent = config.paths.managed; await validate(false); await refreshLocalStatus(); updateDirtyState(); showMessage(""); } function renderNav() { const nav = byId("sectionNav"); nav.innerHTML = ""; VIEW_GROUPS.forEach((view, index) => { const button = document.createElement("button"); button.type = "button"; button.className = `nav-link${index === 0 ? " active" : ""}`; button.dataset.view = view.id; button.textContent = view.label; if (index === 0) { button.setAttribute("aria-current", "page"); } button.addEventListener("click", () => { setActiveView(view.id, { scroll: true }); }); nav.appendChild(button); }); setActiveView(state.activeView, { scroll: false }); } function setActiveView(viewId, { scroll = false } = {}) { const activeView = VIEW_GROUPS.find((view) => view.id === viewId) || VIEW_GROUPS[0]; state.activeView = activeView.id; byId("pageTitle").textContent = activeView.title; document.querySelectorAll(".nav-link").forEach((link) => { const selected = link.dataset.view === activeView.id; link.classList.toggle("active", selected); if (selected) { link.setAttribute("aria-current", "page"); } else { link.removeAttribute("aria-current"); } }); document.querySelectorAll(".admin-view").forEach((view) => { const selected = view.dataset.view === activeView.id; view.classList.toggle("active", selected); view.hidden = !selected; }); if (scroll) { window.scrollTo({ top: 0, behavior: "smooth" }); } } function renderProviders(providerStatus) { const grid = byId("providerGrid"); grid.innerHTML = ""; providerStatus.forEach((provider) => { const card = document.createElement("article"); card.className = "provider-card"; card.dataset.provider = provider.provider_id; const title = document.createElement("div"); title.className = "provider-title"; title.innerHTML = `${providerName(provider.provider_id)}`; const pill = document.createElement("span"); pill.className = `status-pill ${statusClass(provider.status)}`; pill.textContent = provider.label; title.appendChild(pill); const meta = document.createElement("div"); meta.className = "provider-meta"; meta.textContent = provider.kind === "local" ? provider.base_url || "No local URL configured" : provider.credential_env; const button = document.createElement("button"); button.type = "button"; button.className = "test-button"; button.textContent = provider.kind === "local" ? "Test" : "Refresh models"; button.addEventListener("click", () => testProvider(provider.provider_id, button)); card.append(title, meta, button); grid.appendChild(card); }); } function updateProviderCard(providerId, status, label, metaText) { const card = document.querySelector(`[data-provider="${providerId}"]`); if (!card) return; const pill = card.querySelector(".status-pill"); pill.className = `status-pill ${statusClass(status)}`; pill.textContent = label; if (metaText) { card.querySelector(".provider-meta").textContent = metaText; } } function renderSections(sections, fields) { VIEW_GROUPS.forEach((view) => { byId(view.containerId).innerHTML = ""; }); const sectionById = new Map(sections.map((section) => [section.id, section])); const bySection = new Map(); sections.forEach((section) => bySection.set(section.id, [])); fields.forEach((field) => { if (!bySection.has(field.section)) bySection.set(field.section, []); bySection.get(field.section).push(field); }); VIEW_GROUPS.forEach((view) => { const container = byId(view.containerId); view.sections.forEach((sectionId) => { const section = sectionById.get(sectionId); const sectionFields = bySection.get(sectionId) || []; if (!section || sectionFields.length === 0) return; const sectionEl = document.createElement("section"); sectionEl.className = "settings-section"; sectionEl.id = `section-${section.id}`; const heading = document.createElement("div"); heading.className = "section-heading"; heading.innerHTML = `

${section.label}

${section.description}

`; sectionEl.appendChild(heading); const grid = document.createElement("div"); grid.className = "field-grid"; sectionFields.forEach((field) => { grid.appendChild(renderField(field)); }); sectionEl.appendChild(grid); if (sectionFields.some((field) => field.advanced)) { const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "ghost-button advanced-toggle"; toggle.textContent = "Show advanced"; toggle.addEventListener("click", () => { const showing = sectionEl.classList.toggle("show-advanced"); toggle.textContent = showing ? "Hide advanced" : "Show advanced"; }); sectionEl.appendChild(toggle); } container.appendChild(sectionEl); }); }); } function renderField(field) { const wrapper = document.createElement("div"); wrapper.className = `field${field.advanced ? " advanced-field" : ""}`; wrapper.dataset.key = field.key; const label = document.createElement("label"); label.htmlFor = `field-${field.key}`; const labelText = document.createElement("span"); labelText.textContent = field.label; label.appendChild(labelText); const source = sourceText(field); if (source) { const sourceEl = document.createElement("span"); sourceEl.className = "field-source"; sourceEl.textContent = source; label.appendChild(sourceEl); } const input = inputForField(field); input.id = `field-${field.key}`; input.dataset.key = field.key; input.dataset.original = field.value || ""; input.dataset.secret = field.secret ? "true" : "false"; input.dataset.configured = field.configured ? "true" : "false"; input.disabled = field.locked; input.addEventListener("input", updateDirtyState); input.addEventListener("change", updateDirtyState); wrapper.append(label, input); if (field.description) { const description = document.createElement("div"); description.className = "field-description"; description.textContent = field.description; wrapper.appendChild(description); } return wrapper; } function inputForField(field) { if (field.type === "boolean") { const input = document.createElement("input"); input.type = "checkbox"; input.checked = String(field.value).toLowerCase() === "true"; input.dataset.original = input.checked ? "true" : "false"; return input; } if (field.type === "tri_boolean") { const select = document.createElement("select"); [ ["", "Inherit"], ["true", "Enabled"], ["false", "Disabled"], ].forEach(([value, label]) => select.appendChild(option(value, label))); select.value = field.value || ""; return select; } if (field.type === "select") { const select = document.createElement("select"); field.options.forEach((value) => select.appendChild(option(value, value))); select.value = field.value || field.options[0] || ""; return select; } if (field.type === "textarea") { const textarea = document.createElement("textarea"); textarea.value = field.value || ""; return textarea; } const input = document.createElement("input"); input.type = field.type === "number" ? "number" : "text"; if (field.type === "secret") { input.type = "password"; input.placeholder = field.configured ? "Configured - enter a new value to replace" : "Not configured"; input.value = ""; input.autocomplete = "off"; } else { input.value = field.value || ""; } if (field.key.startsWith("MODEL")) { input.setAttribute("list", "model-options"); } return input; } function option(value, label) { const optionEl = document.createElement("option"); optionEl.value = value; optionEl.textContent = label; return optionEl; } function readFieldValue(input) { if (input.type === "checkbox") return input.checked ? "true" : "false"; if (input.dataset.secret === "true" && input.dataset.configured === "true") { return input.value ? input.value : MASKED_SECRET; } return input.value; } function changedValues() { const values = {}; document.querySelectorAll("[data-key]").forEach((input) => { if (input.disabled || !input.matches("input, select, textarea")) return; const value = readFieldValue(input); if (value !== input.dataset.original) { values[input.dataset.key] = value; } }); return values; } function updateDirtyState() { const count = Object.keys(changedValues()).length; byId("dirtyState").textContent = count === 0 ? "No changes" : `${count} unsaved change${count === 1 ? "" : "s"}`; byId("applyButton").disabled = count === 0; } async function validate(showResult = true) { const result = await api("/admin/api/config/validate", { method: "POST", body: JSON.stringify({ values: changedValues() }), }); if (showResult) { showValidationResult(result); } return result; } function showValidationResult(result) { if (result.valid) { showMessage("Config shape is valid", "ok"); } else { showMessage(result.errors.join("; "), "error"); } } async function apply() { const result = await api("/admin/api/config/apply", { method: "POST", body: JSON.stringify({ values: changedValues() }), }); if (!result.applied) { showValidationResult(result); return; } const restart = result.restart || {}; if (restart.required && restart.automatic) { showMessage("Applied. Restarting server...", "ok"); byId("applyButton").disabled = true; setTimeout(() => { window.location.href = restart.admin_url || "/admin"; }, 1600); return; } const pending = restart.required ? restart.fields || [] : result.pending_fields || []; await load(); showMessage( pending.length ? `Applied. Restart fcc-server to use: ${pending.join(", ")}` : "Applied", "ok", ); } async function refreshLocalStatus() { const result = await api("/admin/api/providers/local-status"); result.providers.forEach((provider) => { state.localStatus.set(provider.provider_id, provider); const meta = provider.status_code ? `${provider.base_url} returned HTTP ${provider.status_code}` : provider.base_url; updateProviderCard(provider.provider_id, provider.status, provider.label, meta); }); } async function testProvider(providerId, button) { const original = button.textContent; button.disabled = true; button.textContent = "Testing"; try { const result = await api(`/admin/api/providers/${providerId}/test`, { method: "POST", body: "{}", }); if (result.ok) { updateProviderCard( providerId, "reachable", `${result.models.length} models`, result.models.slice(0, 3).join(", ") || "No models returned", ); state.modelOptions = Array.from( new Set([ ...state.modelOptions, ...result.models.map((model) => `${providerId}/${model}`), ]), ).sort(); syncModelDatalist(); } else { updateProviderCard(providerId, "offline", result.error_type, result.error_type); } } finally { button.disabled = false; button.textContent = original; } } function syncModelDatalist() { let datalist = byId("model-options"); if (!datalist) { datalist = document.createElement("datalist"); datalist.id = "model-options"; document.body.appendChild(datalist); } datalist.innerHTML = ""; state.modelOptions.forEach((model) => datalist.appendChild(option(model, model))); } function showMessage(message, kind = "") { const area = byId("messageArea"); area.textContent = message; area.className = `message-area ${kind}`.trim(); } byId("validateButton").addEventListener("click", () => validate(true)); byId("applyButton").addEventListener("click", apply); load().catch((error) => { showMessage(error.message, "error"); });