1528 lines
105 KiB
JavaScript
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();
|
|
});
|
|
|