/* ========================================================================== librus-tui frontend - authentication, session chrome, clock ========================================================================== */ "use strict"; function initClock() { const update = () => { const now = new Date(); const dateElement = $("#date"); const clockElement = $("#clock"); if (dateElement) { dateElement.textContent = now .toLocaleDateString("en-GB", { weekday: "short", day: "2-digit", month: "short", year: "numeric", }) .toUpperCase(); } if (clockElement) { clockElement.textContent = now.toLocaleTimeString("en-GB", { hour12: false }); } }; update(); setInterval(update, 1000); } function updateDemoUI() { const demoButton = $("#demo-btn"); if (demoButton) demoButton.classList.toggle("hidden", !state.config.demo); const version = $("#version"); if (version) version.textContent = state.config.version || "1.0.0"; } function setLoginBusy(busy) { const loginInput = $("#login"); const passwordInput = $("#password"); const loginButton = $("#login-btn"); const demoButton = $("#demo-btn"); if (loginInput) loginInput.disabled = busy; if (passwordInput) passwordInput.disabled = busy; if (loginButton) { loginButton.disabled = busy; loginButton.textContent = busy ? "[ authenticating… ]" : "[ authenticate ]"; } if (demoButton) { demoButton.disabled = busy; demoButton.textContent = busy ? "[ ... ]" : "[ enter demo mode ]"; } } function showLogin(message) { state.session = null; state.loginBusy = false; state.loadToken += 1; state.messageToken += 1; stopSpinner(); stopBootTimer(); setLoginBusy(false); const loginScreen = $("#login-screen"); const appScreen = $("#app-screen"); if (loginScreen) loginScreen.classList.remove("hidden"); if (appScreen) appScreen.classList.add("hidden"); const demoBadge = $("#demo-badge"); if (demoBadge) demoBadge.classList.add("hidden"); const version = $("#version"); if (version) version.textContent = state.config.version || "1.0.0"; setLed("offline"); setMeta(""); setStatus("ready"); const statusRight = $("#status-right"); if (statusRight) statusRight.textContent = "synergia://offline"; const sideStatus = $("#side-status"); if (sideStatus) sideStatus.textContent = "offline"; const sideName = $("#side-user-name"); if (sideName) sideName.textContent = "disconnected"; const sideMeta = $("#side-user-meta"); if (sideMeta) sideMeta.textContent = "synergia://"; const prompt = $("#prompt"); if (prompt) prompt.textContent = "~$"; closeCommandBar(); closeModal(); document.title = "librus-tui :: synergia"; history.replaceState(null, "", location.pathname); const view = $("#view"); if (view) view.innerHTML = ""; if (message) { clearBoot(); appendBoot("err", `!! ${message}`); appendBoot("", " enter credentials to reconnect"); } updateDemoUI(); if (state.config.demo) { appendBoot("warn", "[!] demo mode available — no Librus account required"); } setTimeout(() => { const loginInput = $("#login"); if (loginInput) loginInput.focus(); }, 40); } function enterApp(login, demo) { state.session = { authenticated: true, login: login || "user", demo: !!demo }; state.loginBusy = false; setLoginBusy(false); stopBootTimer(); stopSpinner(); const loginScreen = $("#login-screen"); const appScreen = $("#app-screen"); if (loginScreen) loginScreen.classList.add("hidden"); if (appScreen) appScreen.classList.remove("hidden"); const demoBadge = $("#demo-badge"); if (demoBadge) demoBadge.classList.toggle("hidden", !demo); const version = $("#version"); if (version) version.textContent = state.config.version || "1.0.0"; setLed("online"); setStatus("[ok] session established", "ok"); const sideStatus = $("#side-status"); if (sideStatus) sideStatus.textContent = "connected"; const sideName = $("#side-user-name"); if (sideName) sideName.textContent = login || "connected"; const sideMeta = $("#side-user-meta"); if (sideMeta) sideMeta.textContent = demo ? "demo dataset" : "synergia://connected"; const statusRight = $("#status-right"); if (statusRight) statusRight.textContent = demo ? "synergia://demo" : "synergia://connected"; const prompt = $("#prompt"); if (prompt) prompt.textContent = `${String(login || "user").toLowerCase()}@librus:~$`; toast(demo ? "Demo session started." : "Signed in to Synergia.", "ok"); startNotificationPolling(); const requested = location.hash.replace(/^#/, ""); const target = VIEWS.includes(requested) ? requested : "dashboard"; navigate(target); } function applyUserChrome(bootstrap) { const me = (bootstrap && bootstrap.me) || {}; const school = (bootstrap && bootstrap.school) || {}; const fullName = personName(me) || state.session?.login || "student"; const sideName = $("#side-user-name"); if (sideName) sideName.textContent = fullName; const sideMeta = $("#side-user-meta"); if (sideMeta) { const classPart = me.Class ? `class ${me.Class}` : "student"; const schoolPart = school.Name ? ` · ${truncate(school.Name, 30)}` : ""; sideMeta.textContent = classPart + schoolPart; } const prompt = $("#prompt"); if (prompt) { const shortName = (me.FirstName || state.session?.login || "user").toLowerCase(); prompt.textContent = `${shortName}@librus:~$`; } const statusRight = $("#status-right"); if (statusRight) { statusRight.textContent = state.session?.demo ? "synergia://demo" : "synergia://connected"; } } async function submitLogin(demo = false) { if (state.loginBusy) return; const loginInput = $("#login"); const passwordInput = $("#password"); const login = demo ? "demo" : (loginInput?.value || "").trim(); const password = demo ? "" : passwordInput?.value || ""; if (!demo && (!login || !password)) { toast("Login and password are required.", "warn"); (login ? passwordInput : loginInput)?.focus(); return; } state.loginBusy = true; setLoginBusy(true); clearBoot(); setLed("busy"); setStatus("[...] authenticating with synergia"); setMeta("oauth handshake…"); appendBoot("cmd", " $ librus-tui auth --oauth client_id=46"); startBootTimer(); try { const result = await api("/api/login", { method: "POST", body: demo ? { demo: true } : { login, password }, }); stopBootTimer(); appendBoot("ok", "[ ok ] credentials accepted"); appendBoot("info", " session established · cookies held server-side"); await sleep(420); appendBoot("cmd", " launching interface …"); if (passwordInput) passwordInput.value = ""; state.session = { authenticated: true, login: demo ? "demo" : login, demo: !!(result && result.demo), }; await sleep(240); enterApp(demo ? "demo" : login, !!(result && result.demo)); } catch (error) { stopBootTimer(); appendBoot("err", `[fail] ${error.message}`); setLed("offline"); setStatus("authentication failed", "error"); setMeta("login failed"); toast(error.message, "err", 7000); state.loginBusy = false; setLoginBusy(false); passwordInput?.focus(); } } function handleExpiredSession() { if (!state.session) return; showLogin("Session expired or revoked by Synergia."); toast("Your session expired. Sign in again.", "warn", 6000); } function confirmLogout() { openModal( "logout", `

Close the current Synergia session?

Server-side cookies are discarded immediately. Nothing is persisted.

`, [ { label: "[ logout ]", action: "logout-confirm", kind: "btn-warn" }, { label: "[ cancel ]", close: true }, ] ); } async function doLogout() { closeModal(); const wasLoggedIn = !!(state.session && state.session.authenticated); if (wasLoggedIn) { try { await api("/api/logout", { method: "POST" }); } catch (error) { /* the local session is dropped regardless */ } } stopNotificationPolling(); state.session = null; state.messages = []; state.messageIndex = -1; state.messageCache.clear(); state.lastNotifySnapshot = null; showLogin("session closed."); toast("Logged out.", "ok"); }