for (const href of ['styles/ps4-packages.css', 'styles/ps5-native-packages.css']) { if (!document.head.querySelector(`link[href="${href}"]`)) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = href; document.head.appendChild(link); } } document.querySelectorAll('.modal-tab').forEach(tab => tab.addEventListener('click', () => { document.querySelectorAll('.modal-tab').forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); t.tabIndex = -1; }); document.querySelectorAll('.modal-panel').forEach(p => { p.classList.remove('active'); p.hidden = true; }); tab.classList.add('active'); tab.setAttribute('aria-selected', 'true'); tab.tabIndex = 0; const panel = $(`mpanel-${tab.dataset.mtab}`); panel.classList.add('active'); panel.hidden = false; }) ); function resetModalTabs() { document.querySelectorAll('.modal-tab').forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); t.tabIndex = -1; }); document.querySelectorAll('.modal-panel').forEach(p => { p.classList.remove('active'); p.hidden = true; }); const first = document.querySelector('.modal-tab[data-mtab="info"]'); first.classList.add('active'); first.setAttribute('aria-selected', 'true'); first.tabIndex = 0; $('mpanel-info').classList.add('active'); $('mpanel-info').hidden = false; } // Promote the four facts people open the dialog for; the full table still follows. function renderModalMetrics(g, infoRows) { let host = $('modal-metrics'); if (!$('mpanel-info')) return; if (!host) { host = document.createElement('div'); host.id = 'modal-metrics'; host.className = 'modal-metrics'; $('mpanel-info').insertBefore(host, $('modal-info')); } const at = k => (infoRows.find(r => r[0] === k) || [])[1] || '—'; const on = window.chimeraLibraryModel.isInstalled(g), copy = at('Local Copy') === 'Yes'; host.innerHTML = [ ['Size', at('Size'), ''], ['Version', at('Version'), ''], ['On console', on ? 'Installed' : 'Not installed', on ? 'ok' : 'idle'], ['Local copy', copy ? 'Backed up' : 'None', copy ? 'ok' : 'warn'], ].map(([l, v, t]) => ``).join(''); } function renderProsperoData(g) { const hasProspero = !!g.prospero_fetched_at; const banner = $('modal-banner'); $('modal').style.setProperty('--modal-hue', String(titleHue(g.game_id))); const art = g.prospero_banner_url || g.prospero_cover_url || g.prospero_icon_url || ''; const setArt = url => { banner.style.backgroundImage = url ? `url('${url}')` : ''; banner.classList.toggle('has-art', !!url); }; setArt(''); if (art) { const i = new Image(); i.onload = () => setArt(art); i.src = art; } const icon = $('modal-icon'); icon.onerror = () => { icon.hidden = true; }; if (g.prospero_icon_url) { icon.src = g.prospero_icon_url; icon.hidden = false; } else { icon.hidden = true; icon.src = ''; } const patchCount = hasProspero ? (g.prospero_patch_count || 0) : 0; const dlcArr = hasProspero ? tryParseJson(g.prospero_dlc, []) : []; const regionArr = hasProspero ? tryParseJson(g.prospero_other_regions, []) : []; $('mtab-patch-count').textContent = patchCount || ''; $('mtab-dlc-count').textContent = dlcArr.length || ''; $('mtab-regions-count').textContent = regionArr.length || ''; if (!hasProspero) { $('modal-patches-content').innerHTML = '
Loading update data…
'; $('modal-dlc-content').innerHTML = '
Loading DLC data…
'; $('modal-regions-content').innerHTML = '
Loading region data…
'; return; } $('modal-description').hidden = true; const patches = tryParseJson(g.prospero_patches, []); if (patches.length) { $('modal-patches-content').innerHTML = ` `; } else { $('modal-patches-content').innerHTML = '
No patch updates found
'; } const dlc = dlcArr; if (dlc.length) { $('modal-dlc-content').innerHTML = ` `; } else { $('modal-dlc-content').innerHTML = '
No additional content
'; } const regions = regionArr; if (regions.length) { $('modal-regions-content').innerHTML = `
${regions.map(r => { const isOwned = r.titleid === g.game_id; return ` `; }).join('')}
`; $('modal-regions-content').querySelector('.region-pills')?.addEventListener('click', e => { const pill = e.target.closest('[data-tid]'); if (pill) openModal(pill.dataset.tid); }); } else { $('modal-regions-content').innerHTML = '
No other regions found
'; } } function tryParseJson(str, fallback) { try { return str ? JSON.parse(str) : fallback; } catch (_) { return fallback; } } const convJobsCache = new Map(); function patchConvJobCard(job) { convJobsCache.set(job.id, job); const card = document.querySelector(`.conv-job-row[data-conv-id="${job.id}"]`); if (!card || card.dataset.convStatus !== job.status) { refreshConvQueuePanel().catch(() => {}); return; } const pct = job.progress?.percent ?? 0; const bar = card.querySelector('.conv-job-bar'); if (bar) bar.style.width = `${pct}%`; const phaseEl = card.querySelector('.conv-job-phase'); if (phaseEl) phaseEl.textContent = job.progress?.phase || job.status; const log = (job.progress?.log ?? []).join('\n'); const showLog = (job.status === 'running' || job.status === 'error') && log; let lb = card.querySelector('.conv-log-box'); if (showLog) { if (!lb) { lb = document.createElement('pre'); lb.className = 'conv-log-box'; lb.dataset.logLen = '0'; card.appendChild(lb); } if (lb.dataset.logLen !== String(job.progress?.log?.length)) { lb.textContent = log; lb.scrollTop = lb.scrollHeight; lb.dataset.logLen = String(job.progress?.log?.length ?? 0); } } } async function refreshConvQueuePanel() { try { const jobs = await window.pork.convQueueList(); const paused = await window.pork.convQueueIsPaused(); const s = await window.pork.getSettings(); const panel = $('conv-queue-panel'); if (!panel) return; panel.hidden = false; const mode = s.gameConversionMode || 'pfs'; document.querySelectorAll('.conv-mode-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.convMode === mode); }); const warnEl = $('conv-mode-warn'); const warnTxt = $('conv-mode-warn-text'); let warnMsg = ''; if (mode === 'ffpkg') { const info = await window.pork.convToolInfo().catch(() => null); warnMsg = info && !info.supported ? info.reason : (!s.ufs2ToolPath ? 'FFPKG mode requires UFS2Tool.exe — set the path in Game Conversion settings.' : ''); } else if (mode === 'exfat') { const info = await window.pork.convExfatToolInfo().catch(() => null); warnMsg = info && !info.supported ? info.reason : (!s.osfmountPath ? 'ExFAT mode requires OSFMount — configure it in Game Conversion settings.' : ''); } if (warnEl) { warnEl.hidden = !warnMsg; if (warnTxt) warnTxt.textContent = warnMsg; } if (mode === 'exfat') { window.pork.convExfatToolInfo().then(info => { if (!warnEl) return; if (info && !info.supported) { warnEl.hidden = false; if (warnTxt) warnTxt.textContent = info.reason; return; } if (info && info.toolAvailable) { warnEl.hidden = true; return; } warnEl.hidden = false; if (warnTxt) warnTxt.textContent = 'ExFAT mode needs OSFMount — install it or set osfmount.com in Game Conversion settings.'; }).catch(() => {}); } const adminWarnEl = $('conv-admin-warn'); if (adminWarnEl) { if (mode === 'ffpkg' || mode === 'exfat') { let isElevated = false; try { isElevated = await window.pork.isAdmin(); } catch (_) {} adminWarnEl.hidden = isElevated; } else { adminWarnEl.hidden = true; } } const listEl = $('conv-queue-list'); const emptyEl = $('conv-queue-empty'); if (listEl) { listEl.innerHTML = jobs.map(j => renderConvJobRowHtml(j)).join(''); jobs.forEach(j => convJobsCache.set(j.id, j)); listEl.querySelectorAll('.conv-log-box').forEach(lb => { lb.scrollTop = lb.scrollHeight; }); } if (emptyEl) emptyEl.hidden = jobs.length > 0; const startBtn = $('btn-conv-start'); if (startBtn) { const hasQueued = jobs.some(j => j.status === 'queued'); startBtn.disabled = !paused || !hasQueued; startBtn.textContent = paused ? '▶ Start Queue' : '⏸ Running…'; } } catch (_) {} } $('conv-queue-panel').addEventListener('click', async e => { const modeBtn = e.target.closest('.conv-mode-btn'); if (modeBtn) { const mode = modeBtn.dataset.convMode; await window.pork.setSettings({ gameConversionMode: mode }); const radio = document.querySelector(`#conv-mode-${mode}`); if (radio) radio.checked = true; applyConvModeUI(mode); await refreshConvQueuePanel(); return; } if (e.target.closest('#btn-conv-goto-settings')) { navigate('settings'); setTimeout(() => { const card = $('settings-conversion-card'); if (card) card.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100); } }); function renderConvJobRowHtml(job) { const pct = job.progress?.percent ?? 0; const phase = escHtml(job.progress?.phase || job.status); const log = (job.progress?.log ?? []).join('\n'); const showLog = (job.status === 'running' || job.status === 'error') && log; const barClass = job.status === 'done' ? 'conv-bar-done' : job.status === 'error' ? 'conv-bar-error' : job.status === 'running' ? 'conv-bar-running' : ''; const cancelBtn = (job.status === 'queued' || job.status === 'running') ? `` : ''; const retryBtn = (job.status === 'error' || job.status === 'cancelled') ? `` : ''; return `
#${job.id} ${escHtml(job.game_name || job.game_id)} ${escHtml((job.mode || '').toUpperCase())} ${phase}
${escHtml(job.status)}
${cancelBtn}${retryBtn}
${showLog ? `
${escHtml(log)}
` : ''} ${job.error ? `
✗ ${escHtml(job.error)}
` : ''}
`; } function showAdminWarning() { openAccessibleDialog($('admin-warn-overlay'), $('admin-warn-close')); } $('admin-warn-close').addEventListener('click', () => { closeAccessibleDialog($('admin-warn-overlay')); }); $('admin-warn-overlay').addEventListener('click', (e) => { if (e.target === $('admin-warn-overlay')) closeAccessibleDialog($('admin-warn-overlay')); }); async function guardAdminForConv() { const s = await window.pork.getSettings(); const mode = s.gameConversionMode || 'pfs'; if (mode === 'pfs' || mode === 'ffpfsc') return true; let ok = false; try { ok = await window.pork.isAdmin(); } catch (_) { ok = false; } if (!ok) { showAdminWarning(); return false; } return true; } $('btn-conv-start').addEventListener('click', async () => { if (!await guardAdminForConv()) return; await window.pork.convQueueStart(); await refreshConvQueuePanel(); }); $('btn-conv-clear-done').addEventListener('click', async () => { await window.pork.convQueueClearDone(); await refreshConvQueuePanel(); }); $('btn-conv-cleanup-temp').addEventListener('click', async () => { const btn = $('btn-conv-cleanup-temp'); btn.disabled = true; btn.textContent = '\u{1F9F9} Cleaning\u2026'; try { const res = await window.pork.convQueueCleanupTemp(); const parts = []; if (res.deletedDirs) parts.push(`${res.deletedDirs} temp folder${res.deletedDirs > 1 ? 's' : ''}`); if (res.deletedFiles) parts.push(`${res.deletedFiles} partial file${res.deletedFiles > 1 ? 's' : ''}`); btn.textContent = parts.length ? `\u2713 Removed ${parts.join(' + ')}` : '\u2713 Nothing to clean'; if (res.errors?.length) console.warn('[CleanTemp]', res.errors); } catch (e) { btn.textContent = '\u2717 Cleanup failed'; } setTimeout(() => { btn.disabled = false; btn.textContent = '\u{1F9F9} Clean Temp'; }, 3000); }); $('btn-conv-queue-toggle').addEventListener('click', () => { const body = $('conv-queue-body'); const collapsed = body.hidden; body.hidden = !collapsed; $('btn-conv-queue-toggle').textContent = collapsed ? '↑' : '↓'; }); $('conv-queue-list').addEventListener('click', async e => { const cancelBtn = e.target.closest('[data-conv-cancel]'); if (cancelBtn) { await window.pork.convQueueCancel(Number(cancelBtn.dataset.convCancel)); await refreshConvQueuePanel(); return; } const retryBtn = e.target.closest('[data-conv-retry]'); if (retryBtn) { await window.pork.convQueueRetry(Number(retryBtn.dataset.convRetry)); await refreshConvQueuePanel(); } }); let _installBackporkEntries = []; async function openInstallModal(game_id, startMode) { const [games, s] = await Promise.all([ window.pork.listLibraryTitles({ search: game_id, limit: 10 }).then(window.chimeraLibraryModel.items), window.pork.getSettings(), ]); const g = games.find(x => x.game_id === game_id); if (!g) return; const remotePaths = Array.isArray(s.remoteGamePaths) ? s.remoteGamePaths : []; $('install-modal-game-label').textContent = g.prospero_name || g.title || game_id; $('install-overlay').dataset.gameId = game_id; $('install-overlay').dataset.gameName = g.prospero_name || g.title || game_id; const backups = await window.pork.listBackups(game_id); const localBackup = backups.find(b => b.source === 'local' && b.backup_type === 'folder') ?? backups.find(b => b.source === 'local') ?? backups[0]; const latest = localBackup; $('install-overlay').dataset.backupPath = latest?.backup_path || ''; const hintEl = $('install-backup-hint'); if (hintEl) { if (latest) { hintEl.textContent = `Source: ${latest.backup_path}`; hintEl.title = latest.backup_path; } else { hintEl.textContent = 'No local backup found — use Browse folder to select one'; hintEl.title = ''; } } let ftpPath = g.ftp_path || ''; const noPathsConfigured = remotePaths.length === 0; if (!ftpPath && !noPathsConfigured) { const base = remotePaths[0].path.replace(/\/?$/, '/'); ftpPath = base + game_id + '-app'; } const ftpPathEl = $('install-ftp-path'); if (ftpPathEl) ftpPathEl.value = ftpPath; const warn = $('install-no-path-warn'); if (warn) warn.hidden = !!ftpPath; $('install-overlay').dataset.requiredBytes = String(Number(latest?.size || 0)); if (window.chimeraInstallDestination) { await window.chimeraInstallDestination.load({ titleId: game_id, required: Number(latest?.size || 0), legacyPath: ftpPath }); } const entries = await window.pork.backporksEntries(game_id); _installBackporkEntries = entries; const bpSel = $('install-backpork-select'); if (bpSel) { bpSel.innerHTML = '' + entries.map((e, i) => ``).join(''); } const convHintEl = $('install-conv-backup-hint'); if (convHintEl) { if (latest) { convHintEl.textContent = `Source: ${latest.backup_path}`; convHintEl.title = latest.backup_path; } else { convHintEl.textContent = 'No local backup found — use Browse folder to select one'; convHintEl.title = ''; } } const convBpSel = $('install-conv-backpork-select'); if (convBpSel) { convBpSel.innerHTML = '' + entries .map((e, i) => ({ e, i })) .filter(({ e }) => !!e.game_path) // skip firmware folders with no patch for this game .map(({ e, i }) => ``) .join(''); } const convFtpPathEl = $('install-conv-ftp-path'); if (convFtpPathEl) { let convDir = ''; if (g.ftp_path) { convDir = g.ftp_path.replace(/\/[^\/]+\/?$/, '') || '/'; if (!convDir.endsWith('/')) convDir += '/'; } else if (!noPathsConfigured) { convDir = remotePaths[0].path.replace(/\/?$/, '/'); } convFtpPathEl.value = convDir; } const convFtpUploadEl = $('install-conv-ftp-upload'); if (convFtpUploadEl) { convFtpUploadEl.checked = !!s.convFtpUpload; const convFtpFields = $('install-conv-ftp-fields'); if (convFtpFields) convFtpFields.style.display = convFtpUploadEl.checked ? 'block' : 'none'; } const convDelEl = $('install-conv-delete-after'); if (convDelEl) convDelEl.checked = !!s.convDeleteAfter; const convToolWarn = $('install-conv-tool-warn'); if (convToolWarn) { const currentMode = s.gameConversionMode || 'pfs'; if (currentMode === 'ffpkg') { const info = s.ufs2ToolPath ? await window.pork.convToolInfo().catch(() => null) : null; convToolWarn.hidden = !!info?.toolAvailable; } else if (currentMode === 'exfat') { const info = await window.pork.convExfatToolInfo().catch(() => null); convToolWarn.hidden = !!info?.toolAvailable; } else { convToolWarn.hidden = true; } } const requestedMode = ['pfs', 'ffpkg', 'ffpfsc', 'exfat'].includes(startMode) ? startMode : null; const currentMode = requestedMode || s.gameConversionMode || 'pfs'; _applyInstallModeTab(currentMode); updateInstallConfirmState(currentMode, latest, ftpPath); openAccessibleDialog($('install-overlay'), $('install-close')); } function _applyInstallModeTab(mode) { document.querySelectorAll('.install-mode-tab').forEach(t => t.classList.toggle('active', t.dataset.installMode === mode) ); const pfsSec = $('install-pfs-section'); const convSec = $('install-conv-section'); if (pfsSec) pfsSec.hidden = mode !== 'pfs'; if (convSec) convSec.hidden = mode === 'pfs'; } function updateInstallConfirmState(mode, latest, ftpPath) { const confirmBtn = $('btn-install-confirm'); if (!confirmBtn) return; if (mode === 'pfs') { const backupPath = $('install-overlay').dataset.backupPath; const destinationReady = window.chimeraInstallDestination?.isReady() || false; confirmBtn.disabled = !backupPath || !state.connected || !destinationReady; } else { const backupPath = $('install-overlay').dataset.backupPath; confirmBtn.disabled = !backupPath; } } $('install-overlay').addEventListener('click', e => { if (e.target === $('install-overlay')) { closeAccessibleDialog($('install-overlay')); return; } const tab = e.target.closest('.install-mode-tab'); if (!tab) return; const mode = tab.dataset.installMode; _applyInstallModeTab(mode); updateInstallConfirmState(mode); }); $('btn-install-browse-local').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; $('install-overlay').dataset.backupPath = folder; $('install-backup-hint').textContent = `Source: ${folder}`; $('install-backup-hint').title = folder; const convHint = $('install-conv-backup-hint'); if (convHint) { convHint.textContent = `Source: ${folder}`; convHint.title = folder; } const ftpPath = $('install-ftp-path').value.trim(); $('btn-install-confirm').disabled = !ftpPath || !state.connected; const gameId = $('install-overlay').dataset.gameId; if (gameId) window.pork.addBackup(gameId, folder).catch(() => {}); }); $('btn-install-browse-ftp').addEventListener('click', async () => { $('install-expert-destination').open = true; const current = $('install-ftp-path').value.replace('(searching PS5…)', '') || '/'; const selected = await openFtpBrowser(current); if (selected) $('install-ftp-path').value = selected; }); $('btn-install-confirm').addEventListener('click', async () => { const game_id = $('install-overlay').dataset.gameId; const backupPath = $('install-overlay').dataset.backupPath; const activeTab = document.querySelector('.install-mode-tab.active'); const mode = activeTab?.dataset.installMode || 'pfs'; if (!backupPath) { setStatus('No local backup found for this game', 'error'); return; } $('btn-install-confirm').disabled = true; $('btn-install-confirm').textContent = 'Queueing…'; try { if (mode === 'pfs') { const destination = window.chimeraInstallDestination?.selection(); if (!destination) { setStatus('Select an eligible destination volume or open the Expert raw-path override', 'error'); return; } const ftpPath = destination.remotePath; if (!state.connected){ setStatus('Connect to PS5 first', 'error'); return; } const bpIdx = $('install-backpork-select').value; let pork = null; if (bpIdx !== '') { const bp = _installBackporkEntries[Number(bpIdx)]; if (bp) pork = { folder_name: bp.folder_name, folder_path: bp.folder_path }; } await window.pork.ftpInstallPfs({ game_id, localPath: backupPath, remotePath: ftpPath, destinationVolumeId: destination.destinationVolumeId, expertRawPath: destination.expertRawPath, pork, }); await window.pork.updateGame({ game_id, fields: { ftp_path: ftpPath } }); setStatus('Install queued — see Transfers tab for progress', 'ok'); } else { const ftpUploadCb = $('install-conv-ftp-upload'); const ftpUpload = ftpUploadCb?.checked ?? false; const ftpPath = ftpUpload ? ($('install-conv-ftp-path')?.value?.trim() || '') : ''; const deleteAfter = ftpUpload && ($('install-conv-delete-after')?.checked ?? false); const bpIdx = $('install-conv-backpork-select')?.value ?? ''; let backpork_path = ''; let firmware_label = ''; if (bpIdx !== '') { const bp = _installBackporkEntries[Number(bpIdx)]; if (bp && bp.game_path) { backpork_path = bp.game_path; firmware_label = bp.folder_name; } } const game_name = $('install-overlay').dataset.gameName || game_id; await window.pork.convQueueAdd({ game_id, game_name, game_path: backupPath, backpork_path, firmware_label, mode, ftpUpload, ftpRemotePath: ftpPath, deleteAfter, }); await refreshConvQueuePanel(); setStatus(`${mode.toUpperCase()} conversion queued — see Conversion Queue on Games page`, 'ok'); } } catch (e) { setStatus('Queue failed: ' + e.message, 'error'); } finally { $('install-overlay').hidden = true; $('btn-install-confirm').disabled = false; $('btn-install-confirm').textContent = '⬆ Queue Install'; } }); ['install-close', 'btn-install-cancel'].forEach(id => $(id).addEventListener('click', () => closeAccessibleDialog($('install-overlay'))) ); $('install-overlay').addEventListener('keydown', event => { if (event.key === 'Escape') closeAccessibleDialog($('install-overlay')); }); $('install-conv-ftp-upload').addEventListener('change', function () { const fields = $('install-conv-ftp-fields'); if (fields) fields.style.display = this.checked ? 'block' : 'none'; }); $('btn-install-conv-browse-local').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; $('install-overlay').dataset.backupPath = folder; const hint = $('install-backup-hint'); const convHint = $('install-conv-backup-hint'); if (hint) { hint.textContent = `Source: ${folder}`; hint.title = folder; } if (convHint) { convHint.textContent = `Source: ${folder}`; convHint.title = folder; } updateInstallConfirmState(document.querySelector('.install-mode-tab.active')?.dataset.installMode || 'pfs'); const gameId = $('install-overlay').dataset.gameId; if (gameId) window.pork.addBackup(gameId, folder).catch(() => {}); }); $('btn-install-conv-browse-ftp').addEventListener('click', async () => { const current = $('install-conv-ftp-path')?.value?.replace('(searching PS5…)', '') || '/'; const selected = await openFtpBrowser(current); if (selected && $('install-conv-ftp-path')) $('install-conv-ftp-path').value = selected; }); function isPs5NativeWorkspaceTitle(gameId) { return /^PPSA\d{5}$/.test(String(gameId || '').toUpperCase()) || String(gameId || '').toUpperCase() === 'FAKE00000'; } function selectPs5RestoreQualificationOperation(installs, titleId) { const expectedTitleId = String(titleId || '').toUpperCase(); const matching = (Array.isArray(installs) ? installs : []).filter(item => String(item?.titleId || '').toUpperCase() === expectedTitleId); const launchRelevant = matching.filter(item => ['installed_observed', 'launch_test_pending', 'launching', 'launch_verified', 'launch_failed'].includes(item.status) || (item.status === 'interrupted' && item.launchOutcome === 'outcome_unknown')); const candidates = launchRelevant.length ? launchRelevant : matching; return candidates.sort((a, b) => Number(b.updatedAt || b.createdAt || 0) - Number(a.updatedAt || a.createdAt || 0))[0] || null; } async function openModal(game_id) { try { const games = window.chimeraLibraryModel.items(await window.pork.listLibraryTitles({ search: game_id, limit: 10 })); const g = games.find(x => x.game_id === game_id); if (!g) return; state.currentGame = g; if (window.chimeraTitleWorkspace) window.chimeraTitleWorkspace.render(g); resetModalTabs(); const displayName = g.prospero_name || g.title || g.game_id; $('modal-title').textContent = displayName; $('modal-subtitle').textContent = `${g.game_id} · ${g.prospero_region || g.version ? (g.prospero_region || '') + (g.version ? ' v' + g.version : '') : 'version unknown'}`; const patches = tryParseJson(g.prospero_patches, []); const fwVersions = [...new Set(patches.map(p => p.requiredFirmware).filter(Boolean))]; const fwDisplay = fwVersions.length ? fwVersions.join(', ') : '—'; const dlc = tryParseJson(g.prospero_dlc, []); const infoRows = [ ['Game ID', g.game_id], ['Content ID', g.content_id || g.prospero_content_id || '—'], ['Version', g.version || g.prospero_version || '—'], ['Size', g.backup_size ? fmt(g.backup_size) : (g.size ? fmt(g.size) : (g.prospero_size || '—'))], ['Installed', window.chimeraLibraryModel.isInstalled(g) ? 'Yes' : 'No'], ['Local Copy', window.chimeraLibraryModel.hasLocalCopy(g) ? 'Yes' : 'No'], ['Prepared for firmware', g.porked_firmware ? `${g.porked_firmware}${g.porked_at ? ' (' + new Date(g.porked_at).toLocaleDateString() + ')' : ''}` : '—'], ['Publisher', g.prospero_publisher || '—'], ['Publisher ID', g.prospero_publisher_id || '—'], ['Region', g.prospero_region || '—'], ['Last Updated', g.prospero_last_updated || '—'], ['Required Firmware', fwDisplay], ['DLC Available', dlc.length ? `${dlc.length} item(s)` : 'None'], ['FTP Path', g.ftp_path || '—'], ['Last Scanned', formatGameTimestamp(g.last_scanned)], ['Metadata Age', g.prospero_fetched_at ? fmtAge(Date.now() - g.prospero_fetched_at) : 'Never fetched'], ]; renderModalMetrics(g, infoRows); $('modal-info').innerHTML = infoRows.map(([k, v]) => `${escHtml(k)}${escHtml(v)}`).join(''); const backups = await window.pork.listBackups(game_id); const localBackups = backups.filter(b => b.source !== 'backpork'); $('modal-backup-list').innerHTML = localBackups.length ? localBackups.map(b => `
  • ${escHtml(b.backup_path)}${fmt(b.size)}
  • `).join('') : '
  • No backups registered
  • '; // Legacy hash/manage endpoints intentionally accept retail CUSA/PPSA IDs only. // The exact pinned FAKE00000 launch reference still needs its guarded native // package workspace, but must not be routed through those unrelated endpoints. if (/^(?:CUSA|PPSA)\d{5}$/.test(game_id)) { await renderHashSection(game_id, backups); await renderManagePanel(g, backups); } else { $('modal-hash-section').hidden = true; $('manage-backpork-section').hidden = true; } renderProsperoData(g); renderCheatsPanel(g); $('btn-modal-backup').disabled = true; // enabled async below if folder found on FTP $('btn-modal-install').disabled = !state.connected || !backups.length; $('btn-modal-ps4-packages').hidden = !/^CUSA\d{5}$/.test(game_id); $('btn-modal-ps5-native-packages').hidden = !isPs5NativeWorkspaceTitle(game_id); if (typeof openAccessibleDialog === 'function') openAccessibleDialog($('modal-overlay'), $('modal-close')); else $('modal-overlay').hidden = false; if (state.connected) { window.pork.ftpFindGame(game_id).then(ftpPath => { if (ftpPath) { $('btn-modal-backup').disabled = false; if (ftpPath !== g.ftp_path) { window.pork.updateGame({ game_id, fields: { ftp_path: ftpPath } }); state.currentGame.ftp_path = ftpPath; } } }).catch(() => {}); } } catch (e) { setStatus(e.message, 'error'); } } function fmtAge(ms) { if (ms < 60000) return 'Just now'; if (ms < 3600000) return `${Math.floor(ms / 60000)}m ago`; if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ago`; return `${Math.floor(ms / 86400000)}d ago`; } function formatGameTimestamp(value) { if (value == null || value === '') return '—'; const numeric = Number(value); const date = Number.isFinite(numeric) ? new Date(numeric) : new Date(value); return Number.isNaN(date.getTime()) ? 'Unknown' : date.toLocaleString(); } $('btn-modal-mark').hidden = true; function ps4PackageText(key, fallback) { return typeof window.t === 'function' ? window.t(key, fallback) : fallback; } function setPs4PackageReadiness(message, stateName = '') { const element = $('ps4-package-readiness'); element.textContent = message; element.dataset.state = stateName; } async function refreshPs4PackageWorkspace() { const game = state.currentGame; if (!game || !/^CUSA\d{5}$/.test(game.game_id)) return; const [builder, jobs, packageData] = await Promise.all([ window.pork.ps4PackageBuilderStatus(), window.pork.ps4PackageJobs(), window.pork.packageRecipesList(game.id), ]); setPs4PackageReadiness(builder.available ? ps4PackageText('ps4Packages.builder.ready', `Builder ${builder.id} ${builder.version} is ready.`) : ps4PackageText('ps4Packages.builder.blocked', 'The pinned PS4 builder is not configured. Add its folder in Settings before building.'), builder.available ? 'ready' : 'blocked'); $('ps4-package-create').disabled = !builder.available || !$('ps4-package-base').value; const sets = (packageData.packageSets || []).filter(item => item.platform === 'ps4' && item.components?.some(component => component.type === 'base')); $('ps4-package-set').innerHTML = sets.length ? sets.map(item => ``).join('') : ``; $('ps4-package-restore').disabled = !sets.length; const titleJobs = jobs.filter(job => job.titleId === game.game_id).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)); $('ps4-package-jobs').innerHTML = titleJobs.length ? titleJobs.map(job => `
    ${escHtml(job.titleId)} · ${job.children.length} PKG${escHtml(job.status)} ${escHtml(job.children.map(child => `${child.type} ${child.version || ''}`).join(' · '))} ${['failed', 'interrupted'].includes(job.status) ? `` : ''}
    `).join('') : `

    ${escHtml(ps4PackageText('ps4Packages.jobs.empty', 'No package activity for this title yet.'))}

    `; window.i18n?.apply($('ps4-packages-overlay')); } async function openPs4PackageWorkspace() { const game = state.currentGame; if (!game || !/^CUSA\d{5}$/.test(game.game_id)) return; $('ps4-packages-game').textContent = `${game.prospero_name || game.title || game.game_id} · ${game.game_id}`; const registeredBackups = await window.pork.listBackups(game.game_id).catch(() => []); const folderBackup = registeredBackups.find(item => item.source !== 'backpork' && item.backup_type === 'folder'); $('ps4-package-base').value = folderBackup?.backup_path || ''; $('ps4-package-update').value = ''; $('ps4-package-create').disabled = true; if (typeof openAccessibleDialog === 'function') openAccessibleDialog($('ps4-packages-overlay'), $('ps4-packages-close')); else $('ps4-packages-overlay').hidden = false; try { await refreshPs4PackageWorkspace(); } catch (error) { setPs4PackageReadiness(error.message, 'blocked'); } } function closePs4PackageWorkspace() { if (typeof closeAccessibleDialog === 'function') closeAccessibleDialog($('ps4-packages-overlay')); else $('ps4-packages-overlay').hidden = true; } $('btn-modal-ps4-packages').addEventListener('click', openPs4PackageWorkspace); ['ps4-packages-close', 'ps4-packages-done'].forEach(id => $(id).addEventListener('click', closePs4PackageWorkspace)); ['base', 'update'].forEach(type => $(`ps4-package-${type}-browse`).addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; $(`ps4-package-${type}`).value = folder; $('ps4-package-create').disabled = !$('ps4-package-base').value; })); $('ps4-package-refresh').addEventListener('click', () => refreshPs4PackageWorkspace().catch(error => setPs4PackageReadiness(error.message, 'blocked'))); $('ps4-package-create').addEventListener('click', async () => { const game = state.currentGame; const sources = [{ sourcePath: $('ps4-package-base').value, componentType: 'base' }]; if ($('ps4-package-update').value) sources.push({ sourcePath: $('ps4-package-update').value, componentType: 'update' }); $('ps4-package-create').disabled = true; setPs4PackageReadiness(ps4PackageText('ps4Packages.create.running', 'Validating dump evidence and building verified packages…')); try { const job = await window.pork.ps4PackageCreate({ expectedTitleId: game.game_id, sources }); await window.pork.ps4PackageRun(job.id); setPs4PackageReadiness(ps4PackageText('ps4Packages.create.completed', 'PKG backup completed, hash-verified and registered.'), 'ready'); await refreshPs4PackageWorkspace(); } catch (error) { setPs4PackageReadiness(error.message, 'blocked'); } finally { $('ps4-package-create').disabled = !$('ps4-package-base').value; } }); $('ps4-package-restore').addEventListener('click', async () => { const game = state.currentGame; const packageSetId = Number($('ps4-package-set').value); if (!packageSetId) return; try { const plan = await window.pork.ps4PackageRestorePlan({ titleId: game.id, packageSetId }); const detail = plan.orderedComponents.map(item => `${item.type.toUpperCase()} · ${item.version || '—'} · ${item.sha256.slice(0, 12)}…`).join('\n'); const confirmation = await showExactConfirmation(`${ps4PackageText('ps4Packages.restore.confirm', 'Hand off this verified package set to etaHEN DPI in the exact order below?')}\n\n${detail}\n\n${ps4PackageText('ps4Packages.restore.boundary', 'A successful handoff is not installed evidence. A console scan remains required.')}`, 'INSTALL PS4 PACKAGES'); if (!confirmation) return; setPs4PackageReadiness(ps4PackageText('ps4Packages.restore.running', 'Sending ordered PKG handoffs…')); const result = await window.pork.ps4PackageRestore({ titleId: game.id, packageSetId, confirmation }); setPs4PackageReadiness(result.status === 'awaiting_post_scan' ? ps4PackageText('ps4Packages.restore.awaitingScan', 'Handoffs accepted. Installation is not yet verified; run the post-install scan.') : result.status, 'ready'); } catch (error) { setPs4PackageReadiness(error.message, 'blocked'); } }); $('ps4-package-verify').addEventListener('click', async () => { const game = state.currentGame; setPs4PackageReadiness(ps4PackageText('ps4Packages.verify.running', 'Scanning the console for installed evidence…')); try { const result = await window.pork.ps4PackageRestoreVerify(game.game_id); setPs4PackageReadiness(result.verified ? ps4PackageText('ps4Packages.verify.completed', 'Console scan verified the installation.') : ps4PackageText('ps4Packages.verify.notObserved', 'The console scan did not observe the installation yet.'), result.verified ? 'ready' : 'blocked'); } catch (error) { setPs4PackageReadiness(error.message, 'blocked'); } }); $('ps4-package-jobs').addEventListener('click', async event => { const button = event.target.closest('[data-ps4-resume]'); if (!button) return; try { await window.pork.ps4PackageResume(button.dataset.ps4Resume); await window.pork.ps4PackageRun(button.dataset.ps4Resume); await refreshPs4PackageWorkspace(); } catch (error) { setPs4PackageReadiness(error.message, 'blocked'); } }); function ps5NativePackageText(key, fallback) { return typeof window.t === 'function' ? window.t(key, fallback) : fallback; } function ps5A10Text(key) { return ps5NativePackageText(`ps5NativePackages.a10Control.${key}`, key); } function confirmPs5A10(prompt, boundary, phrase, evidence = '') { return showExactConfirmation(`${ps5A10Text(prompt)}${evidence ? `\n\n${evidence}` : ''}\n\n${ps5A10Text(boundary)}`, phrase); } let ps5ConsoleDiscovery = null; let ps5PreparedSources = null; let ps5NativeInstallOperationId = null; let ps5UpstreamControlOperationId = null; let ps5LaunchReferenceOperationId = null; let ps5A8ControlPlan = null; let ps5A8ControlOperationId = null; let ps5A10ControlPlan = null; let ps5A10ControlOperationId = null; let ps5P15R2LastPreflight = null; let ps5P15R2QualificationAttempted = false; const ps5Control = suffix => $(`ps5-upstream-control-${suffix}`); const ps5A8Control = suffix => $(`ps5-a8-control-${suffix}`); const ps5A10Control = suffix => $(`ps5-a10-control-${suffix}`); const ps5LaunchReference = suffix => $(`ps5-launch-reference-${suffix}`); function ps5SourcesFromStagedJob(job) { if (!job?.stagedPath || !job?.manifest?.components) return null; const sources = job.manifest.components.filter(item => item.packageBuild?.supported === true).map(item => ({ componentType: item.type, sourcePath: `${job.stagedPath}/components/${item.id}` })); return sources.some(item => item.componentType === 'base') ? sources : null; } function setPs5NativePackageReadiness(message, stateName = '') { const element = $('ps5-native-package-readiness'); element.textContent = message; element.dataset.state = stateName; } function setPs5FpkgEvidence(id, message, stateName = 'unknown') { const element = $(id); if (!element) return; element.textContent = message; element.dataset.state = stateName; } function renderPs5RestoreQualification(operation) { const launchButton = $('ps5-native-package-launch'); if (!operation) { setPs5FpkgEvidence('ps5-native-restore-evidence', ps5NativePackageText('ps5NativePackages.evidence.notInstalled', 'Not installed'), 'unproven'); launchButton.disabled = true; return; } ps5NativeInstallOperationId = operation.id; if (operation.status === 'launch_verified') { setPs5FpkgEvidence('ps5-native-restore-evidence', ps5NativePackageText('ps5NativePackages.launch.qualified', 'Installed · Launch verified · Qualified for restore'), 'qualified'); launchButton.disabled = true; } else if (operation.status === 'launch_failed') { setPs5FpkgEvidence('ps5-native-restore-evidence', `${ps5NativePackageText('ps5NativePackages.launch.failed', 'Installed · launch failed · Not qualified for restore')} · ${operation.error?.code || 'PS5_FPKG_LAUNCH_UNVERIFIED'}`, 'failed'); launchButton.disabled = true; } else if (operation.status === 'launching') { setPs5FpkgEvidence('ps5-native-restore-evidence', ps5NativePackageText('ps5NativePackages.launch.observing', 'Installed · observing exact title process…'), 'running'); launchButton.disabled = true; } else if (operation.status === 'interrupted' && operation.launchOutcome === 'outcome_unknown') { setPs5FpkgEvidence('ps5-native-restore-evidence', ps5NativePackageText('ps5NativePackages.launch.unknown', 'Installed · launch outcome unknown · Not qualified'), 'failed'); launchButton.disabled = true; } else if (operation.status === 'installed_observed') { setPs5FpkgEvidence('ps5-native-restore-evidence', ps5NativePackageText('ps5NativePackages.launch.pending', 'Installed · launch verification pending'), 'pending'); launchButton.disabled = false; } else { setPs5FpkgEvidence('ps5-native-restore-evidence', ps5NativePackageText('ps5NativePackages.evidence.installPending', 'Installation not yet proven'), 'unproven'); launchButton.disabled = true; } } function renderPs5UpstreamControl(control = {}) { const ready = control.status === 'ready'; const proven = control.installTrust?.status === 'post-scan-proven'; ps5Control('trust').textContent = proven ? ps5NativePackageText('ps5NativePackages.control.proven', 'Post-scan proven') : ps5NativePackageText('ps5NativePackages.control.unproven', 'Unproven'); ps5Control('trust').dataset.trust = proven ? 'post-scan-proven' : 'unproven'; ps5Control('status').textContent = ready ? `${ps5NativePackageText('ps5NativePackages.control.ready', 'Pinned upstream control is verified and registered.')} · ${String(control.sha256 || '').slice(0, 12)}…` : ps5NativePackageText('ps5NativePackages.control.notImported', 'Control package not imported.'); ps5Control('import').disabled = ready; ps5Control('restore').disabled = !ready; ps5Control('verify').disabled = !ps5UpstreamControlOperationId; } function renderPs5A8Control(control = {}, operation = null) { const ready = control.status === 'ready'; const retry = control.status === 'built_verified'; const drift = control.status === 'artifact_drift'; const awaitingScan = operation?.status === 'awaiting_post_scan'; const installed = operation?.status === 'installed_observed'; const launching = operation?.status === 'launching'; const launchVerified = operation?.status === 'launch_verified'; const launchFailed = operation?.status === 'launch_failed'; const installInFlight = ['preparing', 'staging', 'installing'].includes(operation?.status); ps5A8Control('trust').removeAttribute('data-i18n'); ps5A8Control('status').removeAttribute('data-i18n'); ps5A8Control('trust').textContent = launchVerified ? ps5NativePackageText('ps5NativePackages.a8Control.launchVerified', 'Installed · launch verified') : launchFailed ? ps5NativePackageText('ps5NativePackages.a8Control.launchFailed', 'Installed · launch failed') : installed || launching ? ps5NativePackageText('ps5NativePackages.a8Control.installedPendingLaunch', 'Installed · launch unproven') : installInFlight || awaitingScan ? ps5NativePackageText('ps5NativePackages.a8Control.installUnprovenTrust', 'Install result · scan required') : ready ? ps5NativePackageText('ps5NativePackages.a8Control.readyTrust', 'Host ready · install unproven') : retry ? ps5NativePackageText('ps5NativePackages.a8Control.retryTrust', 'Verified · registration pending') : drift ? ps5NativePackageText('ps5NativePackages.a8Control.driftTrust', 'Artifact drift blocked') : ps5NativePackageText('ps5NativePackages.a8Control.unprepared', 'Not prepared'); ps5A8Control('trust').dataset.trust = launchVerified ? 'qualified' : launchFailed ? 'failed' : installed || launching ? 'post-scan-proven' : installInFlight || awaitingScan ? 'pending' : drift ? 'failed' : 'unproven'; ps5A8Control('status').textContent = launchVerified ? ps5NativePackageText('ps5NativePackages.a8Control.launchVerified', 'Installed and exact-title launch verified.') : launchFailed ? `${ps5NativePackageText('ps5NativePackages.a8Control.launchFailed', 'Installed, but launch qualification failed.')} · ${operation.error?.code || 'PS5_FPKG_LAUNCH_UNVERIFIED'}` : launching ? ps5NativePackageText('ps5NativePackages.a8Control.launching', 'Observing the exact CHIM00002 process…') : installed ? ps5NativePackageText('ps5NativePackages.a8Control.installedPendingLaunch', 'Installation proven. Exact-title launch remains unproven.') : awaitingScan ? ps5NativePackageText('ps5NativePackages.a8Control.awaitingScan', 'Installer completed. Run the independent CHIM00002 post-scan.') : installInFlight ? ps5NativePackageText('ps5NativePackages.a8Control.installing', 'Installing the exact CHIM00002 control…') : ready ? `${ps5NativePackageText('ps5NativePackages.a8Control.ready', 'Frozen metadata-qualified CHIM00002 artifact is verified and registered. Installation and launch remain unproven.')} · ${String(control.sha256 || '').slice(0, 12)}…` : retry ? ps5NativePackageText('ps5NativePackages.a8Control.registrationPending', 'Artifact verified; retry host registration without rebuilding.') : drift ? ps5NativePackageText('ps5NativePackages.a8Control.drift', 'Selected artifact changed and is blocked.') : ps5NativePackageText('ps5NativePackages.a8Control.notPrepared', 'Frozen artifact is included but not registered.'); const active = installInFlight || awaitingScan || installed || launching || launchVerified; ps5A8Control('prepare').disabled = ready || drift || control.status === 'server-required'; ps5A8Control('preflight').disabled = !ready || active || control.status === 'server-required'; ps5A8Control('install').disabled = !ready || !ps5A8ControlPlan || active; ps5A8Control('verify').disabled = !awaitingScan; ps5A8Control('launch').disabled = !installed; } function renderPs5A10Control(control = {}, operation = null, runtime = {}) { const ui = ps5A10Control; const op = operation?.status || ''; const ready = control.status === 'ready'; const drift = control.status === 'artifact_drift'; const scan = op === 'awaiting_post_scan'; const ins = op === 'installed_observed'; const run = op === 'launching'; const flight = ['preparing', 'staging', 'installing'].includes(op); const active = runtime?.diagnostic?.engine?.available === true && runtime.diagnostic.engine.processOwned === true; const exact = active && runtime?.diagnosticIdentity?.status === 'exact' && runtime.diagnosticIdentity.mutationAllowed === true; const runtimeEligible = ['installed_observed', 'launch_failed', 'launch_verified'].includes(op); const launchEligible = op === 'installed_observed'; const state = op === 'launch_verified' ? 'launchVerified' : op === 'launch_failed' ? 'launchFailed' : op === 'interrupted' ? 'launchInterrupted' : run ? 'launching' : exact ? 'runtimeExact' : active ? 'runtimeActive' : ins ? 'installedPendingRuntime' : scan ? 'awaitingScan' : flight ? 'installing' : ready ? 'ready' : control.status === 'built_verified' ? 'registrationPending' : drift ? 'drift' : 'notPrepared'; const tk = { launchVerified: 'launchVerified', launchFailed: 'launchFailed', launchInterrupted: 'launchInterrupted', runtimeExact: 'runtimeExactTrust', runtimeActive: 'runtimeActiveTrust', installedPendingRuntime: 'installedPendingRuntime', awaitingScan: 'installUnprovenTrust', installing: 'installUnprovenTrust', ready: 'readyTrust', registrationPending: 'retryTrust', drift: 'driftTrust', notPrepared: 'unprepared', launching: 'installedPendingRuntime' }[state]; const t = ui('trust'); const status = ui('status'); t.removeAttribute('data-i18n'); status.removeAttribute('data-i18n'); t.textContent = ps5A10Text(tk); t.dataset.trust = state === 'launchVerified' ? 'qualified' : ['launchFailed', 'drift'].includes(state) ? 'failed' : active || ins || run ? 'post-scan-proven' : scan || flight ? 'pending' : 'unproven'; const sk = state === 'launchVerified' ? 'launchVerifiedDetail' : state === 'launchFailed' ? 'launchFailedDetail' : state === 'launchInterrupted' ? 'launchInterruptedDetail' : state; status.textContent = `${ps5A10Text(sk)}${state === 'launchFailed' ? ` · ${operation.error?.code || 'PS5_FPKG_LAUNCH_UNVERIFIED'}` : state === 'ready' ? ` · ${String(control.sha256 || '').slice(0, 12)}…` : ''}`; const s = $('ps5-a10-control-evidence-summary'); const id = String(operation?.id || ''); if (s.dataset.operationId !== id) { s.hidden = true; s.textContent = ''; s.dataset.operationId = id; } const busy = flight || scan || ins || run || op.startsWith('launch_'); ui('prepare').disabled = ready || drift || control.status === 'server-required'; ui('preflight').disabled = !ready || busy || control.status === 'server-required'; ui('install').disabled = !ready || !ps5A10ControlPlan || busy; ui('verify').disabled = !scan; ui('stage').disabled = !runtimeEligible || active; ui('payload').disabled = !runtimeEligible || !active || exact; ui('launch').disabled = !launchEligible || !exact; ui('restore').disabled = !active; ui('evidence').disabled = !operation?.launchEvidence || !['launch_failed', 'launch_verified', 'interrupted'].includes(op); } function renderPs5P15R2Qualification(status = {}, preflight = ps5P15R2LastPreflight) { const matrix = Array.isArray(preflight?.matrix) ? preflight.matrix : []; const body = $('ps5-p15-r2-matrix'); body.innerHTML = matrix.length ? matrix.map(row => { const kstuff = row.gate === 'kstuff' ? preflight?.evidence?.kstuff : null; const evidence = kstuff ? `${escHtml(JSON.stringify({ reason: row.reason || null, ...kstuff }))}` : escHtml(row.reason || 'exact'); return `${escHtml(row.gate)}${escHtml(row.result)}${evidence}`; }).join('') : `${escHtml(ps5NativePackageText('ps5NativePackages.r4.notCollected', 'No current matrix collected.'))}`; const profileCreated = Boolean(status.profile); const budget = status.budget || { state: 'blocked', used: 0, total: 1 }; const ready = preflight?.allGatesPass === true; const label = profileCreated ? ps5NativePackageText('ps5NativePackages.r4.ready', 'READY · immutable profile created · launch remains locked') : preflight ? ready ? ps5NativePackageText('ps5NativePackages.r4.allPass', 'All gates PASS · qualification is available') : ps5NativePackageText('ps5NativePackages.r4.blocked', 'Preflight blocked · qualification remains disabled') : ps5NativePackageText('ps5NativePackages.r4.statusPending', 'Read-only status loaded; live preflight has not run.'); $('ps5-p15-r2-state').textContent = `${label} · ${budget.used || 0}/${budget.total || 1}`; $('ps5-p15-r2-qualify').disabled = !ready || profileCreated || budget.state === 'consumed' || ps5P15R2QualificationAttempted; } function renderPs5LaunchReference(reference = {}) { const ready = reference.status === 'ready'; const installed = reference.installTrust?.status === 'post-scan-proven'; ps5LaunchReference('trust').textContent = installed ? ps5NativePackageText('ps5NativePackages.launchReference.launchRequired', 'Installed · launch required') : ps5NativePackageText('ps5NativePackages.launchReference.unproven', 'Unproven'); ps5LaunchReference('trust').dataset.trust = installed ? 'post-scan-proven' : 'unproven'; ps5LaunchReference('status').textContent = ready ? `${ps5NativePackageText('ps5NativePackages.launchReference.ready', 'Verified websrv launcher.')} · ${String(reference.sha256 || '').slice(0, 12)}…` : ps5NativePackageText('ps5NativePackages.launchReference.notImported', 'Not imported.'); ps5LaunchReference('import').disabled = ready; ps5LaunchReference('restore').disabled = !ready; ps5LaunchReference('verify').disabled = !ps5LaunchReferenceOperationId; } async function monitorPs5ReferenceInstall(operationId, { active, element, key, label, awaiting, dpiAwaiting, completed }) { while (active() === operationId) { await new Promise(resolve => setTimeout(resolve, 2000)); let operation; try { operation = await window.pork.ps5NativePackageRestoreStatus(operationId); } catch (error) { element('status').textContent = error.message; return; } const component = operation.components?.[operation.currentIndex]; if (operation.status === 'staging' || operation.status === 'installing') { const progress = operation.status === 'staging' ? Number(component?.stagingProgress?.percent || 0) : (() => { const total = Number(component?.total || component?.size || 0); return total > 0 ? Math.min(100, Math.floor((Number(component?.downloaded || component?.installedBytes || 0) / total) * 100)) : 0; })(); element('status').textContent = `${ps5NativePackageText(`ps5NativePackages.${key}.${operation.status}`, `${operation.status === 'staging' ? 'Staging' : 'Installing'} ${label}`)} · ${progress}%`; continue; } if (operation.status === 'awaiting_post_scan') { element('status').textContent = component?.acceptedUnverified && dpiAwaiting ? ps5NativePackageText(`ps5NativePackages.${key}.dpiAwaitingScan`, dpiAwaiting) : ps5NativePackageText(`ps5NativePackages.${key}.awaitingScan`, awaiting); element('verify').disabled = false; return; } if (operation.status === 'installed_observed' && completed) { element('status').textContent = completed; return; } element('status').textContent = operation.error?.message || operation.status; return; } } async function monitorPs5A8ControlLaunch(operationId) { while (ps5A8ControlOperationId === operationId) { await new Promise(resolve => setTimeout(resolve, 2000)); let operation; try { operation = await window.pork.ps5NativePackageRestoreLaunchStatus(operationId); } catch (error) { ps5A8Control('status').textContent = error.message; return; } renderPs5A8Control({ status: 'ready' }, operation); if (operation.status === 'launch_verified' || operation.status === 'launch_failed') return; if (operation.status !== 'launching') { ps5A8Control('status').textContent = operation.error?.message || operation.status; return; } } } async function monitorPs5A10ControlLaunch(operationId) { while (ps5A10ControlOperationId === operationId) { await new Promise(resolve => setTimeout(resolve, 2000)); let operation; let runtime; try { [operation, runtime] = await Promise.all([ window.pork.ps5NativePackageRestoreLaunchStatus(operationId), window.pork.ps5P15A10('runtime-status'), ]); } catch (error) { ps5A10Control('status').textContent = error.message; return; } renderPs5A10Control({ status: 'ready' }, operation, runtime); if (operation.status === 'launch_verified' || operation.status === 'launch_failed') return; if (operation.status !== 'launching') { ps5A10Control('status').textContent = operation.error?.message || operation.status; return; } } } async function monitorPs5NativePackageInstall(operationId) { while (ps5NativeInstallOperationId === operationId) { await new Promise(resolve => setTimeout(resolve, 2000)); let operation; try { operation = await window.pork.ps5NativePackageRestoreStatus(operationId); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); return; } const component = operation.components?.[operation.currentIndex]; if (operation.status === 'staging') { $('ps5-native-package-cancel').disabled = true; const percent = Number(component?.stagingProgress?.percent || 0); setPs5NativePackageReadiness(`${ps5NativePackageText('ps5NativePackages.restore.staging', 'Uploading verified PS5 .pkg to console staging')} · ${String(component?.type || '').toUpperCase()} · ${percent}%`, 'running'); continue; } if (operation.status === 'installing') { $('ps5-native-package-cancel').disabled = false; const total = Number(component?.total || component?.size || 0); const transferred = Number(component?.downloaded || component?.installedBytes || 0); const percent = total > 0 ? Math.min(100, Math.floor((transferred / total) * 100)) : 0; setPs5NativePackageReadiness(`${ps5NativePackageText('ps5NativePackages.restore.installing', 'Installing verified PS5 .pkg')} · ${String(component?.type || '').toUpperCase()} · ${percent}%`, 'running'); continue; } if (operation.status === 'awaiting_post_scan') { $('ps5-native-package-cancel').disabled = true; setPs5NativePackageReadiness(component?.acceptedUnverified ? ps5NativePackageText('ps5NativePackages.restore.dpiAwaitingScan', 'Standalone DPI accepted the package. Only the independent console scan can prove installation.') : ps5NativePackageText('ps5NativePackages.restore.awaitingScan', 'PS5Upload verified completion. Run the independent console scan before treating it as installed.'), 'ready'); return; } $('ps5-native-package-cancel').disabled = true; if (operation.status === 'installed_observed') { renderPs5RestoreQualification(operation); setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.verify.completedPendingLaunch', 'Installation proven. Launch verification is still required.'), 'ready'); return; } setPs5NativePackageReadiness(operation.error?.message || operation.status, 'blocked'); return; } } async function monitorPs5NativePackageBuild(jobId) { const phaseKeys = { queued: ['ps5NativePackages.phase.queued', 'Queued'], source_validation: ['ps5NativePackages.phase.sourceValidation', 'Validating source evidence'], component_build: ['ps5NativePackages.phase.componentBuild', 'Building package components'], artifact_verification: ['ps5NativePackages.phase.artifactVerification', 'Validating FIH and CNT structures'], built: ['ps5NativePackages.phase.built', 'Artifacts built'], registering: ['ps5NativePackages.phase.registering', 'Registering verified artifacts'], registration_pending: ['ps5NativePackages.phase.registrationPending', 'Registration retry queued'], }; for (;;) { await new Promise(resolve => setTimeout(resolve, 2000)); let job; try { job = await window.pork.ps5NativePackageJob(jobId); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); return; } if (job.status === 'artifact_registered') { await refreshPs5NativePackageWorkspace(); setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.create.completed', 'PS5 fPKG set is structurally verified and registered. Install and launch remain unproven.'), 'ready'); return; } if (job.status === 'built_unregistered') { await refreshPs5NativePackageWorkspace(); setPs5NativePackageReadiness(job.error || ps5NativePackageText('ps5NativePackages.create.unregistered', 'Package artifacts are safe on disk, but registration failed. Resume retries registration without rebuilding.'), 'blocked'); return; } if (['failed', 'interrupted'].includes(job.status)) { await refreshPs5NativePackageWorkspace(); setPs5NativePackageReadiness(job.error || job.status, 'blocked'); return; } const [key, fallback] = phaseKeys[job.phase] || ['ps5NativePackages.phase.running', 'Build in progress']; const component = job.componentProgress?.find(item => ['building', 'verifying'].includes(item.status)); const suffix = component ? ` · ${String(component.type || '').toUpperCase()}` : ''; setPs5NativePackageReadiness(`${ps5NativePackageText(key, fallback)}${suffix}`, 'running'); } } function ps5NativePackageSetOptionLabel(item) { const component = item.components?.[0] || {}; const version = component.version || component.metadata?.version?.value || '—'; const digest = String(component.sha256 || '').slice(0, 12) || '—'; return `${item.label || item.setKey} · v${version} · ${digest}… · #${item.id}`; } function renderPs5PackageComparison(result) { const trustText = trust => trust.status === 'post-scan-proven' ? ps5NativePackageText('ps5NativePackages.compare.proven', 'Post-scan proven') : trust.status === 'appinst-rejected' ? `${ps5NativePackageText('ps5NativePackages.compare.rejected', 'AppInst rejected')} · 0x${Number(trust.appInstError || 0).toString(16).toUpperCase()}` : ps5NativePackageText('ps5NativePackages.compare.unproven', 'Trust unproven'); const differenceRows = result.differences.length ? result.differences.map(item => `
  • ${escHtml(item.component)}${escHtml(item.kind)} · ${escHtml(item.fields.join(', '))}
  • `).join('') : `
  • ${escHtml(ps5NativePackageText('ps5NativePackages.compare.identical', 'No material artifact difference'))}${escHtml(ps5NativePackageText('ps5NativePackages.compare.identicalDetail', 'Retrying the same bytes cannot change the known AppInst outcome.'))}
  • `; $('ps5-package-comparison-result').innerHTML = `
    ${escHtml(ps5NativePackageText('ps5NativePackages.compare.candidate', 'Candidate'))}${escHtml(trustText(result.candidateTrust))}${escHtml(ps5NativePackageText('ps5NativePackages.compare.reference', 'Reference'))}${escHtml(trustText(result.referenceTrust))}

    ${escHtml(ps5NativePackageText(`ps5NativePackages.compare.next.${result.nextAction}`, result.nextAction))}

    `; } async function refreshPs5NativePackageWorkspace() { const game = state.currentGame; if (!game || !isPs5NativeWorkspaceTitle(game.game_id)) return; const [builder, jobs, stageJobs, packageData, control, a8Control, a10Control, a10Runtime, launchReference, installs, r2Status] = await Promise.all([window.pork.ps5NativePackageBuilderStatus(), window.pork.ps5NativePackageJobs(), window.pork.ps5ConsoleStagingJobs(), window.pork.packageRecipesList(game.id), window.pork.ps5UpstreamControl('status'), window.pork.ps5A8DistinctControl('status'), window.pork.ps5A10TransitionControl('status'), window.pork.ps5P15A10('runtime-status'), window.pork.ps5LaunchReference('status'), window.pork.ps5NativePackageRestoreList(), window.pork.ps5P15R2Status()]); const awaiting = (installs || []).filter(item => item.status === 'awaiting_post_scan').sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0)); const a8Operation = (installs || []).filter(item => item.titleId === 'CHIM00002' && Number(item.packageSetId) === Number(a8Control.packageSetId) && item.components?.length === 1 && item.components[0]?.sha256 === a8Control.sha256 && Number(item.components[0]?.size) === Number(a8Control.size)).sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0))[0] || null; const a10Operation = (installs || []).filter(item => item.titleId === 'CHIM00003' && Number(item.packageSetId) === Number(a10Control.packageSetId) && item.components?.length === 1 && item.components[0]?.sha256 === a10Control.sha256 && Number(item.components[0]?.size) === Number(a10Control.size)).sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0))[0] || null; ps5UpstreamControlOperationId = awaiting.find(item => item.titleId === 'PPSA99099')?.id || null; ps5LaunchReferenceOperationId = awaiting.find(item => item.titleId === 'FAKE00000')?.id || null; ps5A8ControlOperationId = a8Operation?.id || null; ps5A10ControlOperationId = a10Operation?.id || null; if (a8Operation || a8Control.status !== 'ready') ps5A8ControlPlan = null; if (a10Operation || a10Control.status !== 'ready') ps5A10ControlPlan = null; renderPs5UpstreamControl(control); renderPs5P15R2Qualification(r2Status); renderPs5A8Control(a8Control, a8Operation); renderPs5A10Control(a10Control, a10Operation, a10Runtime); renderPs5LaunchReference(launchReference); setPs5NativePackageReadiness(builder.available ? ps5NativePackageText('ps5NativePackages.builder.ready', 'The pinned native package bridge is ready.') : ps5NativePackageText('ps5NativePackages.builder.blocked', 'The revision-pinned LibProsperoPkg bridge is not configured.'), builder.available ? 'ready' : 'blocked'); $('ps5-native-package-create').disabled = !builder.available || !$('ps5-native-package-source').value; const sets = (packageData.packageSets || []).filter(item => item.platform === 'ps5' && item.manifest?.packageFormat?.container === 'FIH'); $('ps5-native-package-set').innerHTML = sets.length ? sets.map(item => ``).join('') : ``; const comparisonOptions = sets.map(item => ``).join(''); $('ps5-package-comparison-candidate').innerHTML = comparisonOptions || ''; $('ps5-package-comparison-reference').innerHTML = comparisonOptions || ''; if (sets.length > 1) $('ps5-package-comparison-reference').value = String(sets[1].id); $('ps5-package-comparison-run').disabled = sets.length < 2; $('ps5-native-package-restore').disabled = !sets.length; const titleJobs = jobs.filter(job => job.titleId === game.game_id).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)); const titleStageJobs = stageJobs.filter(job => job.titleId === game.game_id).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)); const qualificationOperation = selectPs5RestoreQualificationOperation(installs, game.game_id); renderPs5RestoreQualification(qualificationOperation); const captured = titleStageJobs.find(job => job.status === 'staged' && job.manifest?.components?.length); if (captured) { const components = captured.manifest.components; const complete = components.every(item => item.preservation?.complete === true && item.preservation?.verified === true); const files = components.reduce((sum, item) => sum + Number(item.preservation?.fileCount || item.fileCount || 0), 0); setPs5FpkgEvidence('ps5-native-preservation-evidence', complete ? `${ps5NativePackageText('ps5NativePackages.evidence.backupSafe', 'Backup safe')} · ${files.toLocaleString()} ${ps5NativePackageText('ps5NativePackages.evidence.filesVerified', 'files verified')}` : ps5NativePackageText('ps5NativePackages.evidence.backupIncomplete', 'Backup incomplete'), complete ? 'safe' : 'failed'); const baseReadiness = components.find(item => item.type === 'base')?.fpkgReadiness; if (baseReadiness?.ready === true) { const modules = Number(baseReadiness.modules?.total || 0); const unresolved = Number(baseReadiness.modules?.unresolved || 0); setPs5FpkgEvidence('ps5-native-fpkg-readiness-evidence', `${ps5NativePackageText('ps5NativePackages.evidence.ready', 'Ready for fPKG')} · ${modules} ${ps5NativePackageText('ps5NativePackages.evidence.modulesInspected', 'modules inspected')} · ${unresolved} ${ps5NativePackageText('ps5NativePackages.evidence.unresolved', 'unresolved')}`, 'ready'); } else { const unresolved = Number(baseReadiness?.modules?.unresolved || 0); setPs5FpkgEvidence('ps5-native-fpkg-readiness-evidence', `${ps5NativePackageText('ps5NativePackages.evidence.buildBlocked', 'fPKG build blocked')} · ${unresolved} ${ps5NativePackageText('ps5NativePackages.evidence.unresolved', 'unresolved')}`, 'blocked'); } } else { setPs5FpkgEvidence('ps5-native-preservation-evidence', ps5NativePackageText('ps5NativePackages.evidence.notCaptured', 'Not captured'), 'unknown'); setPs5FpkgEvidence('ps5-native-fpkg-readiness-evidence', ps5NativePackageText('ps5NativePackages.evidence.notAnalyzed', 'Not analyzed'), 'unknown'); } if (!ps5PreparedSources) ps5PreparedSources = ps5SourcesFromStagedJob(titleStageJobs.find(job => job.status === 'staged')); if (ps5PreparedSources?.length) $('ps5-native-package-source').value = ps5PreparedSources.find(item => item.componentType === 'base')?.sourcePath || ''; $('ps5-native-package-create').disabled = !builder.available || !$('ps5-native-package-source').value; const stageRows = titleStageJobs.map(job => `
    ${escHtml(job.titleId)} · console capture${escHtml(job.status)}${escHtml(job.components.map(item => item.type).join(' + '))} · ${job.manifest ? escHtml(`${job.manifest.components.reduce((sum, item) => sum + item.fileCount, 0)} hashed files`) : escHtml(ps5NativePackageText('ps5NativePackages.stage.awaiting', 'awaiting complete inventory'))}${['failed', 'interrupted'].includes(job.status) ? `` : ''}
    `); const packageRows = titleJobs.map(job => `
    ${escHtml(job.titleId)} · native .pkg${escHtml(job.status)}${escHtml(job.version || '—')} · ${escHtml(`${job.components?.length || 1} component(s)`)} · ${escHtml(job.phase || job.container?.validation?.validationLevel || 'awaiting FIH verification')}${['failed', 'interrupted', 'built_unregistered'].includes(job.status) ? `` : ''}
    `); $('ps5-native-package-jobs').innerHTML = stageRows.length || packageRows.length ? [...stageRows, ...packageRows].join('') : `

    ${escHtml(ps5NativePackageText('ps5NativePackages.jobs.empty', 'No native package activity for this title yet.'))}

    `; window.i18n?.apply($('ps5-native-packages-overlay')); } async function openPs5NativePackageWorkspace() { const game = state.currentGame; if (!game || !isPs5NativeWorkspaceTitle(game.game_id)) return; ps5ConsoleDiscovery = null; ps5PreparedSources = null; ps5A8ControlPlan = null; ps5A10ControlPlan = null; ps5P15R2LastPreflight = null; ps5P15R2QualificationAttempted = false; const r4Card = $('ps5-p15-r2-card'); r4Card.className = 'ps5-upstream-control ps5-p15-r2-qualification'; r4Card.setAttribute('aria-labelledby', 'ps5-p15-r2-title'); $('ps5-p15-r2-title').textContent = ps5NativePackageText('ps5NativePackages.r4.title', 'PPSA99099 read-only qualification'); $('ps5-p15-r2-state').className = 'ps5-upstream-control-status'; $('ps5-p15-r2-matrix').closest('table').className = 'ps5-p15-r2-matrix'; $('ps5-p15-r2-preflight').className = 'btn btn-accent'; $('ps5-p15-r2-preflight').textContent = ps5NativePackageText('ps5NativePackages.r4.preflight', 'Collect read-only preflight'); $('ps5-p15-r2-qualify').className = 'btn btn-teal'; $('ps5-p15-r2-qualify').textContent = ps5NativePackageText('ps5NativePackages.r4.qualify', 'Run one qualification'); $('ps5-native-packages-game').textContent = `${game.prospero_name || game.title || game.game_id} · ${game.game_id}`; const backups = await window.pork.listBackups(game.game_id).catch(() => []); const folder = backups.find(item => item.source !== 'backpork' && item.backup_type === 'folder'); $('ps5-native-package-source').value = folder?.backup_path || ''; if (typeof openAccessibleDialog === 'function') openAccessibleDialog($('ps5-native-packages-overlay'), $('ps5-native-packages-close')); else $('ps5-native-packages-overlay').hidden = false; try { await refreshPs5NativePackageWorkspace(); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); } } function closePs5NativePackageWorkspace() { if (typeof closeAccessibleDialog === 'function') closeAccessibleDialog($('ps5-native-packages-overlay')); else $('ps5-native-packages-overlay').hidden = true; } $('btn-modal-ps5-native-packages').addEventListener('click', openPs5NativePackageWorkspace); ['ps5-native-packages-close', 'ps5-native-packages-done'].forEach(id => $(id).addEventListener('click', closePs5NativePackageWorkspace)); $('ps5-native-package-source-browse').addEventListener('click', async () => { const folder = await window.pork.selectFolder(); if (!folder) return; ps5PreparedSources = null; $('ps5-native-package-source').value = folder; await refreshPs5NativePackageWorkspace().catch(error => setPs5NativePackageReadiness(error.message, 'blocked')); }); $('ps5-native-package-refresh').addEventListener('click', () => refreshPs5NativePackageWorkspace().catch(error => setPs5NativePackageReadiness(error.message, 'blocked'))); $('ps5-package-comparison-run').addEventListener('click', async () => { const game = state.currentGame; const candidatePackageSetId = Number($('ps5-package-comparison-candidate').value); const referencePackageSetId = Number($('ps5-package-comparison-reference').value); if (!candidatePackageSetId || !referencePackageSetId) return; $('ps5-package-comparison-run').disabled = true; try { renderPs5PackageComparison(await window.pork.ps5PackageComparison({ titleId: game.id, candidatePackageSetId, referencePackageSetId })); } catch (error) { $('ps5-package-comparison-result').innerHTML = `

    ${escHtml(error.message)}

    `; } finally { $('ps5-package-comparison-run').disabled = false; } }); ps5Control('import').addEventListener('click', async () => { ps5Control('import').disabled = true; ps5Control('status').textContent = ps5NativePackageText('ps5NativePackages.control.importing', 'Downloading and verifying the pinned upstream control…'); try { renderPs5UpstreamControl(await window.pork.ps5UpstreamControl('import')); } catch (error) { ps5Control('status').textContent = error.message; ps5Control('import').disabled = false; } }); ps5Control('restore').addEventListener('click', async () => { const confirmation = await showExactConfirmation(`${ps5NativePackageText('ps5NativePackages.control.confirm', 'Install the exact hash-pinned PPSA99099 upstream control package?')}\n\n${ps5NativePackageText('ps5NativePackages.control.boundary', 'This changes the PS5. Only a fresh post-install console scan may prove installation.')}`, 'INSTALL PS5 UPSTREAM CONTROL PACKAGE'); if (!confirmation) return; ps5Control('restore').disabled = true; ps5Control('status').textContent = ps5NativePackageText('ps5NativePackages.control.starting', 'Starting the guarded upstream control installation…'); try { const result = await window.pork.ps5UpstreamControl('restore', { confirmation }); ps5UpstreamControlOperationId = result.operationId; void monitorPs5ReferenceInstall(result.operationId, { active: () => ps5UpstreamControlOperationId, element: ps5Control, key: 'control', label: 'control package', awaiting: 'Installer completed. Run the independent control post-scan.', dpiAwaiting: 'Standalone DPI accepted the package. Only the independent control post-scan can prove installation.', completed: ps5NativePackageText('ps5NativePackages.control.proven', 'Post-scan proven') }); } catch (error) { ps5Control('status').textContent = error.message; ps5Control('restore').disabled = false; } }); ps5Control('verify').addEventListener('click', async () => { if (!ps5UpstreamControlOperationId) return; ps5Control('verify').disabled = true; ps5Control('status').textContent = ps5NativePackageText('ps5NativePackages.control.scanning', 'Scanning the console for fresh PPSA99099 evidence…'); try { const result = await window.pork.ps5UpstreamControl('restore-verify', { operationId: ps5UpstreamControlOperationId }); ps5Control('status').textContent = result.verified ? ps5NativePackageText('ps5NativePackages.control.proven', 'Post-scan proven') : ps5NativePackageText('ps5NativePackages.control.notObserved', 'The control package was not observed by the fresh console scan.'); ps5Control('trust').textContent = result.verified ? ps5NativePackageText('ps5NativePackages.control.proven', 'Post-scan proven') : ps5NativePackageText('ps5NativePackages.control.unproven', 'Unproven'); ps5Control('trust').dataset.trust = result.verified ? 'post-scan-proven' : 'unproven'; if (!result.verified) ps5Control('verify').disabled = false; } catch (error) { ps5Control('status').textContent = error.message; ps5Control('verify').disabled = false; } }); ps5A8Control('prepare').addEventListener('click', async () => { ps5A8ControlPlan = null; ps5A8Control('prepare').disabled = true; ps5A8Control('status').textContent = ps5NativePackageText('ps5NativePackages.a8Control.preparing', 'Verifying and registering the exact frozen control artifact…'); try { renderPs5A8Control(await window.pork.ps5A8DistinctControl('prepare')); await refreshPs5NativePackageWorkspace(); } catch (error) { ps5A8Control('status').textContent = error.message; ps5A8Control('prepare').disabled = false; } }); ps5A8Control('preflight').addEventListener('click', async () => { ps5A8ControlPlan = null; ps5A8Control('preflight').disabled = true; ps5A8Control('install').disabled = true; ps5A8Control('status').textContent = ps5NativePackageText('ps5NativePackages.a8Control.preflighting', 'Running a complete fresh console scan to prove CHIM00002 is absent…'); try { ps5A8ControlPlan = await window.pork.ps5A8DistinctControl('install-plan'); ps5A8Control('status').textContent = ps5NativePackageText('ps5NativePackages.a8Control.preflightReady', 'Target absence proven. The single-use install approval expires in two minutes.'); ps5A8Control('install').disabled = false; } catch (error) { ps5A8Control('status').textContent = error.message; ps5A8Control('preflight').disabled = false; } }); ps5A8Control('install').addEventListener('click', async () => { if (!ps5A8ControlPlan) return; const plan = ps5A8ControlPlan; const confirmation = await showExactConfirmation(`${ps5NativePackageText('ps5NativePackages.a8Control.confirm', 'Install the exact frozen metadata-qualified CHIM00002 control after the target-absent scan?')}\n\n${plan.artifactSha256}\n\n${ps5NativePackageText('ps5NativePackages.a8Control.boundary', 'This changes the PS5. Installer completion is not installation proof; a fresh post-scan and separate launch qualification remain mandatory.')}`, plan.confirmation); if (!confirmation) return; ps5A8Control('install').disabled = true; ps5A8Control('status').textContent = ps5NativePackageText('ps5NativePackages.a8Control.installing', 'Starting the exact single-use CHIM00002 installation…'); try { const result = await window.pork.ps5A8DistinctControl('install', { planDigest: plan.planDigest, confirmation }); ps5A8ControlPlan = null; ps5A8ControlOperationId = result.operationId; void monitorPs5ReferenceInstall(result.operationId, { active: () => ps5A8ControlOperationId, element: ps5A8Control, key: 'a8Control', label: 'CHIM00002 control', awaiting: 'Installer completed. Run the independent CHIM00002 post-scan.', dpiAwaiting: 'Standalone DPI accepted CHIM00002. Only the independent post-scan can prove installation.' }); } catch (error) { ps5A8ControlPlan = null; ps5A8Control('status').textContent = error.message; ps5A8Control('preflight').disabled = false; } }); ps5A8Control('verify').addEventListener('click', async () => { if (!ps5A8ControlOperationId) return; ps5A8Control('verify').disabled = true; ps5A8Control('status').textContent = ps5NativePackageText('ps5NativePackages.a8Control.scanning', 'Scanning the console for fresh CHIM00002 installation evidence…'); try { const result = await window.pork.ps5NativePackageRestoreVerify({ titleId: 'CHIM00002', operationId: ps5A8ControlOperationId }); if (result.operation) renderPs5A8Control({ status: 'ready' }, result.operation); if (!result.verified) { ps5A8Control('status').textContent = ps5NativePackageText('ps5NativePackages.a8Control.notObserved', 'CHIM00002 was not observed by the fresh console scan.'); ps5A8Control('verify').disabled = false; } } catch (error) { ps5A8Control('status').textContent = error.message; ps5A8Control('verify').disabled = false; } }); ps5A8Control('launch').addEventListener('click', async () => { if (!ps5A8ControlOperationId) return; try { const plan = await window.pork.ps5NativePackageRestoreLaunchPlan(ps5A8ControlOperationId); const confirmation = await showExactConfirmation(`${ps5NativePackageText('ps5NativePackages.a8Control.launchConfirm', 'Launch exact title CHIM00002 and observe its process?')}\n\n${plan.packageIdentitySha256}\n\n${ps5NativePackageText('ps5NativePackages.a8Control.launchBoundary', 'The launch request alone is not success. Chimera requires a stable exact-title process and retains terminal failure evidence.')}`, plan.confirmation); if (!confirmation) return; ps5A8Control('launch').disabled = true; ps5A8Control('status').textContent = ps5NativePackageText('ps5NativePackages.a8Control.launching', 'Requesting CHIM00002 and observing its exact process…'); const operation = await window.pork.ps5NativePackageRestoreLaunch({ operationId: ps5A8ControlOperationId, confirmation }); renderPs5A8Control({ status: 'ready' }, operation); void monitorPs5A8ControlLaunch(operation.id); } catch (error) { ps5A8Control('status').textContent = error.message; } }); $('ps5-a10-control-card').addEventListener('click', async event => { const button = event.target.closest('button[id^="ps5-a10-control-"]'); if (!button || button.disabled) return; const action = button.id.slice('ps5-a10-control-'.length); const status = ps5A10Control('status'); button.disabled = true; try { if (action === 'prepare') { ps5A10ControlPlan = null; status.textContent = ps5A10Text('preparing'); await window.pork.ps5A10TransitionControl('prepare'); } else if (action === 'preflight') { ps5A10ControlPlan = null; status.textContent = ps5A10Text('preflighting'); ps5A10ControlPlan = await window.pork.ps5A10TransitionControl('install-plan'); status.textContent = ps5A10Text('preflightReady'); ps5A10Control('install').disabled = false; return; } else if (action === 'install') { const plan = ps5A10ControlPlan; if (!plan) return; const confirmation = await confirmPs5A10('confirm', 'boundary', plan.confirmation, plan.artifactSha256); if (!confirmation) { button.disabled = false; return; } status.textContent = ps5A10Text('installing'); const result = await window.pork.ps5A10TransitionControl('install', { planDigest: plan.planDigest, confirmation }); ps5A10ControlPlan = null; ps5A10ControlOperationId = result.operationId; void monitorPs5ReferenceInstall(result.operationId, { active: () => ps5A10ControlOperationId, element: ps5A10Control, key: 'a10Control', label: 'CHIM00003', awaiting: ps5A10Text('awaitingScan'), dpiAwaiting: ps5A10Text('dpiAwaitingScan') }); return; } else if (action === 'verify') { status.textContent = ps5A10Text('scanning'); const result = await window.pork.ps5NativePackageRestoreVerify({ titleId: 'CHIM00003', operationId: ps5A10ControlOperationId }); if (!result.verified) { status.textContent = ps5A10Text('notObserved'); button.disabled = false; return; } } else if (action === 'stage') { const plan = await window.pork.ps5P15A10('runtime-plan'); const confirmation = await confirmPs5A10('stageConfirm', 'stageBoundary', plan.confirmation, plan.diagnosticEngineSha256); if (!confirmation) { status.textContent = ps5A10Text('planExpires'); return; } await window.pork.ps5P15A10('runtime-stage', { planDigest: plan.planDigest, confirmation }); } else if (action === 'payload') { const confirmation = await confirmPs5A10('payloadConfirm', 'payloadBoundary', 'START PS5 A10 PROFILE PAYLOAD'); if (!confirmation) { button.disabled = false; return; } await window.pork.ps5P15A10('payload-start', { confirmation }); } else if (action === 'launch') { const plan = await window.pork.ps5NativePackageRestoreLaunchPlan(ps5A10ControlOperationId); const confirmation = await confirmPs5A10('launchConfirm', 'launchBoundary', plan.confirmation, plan.packageIdentitySha256); if (!confirmation) { button.disabled = false; return; } status.textContent = ps5A10Text('launching'); const operation = await window.pork.ps5NativePackageRestoreLaunch({ operationId: ps5A10ControlOperationId, confirmation }); void monitorPs5A10ControlLaunch(operation.id); return; } else if (action === 'restore') { const confirmation = await confirmPs5A10('restoreConfirm', 'restoreBoundary', 'RESTORE PS5 PRODUCTION PROFILE RUNTIME'); if (!confirmation) { button.disabled = false; return; } await window.pork.ps5P15A10('runtime-restore', { confirmation }); } else if (action === 'evidence') { const e = await window.pork.ps5NativePackageRetainedLaunchEvidence(ps5A10ControlOperationId), a = e.analysis || {}, s = $('ps5-a10-control-evidence-summary'), b = a.firstUnprovenBoundary === 'after-accepted-shellui-lnc-before-stable-exact-title-process' ? ps5A10Text('evidencePostLnc') : a.stableExactTitleProcess ? ps5A10Text('evidenceVerified') : ps5A10Text('evidenceInconclusive'); s.hidden = false; s.dataset.operationId = String(e.operation.id); s.textContent = `${b} · ${ps5A10Text('evidenceExactProcesses')}: ${Number(a.exactTitleProcessObservationCount || 0)} · ${e.retention?.rawSourceComplete ? ps5A10Text('evidenceCaptureComplete') : ps5A10Text('evidenceCapturePartial')} · SHA-256 ${String(e.integrity?.retainedLaunchEvidenceSha256 || '').slice(0, 12)}… · ${ps5A10Text('evidenceDownloaded')}`; (window.pork.download || window.xavatarAPI?.download)(new TextEncoder().encode(JSON.stringify(e, null, 2)), `chimera-ps5-launch-evidence-${e.operation.titleId}-${e.operation.id}.json`); button.disabled = false; return; } await refreshPs5NativePackageWorkspace(); } catch (error) { status.textContent = error.message; button.disabled = false; } }); $('ps5-p15-r2-preflight').addEventListener('click', async () => { const button = $('ps5-p15-r2-preflight'); button.disabled = true; $('ps5-p15-r2-state').textContent = ps5NativePackageText('ps5NativePackages.r4.collecting', 'Collecting fresh read-only hardware evidence…'); try { ps5P15R2LastPreflight = await window.pork.ps5P15R2Preflight(); renderPs5P15R2Qualification(await window.pork.ps5P15R2Status(), ps5P15R2LastPreflight); } catch (error) { $('ps5-p15-r2-state').textContent = error.message; } finally { button.disabled = false; } }); $('ps5-p15-r2-qualify').addEventListener('click', async () => { if (ps5P15R2LastPreflight?.allGatesPass !== true || ps5P15R2QualificationAttempted) return; ps5P15R2QualificationAttempted = true; $('ps5-p15-r2-qualify').disabled = true; $('ps5-p15-r2-state').textContent = ps5NativePackageText('ps5NativePackages.r4.qualifying', 'Recollecting every gate for the single qualification…'); try { const status = await window.pork.ps5P15R2Qualify(); const before = ps5P15R2LastPreflight.matrix.map(row => `${row.gate}:${row.result}`).join('|'); const after = (status.lastQualification?.matrix || []).map(row => `${row.gate}:${row.result}`).join('|'); if (!status.profile || status.readiness?.launchEligible !== true || before !== after) throw new Error(ps5NativePackageText('ps5NativePackages.r4.drift', 'Qualification failed closed because independently recollected evidence drifted.')); renderPs5P15R2Qualification(status, status.lastQualification); } catch (error) { $('ps5-p15-r2-state').textContent = error.message; } }); window.addEventListener('chimera:language-changed', () => { const e = $('ps5-a10-control-evidence-summary'); e.hidden = true; e.textContent = ''; }); ps5LaunchReference('import').addEventListener('click', async () => { ps5LaunchReference('import').disabled = true; ps5LaunchReference('status').textContent = ps5NativePackageText('ps5NativePackages.launchReference.importing', 'Downloading and verifying the pinned websrv launcher…'); try { renderPs5LaunchReference(await window.pork.ps5LaunchReference('import')); } catch (error) { ps5LaunchReference('status').textContent = error.message; ps5LaunchReference('import').disabled = false; } }); ps5LaunchReference('restore').addEventListener('click', async () => { const confirmation = await showExactConfirmation(`${ps5NativePackageText('ps5NativePackages.launchReference.confirm', 'Install the exact hash-pinned FAKE00000 websrv launch reference?')}\n\n${ps5NativePackageText('ps5NativePackages.launchReference.boundary', 'This changes the PS5. A post-scan proves installation only; you must launch it manually to prove execution.')}`, 'INSTALL PS5 LAUNCH REFERENCE'); if (!confirmation) return; ps5LaunchReference('restore').disabled = true; ps5LaunchReference('status').textContent = ps5NativePackageText('ps5NativePackages.launchReference.starting', 'Starting the guarded launch-reference installation…'); try { const result = await window.pork.ps5LaunchReference('restore', { confirmation }); ps5LaunchReferenceOperationId = result.operationId; void monitorPs5ReferenceInstall(result.operationId, { active: () => ps5LaunchReferenceOperationId, element: ps5LaunchReference, key: 'launchReference', label: 'reference', awaiting: 'Installer completed. Run the post-scan.' }); } catch (error) { ps5LaunchReference('status').textContent = error.message; ps5LaunchReference('restore').disabled = false; } }); ps5LaunchReference('verify').addEventListener('click', async () => { if (!ps5LaunchReferenceOperationId) return; ps5LaunchReference('verify').disabled = true; ps5LaunchReference('status').textContent = ps5NativePackageText('ps5NativePackages.launchReference.scanning', 'Scanning the console for fresh FAKE00000 evidence…'); try { const result = await window.pork.ps5LaunchReference('restore-verify', { operationId: ps5LaunchReferenceOperationId }); ps5LaunchReference('status').textContent = result.verified ? ps5NativePackageText('ps5NativePackages.launchReference.manualLaunch', 'Installation proven. Launch Homebrew Launcher manually on the PS5; launch is not yet proven.') : ps5NativePackageText('ps5NativePackages.launchReference.notObserved', 'The launch reference was not observed by the fresh console scan.'); ps5LaunchReference('trust').textContent = result.verified ? ps5NativePackageText('ps5NativePackages.launchReference.launchRequired', 'Installed · launch required') : ps5NativePackageText('ps5NativePackages.launchReference.unproven', 'Unproven'); ps5LaunchReference('trust').dataset.trust = result.verified ? 'post-scan-proven' : 'unproven'; if (!result.verified) ps5LaunchReference('verify').disabled = false; } catch (error) { ps5LaunchReference('status').textContent = error.message; ps5LaunchReference('verify').disabled = false; } }); $('ps5-staging-discover').addEventListener('click', async () => { const game = state.currentGame; $('ps5-staging-discover').disabled = true; setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.stage.discovering', 'Discovering exact title-bound console mounts…')); try { ps5ConsoleDiscovery = await window.pork.ps5ConsoleStagingDiscover(game.game_id); $('ps5-staging-components').innerHTML = ps5ConsoleDiscovery.components.length ? ps5ConsoleDiscovery.components.map(item => ``).join('') : `

    ${escHtml(ps5NativePackageText('ps5NativePackages.stage.none', 'No exact app0 or patch0 mount was found for this title.'))}

    `; $('ps5-staging-run').disabled = !ps5ConsoleDiscovery.components.length; setPs5NativePackageReadiness(ps5ConsoleDiscovery.components.length ? ps5NativePackageText('ps5NativePackages.stage.discovered', 'Console components found. Review the selection before staging.') : ps5NativePackageText('ps5NativePackages.stage.none', 'No exact app0 or patch0 mount was found for this title.'), ps5ConsoleDiscovery.components.length ? 'ready' : 'blocked'); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); } finally { $('ps5-staging-discover').disabled = false; } }); $('ps5-staging-run').addEventListener('click', async () => { if (!ps5ConsoleDiscovery) return; const componentIds = [...document.querySelectorAll('[data-ps5-stage-component]:checked')].map(item => item.dataset.ps5StageComponent); $('ps5-staging-run').disabled = true; setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.stage.running', 'Inventorying, transferring and hashing every selected file…')); try { const created = await window.pork.ps5ConsoleStagingCreate({ discoveryId: ps5ConsoleDiscovery.id, componentIds }); const completed = await window.pork.ps5ConsoleStagingRun(created.id); ps5PreparedSources = ps5SourcesFromStagedJob(completed); const base = ps5PreparedSources?.find(item => item.componentType === 'base'); if (base) $('ps5-native-package-source').value = base.sourcePath; setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.stage.completed', 'Console copy staged atomically with complete per-file hash evidence.'), 'ready'); await refreshPs5NativePackageWorkspace(); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); $('ps5-staging-run').disabled = false; } }); $('ps5-native-package-create').addEventListener('click', async () => { const game = state.currentGame; $('ps5-native-package-create').disabled = true; setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.create.running', 'Verifying decrypted modules and building a native FIH package…')); try { const input = ps5PreparedSources?.length ? { sources: ps5PreparedSources, expectedTitleId: game.game_id } : { sourcePath: $('ps5-native-package-source').value, expectedTitleId: game.game_id }; const job = await window.pork.ps5NativePackageCreate(input); await window.pork.ps5NativePackageRun(job.id); void monitorPs5NativePackageBuild(job.id); await refreshPs5NativePackageWorkspace(); setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.create.queued', 'Build accepted. It continues on the server if this browser session closes.'), 'running'); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); } finally { $('ps5-native-package-create').disabled = !$('ps5-native-package-source').value; } }); $('ps5-native-package-restore').addEventListener('click', async () => { const game = state.currentGame; const packageSetId = Number($('ps5-native-package-set').value); if (!packageSetId) return; try { const plan = await window.pork.ps5NativePackageRestorePlan({ titleId: game.id, packageSetId }); const detail = plan.orderedComponents.map(item => `${item.type.toUpperCase()} · ${item.version || '—'} · ${item.sha256.slice(0, 12)}…`).join('\n'); const confirmation = await showExactConfirmation(`${ps5NativePackageText('ps5NativePackages.restore.confirm', 'Install this verified PS5 debug .pkg set?')}\n\n${detail}\n\n${ps5NativePackageText('ps5NativePackages.restore.boundary', 'PS5Upload will install each .pkg in order. Completion still requires an independent console scan.')}`, 'INSTALL PS5 DEBUG PACKAGE'); if (!confirmation) return; setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.restore.running', 'Starting the hash-verified PS5 .pkg installation…'), 'running'); const result = await window.pork.ps5NativePackageRestore({ titleId: game.id, packageSetId, confirmation }); ps5NativeInstallOperationId = result.operationId; $('ps5-native-package-cancel').disabled = true; void monitorPs5NativePackageInstall(result.operationId); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); } }); $('ps5-native-package-cancel').addEventListener('click', async () => { if (!ps5NativeInstallOperationId) return; try { const confirmation = await showExactConfirmation(ps5NativePackageText('ps5NativePackages.restore.cancelConfirm', 'Cancel the active PS5 .pkg installation? The server copy remains intact.'), 'CANCEL PS5 PACKAGE INSTALL'); if (!confirmation) return; await window.pork.ps5NativePackageRestoreCancel(ps5NativeInstallOperationId, confirmation); ps5NativeInstallOperationId = null; $('ps5-native-package-cancel').disabled = true; setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.restore.cancelled', 'PS5 .pkg installation cancelled. Source packages were kept.'), 'blocked'); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); } }); $('ps5-native-package-verify').addEventListener('click', async () => { const game = state.currentGame; setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.verify.running', 'Scanning the console for independent installation evidence…')); try { const result = await window.pork.ps5NativePackageRestoreVerify({ titleId: game.game_id, operationId: ps5NativeInstallOperationId }); if (result.operation) renderPs5RestoreQualification(result.operation); setPs5NativePackageReadiness(result.verified ? ps5NativePackageText('ps5NativePackages.verify.completedPendingLaunch', 'Installation proven. Launch verification is still required.') : ps5NativePackageText('ps5NativePackages.verify.notObserved', 'The console scan has not observed this installation.'), result.verified ? 'ready' : 'blocked'); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); } }); async function monitorPs5NativePackageLaunch(operationId) { while (ps5NativeInstallOperationId === operationId) { await new Promise(resolve => setTimeout(resolve, 2000)); let operation; try { operation = await window.pork.ps5NativePackageRestoreLaunchStatus(operationId); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); return; } renderPs5RestoreQualification(operation); if (operation.status === 'launch_verified') { setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.launch.qualified', 'Installed · Launch verified · Qualified for restore'), 'ready'); return; } if (operation.status === 'launch_failed') { setPs5NativePackageReadiness(`${ps5NativePackageText('ps5NativePackages.launch.failedBoundary', 'Installed — launch failed. Not qualified for restore.')} · ${operation.error?.code || ''}`, 'blocked'); return; } if (operation.status !== 'launching') { setPs5NativePackageReadiness(operation.error?.message || operation.status, 'blocked'); return; } } } $('ps5-native-package-launch').addEventListener('click', async () => { if (!ps5NativeInstallOperationId) return; try { const plan = await window.pork.ps5NativePackageRestoreLaunchPlan(ps5NativeInstallOperationId); const confirmation = await showExactConfirmation(`${ps5NativePackageText('ps5NativePackages.launch.confirm', 'Launch this exact installed title and observe its process?')}\n\n${plan.titleId}\n${plan.packageIdentitySha256}\n\n${ps5NativePackageText('ps5NativePackages.launch.boundary', 'A launch request alone is not success. Chimera requires a stable exact-title process and stores failure evidence.')}`, plan.confirmation); if (!confirmation) return; $('ps5-native-package-launch').disabled = true; setPs5NativePackageReadiness(ps5NativePackageText('ps5NativePackages.launch.starting', 'Requesting the exact title launch; qualification is still pending…'), 'running'); const operation = await window.pork.ps5NativePackageRestoreLaunch({ operationId: ps5NativeInstallOperationId, confirmation }); renderPs5RestoreQualification(operation); void monitorPs5NativePackageLaunch(operation.id); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); } }); $('ps5-native-package-jobs').addEventListener('click', async event => { const packageButton = event.target.closest('[data-ps5-native-resume]'); const stageButton = event.target.closest('[data-ps5-stage-resume]'); if (!packageButton && !stageButton) return; try { if (stageButton) { await window.pork.ps5ConsoleStagingResume(stageButton.dataset.ps5StageResume); await window.pork.ps5ConsoleStagingRun(stageButton.dataset.ps5StageResume); } else { await window.pork.ps5NativePackageResume(packageButton.dataset.ps5NativeResume); await window.pork.ps5NativePackageRun(packageButton.dataset.ps5NativeResume); void monitorPs5NativePackageBuild(packageButton.dataset.ps5NativeResume); } await refreshPs5NativePackageWorkspace(); } catch (error) { setPs5NativePackageReadiness(error.message, 'blocked'); } }); $('btn-modal-backup').addEventListener('click', async () => { if (!state.currentGame || !state.connected) return; const { ftp_path, game_id } = state.currentGame; if (!ftp_path) { setStatus('No FTP path recorded for this game — run a scan first', 'error'); return; } try { await window.pork.ftpDownloadFolder(game_id); setStatus('Backup queued; follow its progress in Transfers.', 'ok'); closeModal(); } catch (e) { setStatus(e.message, 'error'); } }); $('btn-modal-install').addEventListener('click', async () => { if (!state.currentGame || !state.connected) return; const g = state.currentGame; const ftpPath = await ensureGameFtpPath(g.game_id); if (!ftpPath) return; // user cancelled path selection let backups; try { backups = await window.pork.listBackups(g.game_id); } catch { backups = []; } if (!backups.length) { setStatus('No local backups found — add one via the Manage tab', 'error'); return; } const latest = backups.sort((a, b) => (b.id || 0) - (a.id || 0))[0]; try { await window.pork.ftpUpload({ localPath: latest.backup_path, remotePath: ftpPath }); setStatus('Install queued — see Transfers for progress', 'ok'); closeModal(); } catch (e) { setStatus(`Install failed: ${e.message}`, 'error'); } }); $('btn-modal-refresh').addEventListener('click', async () => { if (!state.currentGame) return; const { game_id } = state.currentGame; $('btn-modal-refresh').disabled = true; setStatus('Refreshing metadata…'); try { await window.pork.prosperoFetch(game_id); setStatus('Metadata refreshed', 'ok'); } catch (e) { setStatus(`Refresh failed: ${e.message}`, 'error'); } finally { $('btn-modal-refresh').disabled = false; } }); function closeModal() { if (typeof closeAccessibleDialog === 'function') closeAccessibleDialog($('modal-overlay')); else $('modal-overlay').hidden = true; state.currentGame = null; $('modal-banner').style.backgroundImage = ''; const icon = $('modal-icon'); icon.src = ''; icon.hidden = true; $('modal-description').hidden = true; $('modal-description').textContent = ''; $('modal-hash-section').hidden = true; $('modal-hash-progress').hidden = true; $('hash-progress-fill').style.width = '0%'; uploadLocalPath = null; uploadInProgress = false; porkInProgress = false; $('manage-remote-path').value = ''; $('manage-remote-path').readOnly = true; $('btn-manage-upload').disabled = true; $('manage-upload-log').hidden = true; $('mtab-manage-count').textContent = ''; $('manage-backpork-section').hidden = true; $('manage-pork-progress').hidden = true; $('pork-progress-fill').style.width = '0%'; } $('modal-close').addEventListener('click', closeModal); $('btn-modal-close2').addEventListener('click', closeModal); $('modal-overlay').addEventListener('click', e => { if (e.target === $('modal-overlay')) closeModal(); }); $('modal-workspace-links')?.addEventListener('click', event => { const button = event.target.closest('[data-game-route][data-game-target]'); const gameId = state.currentGame?.game_id; if (!button || !gameId || !setGameWorkspaceContext(gameId, button.dataset.gameTarget)) return; closeModal(); window.chimeraNavigate?.(button.dataset.gameRoute); }); document.addEventListener('keydown', e => { if (e.key === 'Escape' && $('modal-overlay') && !$('modal-overlay').hidden) closeModal(); });