// ── Backups page ────────────────────────────────────────────────────────────── function protectionText(key, fallback) { if (typeof window.t !== 'function') return fallback; const translated = window.t(key); return translated && translated !== key ? translated : fallback; } async function loadBackups() { try { const gameContext = consumeGameWorkspaceContext('backups'); if (gameContext && $('backups-search')) $('backups-search').value = gameContext; const titles = []; let offset = 0; let total = 0; do { const packet = await window.pork.listLibraryTitles({ limit: 200, offset }); const page = window.chimeraLibraryModel.items(packet); titles.push(...page); total = Number(packet && packet.total || page.length); offset += page.length; if (!page.length) break; } while (offset < total); state.backups = window.chimeraProtectionModel.flatten(titles); populateBackupsRegionFilter(); renderBackups(); } catch (e) { setStatus(e.message, 'error'); } } function populateBackupsRegionFilter() { const sel = $('backups-region-filter'); const cur = sel.value; const regions = [...new Set( state.backups.map(copy => copy.region || '').filter(Boolean).sort() )]; sel.innerHTML = '' + regions.map(r => ``).join(''); } function renderBackups() { const search = ($('backups-search')?.value || '').toLowerCase(); const region = $('backups-region-filter')?.value || ''; const list = $('backups-list'); const empty = $('backups-empty'); const filtered = state.backups.filter(copy => { if (region && copy.region !== region) return false; if (search) { const haystack = [copy.titleName, copy.titleId, copy.deviceName, copy.path, copy.hash].filter(Boolean).join(' ').toLowerCase(); if (!haystack.includes(search)) return false; } return true; }); empty.hidden = filtered.length > 0; list.innerHTML = filtered.map(copy => { const verified = copy.lastVerifiedAt ? new Date(copy.lastVerifiedAt).toLocaleString(window.i18n?.current || 'en') : protectionText('protection.copy.never', 'Never'); const hash = copy.hash ? `${copy.hashAlgorithm.toUpperCase()} ${copy.hash}` : `${protectionText('protection.copy.notHashed', 'Not hashed')} (${copy.hashState})`; const integrity = copy.missing ? protectionText('protection.copy.missing', 'Missing copy') : copy.integrityFailed ? `${protectionText('protection.copy.failed', 'Failed')} (${copy.integrityState})` : copy.integrityState === 'unknown' ? protectionText('protection.copy.notVerified', 'Not verified') : copy.integrityState; const policyClass = copy.policy.healthy ? 'is-healthy' : copy.policy.status === 'needs-diversity' ? 'needs-diversity' : 'is-degraded'; const policyLabel = copy.policy.healthy ? `${copy.policy.distinctDeviceCount}/${copy.policy.requiredDistinctDevices} ${protectionText('protection.policy.devicesProtected', 'devices protected')}` : `${copy.policy.distinctDeviceCount}/${copy.policy.requiredDistinctDevices} ${protectionText('protection.policy.distinctDevices', 'distinct devices')}`; return `
${escHtml(copy.titleName)}${escHtml(copy.titleId)} · copy ${escHtml(copy.copyId)}
${escHtml(protectionText('protection.copy.device', 'Device'))}
${escHtml(protectionText('protection.copy.path', 'Path'))}${escHtml(copy.path)}
${escHtml(protectionText('protection.copy.hash', 'Hash'))}${escHtml(hash)}
${escHtml(protectionText('protection.copy.lastVerified', 'Last verified'))}${escHtml(verified)}
${escHtml(protectionText('protection.copy.integrity', 'Integrity'))}${escHtml(integrity)}
${escHtml(protectionText('protection.copy.size', 'Size'))}${escHtml(copy.sizeBytes ? fmt(copy.sizeBytes) : protectionText('protection.copy.unknown', 'Unknown'))}
${escHtml(policyLabel)}
`; }).join(''); list.onclick = async e => { const labelButton = e.target.closest('[data-label-device-id]'); if (labelButton) { const label = await showPrompt(protectionText('protection.copy.renamePrompt', 'Friendly name for this drive:'), labelButton.dataset.deviceLabel || ''); if (label == null) return; await window.pork.labelStorageDevice({ deviceId: Number(labelButton.dataset.labelDeviceId), label }); await loadBackups(); setStatus(protectionText('protection.copy.renamed', 'Drive label saved.'), 'ok'); return; } const restore = e.target.closest('[data-restore-copy-id]'); if (restore) { const copy = state.backups.find(item => item.copyId === restore.dataset.restoreCopyId); if (!copy) return; state.protectionRestoreTarget = copy.restoreTarget; list.querySelectorAll('[data-restore-copy-id]').forEach(button => { button.setAttribute('aria-pressed', String(button === restore)); if (!button.disabled) button.textContent = button === restore ? protectionText('protection.copy.restoreSelected', 'Restore source selected') : protectionText('protection.copy.prepareRestore', 'Prepare restore'); }); setStatus(`${protectionText('protection.copy.restoreSelected', 'Restore source selected')}: ${copy.copyId} · ${copy.deviceName}. ${protectionText('protection.copy.noMutation', 'No restore has started.')}`, 'ok'); return; } const details = e.target.closest('[data-view-game]'); if (details) openModal(details.dataset.viewGame); }; } $('backups-search')?.addEventListener('input', renderBackups); $('backups-region-filter')?.addEventListener('change', renderBackups); $('btn-scan-local').addEventListener('click', async () => { $('btn-scan-local').disabled = true; setStatus('Scanning local game source folders…'); try { const { total, added } = await window.pork.scanBackups(); await loadBackups(); await loadDashboard(); const msg = added > 0 ? `Found ${total} game(s) — ${added} new, fetching metadata…` : `Scan complete: ${total} game(s) found, none new`; setStatus(msg, 'ok'); } catch (e) { setStatus(e.message, 'error'); } finally { $('btn-scan-local').disabled = false; } }); $('btn-rescan-all-backporks').addEventListener('click', async () => { const btn = $('btn-rescan-all-backporks'); btn.disabled = true; const orig = btn.textContent; btn.textContent = 'Scanning…'; setStatus('Rescanning all backpork folders…'); try { const { folders, count, uniqueGames } = await window.pork.backporksScanAll(); await loadBackporks(); await refreshFirmwareFilters(); const uniqueLabel = Number.isFinite(uniqueGames) && uniqueGames > 0 ? ` across ${uniqueGames} unique game(s)` : ''; setStatus(`Rescan complete — ${count} build(s) in ${folders} firmware folder(s)${uniqueLabel}`, 'ok'); } catch (e) { setStatus(e.message, 'error'); } finally { btn.disabled = false; btn.textContent = orig; } });