122 lines
5.0 KiB
JavaScript
122 lines
5.0 KiB
JavaScript
// Theme management (before anything else so it applies immediately, no flash)
|
|
var _themes = ['', 'theme-dark', 'theme-midnight'];
|
|
var _currentTheme = localStorage.getItem('clavitor_theme') || '';
|
|
if (_currentTheme) document.body.className = _currentTheme;
|
|
|
|
function cycleTheme() {
|
|
var idx = _themes.indexOf(_currentTheme);
|
|
_currentTheme = _themes[(idx + 1) % _themes.length];
|
|
document.body.className = _currentTheme;
|
|
localStorage.setItem('clavitor_theme', _currentTheme);
|
|
}
|
|
|
|
// Stateless auth: L1 Bearer from master key in sessionStorage.
|
|
function getL1Bearer() {
|
|
var masterB64 = sessionStorage.getItem('clavitor_master');
|
|
if (!masterB64) return null;
|
|
var bin = atob(masterB64);
|
|
var l1 = new Uint8Array(8);
|
|
for (var i = 0; i < 8; i++) l1[i] = bin.charCodeAt(i);
|
|
var b64 = btoa(String.fromCharCode.apply(null, l1));
|
|
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
|
|
// Global API helper. Every request sends L1 as Bearer.
|
|
async function api(method, path, body) {
|
|
var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
|
|
var bearer = getL1Bearer();
|
|
if (bearer) opts.headers['Authorization'] = 'Bearer ' + bearer;
|
|
if (body) opts.body = JSON.stringify(body);
|
|
var res = await fetch(path, opts);
|
|
if (res.status === 401) {
|
|
sessionStorage.removeItem('clavitor_master');
|
|
location.href = '/app/';
|
|
throw new Error('Unauthorized');
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
// --- Idle timer state (module-level, survives re-init) ---
|
|
var _idleTimer = null;
|
|
var _countdownTimer = null;
|
|
var _countdownVal = 0;
|
|
|
|
function _resetIdle() {
|
|
if (!sessionStorage.getItem('clavitor_master')) return;
|
|
clearTimeout(_idleTimer);
|
|
_clearCountdown();
|
|
_idleTimer = setTimeout(_startCountdown, 600000); // 10 minutes
|
|
}
|
|
|
|
function _startCountdown() {
|
|
_countdownVal = 15;
|
|
var banner = document.createElement('div');
|
|
banner.id = 'lockCountdownBanner';
|
|
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:9999;background:var(--gold,#D4AF37);color:#000;text-align:center;padding:0.5rem;font-weight:600;font-size:0.875rem;';
|
|
banner.innerHTML = 'Vault locking in <span id="lockCountdownSec">15</span>s — press any key to stay unlocked';
|
|
document.body.appendChild(banner);
|
|
|
|
_countdownTimer = setInterval(function() {
|
|
_countdownVal--;
|
|
if (_countdownVal <= 0) {
|
|
_clearCountdown();
|
|
sessionStorage.removeItem('clavitor_master');
|
|
window.location.href = '/app/';
|
|
} else {
|
|
var el = document.getElementById('lockCountdownSec');
|
|
if (el) el.textContent = _countdownVal;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
function _clearCountdown() {
|
|
clearInterval(_countdownTimer);
|
|
_countdownTimer = null;
|
|
var b = document.getElementById('lockCountdownBanner');
|
|
if (b) b.remove();
|
|
}
|
|
|
|
// Initialize topbar: populate nav, start idle timer, check vault lock.
|
|
// Called on page load AND after restoreAppLayout() rebuilds the DOM.
|
|
function initTopbar() {
|
|
var path = location.pathname.replace(/\/$/, '');
|
|
var isVault = (path === '/app' || path === '');
|
|
var nav = '';
|
|
nav += '<a href="/app/"' + (path === '/app' ? ' class="topbar-active"' : '') + '>Vault</a>';
|
|
nav += '<a href="/app/import.html"' + (path.indexOf('import') >= 0 ? ' class="topbar-active"' : '') + '>Import</a>';
|
|
if (typeof showAudit === 'function') nav += '<a href="#" onclick="showAudit();return false">Audit</a>';
|
|
if (typeof showAgents === 'function') nav += '<a href="#" onclick="showAgents();return false">Agents</a>';
|
|
nav += '<span style="font-size:0.65rem;opacity:0.35;margin-left:auto;font-family:var(--font-mono)">v2.0.44</span>';
|
|
nav += '<button onclick="if(typeof lockVault===\'function\'){lockVault()}else{sessionStorage.removeItem(\'clavitor_master\');location.href=\'/app/\'}" class="topbar-lock">Lock</button>';
|
|
|
|
var logo = '<a href="/app/" class="topbar-lockup" style="display:inline-flex;gap:10px;align-items:center;text-decoration:none">' +
|
|
'<span style="width:32px;height:32px;background:var(--text);flex-shrink:0"></span>' +
|
|
'<span style="font-family:var(--font-sans);font-size:18px;font-weight:700;letter-spacing:0.25em;text-transform:uppercase;color:var(--text);line-height:1">CLAVITOR</span>' +
|
|
'</a>';
|
|
|
|
var el = document.getElementById('topbar');
|
|
if (el) {
|
|
el.innerHTML =
|
|
'<header class="topbar">' +
|
|
'<div class="topbar-inner" style="padding:0 1rem">' +
|
|
logo +
|
|
'<div class="topbar-links">' + nav + '</div>' +
|
|
'</div>' +
|
|
'</header>';
|
|
}
|
|
|
|
// Start idle timer if authenticated
|
|
if (getL1Bearer()) {
|
|
_resetIdle();
|
|
['keydown', 'mousedown', 'touchstart'].forEach(function(evt) {
|
|
document.removeEventListener(evt, _resetIdle, true); // prevent duplicates
|
|
document.addEventListener(evt, _resetIdle, true);
|
|
});
|
|
|
|
// Vault lock removed in v2 — scoped access replaces it.
|
|
}
|
|
}
|
|
|
|
// Run on initial page load
|
|
initTopbar();
|