// ── 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 = `