cc-dashboard/src/static/js/app.js
Vadym Samoilenko 436a089e63 fix: wrap login in form tag, remove QUIC for SSE endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 13:22:48 +00:00

157 lines
5.1 KiB
JavaScript

/**
* SPA Router & app init.
*/
const App = (() => {
let _currentUser = null;
const NAV = [
{ id: 'dashboard', label: 'Dashboard', icon: '◈', page: DashboardPage },
{ id: 'projects', label: 'Projects', icon: '▤', page: ProjectsPage },
{ id: 'live', label: 'Live Feed', icon: '⚡', page: LivePage },
{ id: 'keys', label: 'API Keys', icon: '⌘', page: KeysPage },
{ id: 'settings', label: 'Settings', icon: '⚙', page: SettingsPage },
];
async function init() {
if (!Api.isLoggedIn()) {
renderLogin();
return;
}
try {
_currentUser = await Api.get('/api/auth/me');
} catch {
renderLogin();
return;
}
renderShell();
SSE.connect();
// Route from hash
const hash = location.hash.replace('#', '') || 'dashboard';
const [page, param] = hash.split('/');
navigate(page, param);
}
function renderLogin() {
document.getElementById('app').innerHTML = `
<div id="login-page">
<div class="login-box">
<div class="logo">CC<span class="accent">.</span>Dashboard</div>
<div id="login-error" class="error-msg"></div>
<form id="login-form" autocomplete="on" onsubmit="return false">
<div class="form-group">
<label>Email</label>
<input type="email" id="login-email" placeholder="you@example.com" autocomplete="email">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="login-password" placeholder="••••••••" autocomplete="current-password">
</div>
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;margin-top:8px" id="btn-login">
Sign In
</button>
</form>
</div>
</div>
`;
const doLogin = async () => {
const email = document.getElementById('login-email').value.trim();
const password = document.getElementById('login-password').value;
const errEl = document.getElementById('login-error');
errEl.style.display = 'none';
try {
await Api.login(email, password);
init();
} catch (e) {
errEl.textContent = e.message;
errEl.style.display = 'block';
}
};
document.getElementById('btn-login').onclick = doLogin;
document.getElementById('login-form').addEventListener('submit', doLogin);
}
function renderShell() {
const isAdmin = _currentUser?.role === 'admin';
const navItems = [...NAV, ...(isAdmin ? [{ id: 'admin', label: 'Admin', icon: '⚀', page: AdminPage }] : [])];
document.getElementById('app').innerHTML = `
<div class="sidebar">
<div class="sidebar-logo">
<span>CC<span class="accent">.</span>Dashboard</span>
</div>
<nav class="sidebar-nav" id="sidebar-nav">
${navItems.map(n => `
<button class="nav-item" data-page="${n.id}" onclick="App.navigate('${n.id}')">
<span class="icon">${n.icon}</span>
${n.label}
</button>
`).join('')}
</nav>
<div class="sidebar-bottom">
<div class="sidebar-user">
<strong>${_currentUser?.username || ''}</strong>
${_currentUser?.email || ''}
</div>
</div>
</div>
<div class="main">
<div class="topbar">
<h1 id="page-title">Dashboard</h1>
<span class="sse-dot" id="sse-dot"></span>
</div>
<div class="content" id="page-content"></div>
</div>
`;
SSE.setDot(document.getElementById('sse-dot'));
}
function navigate(pageId, param) {
const isAdmin = _currentUser?.role === 'admin';
const allPages = { dashboard: DashboardPage, projects: ProjectsPage, live: LivePage, keys: KeysPage, settings: SettingsPage };
if (isAdmin) allPages.admin = AdminPage;
// Special: project detail
if (pageId === 'project' && param) {
location.hash = `project/${param}`;
const content = document.getElementById('page-content');
if (!content) return;
document.getElementById('page-title').textContent = 'Project Detail';
_setActiveNav(null);
ProjectDetailPage.render(content, param);
return;
}
const page = allPages[pageId] || DashboardPage;
const navDef = [...NAV, { id: 'admin' }].find(n => n.id === pageId) || NAV[0];
location.hash = pageId;
const content = document.getElementById('page-content');
if (!content) return;
document.getElementById('page-title').textContent =
NAV.find(n => n.id === pageId)?.label || (pageId === 'admin' ? 'Admin' : 'Dashboard');
_setActiveNav(pageId);
page.render(content, param);
}
function _setActiveNav(pageId) {
document.querySelectorAll('.nav-item').forEach(el => {
el.classList.toggle('active', el.dataset.page === pageId);
});
}
return { init, navigate };
})();
document.addEventListener('DOMContentLoaded', () => App.init());
window.addEventListener('hashchange', () => {
const hash = location.hash.replace('#', '') || 'dashboard';
const [page, param] = hash.split('/');
App.navigate(page, param);
});