269 lines
13 KiB
JavaScript
269 lines
13 KiB
JavaScript
// ── Connection state ──────────────────────────────────────────────────────────
|
|
function setConnected(on) {
|
|
state.connected = on;
|
|
const dot = $('conn-dot');
|
|
const label = $('conn-label');
|
|
const badge = $('dash-conn-badge');
|
|
const shellHealth = $('chimera-shell-health');
|
|
const shellHealthLabel = $('chimera-shell-health-label');
|
|
const tr = (key, fallback) => typeof window.t === 'function' ? window.t(key, fallback) : fallback;
|
|
|
|
dot.className = `dot ${on ? 'green' : 'red'}`;
|
|
label.textContent = on ? tr('connection.connected', 'Connected') : tr('connection.disconnected', 'Disconnected');
|
|
badge.textContent = on ? tr('connection.online', 'Online') : tr('connection.offline', 'Offline');
|
|
badge.className = `badge ${on ? 'badge-green' : 'badge-red'}`;
|
|
if (shellHealth) {
|
|
shellHealth.classList.toggle('is-online', on);
|
|
shellHealth.classList.toggle('is-offline', !on);
|
|
}
|
|
if (shellHealthLabel) {
|
|
shellHealthLabel.textContent = on
|
|
? tr('connection.ready', 'Ready')
|
|
: tr('connection.disconnected', 'Disconnected');
|
|
shellHealthLabel.dataset.i18n = on ? 'connection.ready' : 'connection.disconnected';
|
|
}
|
|
|
|
$('btn-connect').disabled = on;
|
|
$('btn-disconnect').disabled = !on;
|
|
$('btn-scan').disabled = !on;
|
|
|
|
// Re-evaluate PS5 action buttons — only enabled when connected AND a location is stored
|
|
const deleteBtn = $('btn-manage-delete-ps5');
|
|
const backupBtn = $('btn-manage-backup-ps5');
|
|
if (deleteBtn) deleteBtn.disabled = !on || !state.currentGame?.ftp_path;
|
|
if (backupBtn) backupBtn.disabled = !on || !state.currentGame?.ftp_path;
|
|
}
|
|
|
|
// ── Dashboard ─────────────────────────────────────────────────────────────────
|
|
let lastHomeReadiness = null;
|
|
|
|
async function loadDashboard() {
|
|
try {
|
|
const [settings, stats, catalog, ftpStatus] = await Promise.all([
|
|
window.pork.getSettings(),
|
|
window.pork.getStats(),
|
|
window.pork.gameCatalogHealth().catch(() => ({})),
|
|
window.pork.ftpStatus().catch(() => ({ connected: state.connected })),
|
|
]);
|
|
$('s-total').textContent = stats.totalGames;
|
|
$('s-installed').textContent = stats.installed;
|
|
$('s-backedup').textContent = stats.backedUp;
|
|
$('s-backups').textContent = stats.totalBackups;
|
|
if ($('d-host') && document.activeElement !== $('d-host') && !$('d-host').value) {
|
|
$('d-host').value = settings.ftpHost || '';
|
|
}
|
|
if ($('d-ports') && document.activeElement !== $('d-ports') && !$('d-ports').value) {
|
|
$('d-ports').value = (settings.ftpPorts && settings.ftpPorts.length) ? settings.ftpPorts.join(', ') : '1337, 2121, 21';
|
|
}
|
|
renderHomeReadiness({ settings, stats, catalog, connected: Boolean(ftpStatus.connected) });
|
|
} catch (_) {}
|
|
renderPinnedTiles();
|
|
}
|
|
|
|
function renderHomeReadiness({ settings = {}, stats, catalog, connected }) {
|
|
lastHomeReadiness = { settings, stats, catalog, connected };
|
|
const tr = (key, fallback) => typeof window.t === 'function' ? window.t(key, fallback) : fallback;
|
|
const setIndicator = (id, stateName) => {
|
|
const indicator = $(id);
|
|
if (indicator) indicator.className = `chimera-readiness-dot ${stateName}`;
|
|
};
|
|
const complete = Number(catalog.complete || 0);
|
|
const missing = Number(catalog.missing || catalog.incomplete || 0);
|
|
const total = Number(stats.totalGames || 0);
|
|
const backedUp = Number(stats.backedUp || 0);
|
|
const host = String(settings.ftpHost || '').trim();
|
|
const hostConfigured = Boolean(host);
|
|
const consoleStatus = $('home-console-status');
|
|
const libraryStatus = $('home-library-status');
|
|
const backupStatus = $('home-backup-status');
|
|
const inspectorState = $('home-inspector-state');
|
|
const inspectorReason = $('home-inspector-reason');
|
|
const inspectorTarget = $('home-inspector-target');
|
|
const libraryTruth = $('home-truth-library');
|
|
const protectionTruth = $('home-truth-protection');
|
|
if (consoleStatus) consoleStatus.textContent = connected
|
|
? tr('home.consoleReady', 'Connected · manual operations')
|
|
: hostConfigured
|
|
? tr('home.targetConfiguredOffline', 'Configured · offline')
|
|
: tr('navigation.unavailable', 'Not configured');
|
|
if (libraryStatus) libraryStatus.textContent = total
|
|
? `${complete}/${total} ${tr('home.titlesEnriched', 'titles fully enriched')}${missing ? ` · ${missing} ${tr('home.toComplete', 'to complete')}` : ''}`
|
|
: tr('home.noGames', 'No games detected yet');
|
|
if (backupStatus) backupStatus.textContent = total
|
|
? `${backedUp}/${total} ${tr('home.gamesProtected', 'games protected')}`
|
|
: `${Number(stats.totalBackups || 0)} ${tr('home.backupFiles', 'backup files')}`;
|
|
if (inspectorState) {
|
|
inspectorState.textContent = connected
|
|
? tr('connection.ready', 'Ready')
|
|
: hostConfigured
|
|
? tr('home.targetOffline', 'Offline')
|
|
: tr('navigation.unavailable', 'Not configured');
|
|
inspectorState.className = `chimera-node-state ${connected ? 'ok' : hostConfigured ? 'attention' : 'unknown'}`;
|
|
}
|
|
if (inspectorReason) inspectorReason.textContent = connected
|
|
? tr('home.targetReasonConnected', 'A bounded FTP session with the PS5 is active.')
|
|
: hostConfigured
|
|
? tr('home.targetReasonOffline', 'The PS5 target is saved, but there is no active FTP session.')
|
|
: tr('home.targetReasonMissing', 'Chimera needs an explicit PS5 target before console features can connect.');
|
|
if (inspectorTarget) inspectorTarget.textContent = hostConfigured ? host : tr('navigation.unavailable', 'Not configured');
|
|
if (libraryTruth) libraryTruth.textContent = total
|
|
? `${complete}/${total} ${tr('home.titlesEnriched', 'titles fully enriched')}`
|
|
: tr('home.noGames', 'No games detected yet');
|
|
if (protectionTruth) protectionTruth.textContent = total
|
|
? `${backedUp}/${total} ${tr('home.gamesProtected', 'games protected')}`
|
|
: `${Number(stats.totalBackups || 0)} ${tr('home.backupFiles', 'backup files')}`;
|
|
setIndicator('home-console-indicator', connected ? 'ok' : hostConfigured ? 'attention' : 'unknown');
|
|
setIndicator('home-library-indicator', total && missing === 0 ? 'ok' : total ? 'attention' : 'unknown');
|
|
setIndicator('home-backup-indicator', total && backedUp >= total ? 'ok' : backedUp ? 'attention' : 'unknown');
|
|
|
|
const title = $('home-next-title');
|
|
const detail = $('home-next-detail');
|
|
const action = $('home-next-action');
|
|
if (!title || !detail || !action) return;
|
|
if (!hostConfigured) {
|
|
title.textContent = tr('home.next.configureTitle', 'Configure the PS5 target');
|
|
detail.textContent = tr('home.next.configureDetail', 'Choose the console target yourself. Chimera will not guess an address or start a connection automatically.');
|
|
action.textContent = tr('home.next.configureAction', 'Configure PS5 target');
|
|
action.dataset.route = 'settings/general';
|
|
action.dataset.focusTarget = 's-host';
|
|
} else if (!connected) {
|
|
title.textContent = tr('home.next.connectTitle', 'Connect safely to the PS5 first');
|
|
detail.textContent = tr('home.next.connectDetail', 'The target is configured. Review it and start the bounded FTP connection manually.');
|
|
action.textContent = tr('home.next.connectAction', 'Review connection');
|
|
action.dataset.route = 'home';
|
|
action.dataset.focusTarget = 'd-host';
|
|
} else if (!total) {
|
|
title.textContent = tr('home.next.scanTitle', 'Detect your game library');
|
|
detail.textContent = tr('home.next.scanDetail', 'The PS5 is reachable, but the Chimera catalog does not contain any games yet.');
|
|
action.textContent = tr('home.next.scanAction', 'Scan games');
|
|
action.dataset.route = 'library/games';
|
|
delete action.dataset.focusTarget;
|
|
} else if (missing > 0) {
|
|
title.textContent = `${tr('home.next.completeMetadata', 'Complete metadata for')} ${missing} ${missing === 1 ? tr('home.game', 'game') : tr('home.games', 'games')}`;
|
|
detail.textContent = tr('home.next.metadataDetail', 'Titles, cover art and version information are enriched automatically; review only persistent exceptions.');
|
|
action.textContent = tr('home.next.openLibrary', 'Open library');
|
|
action.dataset.route = 'library/games';
|
|
delete action.dataset.focusTarget;
|
|
} else if (backedUp < total) {
|
|
const remaining = total - backedUp;
|
|
title.textContent = `${remaining} ${remaining === 1 ? tr('home.game', 'game') : tr('home.games', 'games')} ${tr('home.next.notProtected', 'not protected yet')}`;
|
|
detail.textContent = tr('home.next.backupDetail', 'Review backups and create a new copy only from a confirmed source.');
|
|
action.textContent = tr('home.next.checkBackups', 'Check backups');
|
|
action.dataset.route = 'protection/backups';
|
|
delete action.dataset.focusTarget;
|
|
} else {
|
|
title.textContent = tr('home.next.readyTitle', 'Chimera is ready');
|
|
detail.textContent = tr('home.next.readyDetail', 'Console, metadata and library protection are in order. Updates remain explicit and reviewable.');
|
|
action.textContent = tr('home.next.reviewPayloads', 'Review payload updates');
|
|
action.dataset.route = 'payloads/updates';
|
|
delete action.dataset.focusTarget;
|
|
}
|
|
}
|
|
|
|
window.addEventListener('chimera:language-changed', () => {
|
|
if (lastHomeReadiness) renderHomeReadiness(lastHomeReadiness);
|
|
});
|
|
|
|
async function renderPinnedTiles() {
|
|
const grid = $('dash-pinned-grid');
|
|
const empty = $('dash-pinned-empty');
|
|
if (!grid) return;
|
|
try {
|
|
const s = await window.pork.getSettings();
|
|
const pinned = new Set(s.pinnedCards || []);
|
|
const cards = PINNABLE_CARDS.filter(c => pinned.has(c.id));
|
|
|
|
grid.innerHTML = '';
|
|
if (!cards.length) {
|
|
empty.hidden = false;
|
|
} else {
|
|
empty.hidden = true;
|
|
for (const c of cards) {
|
|
const tile = document.createElement('div');
|
|
tile.className = 'dash-pinned-tile';
|
|
tile.dataset.navPage = c.page;
|
|
if (c.type === 'widget') {
|
|
tile.classList.add('dash-pinned-tile--widget');
|
|
tile.removeAttribute('data-nav-page');
|
|
tile.innerHTML = _renderWidgetTile(c);
|
|
grid.appendChild(tile);
|
|
_initWidgetTile(c, tile);
|
|
continue; // already appended
|
|
} else {
|
|
tile.innerHTML = `
|
|
<div class="dash-pinned-tile-title">${escHtml(c.label)}</div>
|
|
<div class="dash-pinned-tile-page">${escHtml(_PIN_PAGE_LABELS[c.page] || c.page)}</div>
|
|
<div class="dash-pinned-tile-hint">${escHtml(c.hint)}</div>
|
|
<button class="btn btn-sm dash-pinned-tile-open">Open →</button>`;
|
|
}
|
|
grid.appendChild(tile);
|
|
}
|
|
}
|
|
|
|
// Sync all pin button visual states across the whole page
|
|
document.querySelectorAll('.btn-pin').forEach(btn => {
|
|
btn.classList.toggle('pinned', pinned.has(btn.dataset.pinId));
|
|
});
|
|
} catch (_) {}
|
|
}
|
|
|
|
// ── FTP Connect ───────────────────────────────────────────────────────────────
|
|
$('btn-connect').addEventListener('click', async () => {
|
|
const host = $('d-host').value.trim();
|
|
const ports = $('d-ports').value.split(',').map(p => p.trim()).filter(Boolean);
|
|
if (!ports.length) ports.push('21');
|
|
if (!host) { setStatus('Enter a host address', 'error'); return; }
|
|
|
|
// Credential fields start blank. Fall back to stored settings when blank
|
|
// so the user can click Connect without re-typing every session.
|
|
const saved = await window.pork.getSettings();
|
|
const user = $('d-user').value.trim() || saved.ftpUser || '';
|
|
const password = $('d-pass').value || saved.ftpPass || '';
|
|
|
|
$('btn-connect').disabled = true;
|
|
setStatus('Connecting…');
|
|
try {
|
|
const result = await window.pork.ftpConnect({ host, ports, user, password });
|
|
setConnected(true);
|
|
setStatus('Connected', 'ok');
|
|
$('sb-sync').textContent = `Connected on :${result?.port || ports[0]} at ${new Date().toLocaleTimeString()}`;
|
|
} catch (e) {
|
|
setConnected(false);
|
|
setStatus(`Connection failed: ${e.message}`, 'error');
|
|
$('btn-connect').disabled = false;
|
|
}
|
|
});
|
|
|
|
$('btn-disconnect').addEventListener('click', async () => {
|
|
await window.pork.ftpDisconnect();
|
|
setConnected(false);
|
|
setStatus('Disconnected');
|
|
});
|
|
|
|
// ── FTP Scan ──────────────────────────────────────────────────────────────────
|
|
$('btn-scan').addEventListener('click', async () => {
|
|
const prog = $('scan-progress');
|
|
prog.hidden = false;
|
|
prog.textContent = 'Scanning PS5 game folders…';
|
|
$('btn-scan').disabled = true;
|
|
|
|
window.pork.on('ftp:progress', ({ current, game }) => {
|
|
prog.textContent = `[${current}] Found: ${game.title || game.game_id}`;
|
|
});
|
|
|
|
try {
|
|
const { count } = await window.pork.ftpScan();
|
|
prog.textContent = `Scan complete — ${count} game(s) found.`;
|
|
$('sb-sync').textContent = `Last sync: ${new Date().toLocaleTimeString()}`;
|
|
setStatus(`Scanned ${count} games`, 'ok');
|
|
await loadDashboard();
|
|
} catch (e) {
|
|
prog.textContent = `Error: ${e.message}`;
|
|
setStatus(e.message, 'error');
|
|
} finally {
|
|
window.pork.off('ftp:progress');
|
|
$('btn-scan').disabled = false;
|
|
}
|
|
});
|
|
|