- Remove dock button creation from display.js initGuiApps (apps belong in right panel) - Register GUI app modules dynamically in loadPanelApps for newly added apps - Refresh right panel immediately after adding/removing apps in Settings - Expose loadPanelApps globally for cross-module panel refresh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
714 lines
27 KiB
JavaScript
714 lines
27 KiB
JavaScript
/* Atlus — Core shell: app switching, WebSocket stats, panel updates */
|
|
(function () {
|
|
'use strict';
|
|
|
|
// =====================================================================
|
|
// Auth guard
|
|
// =====================================================================
|
|
const TOKEN = localStorage.getItem('atlus_token');
|
|
const USER = localStorage.getItem('atlus_user');
|
|
if (!TOKEN) {
|
|
window.location.href = '/';
|
|
return;
|
|
}
|
|
|
|
// =====================================================================
|
|
// Globals
|
|
// =====================================================================
|
|
window.Atlus = {
|
|
token: TOKEN,
|
|
user: USER,
|
|
apps: {}, // registered app modules { id: { init, destroy, title } }
|
|
openApps: [], // ordered list of open app ids
|
|
activeApp: null, // currently focused app id
|
|
layout: 'single', // 'single' | 'split'
|
|
secondaryApp: null,
|
|
|
|
/** Authenticated fetch wrapper */
|
|
async apiFetch(url, opts = {}) {
|
|
opts.headers = Object.assign({ 'Authorization': `Bearer ${TOKEN}` }, opts.headers || {});
|
|
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
|
|
opts.headers['Content-Type'] = 'application/json';
|
|
opts.body = JSON.stringify(opts.body);
|
|
}
|
|
const res = await fetch(url, opts);
|
|
if (res.status === 401) {
|
|
localStorage.removeItem('atlus_token');
|
|
localStorage.removeItem('atlus_user');
|
|
window.location.href = '/';
|
|
return;
|
|
}
|
|
return res;
|
|
},
|
|
|
|
/** Create an authenticated WebSocket URL */
|
|
wsUrl(path) {
|
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const sep = path.includes('?') ? '&' : '?';
|
|
return `${proto}//${location.host}${path}${sep}token=${TOKEN}`;
|
|
},
|
|
|
|
/** Register an app module */
|
|
registerApp(id, module) {
|
|
this.apps[id] = module;
|
|
},
|
|
|
|
/** Format bytes to human readable */
|
|
formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
},
|
|
};
|
|
|
|
// =====================================================================
|
|
// DOM refs
|
|
// =====================================================================
|
|
const $ = (sel) => document.querySelector(sel);
|
|
const $$ = (sel) => document.querySelectorAll(sel);
|
|
|
|
const stageTabs = $('#stageTabs');
|
|
const paneA = $('#paneContentA');
|
|
const paneB = $('#paneContentB');
|
|
const paneTitleA = $('#paneTitleA');
|
|
const paneTitleB = $('#paneTitleB');
|
|
const paneBEl = $('#paneB');
|
|
const welcomeScreen = $('#welcomeScreen');
|
|
|
|
// =====================================================================
|
|
// App switching
|
|
// =====================================================================
|
|
|
|
function openApp(appId) {
|
|
const app = Atlus.apps[appId];
|
|
if (!app) return;
|
|
|
|
// Already open — just focus
|
|
if (Atlus.openApps.includes(appId)) {
|
|
focusApp(appId);
|
|
return;
|
|
}
|
|
|
|
Atlus.openApps.push(appId);
|
|
addTab(appId, app.title || appId);
|
|
|
|
// Create app container
|
|
const container = document.createElement('div');
|
|
container.className = 'app-view';
|
|
container.id = `app-${appId}`;
|
|
container.style.display = 'none';
|
|
paneA.appendChild(container);
|
|
|
|
// Initialize app
|
|
if (app.init) app.init(container);
|
|
|
|
focusApp(appId);
|
|
saveDesktopState();
|
|
}
|
|
|
|
function focusApp(appId) {
|
|
if (welcomeScreen) welcomeScreen.style.display = 'none';
|
|
|
|
// Hide all apps in pane A
|
|
paneA.querySelectorAll('.app-view').forEach(el => el.style.display = 'none');
|
|
|
|
// Show target
|
|
const target = $(`#app-${appId}`);
|
|
if (target) target.style.display = 'flex';
|
|
|
|
// Update tabs
|
|
stageTabs.querySelectorAll('.stage-tab').forEach(tab => {
|
|
tab.classList.toggle('active', tab.dataset.app === appId);
|
|
});
|
|
|
|
// Update dock
|
|
$$('.dock-item[data-app]').forEach(item => {
|
|
item.classList.toggle('active', item.dataset.app === appId);
|
|
});
|
|
|
|
// Update titlebar
|
|
const app = Atlus.apps[appId];
|
|
paneTitleA.textContent = app ? app.title : appId;
|
|
|
|
Atlus.activeApp = appId;
|
|
|
|
// Notify app it got focus
|
|
if (app && app.onFocus) app.onFocus();
|
|
|
|
saveDesktopState();
|
|
}
|
|
|
|
function closeApp(appId) {
|
|
const app = Atlus.apps[appId];
|
|
if (app && app.destroy) app.destroy();
|
|
|
|
// Remove DOM
|
|
const container = $(`#app-${appId}`);
|
|
if (container) container.remove();
|
|
|
|
// Remove from open list
|
|
Atlus.openApps = Atlus.openApps.filter(id => id !== appId);
|
|
|
|
// Remove tab
|
|
const tab = stageTabs.querySelector(`.stage-tab[data-app="${appId}"]`);
|
|
if (tab) tab.remove();
|
|
|
|
// Update dock
|
|
const dockItem = $(`.dock-item[data-app="${appId}"]`);
|
|
if (dockItem) dockItem.classList.remove('active');
|
|
|
|
// Focus next app or show welcome
|
|
if (Atlus.activeApp === appId) {
|
|
if (Atlus.openApps.length > 0) {
|
|
focusApp(Atlus.openApps[Atlus.openApps.length - 1]);
|
|
} else {
|
|
Atlus.activeApp = null;
|
|
paneTitleA.textContent = '';
|
|
if (welcomeScreen) welcomeScreen.style.display = 'flex';
|
|
}
|
|
}
|
|
saveDesktopState();
|
|
}
|
|
|
|
function addTab(appId, title) {
|
|
const tab = document.createElement('button');
|
|
tab.className = 'stage-tab';
|
|
tab.dataset.app = appId;
|
|
tab.innerHTML = `<span>${title}</span><span class="tab-close">×</span>`;
|
|
tab.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('tab-close')) {
|
|
closeApp(appId);
|
|
} else {
|
|
focusApp(appId);
|
|
}
|
|
});
|
|
stageTabs.appendChild(tab);
|
|
}
|
|
|
|
// =====================================================================
|
|
// Dock clicks
|
|
// =====================================================================
|
|
$$('.dock-item[data-app]').forEach(item => {
|
|
item.addEventListener('click', () => openApp(item.dataset.app));
|
|
});
|
|
|
|
// Layout toggle
|
|
$$('.layout-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const layout = btn.dataset.layout;
|
|
Atlus.layout = layout;
|
|
$$('.layout-btn').forEach(b => b.classList.toggle('active', b.dataset.layout === layout));
|
|
paneBEl.classList.toggle('hidden', layout === 'single');
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// System menu
|
|
// =====================================================================
|
|
const systemMenu = $('#systemMenu');
|
|
const logoBtn = $('.dock-logo');
|
|
|
|
logoBtn.addEventListener('click', () => {
|
|
systemMenu.classList.toggle('hidden');
|
|
});
|
|
|
|
systemMenu.addEventListener('click', async (e) => {
|
|
const action = e.target.dataset.action;
|
|
if (!action) {
|
|
// Clicked backdrop
|
|
if (e.target === systemMenu) systemMenu.classList.add('hidden');
|
|
return;
|
|
}
|
|
systemMenu.classList.add('hidden');
|
|
|
|
if (action === 'logout') {
|
|
await Atlus.apiFetch('/api/auth/logout', { method: 'POST' });
|
|
localStorage.removeItem('atlus_token');
|
|
localStorage.removeItem('atlus_user');
|
|
window.location.href = '/';
|
|
}
|
|
});
|
|
|
|
// =====================================================================
|
|
// Panel — Clock
|
|
// =====================================================================
|
|
function updateClock() {
|
|
const now = new Date();
|
|
$('#panelDate').textContent = now.toLocaleDateString('en-US', {
|
|
weekday: 'short', month: 'short', day: 'numeric'
|
|
});
|
|
$('#panelTime').textContent = now.toLocaleTimeString('en-US', {
|
|
hour: '2-digit', minute: '2-digit', hour12: false
|
|
});
|
|
}
|
|
updateClock();
|
|
setInterval(updateClock, 1000);
|
|
|
|
// =====================================================================
|
|
// Panel — WebSocket stats
|
|
// =====================================================================
|
|
let statsWs = null;
|
|
|
|
function connectStats() {
|
|
statsWs = new WebSocket(Atlus.wsUrl('/api/stats/ws'));
|
|
|
|
statsWs.onmessage = (e) => {
|
|
const data = JSON.parse(e.data);
|
|
updatePanel(data);
|
|
};
|
|
|
|
statsWs.onclose = () => {
|
|
setTimeout(connectStats, 3000);
|
|
};
|
|
|
|
statsWs.onerror = () => {
|
|
statsWs.close();
|
|
};
|
|
}
|
|
|
|
function updatePanel(data) {
|
|
// CPU
|
|
const cpuPct = Math.round(data.cpu_percent);
|
|
$('#statCpu').textContent = cpuPct + '%';
|
|
updateBar($('#statCpuBar'), cpuPct);
|
|
|
|
// Memory
|
|
const memPct = Math.round(data.memory.percent);
|
|
const memUsed = Atlus.formatBytes(data.memory.used);
|
|
const memTotal = Atlus.formatBytes(data.memory.total);
|
|
$('#statMem').textContent = `${memUsed} / ${memTotal}`;
|
|
updateBar($('#statMemBar'), memPct);
|
|
|
|
// Disk
|
|
const diskPct = Math.round(data.disk.percent);
|
|
$('#statDisk').textContent = diskPct + '%';
|
|
updateBar($('#statDiskBar'), diskPct);
|
|
|
|
// Temp
|
|
if (data.cpu_temp !== null) {
|
|
const temp = Math.round(data.cpu_temp);
|
|
$('#statTemp').textContent = temp + '\u00B0C';
|
|
// Temp bar: 0-85°C range
|
|
const tempPct = Math.min(100, Math.round((temp / 85) * 100));
|
|
updateBar($('#statTempBar'), tempPct);
|
|
}
|
|
|
|
// Network
|
|
const netContainer = $('#panelNetwork');
|
|
netContainer.innerHTML = '';
|
|
const ifaces = data.network.interfaces;
|
|
for (const [name, info] of Object.entries(ifaces)) {
|
|
const item = document.createElement('div');
|
|
item.className = 'panel-net-item';
|
|
item.innerHTML = `
|
|
<span class="panel-net-dot ${info.up ? 'up' : 'down'}"></span>
|
|
<span class="panel-net-name">${name}</span>
|
|
<span class="panel-net-ip">${info.ipv4 || '--'}</span>
|
|
`;
|
|
netContainer.appendChild(item);
|
|
}
|
|
}
|
|
|
|
function updateBar(barEl, percent) {
|
|
barEl.style.width = percent + '%';
|
|
barEl.className = 'stat-bar-fill';
|
|
if (percent >= 90) barEl.classList.add('crit');
|
|
else if (percent >= 70) barEl.classList.add('warn');
|
|
}
|
|
|
|
// =====================================================================
|
|
// Panel — Hostname
|
|
// =====================================================================
|
|
async function loadHostname() {
|
|
try {
|
|
const res = await Atlus.apiFetch('/api/settings/system');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
$('#panelHostname').textContent = data.hostname;
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
// =====================================================================
|
|
// Panel — Applications (docked GUI apps)
|
|
// =====================================================================
|
|
async function loadPanelApps() {
|
|
const container = $('#panelApps');
|
|
if (!container) return;
|
|
|
|
try {
|
|
const cfgRes = await Atlus.apiFetch('/api/settings');
|
|
if (!cfgRes.ok) return;
|
|
const cfg = await cfgRes.json();
|
|
const guiApps = cfg.gui_apps || [];
|
|
|
|
// Get running apps (may 503 if display deps unavailable)
|
|
let runningApps = [];
|
|
try {
|
|
const runRes = await Atlus.apiFetch('/api/display/apps');
|
|
if (runRes.ok) runningApps = await runRes.json();
|
|
} catch (e) { /* ignore */ }
|
|
|
|
// Ensure all GUI apps are registered as Atlus app modules
|
|
for (const app of guiApps) {
|
|
const appId = 'gui-' + app.id;
|
|
if (!Atlus.apps[appId] && window._atlusRegisterGuiApp) {
|
|
window._atlusRegisterGuiApp(app);
|
|
}
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
|
|
if (guiApps.length === 0) {
|
|
container.innerHTML = '<div style="color:var(--text-muted);font-size:12px;font-family:var(--font-mono);padding:4px 0;">No apps configured</div>';
|
|
} else {
|
|
for (const app of guiApps) {
|
|
const running = runningApps.find(r => r.command === app.command && r.alive);
|
|
const row = document.createElement('div');
|
|
row.className = 'panel-app-row';
|
|
row.innerHTML = `
|
|
<div class="panel-app-dot ${running ? 'active' : ''}"></div>
|
|
<span class="panel-app-icon">${app.icon || '🖥'}</span>
|
|
<span class="panel-app-name">${app.name || app.command}</span>
|
|
<button class="panel-app-action ${running ? 'stop' : 'start'}"
|
|
data-command="${app.command}" data-app-id="${running ? running.app_id : ''}"
|
|
data-gui-id="${app.id}" data-title="${app.name || app.command}"
|
|
data-args="${(app.args || []).join(' ')}" data-fps="${app.target_fps || 10}"
|
|
title="${running ? 'Stop' : 'Launch'}">
|
|
${running ? '■' : '▶'}
|
|
</button>
|
|
`;
|
|
|
|
// Click name to open in tab
|
|
row.querySelector('.panel-app-name').addEventListener('click', () => {
|
|
openApp('gui-' + app.id);
|
|
});
|
|
row.querySelector('.panel-app-icon').addEventListener('click', () => {
|
|
openApp('gui-' + app.id);
|
|
});
|
|
|
|
container.appendChild(row);
|
|
}
|
|
}
|
|
|
|
// Add App button
|
|
const addBtn = document.createElement('button');
|
|
addBtn.className = 'panel-app-add';
|
|
addBtn.textContent = '+ Add';
|
|
addBtn.addEventListener('click', () => {
|
|
openApp('settings');
|
|
// Navigate to Applications section after a brief delay
|
|
setTimeout(() => {
|
|
const navItem = document.querySelector('.settings-nav-item[data-section="applications"]');
|
|
if (navItem) navItem.click();
|
|
}, 200);
|
|
});
|
|
container.appendChild(addBtn);
|
|
|
|
// Action button handlers (launch/stop)
|
|
container.querySelectorAll('.panel-app-action').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
const isRunning = btn.classList.contains('stop');
|
|
btn.disabled = true;
|
|
|
|
if (isRunning) {
|
|
// Stop the app
|
|
const appId = btn.dataset.appId;
|
|
if (appId) {
|
|
await Atlus.apiFetch(`/api/display/apps/${appId}`, { method: 'DELETE' });
|
|
}
|
|
} else {
|
|
// Launch the app
|
|
const args = btn.dataset.args ? btn.dataset.args.split(' ').filter(Boolean) : [];
|
|
await Atlus.apiFetch('/api/display/apps', {
|
|
method: 'POST',
|
|
body: {
|
|
command: btn.dataset.command,
|
|
title: btn.dataset.title,
|
|
args: args,
|
|
target_fps: parseInt(btn.dataset.fps) || 10,
|
|
},
|
|
});
|
|
}
|
|
setTimeout(loadPanelApps, 1500);
|
|
});
|
|
});
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
// =====================================================================
|
|
// Panel — Update checker
|
|
// =====================================================================
|
|
|
|
async function checkForUpdates() {
|
|
const panel = $('#panelUpdates');
|
|
if (!panel) return;
|
|
try {
|
|
const res = await Atlus.apiFetch('/api/updates/check');
|
|
if (!res || !res.ok) {
|
|
// Show error state if endpoint fails (503 = git not found, etc.)
|
|
panel.classList.remove('hidden');
|
|
const status = res ? res.status : 0;
|
|
const detail = res ? await res.json().catch(() => ({})) : {};
|
|
panel.innerHTML = `
|
|
<div class="update-status">
|
|
<span class="update-status-dot" style="background:var(--text-muted);"></span>
|
|
<div class="update-status-text">
|
|
<div class="update-status-title">Updates unavailable</div>
|
|
<div class="update-status-hash">${detail.detail || 'Could not check'}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
const data = await res.json();
|
|
|
|
panel.classList.remove('hidden');
|
|
|
|
if (data.error) {
|
|
showUpdateError(panel, data);
|
|
} else if (data.available && data.behind_count > 0) {
|
|
showUpdateAvailable(panel, data);
|
|
} else {
|
|
showUpToDate(panel, data);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Update check failed:', e);
|
|
}
|
|
}
|
|
|
|
function showUpToDate(panel, data) {
|
|
panel.innerHTML = `
|
|
<div class="update-status update-current">
|
|
<span class="update-status-dot current"></span>
|
|
<div class="update-status-text">
|
|
<div class="update-status-title">Up to date</div>
|
|
<div class="update-status-hash">${data.local_hash}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function showUpdateError(panel, data) {
|
|
panel.innerHTML = `
|
|
<div class="update-status">
|
|
<span class="update-status-dot" style="background:var(--status-red);"></span>
|
|
<div class="update-status-text">
|
|
<div class="update-status-title">Check failed</div>
|
|
<div class="update-status-hash">${data.error}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function showUpdateAvailable(panel, data) {
|
|
const pkgs = data.new_packages || [];
|
|
const hasPkgs = pkgs.length > 0;
|
|
|
|
let pkgHtml = '';
|
|
if (hasPkgs) {
|
|
pkgHtml = `
|
|
<div class="update-packages">
|
|
<div class="update-packages-title">New system packages needed:</div>
|
|
<div class="update-packages-list">${pkgs.join(', ')}</div>
|
|
<label class="update-packages-toggle">
|
|
<input type="checkbox" class="update-install-pkgs-cb" checked>
|
|
<span>Install packages with update</span>
|
|
</label>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
panel.innerHTML = `
|
|
<div class="update-status update-available">
|
|
<span class="update-status-dot available"></span>
|
|
<div class="update-status-text">
|
|
<div class="update-status-title">Update available</div>
|
|
<div class="update-status-hash">${data.behind_count} commit${data.behind_count !== 1 ? 's' : ''} behind (${data.remote_hash})</div>
|
|
</div>
|
|
<button class="update-install-btn" title="Install update">Update</button>
|
|
</div>
|
|
${pkgHtml}
|
|
`;
|
|
|
|
panel.querySelector('.update-install-btn').addEventListener('click', async (e) => {
|
|
const btn = e.target;
|
|
btn.disabled = true;
|
|
|
|
const installPkgs = hasPkgs && panel.querySelector('.update-install-pkgs-cb')?.checked;
|
|
|
|
// Step 1: Install system packages if needed
|
|
if (installPkgs) {
|
|
btn.textContent = 'Installing packages…';
|
|
try {
|
|
const depRes = await Atlus.apiFetch('/api/updates/install-deps', {
|
|
method: 'POST',
|
|
body: { packages: pkgs },
|
|
});
|
|
if (!depRes.ok) {
|
|
const err = await depRes.json().catch(() => ({}));
|
|
btn.textContent = 'Pkg install failed';
|
|
btn.disabled = false;
|
|
console.error('Package install failed:', err);
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
btn.textContent = 'Pkg install failed';
|
|
btn.disabled = false;
|
|
console.error('Package install error:', err);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Step 2: Apply the update (git pull + pip + restart)
|
|
btn.textContent = 'Updating…';
|
|
try {
|
|
const res = await Atlus.apiFetch('/api/updates/apply', { method: 'POST' });
|
|
if (res.ok) {
|
|
btn.textContent = 'Restarting…';
|
|
setTimeout(() => attemptReload(0), 4000);
|
|
} else {
|
|
btn.textContent = 'Failed';
|
|
btn.disabled = false;
|
|
}
|
|
} catch (e) {
|
|
// Connection lost = server restarting
|
|
btn.textContent = 'Restarting…';
|
|
setTimeout(() => attemptReload(0), 4000);
|
|
}
|
|
});
|
|
}
|
|
|
|
function attemptReload(attempt) {
|
|
if (attempt > 10) return; // Give up after ~30s
|
|
fetch('/desktop', { method: 'HEAD' })
|
|
.then(() => window.location.reload())
|
|
.catch(() => setTimeout(() => attemptReload(attempt + 1), 3000));
|
|
}
|
|
|
|
// =====================================================================
|
|
// Session persistence — save/restore desktop state
|
|
// =====================================================================
|
|
let _saveTimer = null;
|
|
|
|
function saveDesktopState() {
|
|
// Debounce — save at most once per second
|
|
if (_saveTimer) clearTimeout(_saveTimer);
|
|
_saveTimer = setTimeout(_doSaveState, 1000);
|
|
}
|
|
|
|
function _doSaveState() {
|
|
const termApp = Atlus.apps.terminal;
|
|
const terminalTabs = (termApp && termApp.getTerminalIds)
|
|
? termApp.getTerminalIds()
|
|
: [];
|
|
|
|
const state = {
|
|
open_apps: Atlus.openApps.slice(),
|
|
active_app: Atlus.activeApp,
|
|
terminal_tabs: terminalTabs,
|
|
};
|
|
|
|
Atlus.apiFetch('/api/session/state', {
|
|
method: 'PUT',
|
|
body: state,
|
|
}).catch(() => {}); // best-effort
|
|
}
|
|
|
|
async function restoreSession() {
|
|
try {
|
|
const res = await Atlus.apiFetch('/api/session');
|
|
if (!res || !res.ok) return;
|
|
const session = await res.json();
|
|
const state = session.desktop_state || {};
|
|
const openApps = state.open_apps || [];
|
|
const activeApp = state.active_app;
|
|
const serverTerminals = session.terminals || {};
|
|
|
|
// Store terminal info for the terminal app to use when it inits
|
|
Atlus._restoredTerminals = Object.values(serverTerminals);
|
|
Atlus._restoredTerminalTabs = state.terminal_tabs || [];
|
|
|
|
// Re-open apps in saved order
|
|
for (const appId of openApps) {
|
|
if (Atlus.apps[appId]) {
|
|
openApp(appId);
|
|
}
|
|
}
|
|
|
|
// Focus the previously active app
|
|
if (activeApp && Atlus.openApps.includes(activeApp)) {
|
|
focusApp(activeApp);
|
|
}
|
|
} catch (e) {
|
|
// First visit or server error — no session to restore
|
|
}
|
|
}
|
|
|
|
// Save state on visibility change (tab switch / minimize)
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'hidden') {
|
|
_doSaveState(); // immediate save on hide
|
|
}
|
|
});
|
|
|
|
// Save state before unload (use sendBeacon for reliability)
|
|
window.addEventListener('beforeunload', () => {
|
|
const termApp = Atlus.apps.terminal;
|
|
const terminalTabs = (termApp && termApp.getTerminalIds)
|
|
? termApp.getTerminalIds() : [];
|
|
const state = {
|
|
open_apps: Atlus.openApps.slice(),
|
|
active_app: Atlus.activeApp,
|
|
terminal_tabs: terminalTabs,
|
|
};
|
|
const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
|
|
navigator.sendBeacon('/api/session/state?token=' + TOKEN, blob);
|
|
});
|
|
|
|
// =====================================================================
|
|
// Init
|
|
// =====================================================================
|
|
loadHostname();
|
|
loadPanelApps();
|
|
connectStats();
|
|
|
|
// Refresh applications panel periodically
|
|
setInterval(loadPanelApps, 30000);
|
|
|
|
// Update scanner — read interval and enabled state from config
|
|
(async function initUpdateChecker() {
|
|
try {
|
|
const cfgRes = await Atlus.apiFetch('/api/settings');
|
|
if (cfgRes.ok) {
|
|
const cfg = await cfgRes.json();
|
|
const enabled = cfg.update_check_enabled !== false;
|
|
const intervalSec = cfg.update_check_interval || 60;
|
|
if (enabled) {
|
|
checkForUpdates();
|
|
setInterval(checkForUpdates, intervalSec * 1000);
|
|
}
|
|
} else {
|
|
// Fallback: check every 60s
|
|
checkForUpdates();
|
|
setInterval(checkForUpdates, 60 * 1000);
|
|
}
|
|
} catch (e) {
|
|
checkForUpdates();
|
|
setInterval(checkForUpdates, 60 * 1000);
|
|
}
|
|
})();
|
|
|
|
// Expose for app modules
|
|
window.Atlus.openApp = openApp;
|
|
window.Atlus.closeApp = closeApp;
|
|
window.Atlus.focusApp = focusApp;
|
|
window.Atlus.saveDesktopState = saveDesktopState;
|
|
window._atlusLoadPanelApps = loadPanelApps;
|
|
|
|
// Restore previous session (after a brief delay to let app modules register)
|
|
setTimeout(restoreSession, 100);
|
|
})();
|