// ── 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 = `
${escHtml(c.label)}
${escHtml(_PIN_PAGE_LABELS[c.page] || c.page)}
${escHtml(c.hint)}
`; } 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; } });