// app.jsx — application root: auth gate, login screen, shell
// (sidebar / topbar / connection banner / route outlet / ⌘K palette).
// Depends on globals from icons.jsx, lib.jsx and components.jsx.
(() => {
const { useState, useEffect, useRef, useMemo, useCallback } = React;
const {
Icon, api, useLive, ROUTES, useRoute, fmtUptime, timeAgo,
Button, IconButton, Badge, Field, Input, Checkbox, Spinner,
EmptyState, ToastProvider, useToast, ErrorBoundary, CommandPalette,
} = window;
const THEME_KEY = "ccp.theme";
/* ────────────────────────────────────────────
Theme
──────────────────────────────────────────── */
function useTheme() {
const [theme, setTheme] = useState(() =>
document.documentElement.dataset.theme === "dark" ? "dark" : "light"
);
const toggle = useCallback(() => {
setTheme((prev) => {
const next = prev === "dark" ? "light" : "dark";
document.documentElement.dataset.theme = next;
try { localStorage.setItem(THEME_KEY, next); } catch (e) {}
return next;
});
}, []);
return [theme, toggle];
}
function ThemeToggle({ theme, onToggle, bordered }) {
return (
);
}
/* ────────────────────────────────────────────
Splash (initial auth check)
──────────────────────────────────────────── */
function Splash() {
return (
);
}
/* ────────────────────────────────────────────
Login screen — login + first-run setup variants.
URL never changes here; the bookmarked route is
restored as soon as the user signs in.
──────────────────────────────────────────── */
function LoginScreen({ auth, expired, onAuthenticated, theme, onToggleTheme }) {
const isSetup = auth.configured === false;
const [username, setUsername] = useState(auth.username || "admin");
const [password, setPassword] = useState("");
const [remember, setRemember] = useState(true); // default CHECKED
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const submit = async (e) => {
e.preventDefault();
if (busy) return;
setBusy(true);
setError("");
try {
if (isSetup) {
await api.post("/ui/api/auth/setup", { username, password, remember: true });
} else {
await api.post("/ui/api/auth/login", { username, password, remember });
}
await onAuthenticated();
} catch (err) {
setError((err && err.message) || String(err));
} finally {
setBusy(false);
}
};
return (
Connect AI Proxy · local control panel
);
}
/* ────────────────────────────────────────────
Sidebar
──────────────────────────────────────────── */
function Sidebar({ routeId, navigate, liveStatus, onNavigated }) {
const groups = useMemo(() => {
const map = new Map();
ROUTES.forEach((r) => {
if (!map.has(r.group)) map.set(r.group, []);
map.get(r.group).push(r);
});
return Array.from(map.entries());
}, []);
const modelCount =
liveStatus && Array.isArray(liveStatus.models) ? liveStatus.models.length : null;
const version = liveStatus && liveStatus.version ? String(liveStatus.version) : null;
const update = liveStatus && liveStatus.update;
const go = (id) => {
navigate(id);
if (onNavigated) onNavigated();
};
return (
);
}
/* ────────────────────────────────────────────
Topbar
──────────────────────────────────────────── */
function UserMenu({ username, onLogout }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!open) return undefined;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
document.addEventListener("mousedown", onDoc);
window.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onDoc);
window.removeEventListener("keydown", onKey);
};
}, [open]);
return (
{open && (
Signed in as {username || "admin"}
API docs
)}
);
}
function Topbar({ liveStatus, onToggleDrawer, onOpenPalette, theme, onToggleTheme, username, onLogout }) {
const toast = useToast();
const hasStatus = !!liveStatus;
const running = !!(liveStatus && liveStatus.proxy_running);
const url =
(liveStatus && (liveStatus.display_url || liveStatus.public_url || liveStatus.local_url)) ||
"http://127.0.0.1:4000";
const copyUrl = () => {
if (navigator.clipboard) {
navigator.clipboard.writeText(url)
.then(() => toast.success("Endpoint URL copied"))
.catch(() => toast.error("Could not copy URL"));
}
};
return (
{hasStatus && (
{running ? "Running" : "Stopped"}
)}
{hasStatus && running && liveStatus.uptime_seconds != null && (
up {fmtUptime(liveStatus.uptime_seconds)}
)}
);
}
/* ────────────────────────────────────────────
Connection banner
──────────────────────────────────────────── */
function ConnectionBanner({ reconnecting, lastUpdated, updating }) {
if (updating) {
return (
Service restarting for update… the panel will reconnect automatically.
);
}
if (reconnecting) {
return (
Reconnecting… last updated {timeAgo(lastUpdated)}
);
}
return null;
}
/* ────────────────────────────────────────────
Shell — owns the single 2s /ui/api/status poll
──────────────────────────────────────────── */
function Shell({ username, onLoggedOut, theme, onToggleTheme }) {
const { route, routeId, navigate } = useRoute();
const toast = useToast();
const [paletteOpen, setPaletteOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const {
data: liveStatus,
error: statusError,
stale: statusStale,
reconnecting,
lastUpdated,
refresh: refreshStatus,
} = useLive("/ui/api/status", { interval: 2000, enabled: true });
const liveStatusRef = useRef(liveStatus);
liveStatusRef.current = liveStatus;
const pushToast = useCallback((msg, tone = "info") => {
toast.push(msg, { tone });
}, [toast]);
const onLogout = useCallback(async () => {
try { await api.post("/ui/api/auth/logout"); } catch (e) {}
onLoggedOut();
}, [onLoggedOut]);
// Ported proxy/update action handler (start/stop/restart/validate/
// update/check-update/toggle-auto-update) with toasts.
const onAction = useCallback(async (action) => {
try {
if (action === "start") {
await api.post("/ui/api/proxy/start");
toast.success("Proxy endpoints started");
} else if (action === "stop") {
await api.post("/ui/api/proxy/stop");
toast.info("Proxy endpoints stopped — the control panel stays online");
} else if (action === "restart") {
await api.post("/ui/api/proxy/restart");
toast.info("Proxy restarting");
} else if (action === "validate") {
const res = await api.get("/ui/api/validate");
const steps = (res && res.steps) || [];
const passed = steps.filter((s) => s.ok).length;
const allOk = passed === steps.length && steps.length > 0;
toast.push(`Validation complete — ${passed}/${steps.length} checks passed`, { tone: allOk ? "success" : "error" });
} else if (action === "update") {
await api.post("/ui/api/update/start");
toast.info("Update started — waiting for reconnect");
} else if (action === "check-update") {
await api.post("/ui/api/update/check");
toast.success("Update check complete");
} else if (action === "toggle-auto-update") {
const cur = liveStatusRef.current;
const enabled = !(cur && cur.update && cur.update.auto_update);
await api.post("/ui/api/update/settings", { auto_update: enabled });
toast.success(enabled ? "Auto update enabled" : "Auto update disabled");
}
refreshStatus();
} catch (e) {
if (action === "update") {
toast.info("Update is running — reconnecting soon");
return;
}
toast.error(`Action failed — ${(e && e.message) || e}`);
}
}, [toast, refreshStatus]);
// Keyboard: Ctrl/⌘+K palette; bare digits 1-9 navigate (guarded).
useEffect(() => {
const onKey = (e) => {
if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === "k") {
e.preventDefault();
setPaletteOpen((o) => !o);
return;
}
if (e.ctrlKey || e.metaKey || e.altKey) return;
const t = e.target;
if (
t &&
(t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable)
) return;
const idx = "123456789".indexOf(e.key);
if (idx >= 0 && idx < ROUTES.length) navigate(ROUTES[idx].id);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [navigate]);
const running = !!(liveStatus && liveStatus.proxy_running);
const paletteActions = useMemo(() => [
running
? { id: "stop", label: "Stop proxy endpoints", icon: "stop", run: () => onAction("stop") }
: { id: "start", label: "Start proxy endpoints", icon: "play", run: () => onAction("start") },
{ id: "restart", label: "Restart proxy", icon: "restart", run: () => onAction("restart") },
{ id: "validate", label: "Run validation", icon: "shield", run: () => onAction("validate") },
{ id: "check-update", label: "Check for updates", icon: "download", run: () => onAction("check-update") },
{
id: "toggle-theme",
label: theme === "dark" ? "Switch to light theme" : "Switch to dark theme",
icon: theme === "dark" ? "sun" : "moon",
run: onToggleTheme,
},
{ id: "logout", label: "Log out", icon: "log-out", run: onLogout },
], [running, theme, onAction, onToggleTheme, onLogout]);
const ScreenComponent = window[route.component];
const updating = !!(liveStatus && liveStatus.update && liveStatus.update.running && statusError);
return (
setDrawerOpen(false)}
/>
{drawerOpen && (
);
}
/* ────────────────────────────────────────────
App root — auth gate
──────────────────────────────────────────── */
function App() {
const [theme, toggleTheme] = useTheme();
const [auth, setAuth] = useState({
loading: true,
configured: true,
authenticated: false,
username: "",
expired: false,
});
const refreshAuth = useCallback(async () => {
try {
const res = await api.get("/ui/api/auth/status");
setAuth({ loading: false, expired: false, configured: true, authenticated: false, username: "", ...res });
} catch (e) {
setAuth((a) => ({ ...a, loading: false, authenticated: false }));
}
}, []);
useEffect(() => {
api.onAuthExpired = () => {
setAuth((a) => (a.authenticated ? { ...a, authenticated: false, expired: true } : a));
};
refreshAuth();
return () => { api.onAuthExpired = null; };
}, [refreshAuth]);
let body;
if (auth.loading) {
body = ;
} else if (!auth.authenticated) {
body = (
);
} else {
body = (
);
}
return {body};
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render();
})();