free-claude-code/api/admin_static/admin.js
2026-05-10 15:57:56 -07:00

426 lines
13 KiB
JavaScript

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 = `<strong>${providerName(provider.provider_id)}</strong>`;
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 = `<div><h3>${section.label}</h3><p>${section.description}</p></div>`;
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 = `<span>${field.label}</span><span class="field-source">${sourceLabel(
field.source,
)}${field.locked ? " locked" : ""}</span>`;
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");
});