diff --git a/README.md b/README.md index 36194d6..a85ba6f 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/api/admin_static/admin.css b/api/admin_static/admin.css index 93abda2..c8d795e 100644 --- a/api/admin_static/admin.css +++ b/api/admin_static/admin.css @@ -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; diff --git a/api/admin_static/admin.js b/api/admin_static/admin.js index b1f6e20..56dc08b 100644 --- a/api/admin_static/admin.js +++ b/api/admin_static/admin.js @@ -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"); }); diff --git a/api/admin_static/index.html b/api/admin_static/index.html index 0178017..90ef15b 100644 --- a/api/admin_static/index.html +++ b/api/admin_static/index.html @@ -23,13 +23,8 @@
-

Local Admin

Providers

-
- Loading - -
diff --git a/api/runtime.py b/api/runtime.py index 064e0fd..2090c28 100644 --- a/api/runtime.py +++ b/api/runtime.py @@ -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) diff --git a/tests/api/test_admin.py b/tests/api/test_admin.py index c7c2ab4..71a4fec 100644 --- a/tests/api/test_admin.py +++ b/tests/api/test_admin.py @@ -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") diff --git a/tests/api/test_app_lifespan_and_errors.py b/tests/api/test_app_lifespan_and_errors.py index cbb0a1c..aba0ab9 100644 --- a/tests/api/test_app_lifespan_and_errors.py +++ b/tests/api/test_app_lifespan_and_errors.py @@ -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