Admin UX refactor, runtime fixes, and startup logging (#472)

## Summary

- Refactors the admin interface into focused views and simplifies the
header (removes noisy status labels; hides managed-source labels where
appropriate).
- Fixes Claude runtime settings handling, reduces Z.ai base URL leakage
in the admin UI, and streamlines API startup logging.
- Updates configuration and catalog behavior (including `.env.example` /
README), and expands automated tests around admin, app lifespan, and
config/registry behavior.

## Test plan

- [ ] `uv run ruff format`, `uv run ruff check`, `uv run ty check`, `uv
run pytest`
- [ ] Smoke the admin UI: navigation between views, settings save/load,
no sensitive URL leakage in the UI
- [ ] Confirm API startup logs are readable and not overly verbose in
normal operation
This commit is contained in:
Ali Khokhar
2026-05-17 12:55:00 -07:00
committed by GitHub
parent 37974db1ab
commit 943c3db61d
7 changed files with 77 additions and 63 deletions
+2 -3
View File
@@ -94,11 +94,10 @@ Use the same command to update to the latest version.
fcc-server
```
After startup, the terminal prints the proxy and admin URLs:
After startup, Uvicorn prints the proxy bind address and the app logs the admin URL:
```text
Server URL: http://127.0.0.1:8082
Admin UI: http://127.0.0.1:8082/admin
INFO: Admin UI: http://127.0.0.1:8082/admin (local-only)
```
Many terminals make these clickable. Use your configured `PORT` if it is not `8082`.
+1 -30
View File
@@ -78,7 +78,6 @@ textarea {
.brand h1,
.brand p,
.topbar h2,
.topbar p,
.section-heading h3,
.section-heading p,
.strip-header h3 {
@@ -91,7 +90,6 @@ textarea {
}
.brand p,
.eyebrow,
.section-heading p,
.action-meta span {
color: var(--muted);
@@ -136,33 +134,15 @@ textarea {
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.eyebrow {
margin-bottom: 4px;
text-transform: uppercase;
font-weight: 700;
}
.topbar h2 {
font-size: 28px;
line-height: 1.15;
}
.server-state {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.status-pill,
.model-badge {
.status-pill {
display: inline-flex;
align-items: center;
min-height: 30px;
@@ -463,15 +443,6 @@ textarea {
padding: 18px;
}
.topbar {
align-items: flex-start;
flex-direction: column;
}
.server-state {
justify-content: flex-start;
}
.action-bar {
left: 0;
grid-template-columns: 1fr;
+1 -16
View File
@@ -1,6 +1,5 @@
const state = {
config: null,
status: null,
fields: new Map(),
localStatus: new Map(),
modelOptions: [],
@@ -98,14 +97,9 @@ async function api(path, options = {}) {
async function load() {
showMessage("Loading admin config");
const [config, status] = await Promise.all([
api("/admin/api/config"),
api("/admin/api/status"),
]);
const config = await api("/admin/api/config");
state.config = config;
state.status = status;
state.fields = new Map(config.fields.map((field) => [field.key, field]));
updateHeader(status);
renderNav();
renderProviders(config.provider_status);
renderSections(config.sections, config.fields);
@@ -116,13 +110,6 @@ async function load() {
showMessage("");
}
function updateHeader(status) {
const serverStatus = byId("serverStatus");
serverStatus.textContent = "Running";
serverStatus.className = "status-pill ok";
byId("modelBadge").textContent = status.model || "";
}
function renderNav() {
const nav = byId("sectionNav");
nav.innerHTML = "";
@@ -505,7 +492,5 @@ 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");
});
-5
View File
@@ -23,13 +23,8 @@
<main class="main">
<header class="topbar">
<div>
<p class="eyebrow">Local Admin</p>
<h2 id="pageTitle">Providers</h2>
</div>
<div class="server-state">
<span id="serverStatus" class="status-pill neutral">Loading</span>
<span id="modelBadge" class="model-badge"></span>
</div>
</header>
<div id="adminViews" class="admin-views">
+4 -9
View File
@@ -3,15 +3,15 @@
from __future__ import annotations
import asyncio
import logging
import os
import sys
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from fastapi import FastAPI
from loguru import logger
from api.admin_urls import local_admin_url, local_proxy_root_url
from api.admin_urls import local_admin_url
from config.settings import Settings, get_settings
from providers.exceptions import ServiceUnavailableError
from providers.registry import ProviderRegistry
@@ -102,7 +102,6 @@ class AppRuntime:
async def startup(self) -> None:
logger.info("Starting Claude Code Proxy...")
root_url = local_proxy_root_url(self.settings)
admin_url = local_admin_url(self.settings)
self._provider_registry = ProviderRegistry()
self.app.state.provider_registry = self._provider_registry
@@ -112,12 +111,8 @@ class AppRuntime:
self._provider_registry.start_model_list_refresh(self.settings)
await self._start_messaging_if_configured()
self._publish_state()
logger.info("Server URL: {}", root_url)
logger.info("Admin UI: {} (local-only)", admin_url)
print(
f"Server URL: {root_url}\nAdmin UI: {admin_url} (local-only)",
file=sys.stderr,
flush=True,
logging.getLogger("uvicorn.error").info(
"Admin UI: %s (local-only)", admin_url
)
except Exception as exc:
log_startup_failure(self.settings, exc)
+22
View File
@@ -59,6 +59,28 @@ def test_admin_page_no_longer_renders_generated_env_panel(monkeypatch, tmp_path)
assert "envPreview" not in response.text
def test_admin_page_no_longer_renders_global_status_header(monkeypatch, tmp_path):
_set_home(monkeypatch, tmp_path)
app = create_app(lifespan_enabled=False)
response = _local_client(app).get("/admin")
assert response.status_code == 200
assert "Local Admin" not in response.text
assert "serverStatus" not in response.text
assert "modelBadge" not in response.text
def test_admin_static_no_longer_fetches_global_status_header():
script = Path("api/admin_static/admin.js").read_text(encoding="utf-8")
assert 'api("/admin/api/status")' not in script
assert "updateHeader" not in script
assert '"Running"' not in script
assert "serverStatus" not in script
assert "modelBadge" not in script
def test_admin_static_hides_managed_source_label():
script = Path("api/admin_static/admin.js").read_text(encoding="utf-8")
+47
View File
@@ -64,6 +64,53 @@ def test_warn_if_process_auth_token_skips_explicit_dotenv_config():
warning.assert_not_called()
@pytest.mark.asyncio
async def test_runtime_startup_logs_admin_url_without_printed_server_banner(tmp_path):
import api.runtime as api_runtime_mod
settings = _app_settings(
messaging_platform="none",
telegram_bot_token=None,
allowed_telegram_user_id=None,
discord_bot_token=None,
allowed_discord_channels=None,
allowed_dir=str(tmp_path / "workspace"),
claude_workspace=str(tmp_path / "data"),
host="127.0.0.1",
port=9099,
)
runtime = api_runtime_mod.AppRuntime(
app=FastAPI(), settings=cast(Settings, settings)
)
uvicorn_logger = MagicMock()
with (
patch("builtins.print") as printed,
patch.object(
api_runtime_mod.logging, "getLogger", return_value=uvicorn_logger
) as get_logger,
patch.object(api_runtime_mod.logger, "info") as app_info,
patch.object(ProviderRegistry, "validate_configured_models", new=AsyncMock()),
patch.object(ProviderRegistry, "start_model_list_refresh"),
patch.object(ProviderRegistry, "cleanup", new=AsyncMock()),
patch(
"messaging.platforms.factory.create_messaging_platform",
return_value=None,
),
):
await runtime.startup()
await runtime.shutdown()
printed.assert_not_called()
get_logger.assert_called_with("uvicorn.error")
uvicorn_logger.info.assert_called_once_with(
"Admin UI: %s (local-only)",
"http://127.0.0.1:9099/admin",
)
logged = " ".join(str(arg) for call in app_info.call_args_list for arg in call.args)
assert "Server URL:" not in logged
def test_create_app_provider_error_handler_returns_anthropic_format():
from api.app import create_app
from providers.exceptions import AuthenticationError