// ── Payload Manager ───────────────────────────────────────────────────────────
function renderPs5PayloadManagerObservability(snapshot) {
const root = $('pldmgr-observability');
if (!root || !snapshot) return;
const autoload = snapshot.autoload || {};
const processes = snapshot.processes || {};
const logs = snapshot.logs || {};
const stateLabels = {
idle: window.t('payload.state.idle', 'idle'),
countdown: window.t('payload.state.countdown', 'countdown'),
running: window.t('payload.state.running', 'running'),
done: window.t('payload.state.done', 'done'),
unknown: window.t('common.unknown', 'unknown'),
};
const stateClasses = { done: 'badge-green', running: 'badge-blue', countdown: 'badge-yellow', idle: '', unknown: 'badge-red' };
const state = autoload.supported ? (autoload.state || 'unknown') : 'unknown';
const daemonItems = (processes.items || []).filter(item => item.daemon).slice(0, 40);
root.hidden = false;
$('pldmgr-autoload-metric').textContent = autoload.supported ? `${autoload.done || 0}/${autoload.total || autoload.list?.length || 0}` : 'n/b';
$('pldmgr-process-metric').textContent = processes.supported ? String(processes.total || 0) : 'n/b';
$('pldmgr-memory-metric').textContent = processes.supported ? `${Number(processes.memoryMiB || 0).toLocaleString(window.i18n?.current || 'en', { maximumFractionDigits: 1 })} MiB` : 'n/a';
$('pldmgr-log-metric').textContent = logs.supported ? String(logs.total || 0) : 'n/b';
$('pldmgr-autoload-state').textContent = stateLabels[state] || state;
$('pldmgr-autoload-state').className = `badge ${stateClasses[state] || ''}`.trim();
const detail = [];
if (autoload.supported) {
if (autoload.current) detail.push(window.tf('payload.current', 'Current: {value}', { value: autoload.current }));
if (autoload.remaining > 0) detail.push(window.tf('payload.startsIn', 'starts in {seconds}s', { seconds: autoload.remaining }));
if (autoload.list?.length) detail.push(window.tf('payload.itemsConfigured', '{count} item(s) configured', { count: autoload.list.length }));
if (autoload.scanUsbPayloads) detail.push(window.t('payload.usbScanActive', 'USB scan active'));
} else detail.push(autoload.unavailable || window.t('payload.endpointUnsupported', 'This endpoint is not supported by this Payload Manager version.'));
$('pldmgr-autoload-detail').textContent = detail.join(' · ');
$('pldmgr-process-list').innerHTML = processes.supported
? (daemonItems.length ? daemonItems.map(item => `
${escHtml(item.name)} PID ${item.pid}
${Number(item.memoryMiB || 0).toLocaleString(window.i18n?.current || 'en', { maximumFractionDigits: 1 })} MiB `).join('') : `${escHtml(window.t('payload.noDaemons', 'No separate payload daemons detected.'))}
`)
: `${escHtml(processes.unavailable || window.t('payload.processesUnsupported', 'Processes are unavailable in this Payload Manager version.'))}
`;
$('pldmgr-log-output').textContent = logs.supported && logs.lines?.length
? `${logs.truncated ? `[${window.t('payload.latestLogsOnly', 'latest lines only')}]\n` : ''}${logs.lines.join('\n')}`
: (logs.unavailable || window.t('payload.noLogs', 'No log lines yet.'));
$('pldmgr-observability-note').textContent = window.tf(
'payload.snapshotNote',
'Read-only snapshot from {time}. Chimera never terminates a process automatically.',
{ time: new Date(snapshot.checkedAt || Date.now()).toLocaleTimeString(window.i18n?.current || 'en') },
);
}
function renderPs5PayloadManagerInventory(inventory) {
const list = $('pldmgr-installed-list');
if (!list) return;
const payloads = (inventory?.payloads || []).slice(0, 200);
list.innerHTML = payloads.length ? payloads.map(item => `${escHtml(item.name || item.filename)} ${escHtml(item.version || window.t('payload.unknownVersion', 'unknown version'))}${item.checksumVerified ? ` · ${escHtml(window.t('payload.checksumKnown', 'checksum known'))}` : ''}
${escHtml(window.t('payload.preflightStart', 'Preflight & start'))} `).join('') : `${escHtml(window.t('payload.noneInstalled', 'No installed payloads reported.'))}
`;
}
async function refreshPs5PayloadManagerObservability({ quiet = false } = {}) {
if (!window.pork.pldmgrObservability) return null;
try {
const result = await window.pork.pldmgrObservability();
renderPs5PayloadManagerObservability(result);
return result;
} catch (error) {
if (!quiet) showToast(error.message, 'error');
return null;
}
}
async function connectPs5PayloadManager({ quiet = false, persistPort = false } = {}) {
const status = $('pldmgr-status');
const version = $('pldmgr-version');
const frame = $('pldmgr-frame');
if (!window.pork.pldmgrProbe || !status || !frame) return;
if (!quiet) status.textContent = window.t('payload.connectingPs5', 'Connecting to PS5…');
try {
const port = Number($('pldmgr-port')?.value || 8084);
if (persistPort) await window.pork.setSettings({ payloadManagerPort: port });
const settings = await window.pork.getSettings();
if (!settings.ftpHost) throw new Error(window.t('payload.configureIp', 'Configure the PS5 IP address first.'));
const [result] = await Promise.all([
window.pork.pldmgrProbe(),
refreshPs5PayloadManagerObservability({ quiet: true }),
]);
const versionValue = typeof result.version === 'string'
? result.version
: (result.version?.version || result.version?.menu_version || 'online');
version.textContent = String(versionValue).replace(/^"|"$/g, '') || 'online';
const payloadCount = Number(result.inventory?.total || 0);
renderPs5PayloadManagerInventory(result.inventory);
const sourceCount = Number(result.sources?.total || 0);
const repositoryCount = Number(result.repository?.total || 0);
const autoloadRaw = result.autoload?.list || result.autoload?.AUTOLOAD_LIST || '';
const autoloadCount = Array.isArray(autoloadRaw)
? autoloadRaw.length
: String(autoloadRaw || '').split(',').filter(Boolean).length;
status.textContent = window.tf(
'payload.connectedSummary',
'Connected · {payloads} installed · {autoload} autoload · {sources} sources · {available} available',
{ payloads: payloadCount, autoload: autoloadCount, sources: sourceCount, available: repositoryCount },
);
const embedUrl = await window.pork.pldmgrEmbedUrl();
frame.dataset.url = embedUrl;
if (!quiet || frame.dataset.userOpened === 'true') {
frame.src = embedUrl;
frame.hidden = false;
} else {
frame.hidden = true;
}
} catch (error) {
version.textContent = 'offline';
frame.hidden = true;
if ($('pldmgr-observability')) $('pldmgr-observability').hidden = true;
status.textContent = `${window.t('payload.unreachable', 'Unavailable')}: ${error.message}`;
}
}
$('btn-pldmgr-refresh')?.addEventListener('click', () => connectPs5PayloadManager());
$('btn-pldmgr-logs-refresh')?.addEventListener('click', () => refreshPs5PayloadManagerObservability());
$('pldmgr-installed-list')?.addEventListener('click', async event => {
const button = event.target.closest('.pldmgr-safe-launch');
if (!button) return;
const payloadPath = button.dataset.payloadPath;
button.disabled = true;
try {
const preflight = await window.pork.pldmgrLaunchPreflight(payloadPath);
let confirmConflict = false;
if (preflight.conflict) {
const conflictConfirmation = await showExactConfirmation(
`${preflight.warning}\n\nActive: ${preflight.active.map(item => `${item.name} (PID ${item.pid})`).join(', ')}\n\nA duplicate instance can make the console unstable.`,
'START PAYLOAD',
);
if (!conflictConfirmation) return;
confirmConflict = true;
} else if (!await showExactConfirmation(`Start ${preflight.selected} on the PS5?`, 'START PAYLOAD')) return;
await window.pork.pldmgrLaunch(payloadPath, confirmConflict, 'START PAYLOAD');
showToast(`${preflight.selected} was sent to Payload Manager.`, 'ok');
setTimeout(() => refreshPs5PayloadManagerObservability({ quiet: true }), 1_000);
} catch (error) { showToast(error.message, 'error'); }
finally { button.disabled = false; }
});
$('pldmgr-port')?.addEventListener('change', () => connectPs5PayloadManager({ persistPort: true }));
$('btn-pldmgr-open')?.addEventListener('click', async () => {
try {
const frame = $('pldmgr-frame');
const url = frame?.dataset.url || await window.pork.pldmgrEmbedUrl();
if (window.pork.isWebMode && String(url).startsWith('/')) {
frame.dataset.userOpened = 'true';
frame.src = url;
frame.hidden = false;
frame.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else await window.pork.openShell(url);
} catch (error) { setStatus(error.message, 'error'); }
});
setInterval(() => {
const page = $('page-jailbreak');
if (page?.classList.contains('active') && $('pldmgr-version')?.textContent !== 'offline') {
refreshPs5PayloadManagerObservability({ quiet: true });
}
}, 15_000);
async function loadPs5ServiceMatrix() {
const grid = $('service-matrix-grid');
const summary = $('service-matrix-summary');
const button = $('btn-service-matrix-refresh');
if (!grid || !window.pork.ps5ServiceMatrix) return;
if (button) button.disabled = true;
summary.textContent = 'controleren…';
try {
const result = await window.pork.ps5ServiceMatrix();
summary.textContent = `${result.online}/${result.total} online`;
grid.innerHTML = result.services.map(service => `
${escHtml(service.name)}
:${service.port}${service.online && service.latencyMs != null ? ` · ${service.latencyMs} ms` : ''}
${service.online ? `${escHtml(service.section)} is reachable.` : escHtml(service.hint)}
`).join('');
} catch (error) {
summary.textContent = 'check failed';
grid.innerHTML = `${escHtml(error.message)}
`;
} finally { if (button) button.disabled = false; }
}
$('btn-service-matrix-refresh')?.addEventListener('click', loadPs5ServiceMatrix);
async function renderSettingsPayloadSources() {
const sources = await window.pork.payloadSourcesList();
const cont = $('settings-payload-sources');
if (!sources.length) {
cont.innerHTML = 'No sources yet. Add one below.
';
return;
}
const expertMode = document.body.classList.contains('chimera-expert-mode');
const visibleSources = sources.filter(source =>
expertMode || payloadCatalogGroup({ source, files: [] }) !== 'expert'
);
const hiddenCount = sources.length - visibleSources.length;
const hiddenNotice = hiddenCount > 0
? `${escHtml(window.tf(
'payload.settings.expertHidden',
'{count} expert source(s) hidden. Enable Expert tools to manage them.',
{ count: hiddenCount },
))}
`
: '';
cont.innerHTML = hiddenNotice + visibleSources.map(s => `
${escHtml(s.name)}
${escHtml(s.github_url)}
${s.latest_tag ? `${escHtml(s.latest_tag)} ` : ''}
${s.built_in ? `${escHtml(window.t('payload.builtIn', 'built-in'))} ` : '✕ '}
`).join('');
}
window.addEventListener('chimera:expert-mode-changed', () => {
if ($('page-settings')?.classList.contains('active')) renderSettingsPayloadSources().catch(error => showToast(error.message, 'error'));
});
function payloadStatusBadge(file) {
if (file.local_hash && file.local_version && file.version && file.local_version !== file.version) {
return `↓ ${escHtml(window.t('payload.status.update', 'Update available'))} `;
}
if (file.local_hash && file.remote_hash) {
if (file.local_hash === file.remote_hash) return `✓ ${escHtml(window.t('payload.status.current', 'Up to date'))} `;
return `↓ ${escHtml(window.t('payload.status.update', 'Update available'))} `;
}
if (file.local_hash) return `${escHtml(window.t('payload.status.downloadedUnverified', 'Downloaded (no checksum)'))} `;
if (file.remote_hash) return `${escHtml(window.t('payload.status.notDownloaded', 'Not downloaded'))} `;
return '— ';
}
function payloadActionBtn(file, source_id) {
const upToDate = !!(file.local_hash && (
(file.remote_hash && file.local_hash === file.remote_hash) ||
(!file.remote_hash && file.local_version && file.version && file.local_version === file.version)
));
const hasUpdate = !!(file.local_hash && (
(file.remote_hash && file.local_hash !== file.remote_hash) ||
(file.local_version && file.version && file.local_version !== file.version)
));
const dlLabel = hasUpdate
? `↓ ${escHtml(window.t('payload.action.update', 'Update'))}`
: `↓ ${escHtml(window.t('payload.action.download', 'Download'))}`;
const dlCls = hasUpdate ? 'btn btn-sm btn-teal btn-payload-download' : 'btn btn-sm btn-payload-download';
const dlBtn = !upToDate
? `${dlLabel} `
: '';
const pushBtn = file.local_path
? `↑ PS5 `
: '';
return (dlBtn || pushBtn)
? `${dlBtn}${pushBtn} `
: '';
}
function payloadHasUpdate(file) {
return Boolean(file.local_hash && (
(file.remote_hash && file.local_hash !== file.remote_hash) ||
(file.local_version && file.version && file.local_version !== file.version)
));
}
function payloadCatalogGroup({ source, files }) {
const fingerprint = `${source.name || ''} ${source.category || ''} ${source.description || ''} ${(files || []).map(file => file.asset_name).join(' ')}`.toLowerCase();
const expertCategory = ['kernel', 'debug', 'linux'].includes(String(source.category || '').toLowerCase());
if (expertCategory || /(kernel|kstuff|kpatch|exploit|debugger|gdb|elf\s*(?:ldr|loader)|binloader|shell(?:code)?|jailbreak|linux\s+loader)/.test(fingerprint)) return 'expert';
if ((files || []).some(payloadHasUpdate)) return 'updates';
if (source.trust === 'custom' || /(experimental|prototype|research|legacy)/.test(fingerprint)) return 'experimental';
if (source.trust === 'official' && (files || []).some(file => file.remote_hash)) return 'recommended';
return 'compatible';
}
const PAYLOAD_GROUPS = [
{ id: 'updates', label: 'Updates', detail: 'Downloaded payloads with a newer or changed upstream asset.' },
{ id: 'recommended', label: 'Recommended', detail: 'Reviewed official sources with a published checksum.' },
{ id: 'compatible', label: 'Compatible', detail: 'Normal payloads that still require a manual compatibility check.' },
{ id: 'experimental', label: 'Experimental', detail: 'Custom, research or incomplete sources. Review before downloading.' },
{ id: 'expert', label: 'Expert', detail: 'Low-level payloads that can affect console stability.' },
];
function payloadSourceMarkup({ source, files, error }, forceOpen = false) {
const usableFiles = (files || []).filter(file =>
/\.(bin|elf|js)$/i.test(file.asset_name) &&
!/sha256|checksums|sha2|sums/i.test(file.asset_name)
);
const tag = source.latest_tag ? `${escHtml(source.latest_tag)} ` : '';
const trust = escHtml(source.trust || 'custom');
const checked = source.last_checked
? `${escHtml(window.tf('payload.checkedAt', 'checked {date}', { date: new Date(source.last_checked).toLocaleString() }))} `
: '';
const errRow = error ? `${escHtml(error)}
` : '';
const rows = usableFiles.map(file => `
${escHtml(file.asset_name)}
${payloadStatusBadge(file)}
${file.remote_hash ? file.remote_hash.slice(0, 12) + '…' : '—'}
${escHtml(file.version || '—')} · ${file.asset_size ? fmt(file.asset_size) : escHtml(window.t('payload.unknownSize', 'unknown size'))}
${payloadActionBtn(file, source.id)}
`).join('');
return `
${source.description ? `
${escHtml(source.description)}
` : ''}
${errRow}
${rows ? `
` : `
${escHtml(window.t('payload.noAssets', 'No assets found.'))}
`}
`;
}
function renderPayloadSourceCheckList(results) {
const cont = $('payload-sources-check-list');
if (!results || !results.length) {
cont.innerHTML = `⌁ ${escHtml(window.t('payload.empty.title', 'No payload sources match'))} ${escHtml(window.t('payload.empty.detail', 'Adjust the filters or add a source in Settings.'))}
`;
return;
}
const queryActive = Boolean(String($('payload-catalog-search')?.value || '').trim());
const grouped = Object.groupBy
? Object.groupBy(results, payloadCatalogGroup)
: results.reduce((groups, item) => {
const id = payloadCatalogGroup(item);
(groups[id] ||= []).push(item);
return groups;
}, {});
cont.innerHTML = PAYLOAD_GROUPS.map(group => {
const items = grouped[group.id] || [];
if (!items.length) return '';
const label = window.t(`payload.groups.${group.id}.label`, group.label);
const detail = window.t(`payload.groups.${group.id}.detail`, group.detail);
const risk = group.id === 'expert' || group.id === 'experimental' ? group.id : 'standard';
return `
${escHtml(label)} ${escHtml(detail)} ${items.length}
${items.map(item => payloadSourceMarkup(item, queryActive)).join('')}
`;
}).join('');
}
let _payloadCatalog = null;
function catalogResults(catalog) {
return (catalog.sources || []).map(source => ({
source,
files: (catalog.files || []).filter(file => file.source_id === source.id),
error: source.last_error || null,
}));
}
function applyPayloadCatalogFilters() {
if (!_payloadCatalog) return;
const query = String($('payload-catalog-search')?.value || '').trim().toLowerCase();
const category = String($('payload-catalog-category')?.value || 'all');
const showExpert = Boolean($('payload-show-expert')?.checked);
const results = catalogResults(_payloadCatalog)
.filter(({ source }) => category === 'all' || source.category === category)
.map(item => ({
...item,
files: item.files.filter(file => !query || `${file.asset_name} ${item.source.name} ${item.source.description || ''}`.toLowerCase().includes(query)),
}))
.filter(item => !query || item.files.length || `${item.source.name} ${item.source.description || ''}`.toLowerCase().includes(query))
.filter(item => showExpert || payloadCatalogGroup(item) !== 'expert');
renderPayloadSourceCheckList(results);
}
function renderPayloadCatalog(catalog) {
_payloadCatalog = catalog;
const stats = catalog.stats || {};
$('payload-stat-assets').textContent = String(stats.available || 0);
$('payload-stat-sources').textContent = String(stats.enabledSources || stats.sources || 0);
$('payload-stat-downloaded').textContent = String(stats.downloaded || 0);
$('payload-stat-updates').textContent = String(stats.updates || 0);
$('payload-stat-verified').textContent = String(stats.verified || 0);
const select = $('payload-catalog-category');
const selected = select.value || 'all';
const categories = [...new Set((catalog.sources || []).map(source => source.category || 'other'))].sort();
select.innerHTML = `${escHtml(window.t('payload.allCategories', 'All categories'))} ` + categories.map(value => `${escHtml(value)} `).join('');
select.value = categories.includes(selected) ? selected : 'all';
applyPayloadCatalogFilters();
}
async function loadPayloadCatalog() {
const catalog = await window.pork.payloadCatalog();
renderPayloadCatalog(catalog);
return catalog;
}
async function renderLocalPayloads() {
let files;
try { files = await window.pork.payloadListLocal(); } catch { files = []; }
const list = $('payload-local-list');
const empty = $('payload-local-empty');
if (!files.length) {
list.innerHTML = '';
empty.hidden = false;
return;
}
empty.hidden = true;
list.innerHTML = files.map(f => {
const ext = (f.name.split('.').pop() || '').toLowerCase();
const extClass = { elf: 'al-ext-elf', bin: 'al-ext-bin', js: 'al-ext-js' }[ext] || '';
return `
${escHtml(ext || '?')}
${escHtml(f.name)}
${fmt(f.size)}
${payloadStatusBadge(f)}
↑ FTP
🗑
`;
}).join('');
}
async function renderPayloadSender() {
let files;
try { files = await window.pork.payloadListLocal(); } catch { files = []; }
const list = $('payload-sender-list');
const empty = $('payload-sender-empty');
if (!files.length) {
list.innerHTML = '';
empty.hidden = false;
return;
}
empty.hidden = true;
const status = $('payload-sender-status');
if (status) status.textContent = '';
list.innerHTML = files.map(f => {
const ext = (f.name.split('.').pop() || '').toLowerCase();
const extClass = { elf: 'al-ext-elf', bin: 'al-ext-bin', js: 'al-ext-js' }[ext] || '';
return `
${escHtml(ext || '?')}
${escHtml(f.name)}
${fmt(f.size)}
⇒ Send
`;
}).join('');
}
$('btn-payload-check-all').addEventListener('click', async () => {
const btn = $('btn-payload-check-all');
const status = $('payload-check-status');
btn.disabled = true;
status.textContent = 'Checking…';
try {
const results = await window.pork.payloadCheckUpdates();
await loadPayloadCatalog();
await renderLocalPayloads();
status.textContent = `Done — ${results.length} source(s) checked`;
} catch (e) {
status.textContent = `Error: ${e.message}`;
setStatus(`Payload check failed: ${e.message}`, 'error');
} finally {
btn.disabled = false;
}
});
$('payload-catalog-search')?.addEventListener('input', applyPayloadCatalogFilters);
$('payload-catalog-category')?.addEventListener('change', applyPayloadCatalogFilters);
$('payload-show-expert')?.addEventListener('change', event => {
localStorage.setItem('chimera.payload.showExpert', event.target.checked ? '1' : '0');
applyPayloadCatalogFilters();
});
$('btn-payload-update-downloaded')?.addEventListener('click', async () => {
const btn = $('btn-payload-update-downloaded');
const status = $('payload-check-status');
btn.disabled = true;
status.textContent = 'Checking and updating local payloads…';
try {
const result = await window.pork.payloadUpdateDownloaded();
await loadPayloadCatalog();
await renderLocalPayloads();
await renderPayloadSender();
status.textContent = `${result.checked} checked locally · ${result.updated} updated${result.failed ? ` · ${result.failed} failed` : ''}`;
} catch (error) {
status.textContent = `Update failed: ${error.message}`;
} finally { btn.disabled = false; }
});
$('payload-sources-check-list').addEventListener('click', async e => {
const pushBtn = e.target.closest('.btn-payload-push-remote');
if (pushBtn) {
const local_path = pushBtn.dataset.localPath;
const filename = pushBtn.dataset.filename;
const origText = pushBtn.textContent;
pushBtn.disabled = true;
pushBtn.textContent = 'Queuing…';
try {
await window.pork.payloadPush({ local_path, filename });
setStatus(`${filename} queued for FTP transfer to PS5`, 'ok');
navigate('transfers');
} catch (err) {
setStatus(`FTP push failed: ${err.message}`, 'error');
pushBtn.disabled = false;
pushBtn.textContent = origText;
}
return;
}
const dlBtn = e.target.closest('.btn-payload-download');
if (!dlBtn) return;
const source_id = Number(dlBtn.dataset.sourceId);
const asset_name = dlBtn.dataset.assetName;
const asset_url = dlBtn.dataset.assetUrl;
const version = dlBtn.dataset.version;
dlBtn.disabled = true;
dlBtn.textContent = 'Downloading…';
try {
await window.pork.payloadDownload({ source_id, asset_name, asset_url, version });
setStatus(`Downloaded ${asset_name}`, 'ok');
await loadPayloadCatalog();
await renderLocalPayloads();
await renderPayloadSender();
} catch (err) {
setStatus(`Download failed: ${err.message}`, 'error');
dlBtn.disabled = false;
dlBtn.textContent = '↓ Retry';
}
});
$('payload-local-list').addEventListener('click', async e => {
const pushBtn = e.target.closest('.btn-payload-push');
if (pushBtn) {
const local_path = pushBtn.dataset.localPath;
const filename = pushBtn.dataset.filename;
try {
await window.pork.payloadPush({ local_path, filename });
setStatus(`${filename} queued for transfer`, 'ok');
navigate('transfers');
} catch (e2) {
setStatus(`Push failed: ${e2.message}`, 'error');
}
return;
}
const delBtn = e.target.closest('.btn-payload-delete-local');
if (delBtn) {
const confirmation = await showExactConfirmation(`Delete local file "${delBtn.dataset.localPath}"?`, 'VERWIJDER');
if (!confirmation) return;
try {
const id = delBtn.dataset.id ? Number(delBtn.dataset.id) : null;
const local_path = delBtn.dataset.localPath;
await window.pork.payloadDeleteLocal({ id, local_path, confirmation });
await renderLocalPayloads();
await renderPayloadSender();
} catch (e2) {
setStatus(`Delete failed: ${e2.message}`, 'error');
}
}
});
$('payload-sender-list').addEventListener('click', async e => {
const btn = e.target.closest('.btn-payload-tcp-send');
if (!btn) return;
const local_path = btn.dataset.localPath;
const filename = btn.dataset.filename;
const port = parseInt($('payload-sender-port')?.value || '9021', 10);
const status = $('payload-sender-status');
btn.disabled = true;
btn.textContent = 'Sending\u2026';
if (status) { status.textContent = `Sending ${filename} on port ${port}\u2026`; status.style.color = ''; }
try {
await window.pork.payloadTcpSend({ localPath: local_path, port, filename });
if (status) { status.textContent = `\u2713 ${filename} sent \u2014 PS5 notified`; status.style.color = 'var(--teal)'; }
setStatus(`${filename} sent to PS5 on port ${port}`, 'ok');
} catch (e2) {
if (status) { status.textContent = `Error: ${e2.message}`; status.style.color = 'var(--red)'; }
setStatus(`TCP send failed: ${e2.message}`, 'error');
} finally {
btn.disabled = false;
btn.textContent = '\u21D2 Send';
}
});
$('autoloader-file-list').addEventListener('click', async e => {
const saveBtn = e.target.closest('.btn-al-save-local');
if (saveBtn) {
e.stopPropagation(); // don't also add to sequence
const filename = saveBtn.dataset.filename;
const orig = saveBtn.textContent;
saveBtn.disabled = true;
saveBtn.textContent = '⋯';
try {
await window.pork.autoloaderSaveLocal({ filename });
setStatus(`${filename} saved to local payloads`, 'ok');
saveBtn.textContent = '✓';
await renderLocalPayloads();
await renderPayloadSender();
setTimeout(() => { saveBtn.textContent = orig; saveBtn.disabled = false; }, 2000);
} catch (e2) {
setStatus(`Save failed: ${e2.message}`, 'error');
saveBtn.textContent = orig;
saveBtn.disabled = false;
}
return;
}
const card = e.target.closest('[data-al-add]');
if (!card) return;
_alSequence.push({ type: 'payload', filename: card.dataset.alAdd });
_alRenderSequence();
$('btn-autoloader-save').disabled = false;
});
window.pork.on('payload:download:progress', ({ asset_name, percent }) => {
const btn = document.querySelector(`.btn-payload-download[data-asset-name="${CSS.escape(asset_name)}"]`);
if (btn) btn.textContent = `${percent}%`;
});