Initial curated chimera source from a8ae5b1db07fb49926c94585e2718e5130330974

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:38:08 +02:00
commit c737a37f71
531 files changed
+119270

No files matched your search

+636
View File
@@ -0,0 +1,636 @@
// ── 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 => `<div class="payload-process-row"><div><strong>${escHtml(item.name)}</strong><span>PID ${item.pid}</span></div><b>${Number(item.memoryMiB || 0).toLocaleString(window.i18n?.current || 'en', { maximumFractionDigits: 1 })} MiB</b></div>`).join('') : `<p class="hint">${escHtml(window.t('payload.noDaemons', 'No separate payload daemons detected.'))}</p>`)
: `<p class="hint">${escHtml(processes.unavailable || window.t('payload.processesUnsupported', 'Processes are unavailable in this Payload Manager version.'))}</p>`;
$('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 => `<div class="payload-process-row"><div><strong>${escHtml(item.name || item.filename)}</strong><span>${escHtml(item.version || window.t('payload.unknownVersion', 'unknown version'))}${item.checksumVerified ? ` · ${escHtml(window.t('payload.checksumKnown', 'checksum known'))}` : ''}</span></div><button class="btn btn-sm pldmgr-safe-launch" data-payload-path="${escHtml(item.path)}">${escHtml(window.t('payload.preflightStart', 'Preflight & start'))}</button></div>`).join('') : `<p class="hint">${escHtml(window.t('payload.noneInstalled', 'No installed payloads reported.'))}</p>`;
}
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 => `
<div class="service-matrix-item ${service.online ? 'online' : 'offline'}">
<div class="service-matrix-head">
<span class="service-matrix-dot"></span>
<span class="service-matrix-name">${escHtml(service.name)}</span>
<span class="service-matrix-port">:${service.port}${service.online && service.latencyMs != null ? ` · ${service.latencyMs} ms` : ''}</span>
</div>
<p>${service.online ? `${escHtml(service.section)} is reachable.` : escHtml(service.hint)}</p>
</div>
`).join('');
} catch (error) {
summary.textContent = 'check failed';
grid.innerHTML = `<p class="hint">${escHtml(error.message)}</p>`;
} 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 = '<p class="hint" style="margin:6px 0">No sources yet. Add one below.</p>';
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
? `<p class="hint payload-expert-hidden">${escHtml(window.tf(
'payload.settings.expertHidden',
'{count} expert source(s) hidden. Enable Expert tools to manage them.',
{ count: hiddenCount },
))}</p>`
: '';
cont.innerHTML = hiddenNotice + visibleSources.map(s => `
<div class="payload-settings-source-row" data-source-id="${s.id}">
<input type="checkbox" class="source-toggle" ${s.enabled ? 'checked' : ''}/>
<span class="payload-settings-source-name">${escHtml(s.name)}</span>
<span class="payload-settings-source-url" title="${escHtml(s.github_url)}">${escHtml(s.github_url)}</span>
${s.latest_tag ? `<span class="payload-tag-chip">${escHtml(s.latest_tag)}</span>` : ''}
${s.built_in ? `<span class="payload-trust-chip payload-trust-official">${escHtml(window.t('payload.builtIn', 'built-in'))}</span>` : '<button class="btn btn-sm btn-source-remove" style="margin-left:auto">&#10005;</button>'}
</div>
`).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 `<span class="badge badge-yellow">&#8595; ${escHtml(window.t('payload.status.update', 'Update available'))}</span>`;
}
if (file.local_hash && file.remote_hash) {
if (file.local_hash === file.remote_hash) return `<span class="badge badge-green">&#10003; ${escHtml(window.t('payload.status.current', 'Up to date'))}</span>`;
return `<span class="badge badge-yellow">&#8595; ${escHtml(window.t('payload.status.update', 'Update available'))}</span>`;
}
if (file.local_hash) return `<span class="badge badge-dim">${escHtml(window.t('payload.status.downloadedUnverified', 'Downloaded (no checksum)'))}</span>`;
if (file.remote_hash) return `<span class="badge badge-dim">${escHtml(window.t('payload.status.notDownloaded', 'Not downloaded'))}</span>`;
return '<span class="badge badge-dim">—</span>';
}
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
? `&#8595;&nbsp;${escHtml(window.t('payload.action.update', 'Update'))}`
: `&#8595;&nbsp;${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
? `<button class="${dlCls}"
data-source-id="${source_id}"
data-asset-name="${escHtml(file.asset_name)}"
data-asset-url="${escHtml(file.asset_url || '')}"
data-version="${escHtml(file.version || '')}">${dlLabel}</button>`
: '';
const pushBtn = file.local_path
? `<button class="btn btn-sm btn-payload-push-remote"
data-local-path="${escHtml(file.local_path)}"
data-filename="${escHtml(file.asset_name)}"
title="Upload to PS5 via FTP transfer queue">&#8593;&nbsp;PS5</button>`
: '';
return (dlBtn || pushBtn)
? `<span style="display:inline-flex;gap:4px">${dlBtn}${pushBtn}</span>`
: '';
}
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 ? `<span class="payload-tag-chip">${escHtml(source.latest_tag)}</span>` : '';
const trust = escHtml(source.trust || 'custom');
const checked = source.last_checked
? `<span class="hint payload-checked-at">${escHtml(window.tf('payload.checkedAt', 'checked {date}', { date: new Date(source.last_checked).toLocaleString() }))}</span>`
: '';
const errRow = error ? `<p class="hint payload-source-error">${escHtml(error)}</p>` : '';
const rows = usableFiles.map(file => `
<tr>
<td class="payload-asset-name">${escHtml(file.asset_name)}</td>
<td>${payloadStatusBadge(file)}</td>
<td class="payload-hash-snippet">${file.remote_hash ? file.remote_hash.slice(0, 12) + '…' : '—'}</td>
<td class="payload-asset-meta">${escHtml(file.version || '—')} · ${file.asset_size ? fmt(file.asset_size) : escHtml(window.t('payload.unknownSize', 'unknown size'))}</td>
<td>${payloadActionBtn(file, source.id)}</td>
</tr>
`).join('');
return `
<details class="payload-check-source" ${forceOpen ? 'open' : ''}>
<summary class="payload-check-source-header">
<span class="payload-check-source-name">${escHtml(source.name)}</span>
<span class="payload-source-count">${usableFiles.length}</span>
${tag} ${checked}
<span class="payload-source-meta">
<span class="payload-trust-chip payload-trust-${trust}">${trust}</span>
<span class="payload-tag-chip">${escHtml(source.category || 'other')}</span>
</span>
</summary>
<div class="payload-source-body">
${source.description ? `<p class="payload-source-description">${escHtml(source.description)}</p>` : ''}
${errRow}
${rows ? `<div class="payload-asset-scroll"><table class="payload-asset-table"><tbody>${rows}</tbody></table></div>` : `<p class="hint">${escHtml(window.t('payload.noAssets', 'No assets found.'))}</p>`}
</div>
</details>`;
}
function renderPayloadSourceCheckList(results) {
const cont = $('payload-sources-check-list');
if (!results || !results.length) {
cont.innerHTML = `<div class="library-empty"><span class="library-empty-icon">⌁</span><strong>${escHtml(window.t('payload.empty.title', 'No payload sources match'))}</strong><span>${escHtml(window.t('payload.empty.detail', 'Adjust the filters or add a source in Settings.'))}</span></div>`;
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 `<details class="payload-catalog-group payload-catalog-group-${risk}" ${queryActive || group.id === 'updates' || group.id === 'recommended' ? 'open' : ''}>
<summary><span><strong>${escHtml(label)}</strong><small>${escHtml(detail)}</small></span><b>${items.length}</b></summary>
<div class="payload-catalog-group-body">${items.map(item => payloadSourceMarkup(item, queryActive)).join('')}</div>
</details>`;
}).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 = `<option value="all">${escHtml(window.t('payload.allCategories', 'All categories'))}</option>` + categories.map(value => `<option value="${escHtml(value)}">${escHtml(value)}</option>`).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 `<div class="al-file-card payload-local-card">
<span class="al-ext ${extClass}">${escHtml(ext || '?')}</span>
<span class="al-file-name" title="${escHtml(f.local_path)}">${escHtml(f.name)}</span>
<span class="payload-card-size">${fmt(f.size)}</span>
${payloadStatusBadge(f)}
<div class="payload-card-actions">
<button class="btn btn-sm btn-teal btn-payload-push"
data-local-path="${escHtml(f.local_path)}"
data-filename="${escHtml(f.name)}" title="Push to PS5 via FTP transfer queue">&#8593;&nbsp;FTP</button>
<button class="btn btn-sm btn-payload-delete-local"
data-id="${f.db_id ?? ''}"
data-local-path="${escHtml(f.local_path)}" title="Delete local file">&#128465;</button>
</div>
</div>`;
}).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 `<div class="al-file-card payload-sender-card">
<span class="al-ext ${extClass}">${escHtml(ext || '?')}</span>
<span class="al-file-name" title="${escHtml(f.local_path)}">${escHtml(f.name)}</span>
<span class="payload-card-size">${fmt(f.size)}</span>
<button class="btn btn-sm btn-teal btn-payload-tcp-send"
data-local-path="${escHtml(f.local_path)}"
data-filename="${escHtml(f.name)}">&#x21D2; Send</button>
</div>`;
}).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}%`;
});