Files
chimera-Public/renderer/modules/modal.js
T

1528 lines
105 KiB
JavaScript

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]) => `<div class="modal-metric${t ? ' is-' + t : ''}"><span>${escHtml(l)}</span><strong>${escHtml(v)}</strong></div>`).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 = '<div class="prospero-placeholder">Loading update data…</div>';
$('modal-dlc-content').innerHTML = '<div class="prospero-placeholder">Loading DLC data…</div>';
$('modal-regions-content').innerHTML = '<div class="prospero-placeholder">Loading region data…</div>';
return;
}
$('modal-description').hidden = true;
const patches = tryParseJson(g.prospero_patches, []);
if (patches.length) {
$('modal-patches-content').innerHTML = `
<ul class="patch-list">
${patches.map(p => `
<li class="patch-item${p.isLatest ? ' patch-item-latest' : ''}">
<div class="patch-header">
<span class="patch-ver">v${escHtml(p.contentVer || '?')}</span>
${p.isLatest ? '<span class="patch-latest">LATEST</span>' : ''}
<span class="patch-meta">
<span title="File size">${escHtml(p.filesize || '—')}</span>
<span title="Required firmware">FW ${escHtml(p.requiredFirmware || '?')}</span>
<span title="Import date">${escHtml(p.importDate || '')}</span>
</span>
</div>
${p.changelogPreview ? `<div class="patch-changelog">${escHtml(p.changelogPreview)}</div>` : ''}
</li>`).join('')}
</ul>`;
} else {
$('modal-patches-content').innerHTML = '<div class="prospero-placeholder">No patch updates found</div>';
}
const dlc = dlcArr;
if (dlc.length) {
$('modal-dlc-content').innerHTML = `
<ul class="dlc-list">
${dlc.map(d => `
<li class="dlc-item">
${d.iconUrl ? `<img class="dlc-icon" src="${escHtml(d.iconUrl)}" alt="" loading="lazy" data-image-error="hide"/>` : '<div class="dlc-icon"></div>'}
<div>
<div class="dlc-name">${escHtml(d.name || d.contentid || '—')}</div>
<div class="dlc-meta">
${d.contentVer ? `v${escHtml(d.contentVer)}` : ''}
${d.filesize ? ` · ${escHtml(d.filesize)}` : ''}
${d.requiredFirmware ? ` · FW ${escHtml(d.requiredFirmware)}` : ''}
</div>
</div>
</li>`).join('')}
</ul>`;
} else {
$('modal-dlc-content').innerHTML = '<div class="prospero-placeholder">No additional content</div>';
}
const regions = regionArr;
if (regions.length) {
$('modal-regions-content').innerHTML = `
<div class="region-pills">
${regions.map(r => {
const isOwned = r.titleid === g.game_id;
return `
<button class="region-pill${isOwned ? ' owned' : ''}" data-tid="${escHtml(r.titleid)}">
<code>${escHtml(r.titleid)}</code>
<span>${escHtml(r.region || '')}</span>
</button>`;
}).join('')}
</div>`;
$('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 = '<div class="prospero-placeholder">No other regions found</div>';
}
}
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')
? `<button class="btn btn-sm btn-danger" data-conv-cancel="${job.id}" style="padding:2px 6px;font-size:10px">✕</button>`
: '';
const retryBtn = (job.status === 'error' || job.status === 'cancelled')
? `<button class="btn btn-sm" data-conv-retry="${job.id}" style="padding:2px 6px;font-size:10px">↺</button>`
: '';
return `<div class="conv-job-row" data-conv-id="${job.id}" data-conv-status="${escHtml(job.status)}">
<div class="conv-job-summary">
<span class="conv-job-id">#${job.id}</span>
<span class="conv-job-game" title="${escHtml(job.game_id)}">${escHtml(job.game_name || job.game_id)}</span>
<span class="conv-job-mode">${escHtml((job.mode || '').toUpperCase())}</span>
<span class="conv-job-phase">${phase}</span>
<div class="conv-job-bar-wrap"><div class="conv-job-bar ${barClass}" style="width:${pct}%"></div></div>
<span class="conv-job-status conv-status-${job.status}">${escHtml(job.status)}</span>
<div class="conv-job-actions">${cancelBtn}${retryBtn}</div>
</div>
${showLog ? `<pre class="conv-log-box" data-log-len="${(job.progress?.log?.length ?? 0)}">${escHtml(log)}</pre>` : ''}
${job.error ? `<div class="conv-job-error">✗ ${escHtml(job.error)}</div>` : ''}
</div>`;
}
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 = '<option value="">None</option>' +
entries.map((e, i) => `<option value="${i}">${escHtml(e.folder_name)}</option>`).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 = '<option value="">None</option>' +
entries
.map((e, i) => ({ e, i }))
.filter(({ e }) => !!e.game_path) // skip firmware folders with no patch for this game
.map(({ e, i }) => `<option value="${i}">${escHtml(e.folder_name)}</option>`)
.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]) => `<tr><th>${escHtml(k)}</th><td>${escHtml(v)}</td></tr>`).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 => `<li><span>${escHtml(b.backup_path)}</span><span>${fmt(b.size)}</span></li>`).join('')
: '<li style="justify-content:center">No backups registered</li>';
// 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 => `<option value="${item.id}">${escHtml(item.label || item.setKey)} · ${item.components.length} PKG</option>`).join('')
: `<option value="">${escHtml(ps4PackageText('ps4Packages.packageSet.empty', 'No verified package set available'))}</option>`;
$('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 => `
<div class="ps4-package-job" data-job-id="${escHtml(job.id)}">
<strong>${escHtml(job.titleId)} · ${job.children.length} PKG</strong><span class="ps4-package-job-state">${escHtml(job.status)}</span>
<small>${escHtml(job.children.map(child => `${child.type} ${child.version || ''}`).join(' · '))}</small>
${['failed', 'interrupted'].includes(job.status) ? `<button class="btn btn-sm" data-ps4-resume="${escHtml(job.id)}">${escHtml(ps4PackageText('ps4Packages.jobs.resume', 'Resume'))}</button>` : '<span></span>'}
</div>`).join('') : `<p class="hint">${escHtml(ps4PackageText('ps4Packages.jobs.empty', 'No package activity for this title yet.'))}</p>`;
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
? `<code class="ps5-p15-r2-kstuff-evidence">${escHtml(JSON.stringify({ reason: row.reason || null, ...kstuff }))}</code>`
: escHtml(row.reason || 'exact');
return `<tr><td>${escHtml(row.gate)}</td><td data-result="${escHtml(row.result)}">${escHtml(row.result)}</td><td>${evidence}</td></tr>`;
}).join('')
: `<tr><td colspan="3">${escHtml(ps5NativePackageText('ps5NativePackages.r4.notCollected', 'No current matrix collected.'))}</td></tr>`;
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 => `<li><strong>${escHtml(item.component)}</strong><span>${escHtml(item.kind)} · ${escHtml(item.fields.join(', '))}</span></li>`).join('')
: `<li><strong>${escHtml(ps5NativePackageText('ps5NativePackages.compare.identical', 'No material artifact difference'))}</strong><span>${escHtml(ps5NativePackageText('ps5NativePackages.compare.identicalDetail', 'Retrying the same bytes cannot change the known AppInst outcome.'))}</span></li>`;
$('ps5-package-comparison-result').innerHTML = `<div class="ps5-package-comparison-trust"><span><small>${escHtml(ps5NativePackageText('ps5NativePackages.compare.candidate', 'Candidate'))}</small><strong data-trust="${escHtml(result.candidateTrust.status)}">${escHtml(trustText(result.candidateTrust))}</strong></span><i aria-hidden="true">↔</i><span><small>${escHtml(ps5NativePackageText('ps5NativePackages.compare.reference', 'Reference'))}</small><strong data-trust="${escHtml(result.referenceTrust.status)}">${escHtml(trustText(result.referenceTrust))}</strong></span></div><ul>${differenceRows}</ul><p>${escHtml(ps5NativePackageText(`ps5NativePackages.compare.next.${result.nextAction}`, result.nextAction))}</p>`;
}
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 => `<option value="${item.id}">${escHtml(ps5NativePackageSetOptionLabel(item))} · ${item.components.length} PKG</option>`).join('') : `<option value="">${escHtml(ps5NativePackageText('ps5NativePackages.packageSet.empty', 'No verified PS5 FIH package available'))}</option>`;
const comparisonOptions = sets.map(item => `<option value="${item.id}">${escHtml(ps5NativePackageSetOptionLabel(item))}</option>`).join('');
$('ps5-package-comparison-candidate').innerHTML = comparisonOptions || '<option value="">—</option>';
$('ps5-package-comparison-reference').innerHTML = comparisonOptions || '<option value="">—</option>';
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 => `<div class="ps4-package-job"><strong>${escHtml(job.titleId)} · console capture</strong><span class="ps4-package-job-state">${escHtml(job.status)}</span><small>${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'))}</small>${['failed', 'interrupted'].includes(job.status) ? `<button class="btn btn-sm" data-ps5-stage-resume="${escHtml(job.id)}">${escHtml(ps5NativePackageText('ps5NativePackages.jobs.resume', 'Resume'))}</button>` : '<span></span>'}</div>`);
const packageRows = titleJobs.map(job => `<div class="ps4-package-job"><strong>${escHtml(job.titleId)} · native .pkg</strong><span class="ps4-package-job-state">${escHtml(job.status)}</span><small>${escHtml(job.version || '—')} · ${escHtml(`${job.components?.length || 1} component(s)`)} · ${escHtml(job.phase || job.container?.validation?.validationLevel || 'awaiting FIH verification')}</small>${['failed', 'interrupted', 'built_unregistered'].includes(job.status) ? `<button class="btn btn-sm" data-ps5-native-resume="${escHtml(job.id)}">${escHtml(ps5NativePackageText('ps5NativePackages.jobs.resume', 'Resume'))}</button>` : '<span></span>'}</div>`);
$('ps5-native-package-jobs').innerHTML = stageRows.length || packageRows.length ? [...stageRows, ...packageRows].join('') : `<p class="hint">${escHtml(ps5NativePackageText('ps5NativePackages.jobs.empty', 'No native package activity for this title yet.'))}</p>`;
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 = `<p class="hint">${escHtml(error.message)}</p>`; }
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 => `<label class="ps5-staging-component"><input type="checkbox" data-ps5-stage-component="${escHtml(item.id)}" checked><span><strong>${escHtml(item.type)}</strong><small title="${escHtml(item.remotePath)}">${escHtml(item.remotePath)}</small></span><em>${escHtml(item.type === 'update' ? ps5NativePackageText('ps5NativePackages.stage.preserveOnly', 'Preserve only') : ps5NativePackageText('ps5NativePackages.stage.captureEligible', 'Capture eligible'))}</em></label>`).join('') : `<p class="hint">${escHtml(ps5NativePackageText('ps5NativePackages.stage.none', 'No exact app0 or patch0 mount was found for this title.'))}</p>`;
$('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();
});