mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-07-09 16:00:45 +00:00
## Problem
Admin config was a single responsibility hub with manually duplicated
provider metadata. Provider labels, fields, template loading,
validation, persistence, and status lived in one place.
## Changes
| Before | After |
| --- | --- |
| Admin config lived in one large `api/admin_config.py` module. | Admin
config lives in package modules for manifest, sources, values,
validation, persistence, and status. |
| Provider admin fields and UI labels were manually duplicated. |
Provider admin fields and display names derive from `PROVIDER_CATALOG`
with admin-only help overrides. |
| `fcc-init` and Admin UI loaded `.env.example` separately. | `fcc-init`
and Admin UI use shared `config.env_template` loading. |
| Architecture docs pointed to the old admin config module. |
Architecture docs describe the package owners and catalog-driven
provider manifest. |
<!-- greptile_comment -->
<details open><summary><h3>Greptile Summary</h3></summary>
This PR refactors admin configuration into a catalog-driven package. The
main changes are:
- Split the former monolithic `api/admin_config.py` into manifest,
source loading, value presentation, validation, persistence, and
provider status modules.
- Generate provider admin fields and display names from
`PROVIDER_CATALOG` with admin-specific help overrides.
- Share `.env.example` loading between `fcc-init` and Admin UI defaults
through `config.env_template`.
- Update admin routes, Admin UI provider labels, architecture docs,
version metadata, and contract/API tests for the new module layout.
</details>
<h3>Confidence Score: 5/5</h3>
The refactor appears merge-safe with no code issues identified in the
reviewed changes.
The package split, catalog-driven provider metadata, shared environment
template loading, route updates, and tests/docs changes are cohesive and
covered by corresponding contract/API/CLI test updates.
<details><summary><h3><a href="https://www.greptile.com/trex"><img
alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="20" align="absmiddle"></a> T-Rex Logs</h3></summary>
**What T-Rex did**
- T-Rex ran manifest validation for catalog provider before and after
routes, capturing base and head responses and catalog-alignment checks,
and confirmed the validation completed successfully.
- T-Rex evaluated the shared-env-template scenarios, observing the
before run with no config.env\_template module and the after run with
the module present, with patched loader values and all consistency
checks passing, and the run exited with code 0.
- T-Rex executed the package-admin-workflow validation, verifying the
base and after import paths, the load/validate/write workflow produced
matching outputs, and the run completed with exit code 0.
<a
href="https://app.greptile.com/trex/runs/12529845/artifacts"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=1"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1"><img
alt="View all artifacts"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1"
height="32"></picture></a>
<sub><a href="https://www.greptile.com/trex"><img alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="14" align="absmiddle"></a> Ran code and verified through
T-Rex</sub>
</details>
<sub>Reviews (1): Last reviewed commit: ["Refactor admin config into
catalog-drive..."](d6239d7953)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=40315222)</sub>
<!-- /greptile_comment -->
475 lines
14 KiB
JavaScript
475 lines
14 KiB
JavaScript
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 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 = `<strong>${provider.display_name || 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) {
|
|
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 = `<div><h3>${section.label}</h3><p>${section.description}</p></div>`;
|
|
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");
|
|
});
|