const state = { config: null, status: null, fields: new Map(), localStatus: new Map(), modelOptions: [], }; const MASKED_SECRET = "********"; const byId = (id) => document.getElementById(id); function sourceLabel(source) { const labels = { default: "default", template: "template", repo_env: "repo .env", managed_env: "managed", explicit_env_file: "FCC_ENV_FILE", process: "process env", }; return labels[source] || source; } 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", }; 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, status] = await Promise.all([ api("/admin/api/config"), api("/admin/api/status"), ]); state.config = config; state.status = status; state.fields = new Map(config.fields.map((field) => [field.key, field])); updateHeader(status); renderNav(config.sections); renderProviders(config.provider_status); renderSections(config.sections, config.fields); byId("configPath").textContent = config.paths.managed; await validate(false); await refreshLocalStatus(); updateDirtyState(); showMessage(""); } function updateHeader(status) { const serverStatus = byId("serverStatus"); serverStatus.textContent = "Running"; serverStatus.className = "status-pill ok"; byId("modelBadge").textContent = status.model || ""; } function renderNav(sections) { const nav = byId("sectionNav"); nav.innerHTML = ""; sections.forEach((section, index) => { const button = document.createElement("button"); button.type = "button"; button.className = `nav-link${index === 0 ? " active" : ""}`; button.textContent = section.label; button.addEventListener("click", () => { document.querySelectorAll(".nav-link").forEach((link) => { link.classList.remove("active"); }); button.classList.add("active"); byId(`section-${section.id}`).scrollIntoView({ behavior: "smooth" }); }); nav.appendChild(button); }); } 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) { const container = byId("formSections"); container.innerHTML = ""; 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); }); sections.forEach((section) => { 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"; bySection.get(section.id).forEach((field) => { grid.appendChild(renderField(field)); }); sectionEl.appendChild(grid); if (bySection.get(section.id).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}`; label.innerHTML = `${field.label}${sourceLabel( field.source, )}${field.locked ? " locked" : ""}`; 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() }), }); byId("envPreview").textContent = result.env_preview || ""; 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() }), }); byId("envPreview").textContent = result.env_preview || ""; 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); byId("refreshLocal").addEventListener("click", refreshLocalStatus); load().catch((error) => { byId("serverStatus").textContent = "Error"; byId("serverStatus").className = "status-pill error"; showMessage(error.message, "error"); });