Files
chimera-Public/renderer/modules/elf-arsenal.js
T

214 lines
11 KiB
JavaScript

// ELF Arsenal native Chimera client. The upstream WebUI remains available in a
// separate browser tab, but is no longer embedded: its opaque iframe errors
// cannot be diagnosed and its storage assumptions conflict with sandboxing.
let _eaPath = '/';
let _eaConnected = false;
function eaFriendlyError(value) {
const message = String(value || '').trim();
if (/Configureer eerst een geldig PS5-adres/i.test(message)) return 'Configure a PS5 host in Settings first.';
if (/ELF Arsenal-payload.*gestart/i.test(message)) return 'Start the ELF Arsenal payload explicitly, then retry.';
return message || 'ELF Arsenal is unavailable on port 6969.';
}
function eaBytes(value) {
const bytes = Number(value || 0);
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
}
function eaJoin(base, name) {
const cleanName = String(name || '').replace(/^\/+|\/+$/g, '');
return base === '/' ? `/${cleanName}` : `${String(base).replace(/\/+$/, '')}/${cleanName}`;
}
function eaParent(value) {
const parts = String(value || '/').split('/').filter(Boolean);
parts.pop();
return parts.length ? `/${parts.join('/')}` : '/';
}
function eaSetStatus(message, type = '') {
const status = document.getElementById('ea-embed-status');
if (!status) return;
status.textContent = message;
status.className = `ea-native-status ${type}`;
}
function eaRenderList(packet) {
const target = document.getElementById('ea-native-list');
const pathLabel = document.getElementById('ea-native-path');
if (!target || !pathLabel) return;
pathLabel.innerHTML = `<button class="btn btn-xs ea-native-parent" ${_eaPath === '/' ? 'disabled' : ''} data-path="${escHtml(eaParent(_eaPath))}">← Up</button><code>${escHtml(_eaPath)}</code>`;
const entries = (packet.entries || []).slice().sort((left, right) =>
Number(Boolean(right.is_dir)) - Number(Boolean(left.is_dir)) || String(left.name).localeCompare(String(right.name)));
target.innerHTML = entries.length ? entries.map(entry => {
const remotePath = eaJoin(_eaPath, entry.name);
const isElf = !entry.is_dir && /\.elf$/i.test(entry.name);
return `<article class="ea-native-row" data-path="${escHtml(remotePath)}" data-name="${escHtml(entry.name)}" data-dir="${entry.is_dir ? 'true' : 'false'}">
<span class="ea-native-icon" aria-hidden="true">${entry.is_dir ? '📁' : isElf ? '◆' : '•'}</span>
<div><strong>${escHtml(entry.name)}</strong><span>${entry.is_dir ? 'Directory' : eaBytes(entry.size)}${entry.mode ? ` · ${escHtml(entry.mode)}` : ''}</span></div>
<div class="ecosystem-inline-actions">
${entry.is_dir ? '<button class="btn btn-sm ea-native-open">Open</button>' : `<button class="btn btn-sm ea-native-download">Download</button>${isElf ? '<button class="btn btn-sm btn-accent ea-native-launch">Launch ELF</button>' : ''}`}
</div>
</article>`;
}).join('') : '<div class="operations-empty"><strong>This directory is empty</strong><span>Choose another safe root or upload a file explicitly.</span></div>';
const lock = document.getElementById('ea-native-lock');
if (lock) {
lock.textContent = packet.safety_lock ? 'Safety lock enabled' : 'Safety lock not reported';
lock.className = `operations-status ${packet.safety_lock ? 'operations-status-compatible' : 'operations-status-unknown'}`;
}
}
async function eaLoadDirectory(path = _eaPath) {
_eaPath = String(path || '/');
const target = document.getElementById('ea-native-list');
if (target) target.innerHTML = '<p class="hint">Loading directory…</p>';
try {
const [packet, job] = await Promise.all([
window.pork.eaList(_eaPath),
window.pork.eaJobStatus().catch(() => ({ busy: false })),
]);
eaRenderList(packet);
const jobTarget = document.getElementById('ea-native-job');
if (jobTarget) jobTarget.textContent = job.busy
? `File operation active${job.progress != null ? ` · ${job.progress}%` : ''}`
: 'No active file operation';
} catch (error) {
if (target) target.innerHTML = `<div class="operations-empty"><strong>Directory unavailable</strong><span>${escHtml(error.message)}</span></div>`;
eaSetStatus(`File API error: ${error.message}`, 'error');
}
}
async function eaReload() {
eaSetStatus('Checking ELF Arsenal…');
try {
const result = await window.pork.eaEmbedUrl();
const open = document.getElementById('ea-embed-open');
_eaConnected = result.connected === true;
if (!_eaConnected) {
if (open) { open.disabled = true; delete open.dataset.url; }
eaSetStatus(eaFriendlyError(result.error), 'error');
const list = document.getElementById('ea-native-list');
if (list) list.innerHTML = '<div class="operations-empty"><strong>ELF Arsenal is offline</strong><span>Start it explicitly through Payload Manager, then retry.</span></div>';
return;
}
if (open) {
open.disabled = false;
open.dataset.url = result.directUrl || result.url;
}
eaSetStatus(result.version ? `Online · ${result.version}` : 'Online', 'ok');
document.getElementById('ea-native-version').textContent = result.version || 'Unknown';
await eaLoadDirectory(_eaPath);
} catch (error) {
_eaConnected = false;
eaSetStatus(`Connection error: ${eaFriendlyError(error.message)}`, 'error');
}
}
function buildElfArsenalEmbed() {
const root = document.getElementById('page-elf-arsenal');
if (!root) return;
root.innerHTML = `
<div class="page-header">
<div>
<h1>ELF Arsenal</h1>
<p class="page-subtitle">Native Chimera file browser and launcher for the ELF Arsenal service on your PS5.</p>
</div>
<div class="header-tools">
<span id="ea-embed-status" class="ea-native-status">Checking connection…</span>
<button id="ea-embed-start" class="btn btn-accent">Start via Payload Manager</button>
<button id="ea-embed-open" class="btn" disabled>Open original WebUI</button>
<button id="ea-embed-reload" class="btn">Refresh</button>
</div>
</div>
<div class="ecosystem-workflow-strip ea-workflow" aria-label="ELF Arsenal workflow">
<span><b>1</b><strong>Start service</strong><small>Payload Manager, explicit</small></span>
<span><b>2</b><strong>Browse</strong><small>Choose a reviewed ELF</small></span>
<span><b>3</b><strong>Inspect</strong><small>Path and operation</small></span>
<span><b>4</b><strong>Launch</strong><small>Exact confirmation</small></span>
</div>
<div class="ea-native-summary">
<div class="operations-metric"><strong id="ea-native-version">—</strong><span>Version</span></div>
<div class="operations-metric"><strong id="ea-native-job">No active job</strong><span>File operation</span></div>
<div class="operations-metric"><strong id="ea-native-lock" class="operations-status operations-status-unknown">Unknown</strong><span>Upstream safety lock</span></div>
</div>
<div class="card ea-native-card">
<div class="card-header ecosystem-card-header">
<div><h3>PS5 files</h3><p class="hint">Browse with Chimera's authenticated API. The original upstream WebUI opens separately and can no longer crash this page.</p></div>
<div class="ecosystem-inline-actions">
<button class="btn btn-sm ea-native-root" data-path="/">Root</button>
<button class="btn btn-sm ea-native-root" data-path="/data">Data</button>
<button class="btn btn-sm ea-native-root" data-path="/mnt">USB / mounts</button>
<button id="ea-native-upload" class="btn btn-sm btn-accent">Upload here</button>
</div>
</div>
<div id="ea-native-path" class="ea-native-path"></div>
<div id="ea-native-list" class="ea-native-list"><p class="hint">Waiting for ELF Arsenal…</p></div>
</div>
<p class="hint">FTP alone is not enough: ELF Arsenal must be running on port 6969. Launch and upload actions always require an explicit confirmation.</p>`;
document.getElementById('ea-embed-open').addEventListener('click', () => {
const url = document.getElementById('ea-embed-open')?.dataset.url;
if (url) window.pork.openShell(url);
});
document.getElementById('ea-embed-start').addEventListener('click', async (event) => {
const button = event.currentTarget;
button.disabled = true;
eaSetStatus('Starting ELF Arsenal explicitly through Payload Manager…', 'warning');
try {
await window.pork.eaStart();
showToast('ELF Arsenal started. Rechecking the service.', 'ok');
await eaReload();
} catch (error) {
eaSetStatus(`Start failed: ${error.message}`, 'error');
showToast(`ELF Arsenal start failed: ${error.message}`, 'error');
} finally {
button.disabled = false;
}
});
document.getElementById('ea-embed-reload').addEventListener('click', eaReload);
document.getElementById('ea-native-path').addEventListener('click', event => {
const button = event.target.closest('.ea-native-parent');
if (button) eaLoadDirectory(button.dataset.path);
});
root.querySelectorAll('.ea-native-root').forEach(button => button.addEventListener('click', () => eaLoadDirectory(button.dataset.path)));
document.getElementById('ea-native-list').addEventListener('click', async event => {
const row = event.target.closest('.ea-native-row');
if (!row) return;
const button = event.target.closest('button');
try {
if (button?.classList.contains('ea-native-open')) await eaLoadDirectory(row.dataset.path);
if (button?.classList.contains('ea-native-download')) await window.pork.eaDownload(row.dataset.path, row.dataset.name);
if (button?.classList.contains('ea-native-launch')) {
const confirmation = await showExactConfirmation(`Launch ${row.dataset.path} through ELF Arsenal?`, 'LAUNCH ELF');
if (!confirmation) return;
await window.pork.eaLaunch(row.dataset.path, [], false);
showToast(`${row.dataset.name} launch request sent.`, 'ok');
}
} catch (error) { showToast(error.message, 'error', 10000); }
});
document.getElementById('ea-native-upload').addEventListener('click', async () => {
if (!_eaConnected) return showToast('Start ELF Arsenal first.', 'error');
const confirmation = await showExactConfirmation(`Upload one selected file to ${_eaPath}?`, 'UPLOAD FILE');
if (!confirmation) return;
try {
const uploadRoot = _eaPath === '/' ? '/' : `${_eaPath.replace(/\/+$/, '')}/`;
const result = await window.pork.eaUpload(null, uploadRoot);
if (result) {
showToast(`Uploaded ${eaBytes(result.bytes)} to ${result.remotePath}.`, 'ok');
await eaLoadDirectory(_eaPath);
}
} catch (error) { showToast(error.message, 'error', 10000); }
});
eaReload();
}
async function loadElfArsenal() {
if (!document.getElementById('ea-native-list')) buildElfArsenalEmbed();
else await eaReload();
}