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

1497 lines
175 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
let _ecosystemBuilt = false;
let _ecosystemLoaded = false;
let _ecosystemActiveTab = 'overview';
let _ecosystemEtaHash = null;
let _ecosystemEtaProposed = null;
let _ecosystemCheatTimer = null;
let _ecosystemCheatOffset = 0;
let _ecosystemCheatRequest = 0;
const ECOSYSTEM_CHEAT_PAGE_SIZE = 50;
let _timelineFilterTimer = null;
let _ecosystemSaveCapabilities = null;
let _gameUpdatePlan = null;
let _gameUpdateRefreshTimer = null;
let _websrvFsPath = '/';
let _websrvSmbPath = '';
let _pacbrewPacket = null;
let _itemzflowPacket = null;
let _remotePlayPacket = null;
let _remotePlayPollTimer = null;
let _ecosystemComponentsPacket = null;
function ecosystemDate(value) {
if (!value) return 'Not yet';
try { return new Date(value).toLocaleString(window.i18n?.current || 'en'); } catch (_) { return 'Unknown'; }
}
function ecosystemBytes(value) {
const bytes = Number(value || 0);
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let amount = bytes; let unit = 0;
while (amount >= 1024 && unit < units.length - 1) { amount /= 1024; unit += 1; }
return `${amount >= 10 ? amount.toFixed(0) : amount.toFixed(1)} ${units[unit]}`;
}
function ecosystemStatus(status, label = null) {
const normalized = ['compatible', 'online', 'ok'].includes(status) ? 'compatible'
: ['incompatible', 'offline', 'error'].includes(status) ? 'warning'
: status || 'unknown';
const text = label || ({ compatible: 'Compatible', warning: 'Warning', unknown: 'Unknown' }[normalized] || status || 'Unknown');
return `<span class="operations-status operations-status-${escHtml(normalized)}">${escHtml(text)}</span>`;
}
function buildEcosystemPage() {
if (_ecosystemBuilt) return;
const page = $('page-ecosystem');
page.innerHTML = `
<div class="page-header ecosystem-header">
<div><h1>Console status</h1><p class="page-subtitle">Services, readiness, etaHEN, homebrew, network sources, plugins, cheats, saves and console logs.</p></div>
<div class="ecosystem-header-actions"><span id="ecosystem-health" class="operations-health" data-i18n="ecosystem.status.loading">Loading status…</span><button id="ecosystem-health-run" class="btn btn-accent" data-i18n="ecosystem.health.deepScan">Deep health scan</button><button id="ecosystem-refresh" class="btn btn-sm" data-i18n="common.refresh">Refresh</button></div>
</div>
<div class="ecosystem-tabs" role="tablist" aria-label="Console and platform sections">
${[
['overview', 'Command Center'], ['components', 'Components'], ['updates', 'Payload updates'], ['game-updates', 'Game updates'], ['etahen', 'Console profile'],
['live', 'Live Session'], ['remote-play', 'Remote Play'], ['files', 'Files'], ['homebrew', 'Homebrew'], ['sources', 'Sources'], ['plugins', 'Plugins'],
['cheats', 'Cheats & patches'], ['saves', 'Save Vault'], ['itemzflow', 'Itemzflow'], ['platform-lab', window.t('platformLab.tab', 'Platform Lab')], ['research', 'Research Lab'], ['timeline', 'Timeline'],
].map(([id, label], index) => `<button class="ecosystem-tab ${index ? '' : 'active'}" data-ecosystem-tab="${id}" role="tab" aria-selected="${index ? 'false' : 'true'}" tabindex="${index ? '-1' : '0'}">${label}</button>`).join('')}
</div>
<section class="ecosystem-panel active" data-ecosystem-panel="overview"><div id="etahen-link-root"></div><div id="ecosystem-overview"><p class="hint">Loading overview…</p></div></section>
<section class="ecosystem-panel" data-ecosystem-panel="components" hidden>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>Reviewed component sources</h3><p class="hint">Grouped by purpose, with versions, dependencies and risk levels. Nothing starts automatically.</p></div><button id="ecosystem-components-refresh" class="btn btn-accent">Check upstreams</button></div>
<div class="ecosystem-filter-row ecosystem-component-filters"><input id="ecosystem-components-search" class="search-input" type="search" placeholder="Search component, type or dependency…"><select id="ecosystem-components-platform"><option value="all">All platforms</option><option value="ps5">PS5</option><option value="ps4-ps5">PS4 + PS5</option><option value="desktop-mobile">Desktop / mobile</option></select><select id="ecosystem-components-risk"><option value="all">All risk levels</option><option value="normal">Normal</option><option value="advanced">Advanced</option><option value="expert">Expert</option></select></div>
<div id="ecosystem-components"><p class="hint">Loading components…</p></div>
</div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="updates" hidden>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>Payload Runtime Watchdog</h3><p class="hint">Distinguishes installed, autoloaded and actually running payloads. Checks are read-only; updates never start automatically.</p></div><div class="ecosystem-inline-actions"><button id="ecosystem-manager-refresh" class="btn btn-sm">Sync manager sources</button><button id="ecosystem-watchdog-refresh" class="btn btn-accent">Check PS5</button></div></div><div id="ecosystem-watchdog"><p class="hint">Loading runtime status…</p></div><div id="ecosystem-runtime-updates"></div></div>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>Safe Update Planner</h3><p class="hint">Tracks console versions, orders dependencies and downloads official assets to the local Payload Catalog first. Nothing executes automatically.</p></div><button id="ecosystem-updates-refresh" class="btn btn-accent">Sync catalogs</button></div>
<div class="ecosystem-repository-bridge"><div><strong>Payload Manager bridge</strong><span>Add this CORS-compatible source through Payload Manager → Settings → Manage Sources. Chimera publishes only upstream URLs and available SHA-256 checks.</span></div><input id="ecosystem-repository-url" readonly aria-label="Payload Manager repository URL"><button id="ecosystem-repository-copy" class="btn btn-sm">Copy URL</button><button id="ecosystem-payload-manager-open" class="btn btn-sm">Open Payload Manager</button></div>
<div id="ecosystem-update-summary"></div><div id="ecosystem-update-plan"><p class="hint">Loading update plan…</p></div>
</div>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>Payload Supply-Chain Vault</h3><p class="hint">Recalculates local hashes, checks provenance and HTTPS, and blocks execution without a published digest according to your policy.</p></div><button id="ecosystem-supply-audit" class="btn btn-accent">Run full integrity audit</button></div>
<form id="ecosystem-supply-policy" class="ecosystem-filter-row"><label class="ecosystem-check"><input id="ecosystem-supply-official" type="checkbox"> Official sources only</label><label class="ecosystem-check"><input id="ecosystem-supply-https" type="checkbox"> Require HTTPS</label><label class="ecosystem-check"><input id="ecosystem-supply-hash" type="checkbox"> Require published SHA-256</label><button class="btn btn-sm" type="submit">Save policy</button></form>
<div id="ecosystem-supply-report" class="ecosystem-plan-safety"><strong>Not checked yet.</strong><span>Automatic execution always remains disabled, regardless of these settings.</span></div>
</div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="game-updates" hidden>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header"><div><h3 data-i18n="ecosystem.gameUpdates.title">Official Game Update Center</h3><p class="hint" data-i18n="ecosystem.gameUpdates.description">Compares installed versions with patch metadata. Downloads require an official Sony manifest; only playstation.net is accepted.</p></div></div>
<div id="game-updates-summary"><p class="hint" data-i18n="ecosystem.gameUpdates.loading">Loading update catalog…</p></div>
<form id="game-updates-manifest-form" class="ecosystem-source-form game-updates-primary-form">
<label><span data-i18n="ecosystem.gameUpdates.game">Game</span><select id="game-updates-game" required><option value="" data-i18n="ecosystem.gameUpdates.selectGame">Select a library game</option></select></label>
<button class="btn btn-accent" id="game-updates-auto-inspect" type="submit" data-i18n="ecosystem.gameUpdates.findOfficial">Find official update</button>
<button class="btn btn-sm" id="game-updates-scan-all" type="button" data-i18n="ecosystem.gameUpdates.checkLibrary">Check entire library</button>
<details class="ecosystem-raw-config">
<summary data-i18n="ecosystem.gameUpdates.advanced">Advanced · manual Sony manifest fallback</summary>
<label><span data-i18n="ecosystem.gameUpdates.manifest">Sony JSON manifest</span><input id="game-updates-manifest-url" type="url" placeholder="https://…playstation.net/…/update.json"></label>
<button class="btn btn-sm" id="game-updates-manual-inspect" type="button" data-i18n="ecosystem.gameUpdates.inspect">Inspect pasted manifest</button>
</details>
</form>
<div id="game-updates-plan" class="ecosystem-plan-safety"><strong data-i18n="ecosystem.gameUpdates.notInspected">No manifest inspected yet.</strong><span data-i18n="ecosystem.gameUpdates.inspectDetail">Inspection downloads only bounded JSON metadata and checks the host, offsets, part hashes, package hash and free space.</span></div>
</div>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3 data-i18n="ecosystem.gameUpdates.jobsTitle">Download and staging jobs</h3><p class="hint" data-i18n="ecosystem.gameUpdates.jobsDetail">Packages are assembled atomically and fully verified. PS5 staging installs or starts nothing.</p></div><button id="game-updates-refresh" class="btn btn-sm" data-i18n="common.refresh">Refresh</button></div><div id="game-updates-jobs"><p class="hint" data-i18n="ecosystem.gameUpdates.jobsLoading">Loading jobs…</p></div></div>
</div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="etahen" hidden>
<div class="ecosystem-workflow-strip" aria-label="etaHEN configuration workflow">
<span class="active"><b>1</b><strong>Choose</strong><small>Preset or known setting</small></span>
<span><b>2</b><strong>Review</strong><small>Exact diff and risks</small></span>
<span><b>3</b><strong>Apply</strong><small>Backup, write and verify</small></span>
</div>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>etaHEN profile</h3><p class="hint">Only known keys are changed; unknown lines and comments are preserved.</p></div><button id="ecosystem-etahen-reload" class="btn btn-sm">Reload</button></div>
<div class="form-row ecosystem-preset-row"><label>Preset<select id="ecosystem-etahen-preset"><option value="">No preset</option></select></label><button id="ecosystem-etahen-preview" class="btn">Preview</button><button id="ecosystem-etahen-apply" class="btn btn-accent" disabled>Apply safely</button></div>
<div id="ecosystem-etahen-fields" class="ecosystem-config-grid"><p class="hint">Connect through FTP first.</p></div>
<details class="ecosystem-raw-config"><summary>View raw config.ini</summary><textarea id="ecosystem-etahen-raw" class="operations-code-editor" rows="16" spellcheck="false"></textarea></details>
</div>
<div class="card"><div class="card-header"><h3>Changes and recovery</h3></div><div id="ecosystem-etahen-diff"><p class="hint">No preview yet.</p></div><div id="ecosystem-etahen-history" class="ecosystem-history"></div></div>
</div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="live" hidden>
<div class="card ecosystem-live-card"><div class="card-header ecosystem-card-header"><div><span class="ecosystem-section-kicker">Console telemetry</span><h3>Live Session · strictly read-only</h3><p class="hint">A deliberately small ps5debug-NG allowlist reads console identity and the foreground game. Memory, attach, kernel, notify and reboot commands are unavailable.</p></div><button id="ecosystem-live-refresh" class="btn btn-accent">Read live status</button></div><div id="ecosystem-live-session"><div class="ecosystem-live-prerequisite"><span>1</span><div><strong>ps5debug-NG 1.3.0</strong><small>Required on PS5 port 744</small></div><span>2</span><div><strong>Read-only handshake</strong><small>Runs only when requested</small></div><span>3</span><div><strong>Session context</strong><small>Firmware and foreground title</small></div></div></div></div>
</section>
<section class="ecosystem-panel remote-play-page" data-ecosystem-panel="remote-play" hidden>
<div class="card remote-play-hero">
<div class="card-header ecosystem-card-header"><div><span class="remote-play-kicker">Native PS5 streaming</span><h3>Remote Play</h3><p class="hint" data-i18n="remotePlay.description">Chimera discovers and wakes the PS5, then a paired worker opens chiaki-ng locally for video, audio, controller and DualSense features.</p></div><div class="ecosystem-inline-actions"><button id="remote-play-discover" class="btn btn-sm" data-i18n="remotePlay.checkPs5">Check PS5</button><button id="remote-play-refresh" class="btn btn-sm" data-i18n="common.refresh">Refresh</button></div></div>
<div id="remote-play-readiness" class="remote-play-readiness"><p class="hint" data-i18n="remotePlay.readinessLoading">Loading readiness…</p></div>
<div class="remote-play-primary-actions"><button id="remote-play-start" class="btn btn-accent" type="button" data-i18n="remotePlay.start">Start Remote Play</button><button id="remote-play-wake" class="btn" type="button" data-i18n="remotePlay.wake">Wake PS5</button><span id="remote-play-start-hint" class="hint" data-i18n="remotePlay.preflightHint">All safety and connectivity checks run first.</span></div>
</div>
<div class="ecosystem-two-column remote-play-columns">
<div class="card">
<div class="card-header"><div><h3 data-i18n="remotePlay.profile.title">Session profile</h3><p class="hint" data-i18n="remotePlay.profile.description">The console name must exactly match the name shown by the local chiaki-ng client after registration.</p></div></div>
<form id="remote-play-profile" class="ecosystem-source-form remote-play-profile-form">
<label><span data-i18n="remotePlay.profile.name">Profile name</span><input id="remote-play-nickname" maxlength="80" value="My PS5"></label>
<label><span data-i18n="remotePlay.profile.consoleName">chiaki-ng console name</span><input id="remote-play-client-nickname" maxlength="80" placeholder="for example PS5-313" data-i18n-placeholder="remotePlay.profile.consolePlaceholder"></label>
<label><span data-i18n="remotePlay.profile.worker">Local worker</span><select id="remote-play-worker"><option value="" data-i18n="remotePlay.worker.none">No worker yet</option></select></label>
<label><span data-i18n="remotePlay.profile.display">Display</span><select id="remote-play-display-mode"><option value="fullscreen" data-i18n="remotePlay.display.fullscreen">Fullscreen</option><option value="windowed" data-i18n="remotePlay.display.windowed">Windowed</option><option value="zoom">Zoom</option><option value="stretch" data-i18n="remotePlay.display.stretch">Stretch</option></select></label>
<label><span data-i18n="remotePlay.profile.wakeKey">Registration key for wake</span><input id="remote-play-key" type="password" maxlength="8" autocomplete="new-password" placeholder="Unchanged when blank" data-i18n-placeholder="settings.notifications.tokenPlaceholder"></label>
<button class="btn btn-accent" type="submit" data-i18n="remotePlay.profile.save">Save profile safely</button>
</form>
<div id="remote-play-status" class="ecosystem-plan-safety"><strong data-i18n="remotePlay.profile.loading">Loading profile…</strong></div>
</div>
<div class="card">
<div class="card-header"><div><h3 data-i18n="remotePlay.worker.title">Local Remote Play worker</h3><p class="hint" data-i18n="remotePlay.worker.description">Install this once on the Windows device where you play. The worker accepts no shell or free-form arguments.</p></div></div>
<form id="remote-play-worker-package-form" class="ecosystem-filter-row"><label><span data-i18n="remotePlay.worker.deviceName">Device name</span><input id="remote-play-worker-label" maxlength="80" value="Windows gaming-pc"></label><button class="btn btn-accent" type="submit" data-i18n="remotePlay.worker.download">Download worker package</button></form>
<div class="ecosystem-plan-safety"><strong>Three-step installation</strong><span>Extract the ZIP → run <code>install.ps1</code> → return here and click Refresh.</span><span>The short-lived enrollment token is erased after the first HTTPS connection.</span></div>
<div id="remote-play-workers" class="remote-play-workers"><p class="hint" data-i18n="remotePlay.worker.loading">Loading workers…</p></div>
</div>
</div>
<div class="ecosystem-two-column remote-play-columns">
<div class="card">
<div class="card-header ecosystem-card-header"><div><h3>chiaki-ng client</h3><p class="hint" data-i18n="remotePlay.client.description">Registration happens locally with the PSN Account ID and PS5 Link Device PIN; Chimera never receives that data.</p></div><button id="remote-play-upstream" class="btn btn-sm" data-i18n="remotePlay.client.guide">Guide</button></div>
<div class="ecosystem-inline-actions"><button id="remote-play-diagnose" class="btn btn-sm" data-i18n="remotePlay.client.diagnose">Diagnose</button><button id="remote-play-open-client" class="btn btn-sm">Open / register client</button><button id="remote-play-install-client" class="btn btn-sm">Install chiaki-ng</button><button id="remote-play-update-client" class="btn btn-sm">Update chiaki-ng</button><button id="remote-play-stop" class="btn btn-danger btn-sm">Stop session</button></div>
<div id="remote-play-companion"><p class="hint" data-i18n="remotePlay.client.loading">Loading client status…</p></div>
</div>
<div class="card">
<div class="card-header ecosystem-card-header"><div><h3 data-i18n="remotePlay.sessions.title">Sessions & actions</h3><p class="hint" data-i18n="remotePlay.sessions.description">The most recent Remote Play jobs and their verified worker result.</p></div><button id="remote-play-profile-export" class="btn btn-sm" data-i18n="remotePlay.sessions.export">Download profile report</button></div>
<div id="remote-play-jobs"><p class="hint" data-i18n="remotePlay.sessions.empty">No Remote Play action has run yet.</p></div>
</div>
</div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="files" hidden>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>PS5 File Explorer</h3><p class="hint">Read-only websrv browser with a fixed allowlist. Downloads are bounded locally, hashed and delivered through the authenticated browser.</p></div><button id="websrv-files-root" class="btn btn-sm">Safe roots</button></div><div id="websrv-files-path" class="ecosystem-plan-safety"></div><div id="websrv-files-list"><p class="hint">Loading files…</p></div></div>
<div class="card"><div class="card-header"><div><h3>SMB through PS5</h3><p class="hint">websrv only browses previously saved SMB sources; arbitrary hosts cannot be entered.</p></div></div><label>SMB source<select id="websrv-smb-source"><option value="">Choose a source</option></select></label><div id="websrv-smb-path" class="ecosystem-plan-safety"></div><div id="websrv-smb-list"><p class="hint">Choose a saved SMB source.</p></div></div>
</div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="homebrew" hidden>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>Homebrew library</h3><p class="hint">Inventories the official websrv paths on internal, USB and external storage.</p></div><div class="ecosystem-inline-actions"><button id="ecosystem-websrv-discover" class="btn btn-sm">Discover mDNS</button><button id="ecosystem-homebrew-webui" class="btn btn-sm">Open websrv</button><button id="ecosystem-homebrew-scan" class="btn btn-accent">Scan PS5</button></div></div><div id="ecosystem-websrv-discovery"></div><div id="ecosystem-homebrew" class="ecosystem-homebrew-grid"><p class="hint">Not scanned yet.</p></div></div>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>PacBrew Catalog</h3><p class="hint">Read-only index of official PacBrew build recipes. API rate limits automatically fall back to the bounded official branch archive.</p></div><button id="ecosystem-pacbrew-refresh" class="btn btn-sm">Refresh upstream</button></div><div class="ecosystem-filter-row"><input id="ecosystem-pacbrew-search" class="search-input" placeholder="Search package or library…"><select id="ecosystem-pacbrew-kind"><option value="all">All types</option><option value="application">Applications</option><option value="service">Services</option><option value="library">Libraries</option><option value="toolchain">Toolchain</option></select></div><div id="ecosystem-pacbrew-detail"></div><div id="ecosystem-pacbrew"><p class="hint">Open this tab to load the catalog.</p></div></div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="sources" hidden>
<div class="ecosystem-two-column"><div class="card"><div class="card-header"><h3>Add remote source</h3></div>
<form id="ecosystem-source-form" class="ecosystem-source-form">
<input id="ecosystem-source-id" type="hidden"><label>Name<input id="ecosystem-source-label" maxlength="80" required placeholder="Unraid ROMs"></label>
<label>Protocol<select id="ecosystem-source-protocol"><option>smb</option><option>nfs</option><option>sftp</option><option>ftp</option><option>webdav</option><option>http</option><option>https</option></select></label>
<label>Host<input id="ecosystem-source-host" required placeholder="server.example.test"></label><label>Port<input id="ecosystem-source-port" type="number" min="1" max="65535" placeholder="445"></label>
<label>Path<input id="ecosystem-source-path" value="/" placeholder="/Media/roms"></label><label>User<input id="ecosystem-source-user" autocomplete="username"></label>
<label>Password<input id="ecosystem-source-password" type="password" autocomplete="new-password" placeholder="Unchanged when empty"></label>
<button class="btn btn-accent" type="submit">Save encrypted</button>
</form>
<p class="hint">Passwords are encrypted with AES-256-GCM and are never returned to the browser.</p>
</div><div class="card"><div class="card-header ecosystem-card-header"><h3>Sources and upstream WebUIs</h3><div class="ecosystem-inline-actions"><button id="ecosystem-ezremote-webui" class="btn btn-sm">ezRemote</button><button id="ecosystem-dpi-webui" class="btn btn-sm">DPI V2</button></div></div><div id="ecosystem-sources"><p class="hint">Loading sources…</p></div></div></div>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>ezRemote Server · remote → PS5</h3><p class="hint">Uses the real PS5 server API on port 6701. Upstream resumes downloads after a server restart, but exposes no safe pause or cancel API.</p></div><button id="ezremote-refresh" class="btn btn-sm">Refresh status</button></div>
<div id="ezremote-status" class="ecosystem-plan-safety"><strong>Loading status…</strong></div>
<form id="ezremote-download-form" class="ecosystem-source-form">
<label>Saved source<select id="ezremote-source" required><option value="">Choose a source</option></select></label>
<label>File or folder in source<input id="ezremote-source-path" required maxlength="1024" placeholder="PS5/Game/game.pkg"></label>
<label>Safe PS5 destination<input id="ezremote-destination" required maxlength="1024" value="/data/ezremote/game.pkg"></label>
<label>Size in bytes (optional)<input id="ezremote-size" type="number" min="0" step="1" value="0"></label>
<button class="btn btn-accent" type="submit">Send to PS5 with checks</button>
</form>
<div class="operations-warning"><strong>Port 9090 is only the ezRemote Client WebUI.</strong><p>etaHEN DPI can use the same port. Chimera uses only ezRemote Server on 6701 for downloads and starts or stops neither service.</p></div>
<div id="ezremote-downloads"><p class="hint">No download status loaded yet.</p></div>
</div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="plugins" hidden>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>Installed etaHEN plugins</h3><p class="hint">USB has upstream-documented priority over internal plugins. Every toggle creates a backup first.</p></div><button id="ecosystem-plugins-scan" class="btn btn-accent">Scan plugins</button></div><div id="ecosystem-plugins"><p class="hint">Not scanned yet.</p></div></div>
<div class="card"><div class="card-header"><div><h3>Install a plugin</h3><p class="hint">Choose a local ELF or PRX. Chimera uploads it, downloads it again, and verifies SHA-256 before reporting success.</p></div></div>
<label>Destination<select id="ecosystem-plugin-target"><option value="/data/etaHEN/plugins">Internal · /data/etaHEN/plugins</option>${Array.from({ length: 7 }, (_, index) => `<option value="/mnt/usb${index}/etaHEN/plugins">USB ${index} · /mnt/usb${index}/etaHEN/plugins</option>`).join('')}</select></label>
<button id="ecosystem-plugin-install" class="btn btn-accent" type="button">Choose and install ELF / PRX</button>
<div class="ecosystem-plan-safety"><strong>Explicit and reversible</strong><span>An existing file with the same name is backed up first. Reload etaHEN or use Toolbox after installation.</span></div>
</div>
</div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="cheats" hidden>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>Cheats & Patches 2.0</h3><p class="hint">etaHEN, GoldHEN and PS Game Patch with source attribution, version checks and conflict protection.</p></div><button id="ecosystem-cheats-sync" class="btn btn-accent">Sync sources</button></div>
<div class="ecosystem-filter-row"><input id="ecosystem-cheats-search" class="search-input" aria-label="Search cheats" placeholder="Title, game ID or source…"><select id="ecosystem-cheats-platform" aria-label="Platform"><option value="all">PS4 + PS5</option><option value="ps4">PS4</option><option value="ps5">PS5</option></select><select id="ecosystem-cheats-kind" aria-label="Type"><option value="all">Cheats + patches</option><option value="cheat">Cheats</option><option value="patch">Patches</option></select><label class="ecosystem-check"><input id="ecosystem-cheats-installed" type="checkbox" checked> My games only</label></div>
<div id="ecosystem-cheats"><p class="hint">Loading index…</p></div>
</div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="saves" hidden>
<div class="ecosystem-two-column"><div class="card"><div class="card-header ecosystem-card-header"><div><h3>PS5 saves</h3><p class="hint" data-i18n="ecosystem.saves.description">Garlic exports encrypted saves per title ID into Chimera.</p></div><div class="ecosystem-inline-actions"><button id="ecosystem-saves-refresh" class="btn btn-sm" data-i18n="ecosystem.saves.load">Load saves</button><button id="ecosystem-saves-all" class="btn btn-accent" data-i18n="ecosystem.saves.backupAll">Back up all</button></div></div><div id="ecosystem-remote-saves"><div class="operations-empty"><strong data-i18n="ecosystem.saves.notScanned">Console saves not scanned yet</strong><span data-i18n="ecosystem.saves.serviceRequired">Garlic Save Manager must be running on port 8082.</span></div></div></div>
<div class="card"><div class="card-header"><h3>Local Save Vault</h3></div><div id="ecosystem-save-restore-safety" class="ecosystem-plan-safety"><strong>Loading restore safeguards…</strong></div><div id="ecosystem-save-vault"><p class="hint">Loading snapshots…</p></div></div></div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="itemzflow" hidden>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>Itemzflow Log & Job Center</h3><p class="hint">Reads the official app, dumper, loader, daemon and libfuse logs. Chimera does not invent an undocumented job API or send commands from this screen.</p></div><div class="ecosystem-inline-actions"><button id="itemzflow-homebrew-open" class="btn btn-sm">Open Homebrew</button><button id="itemzflow-refresh" class="btn btn-accent">Refresh all logs</button></div></div><div id="itemzflow-summary"><p class="hint">Loading Itemzflow status…</p></div><div class="ecosystem-filter-row"><label>Log<select id="itemzflow-log-select"><option value="all">All logs</option></select></label><label>Level<select id="itemzflow-level"><option value="all">All</option><option value="error">Errors</option><option value="warning">Warnings</option><option value="info">Info</option></select></label><label class="ecosystem-check"><input id="itemzflow-jobs-only" type="checkbox" checked> Jobs and signals only</label></div><div id="itemzflow-events" class="ecosystem-log"></div></div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="platform-lab" hidden>
<div class="card operations-warning-card"><strong data-i18n="platformLab.boundary.title">Review-first integration layer.</strong> <span data-i18n="platformLab.boundary.body">This area observes upstreams and prepares inert plans. It never deploys payloads or mutates a console.</span></div>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3 data-i18n="platformLab.bindings.title">Upstream binding registry</h3><p class="hint" data-i18n="platformLab.bindings.hint">Versioned release evidence from official repositories.</p></div><button id="platform-bindings-refresh" class="btn btn-accent" data-i18n="platformLab.bindings.refresh">Refresh metadata</button></div><div id="platform-bindings"><p class="hint" data-i18n="common.loading">Loading...</p></div></div>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header"><h3 data-i18n="platformLab.autoloader.title">Unified autoloader planner</h3></div><p class="hint" data-i18n="platformLab.autoloader.hint">Preview Y2JB, BD-JB, and Lua profiles without writing removable media.</p><div class="ecosystem-filter-row"><select id="platform-autoloader-profile" aria-label="Autoloader profile"></select><button id="platform-autoloader-preview" class="btn btn-accent" data-i18n="platformLab.preview">Preview</button></div><div id="platform-autoloader-output"></div></div>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3 data-i18n="platformLab.websrv.title">websrv topology</h3><p class="hint" data-i18n="platformLab.websrv.hint">Read-only SSDP and DLNA MediaServer discovery.</p></div><button id="platform-websrv-refresh" class="btn btn-sm" data-i18n="common.refresh">Refresh</button></div><div id="platform-websrv"><p class="hint" data-i18n="common.loading">Loading...</p></div></div>
</div>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header ecosystem-card-header"><div><h3 data-i18n="platformLab.remote.title">chiaki-ng intelligence</h3><p class="hint" data-i18n="platformLab.remote.hint">Worker versions, stream health, and recommendations.</p></div><button id="platform-remote-refresh" class="btn btn-sm" data-i18n="common.refresh">Refresh</button></div><div id="platform-remote"><p class="hint" data-i18n="common.loading">Loading...</p></div></div>
<div class="card"><div class="card-header"><h3 data-i18n="platformLab.shadps4.title">shadPS4 library bridge</h3></div><p class="hint" data-i18n="platformLab.shadps4.hint">Read-only inventory of emulator copies and ZArchive candidates.</p><div class="ecosystem-filter-row"><select id="platform-shadps4-root" aria-label="shadPS4 library root"></select><button id="platform-shadps4-scan" class="btn btn-accent" data-i18n="platformLab.scan">Scan</button></div><div id="platform-shadps4"><p class="hint" data-i18n="common.loading">Loading...</p></div></div>
</div>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header"><h3 data-i18n="platformLab.artifacts.title">Artifact evidence and staging</h3></div><p class="hint" data-i18n="platformLab.artifacts.hint">Inspect a root-relative payload, review its exact hash, then stage a complete profile atomically.</p><div class="ecosystem-filter-row"><select id="platform-artifact-root"></select><input id="platform-artifact-relative" maxlength="500" placeholder="payloads/websrv.elf"><button id="platform-artifact-inspect" class="btn" data-i18n="platformLab.inspect">Inspect</button></div><div id="platform-artifacts"></div><div class="ecosystem-filter-row"><select id="platform-stage-root"></select><button id="platform-autoloader-stage" class="btn btn-accent" data-i18n="platformLab.stage">Stage reviewed profile</button></div><div id="platform-stage-output"></div></div>
<div class="card"><div class="card-header"><h3 data-i18n="platformLab.profileEditor.title">Profile editor</h3></div><textarea id="platform-profile-json" class="ecosystem-log" rows="12" spellcheck="false"></textarea><div class="ecosystem-inline-actions"><button id="platform-profile-save" class="btn btn-accent" data-i18n="common.save">Save</button><button id="platform-profile-delete" class="btn" data-i18n="common.delete">Delete</button><button id="platform-profile-recovery" class="btn" data-i18n="platformLab.recovery">Recovery manifest</button></div><div id="platform-profile-output"></div></div>
</div>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header"><h3 data-i18n="platformLab.contracts.title">External capability contracts</h3></div><div id="platform-contracts"></div></div>
<div class="card"><div class="card-header"><h3 data-i18n="platformLab.notifications.title">Recommendations and blockers</h3></div><div id="platform-notifications"></div></div>
</div>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header"><h3 data-i18n="platformLab.launch.title">Fixed shadPS4 launcher</h3></div><p class="hint" data-i18n="platformLab.launch.hint">A dedicated no-shell adapter accepts only an indexed game and an exact confirmation.</p><div class="ecosystem-filter-row"><select id="platform-shadps4-exe-root"></select><input id="platform-shadps4-exe-relative" maxlength="500" placeholder="shadps4/shadPS4.exe"><button id="platform-shadps4-configure" class="btn" data-i18n="platformLab.configure">Configure</button></div><div id="platform-shadps4-launch"></div></div>
<div class="card"><div class="card-header"><h3 data-i18n="platformLab.search.title">Platform search</h3></div><form id="platform-search-form" class="ecosystem-filter-row"><input id="platform-search-query" class="search-input" minlength="2" maxlength="120" placeholder="Search upstreams, profiles, artifacts and emulator copies"><button class="btn" type="submit" data-i18n="common.search">Search</button></form><div id="platform-search-results"></div><div class="card-header"><h3 data-i18n="platformLab.operations.title">Background operations</h3></div><div id="platform-operations"></div></div>
</div>
<div class="ecosystem-inline-actions"><button id="platform-lab-refresh" class="btn" data-i18n="platformLab.refreshAll">Refresh Platform Lab</button></div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="research" hidden>
<div class="card operations-warning-card"><strong>Expert area without execution controls.</strong> Chimera checks only the profile, upstream compatibility and safety requirements. Sending a payload, running an exploit, entering rest mode, writing USB/M.2 storage and disabling etaHEN/kstuff are technically unavailable here.</div>
<div class="ecosystem-two-column"><div class="card"><div class="card-header ecosystem-card-header"><div><h3>PS5 Linux Preflight</h3><p class="hint">Based on the official ps5-linux-loader upstream. Unknown hardware requirements remain explicitly unknown.</p></div><button id="ecosystem-linux-refresh" class="btn btn-accent">Run preflight again</button></div><div id="ecosystem-linux-summary"><p class="hint">Loading preflight…</p></div><div id="ecosystem-linux-checks"></div></div><div class="card"><div class="card-header"><h3>Official project chain</h3></div><div id="ecosystem-linux-components"></div><div id="ecosystem-linux-policy" class="ecosystem-plan-safety"></div></div></div>
</section>
<section class="ecosystem-panel" data-ecosystem-panel="timeline" hidden>
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>Console Timeline</h3><p class="hint">Live klog plus etaHEN, ShadowMount and Itemzflow logs. GDB and shell remain outside this interface.</p></div><div class="ecosystem-inline-actions"><input id="ecosystem-timeline-port" type="number" min="1" max="65535" value="9081" title="etaHEN: 9081, klogsrv: 3232"><button id="ecosystem-timeline-start" class="btn btn-accent">Connect</button><button id="ecosystem-timeline-stop" class="btn">Stop</button><button id="ecosystem-timeline-import" class="btn">Import logs</button></div></div><form id="ecosystem-timeline-options" class="ecosystem-filter-row"><label class="ecosystem-check"><input id="ecosystem-timeline-persist" type="checkbox"> Bounded persistent storage</label><label>Retention (days)<input id="ecosystem-timeline-retention" type="number" min="1" max="365" value="14"></label><label>Maximum events<input id="ecosystem-timeline-max" type="number" min="100" max="50000" value="5000"></label><button class="btn btn-sm" type="submit">Save policy</button><button id="ecosystem-timeline-export" class="btn btn-sm" type="button">Redacted export</button></form><div class="ecosystem-filter-row"><input id="ecosystem-timeline-search" class="search-input" maxlength="200" placeholder="Search source or message…"><select id="ecosystem-timeline-level"><option value="">All levels</option><option value="error">Errors</option><option value="warning">Warnings</option><option value="info">Info</option></select></div><div id="ecosystem-timeline-state"></div><div id="ecosystem-timeline-events" class="ecosystem-log"></div></div>
</section>`;
bindEcosystemEvents();
window.ChimeraLinkUx?.mount({ root: $('etahen-link-root'), loadStatus: () => window.pork.etahenLinkInventoryStatus(), translate: (key, fallback) => window.t?.(key, fallback) || fallback });
_ecosystemBuilt = true;
}
async function ecosystemBusy(button, task) {
const prior = button?.textContent;
if (button) { button.disabled = true; button.textContent = window.t('common.loading', 'Loading…'); }
try { return await task(); }
finally { if (button) { button.disabled = false; button.textContent = prior; } }
}
function localizedReadinessStep(step) {
if (window.i18n?.current === 'nl') return step;
const english = {
profile: { label: 'Console profile', detail: step.state === 'ready' ? step.detail : 'IP address not configured' },
transport: { label: 'File transport', detail: step.state === 'ready' ? 'FTP transport reachable' : 'etaHEN FTP or ftpsrv is required for files' },
elfldr: { label: 'ELF Loader', detail: 'Foundation for Payload Manager and multiple homebrew services' },
'payload-manager': { label: 'Payload Manager', detail: 'Catalog, imports and explicit payload launch' },
websrv: { label: 'websrv', detail: 'Homebrew, filesystem and additional network services' },
diagnostics: { label: 'Diagnostics', detail: 'Klog is optional and only needed during investigation' },
};
return { ...step, ...(english[step.id] || {}) };
}
function localizedEcosystemAlert(alert, field) {
const fallback = alert?.[field] || '';
const count = String(alert?.title || '').match(/\d+(?:\/\d+)?/)?.[0] || '';
return window.tf?.(`navigation.alerts.${alert?.id}.${field}`, fallback, { count }) || fallback;
}
function localizedEcosystemError(value) {
const parts = [...new Set(String(value || '').split(/\s+·\s+/).map(part => part.trim()).filter(Boolean))];
const message = parts.join(' · ');
if (window.i18n?.current === 'nl') return message;
return message
.replace(/Configureer eerst een geldig PS5-hostadres(?: in Instellingen)?\.?/gi, 'Configure a valid PS5 host in Settings first.') // i18n: normalize persisted server copy
.replace(/Maak eerst verbinding met de PS5 via FTP\.?/gi, 'Connect to the PS5 through FTP first.') // i18n: normalize persisted server copy
.replace(/Start ([^.]+) op de PS5\.?/gi, 'Start $1 on the PS5.');
}
function localizedEcosystemActivity(value) {
const message = String(value || '');
if (window.i18n?.current === 'nl') return message;
return message
.replace('Console Health wacht op configuratie', 'Console Health is waiting for configuration')
.replace(/(\d+) aandachtspunt\(en\) gevonden\./, '$1 attention item(s) found.'); // i18n: normalize persisted activity copy.
}
function ecosystemCapabilityText(key, fallback) {
return window.t?.(`ecosystem.capabilities.${key}`, fallback) || fallback;
}
function renderCapabilityGraph(graph) {
if (!graph?.features) return '';
const groups = ['core', 'connected', 'planned'];
return `<div class="card ecosystem-capability-card">
<div class="card-header ecosystem-card-header"><div><h3>${ecosystemCapabilityText('title', 'Capability map')}</h3><p class="hint">${ecosystemCapabilityText('description', 'Live, conditional and planned functionality for the active console profile.')}</p></div>${ecosystemStatus(graph.summary?.blocked ? 'warning' : 'compatible', `${graph.summary?.available || 0}/${graph.summary?.total || 0} ${ecosystemCapabilityText('available', 'available')}`)}</div>
<div class="ecosystem-metrics">${[
['available', graph.summary?.available || 0], ['conditional', graph.summary?.conditional || 0], ['blocked', graph.summary?.blocked || 0], ['bindings', graph.bindings?.length || 0],
].map(([label, count]) => `<div class="operations-metric"><strong>${count}</strong><span>${escHtml(ecosystemCapabilityText(`status.${label}`, label))}</span></div>`).join('')}</div>
<div class="ecosystem-readiness-groups">${groups.map(group => `<section><h4>${escHtml(ecosystemCapabilityText(`group.${group}`, group))}</h4><div class="ecosystem-update-list">${graph.features.filter(item => item.group === group).map(item => `<div class="ecosystem-service-row"><div><strong>${escHtml(ecosystemCapabilityText(`feature.${item.id}`, item.id))}</strong><span>${escHtml(ecosystemCapabilityText(`reason.${item.reasonCode}`, item.reasonCode))} · ${escHtml(ecosystemCapabilityText(`mode.${item.mode}`, item.mode))}</span></div>${ecosystemStatus(item.status === 'available' ? 'compatible' : item.status === 'conditional' ? 'warning' : item.status === 'blocked' ? 'offline' : 'unknown', ecosystemCapabilityText(`status.${item.status}`, item.status))}</div>`).join('')}</div></section>`).join('')}</div>
<p class="hint">${graph.freshness?.stale ? ecosystemCapabilityText('stale', 'Upstream evidence is stale; refresh components before relying on version-sensitive actions.') : ecosystemCapabilityText('fresh', 'Upstream evidence is current.')}</p>
</div>`;
}
function renderEcosystemOverview(data, healthPacket = {}, activity = [], healthHistory = [], capabilityGraph = null) {
const services = data.services?.services || [];
const online = services.filter(service => service.reachable).length;
const report = healthPacket.report || null;
const healthAge = report ? Math.max(0, Date.now() - Number(report.generatedAt || 0)) : null;
const healthStale = healthAge !== null && healthAge > 5 * 60 * 1000;
const alerts = report?.alerts || [];
const openAlerts = alerts.filter(alert => !alert.acknowledged);
const readiness = data.readiness || { state: 'not-configured', steps: [] };
const readinessSteps = (readiness.steps || []).map(localizedReadinessStep);
const isDutch = window.i18n?.current === 'nl';
const readinessLabels = new Map(readinessSteps.map(step => [step.id, step.label]));
$('ecosystem-overview').innerHTML = `
<div class="card ecosystem-readiness-card">
<div class="card-header"><div><h3>Console Readiness</h3><p class="hint">Dependencies and safe next steps. Chimera starts nothing automatically.</p></div>${ecosystemStatus(readiness.state === 'ready' ? 'compatible' : readiness.state === 'attention' ? 'warning' : 'unknown', readiness.state === 'ready' ? 'Ready' : readiness.state === 'attention' ? 'Action needed' : 'Not configured')}</div>
<div class="ecosystem-readiness-groups">${[
[isDutch ? 'Kernverbinding' : 'Core connection', readinessSteps.filter(step => step.required)],
[isDutch ? 'Optionele diensten' : 'Optional services', readinessSteps.filter(step => !step.required)],
].map(([label, steps]) => `<section><h4>${label}<span>${steps.filter(step => step.state === 'ready').length}/${steps.length}</span></h4><div class="ecosystem-readiness-flow">${steps.map((step, index) => `<button type="button" data-route="${escHtml(step.route)}" class="ecosystem-readiness-step ${escHtml(step.state)}" aria-label="${escHtml(step.label)}: ${escHtml(step.detail)}"><span class="ecosystem-readiness-step-dot" aria-hidden="true">${step.state === 'ready' ? '✓' : step.state === 'attention' ? '!' : index + 1}</span><span><strong>${escHtml(step.label)}</strong><small>${escHtml(step.detail)}</small>${step.dependsOn?.length ? `<em>${isDutch ? 'Afhankelijk van' : 'Depends on'} ${step.dependsOn.map(id => escHtml(readinessLabels.get(id) || id)).join(', ')}</em>` : ''}</span></button>`).join('')}</div></section>`).join('')}</div>
</div>
${renderCapabilityGraph(capabilityGraph)}
<div class="ecosystem-metrics ecosystem-overview-kpis">
${[
['Console Health', report ? (report.score == null ? 'Not configured' : `${report.score}/100`) : 'Not yet'],
['Services online', `${online}/${services.length}`],
['Managed assets', (data.homebrew?.count || 0) + (data.plugins?.count || 0) + (data.cheats?.count || 0)],
['Protected data', `${data.saveVault?.snapshots || 0} snapshots`],
].map(([label, value]) => `<div class="operations-metric"><strong>${escHtml(value)}</strong><span>${escHtml(label)}</span></div>`).join('')}
</div>
<div class="ecosystem-two-column">
<div class="card"><div class="card-header"><h3>${isDutch ? 'PS5-diensten' : 'PS5 services'}</h3></div>${data.services?.error ? `<p class="operations-warning">${escHtml(localizedEcosystemError(data.services.error))}</p>` : services.map(service => `<div class="ecosystem-service-row"><span class="operations-service-dot ${service.reachable ? 'online' : ''}"></span><div><strong>${escHtml(service.label)}</strong><span>${isDutch ? 'Poort' : 'Port'} ${service.port}${service.error ? ` · ${escHtml(service.error)}` : ''}</span></div>${ecosystemStatus(service.reachable ? 'online' : 'offline', service.reachable ? 'Online' : 'Offline')}</div>`).join('')}</div>
<div class="card"><div class="card-header"><h3>Safety policy</h3></div><div class="ecosystem-safety"><p><strong>No automatic execution</strong><span>Updates are never started immediately on the PS5.</span></p><p><strong>Source and checksum</strong><span>Official upstream and published digests remain visible.</span></p><p><strong>Expert isolation</strong><span>Shell and GDB are not proxied through the public Chimera interface.</span></p></div></div>
</div>
<div class="ecosystem-two-column ecosystem-command-grid">
<div class="card"><div class="card-header ecosystem-card-header"><div><h3>${isDutch ? 'Actiecentrum' : 'Action Center'}</h3><p class="hint">${report ? `${isDutch ? 'Scan van' : 'Scan from'} ${ecosystemDate(report.generatedAt)}${healthStale ? ` · ${isDutch ? 'verouderd' : 'stale'}` : ''} · ${openAlerts.length} open` : (isDutch ? 'Voer een diepe healthscan uit voor concrete acties.' : 'Run a deep health scan for concrete actions.')}</p></div><label class="ecosystem-check"><input id="ecosystem-health-notifications" type="checkbox" ${healthPacket.options?.notificationsEnabled ? 'checked' : ''} ${healthPacket.options?.discordConfigured ? '' : 'disabled'}> ${isDutch ? 'Discord bij verslechtering' : 'Discord when health declines'}</label></div>
<div id="ecosystem-health-alerts">${alerts.length ? alerts.map(alert => `<div class="ecosystem-alert ecosystem-alert-${escHtml(alert.severity)} ${alert.acknowledged ? 'acknowledged' : ''}" data-id="${escHtml(alert.id)}"><div><strong>${escHtml(localizedEcosystemAlert(alert, 'title'))}</strong><span>${escHtml(localizedEcosystemAlert(alert, 'detail'))}</span></div><div class="ecosystem-inline-actions">${ecosystemStatus(alert.severity === 'critical' ? 'error' : alert.severity === 'warning' ? 'warning' : 'unknown', alert.severity)}${alert.action ? `<button class="btn btn-sm ecosystem-alert-open" ${alert.action.tab ? `data-tab="${escHtml(alert.action.tab)}"` : ''} ${alert.action.page ? `data-page="${escHtml(alert.action.page)}"` : ''}>Open</button>` : ''}${alert.acknowledged ? '<span class="hint">Read</span>' : '<button class="btn btn-sm ecosystem-alert-ack">Mark seen</button>'}</div></div>`).join('') : '<div class="operations-empty-ok">✓ No known issues.</div>'}</div>
</div>
<div class="card"><div class="card-header"><h3>Health trend & activity</h3></div>${healthHistory.some(item => item.score != null) ? `<div class="ecosystem-health-trend" aria-label="Console Health history">${healthHistory.filter(item => item.score != null).slice(0, 20).reverse().map(item => `<div title="${ecosystemDate(item.generatedAt)} · ${item.score}/100"><i style="height:${Math.max(3, Number(item.score) || 0)}%"></i><span>${escHtml(item.score)}</span></div>`).join('')}</div>` : '<p class="hint">The trend appears after multiple configured health scans.</p>'}<div class="ecosystem-activity">${activity.length ? activity.slice(0, 12).map(item => `<div class="ecosystem-activity-row"><time>${ecosystemDate(item.at)}</time><div><strong>${escHtml(localizedEcosystemActivity(item.title))}</strong><span>${escHtml(localizedEcosystemActivity(item.detail || item.type))}</span></div></div>`).join('') : '<p class="hint">No management activity recorded yet.</p>'}</div></div>
</div>`;
const health = $('ecosystem-health');
health.textContent = report ? (report.score == null ? 'Health · not configured' : `Health ${report.score}/100 · ${report.grade}${healthStale ? ' · stale' : ''}`) : (services.length ? `${online}/${services.length} PS5 services online` : (data.services?.error || 'PS5 not configured'));
health.className = `operations-health ${report?.score == null ? '' : !healthStale && (report?.score >= 75 || (!report && online)) ? 'healthy' : 'warning'}`;
const badge = $('nav-ecosystem-badge');
const issues = openAlerts.filter(alert => ['critical', 'warning'].includes(alert.severity)).length || Number(data.plugins?.conflicts || 0);
if (badge) { badge.hidden = issues === 0; badge.textContent = issues || ''; }
}
async function loadEcosystemOverview() {
const [data, healthPacket, activity, healthHistory, capabilityGraph] = await Promise.all([window.pork.ecosystemOverview(), window.pork.ecosystemHealthLatest(), window.pork.ecosystemActivity(50), window.pork.ecosystemHealthHistory(30), window.pork.ecosystemCapabilityGraph()]);
renderEcosystemOverview(data, healthPacket, activity, healthHistory, capabilityGraph);
_ecosystemLoaded = true;
}
function ecosystemComponentGroup(component) {
if (['experimental-os', 'expert', 'kernel'].includes(component.kind) || component.risk === 'expert') return 'Experimental & expert';
if (['catalog', 'plugin', 'plugin-catalog', 'database'].includes(component.kind)) return 'Catalogs, plugins & data';
if (['homebrew', 'client-companion', 'download-tool', 'remote-play-helper', 'account-tool', 'diagnostic'].includes(component.kind)) return 'Applications & tools';
return 'Core services';
}
function renderEcosystemComponents() {
const result = _ecosystemComponentsPacket;
if (!result) return;
const query = String($('ecosystem-components-search')?.value || '').trim().toLowerCase();
const platform = $('ecosystem-components-platform')?.value || 'all';
const risk = $('ecosystem-components-risk')?.value || 'all';
const filtered = result.components.filter(component => {
const haystack = `${component.name} ${component.kind} ${component.platform} ${(component.dependencies || []).join(' ')}`.toLowerCase();
return (!query || haystack.includes(query))
&& (platform === 'all' || component.platform === platform)
&& (risk === 'all' || component.risk === risk);
});
const groups = new Map();
for (const component of filtered) {
const group = ecosystemComponentGroup(component);
if (!groups.has(group)) groups.set(group, []);
groups.get(group).push(component);
}
const counts = {
visible: filtered.length,
total: result.components.length,
compatible: filtered.filter(component => component.compatibility?.status === 'compatible').length,
attention: filtered.filter(component => component.compatibility?.status === 'incompatible' || component.risk === 'expert').length,
};
counts.sourceDependent = Math.max(0, counts.visible - counts.compatible - counts.attention);
$('ecosystem-components').innerHTML = `
<div class="ecosystem-component-summary">
<div><strong>${counts.visible}/${counts.total}</strong><span>Visible components</span></div>
<div><strong>${counts.compatible}</strong><span>Compatible</span></div>
<div><strong>${counts.attention}</strong><span>Needs attention</span></div>
<div><strong>${counts.sourceDependent}</strong><span>Source-dependent</span></div>
<div><strong>${ecosystemDate(result.checkedAt)}</strong><span>Last upstream check</span></div>
</div>
${groups.size ? [...groups.entries()].map(([label, components]) => `<section class="ecosystem-component-group"><header><div><h4>${escHtml(label)}</h4><span>${components.length} component${components.length === 1 ? '' : 's'}</span></div></header><div class="ecosystem-component-list">${components.map(component => `
<article class="ecosystem-component ${component.risk === 'expert' ? 'expert' : ''}">
<div><strong>${escHtml(component.name)}</strong><span>${escHtml(component.kind)} · ${escHtml(component.platform)} · ${escHtml(component.trust)}</span></div>
<div><code>${escHtml(component.release?.tag || 'Unknown version')}</code>${ecosystemStatus(component.compatibility?.status, component.compatibility?.status === 'incompatible' ? 'Incompatible' : component.compatibility?.status === 'compatible' ? 'Compatible' : 'Source-dependent')}</div>
<p>${escHtml(component.compatibility?.reason || '')}${component.dependencies?.length ? ` Depends on: ${component.dependencies.map(escHtml).join(', ')}.` : ''}</p>
<button class="btn btn-sm ecosystem-open-upstream" data-url="${escHtml(component.release?.url || component.upstream)}">Open upstream</button>
</article>`).join('')}</div></section>`).join('') : '<div class="operations-empty"><strong>No matching components</strong><span>Clear one or more filters to expand the catalog.</span></div>'}`;
}
async function loadEcosystemComponents() {
_ecosystemComponentsPacket = await window.pork.ecosystemComponents();
renderEcosystemComponents();
}
function ecosystemUpdateState(item) {
const labels = { current: 'Current', ahead: 'Console newer', 'version-different': 'Manual review', 'update-available': 'Update', staged: 'Staged', 'installed-unknown': 'Release unknown', 'not-tracked': 'Not tracked' };
const status = ['current', 'ahead'].includes(item.state) ? 'compatible' : ['update-available', 'version-different'].includes(item.state) ? 'warning' : 'unknown';
return ecosystemStatus(status, labels[item.state] || item.state);
}
function ecosystemEvidenceSummary(item) {
if (!item.installedSource) return ` ${window.t('ecosystem.payloadEvidence.unavailable', 'evidence unavailable')}`;
const confidence = item.installedSourceConfidence || 'unknown';
const seenAt = item.installedSourceSeenAt ? ecosystemDate(item.installedSourceSeenAt) : window.t('ecosystem.payloadEvidence.unknownTime', 'unknown time');
return ` ${'\u00B7'} ${escHtml(window.t('ecosystem.payloadEvidence.source', 'source'))}: ${escHtml(item.installedSource)} ${'\u00B7'} ${escHtml(window.t('ecosystem.payloadEvidence.confidence', 'confidence'))}: ${escHtml(confidence)} ${'\u00B7'} ${escHtml(window.t('ecosystem.payloadEvidence.seen', 'seen'))}: ${escHtml(seenAt)}`;
}
function ecosystemEvidenceConflictSummary(item) {
if (!item.installedVersionConflicts?.length) return '';
return ` ${'\u2014'} conflicts ${item.installedVersionConflicts.map(conflict => `${escHtml(conflict.version)} (${escHtml(conflict.sources.join(', '))})`).join(' ; ')}`;
}
const ecosystemBullet = '\u00B7';
function renderEcosystemUpdates(inventory, plan) {
$('ecosystem-update-summary').innerHTML = `<div class="ecosystem-metrics">${[
['Tracked', inventory.stats.total], ['Current', inventory.stats.current], ['Detected', inventory.stats.detected], ['Console newer', inventory.stats.ahead], ['Updates ready', inventory.stats.updates], ['Blocked steps', plan.blocked],
].map(([label, value]) => `<div class="operations-metric"><strong>${escHtml(value)}</strong><span>${escHtml(label)}</span></div>`).join('')}</div>
<div class="ecosystem-plan-safety"><strong>Safe dependency order</strong><span>${plan.steps.length ? plan.steps.map(step => step.component.name).join(' → ') : 'No tracked updates yet. Record the installed console version below to enable reliable comparison.'}</span></div>`;
const groups = [
{ id: 'ready', title: 'Ready to prepare', description: 'A newer upstream asset can be downloaded and verified locally.', items: inventory.components.filter(item => item.state === 'update-available') },
{ id: 'version', title: 'Version required', description: 'Enter the version currently installed on your console.', items: inventory.components.filter(item => ['installed-unknown', 'not-tracked', 'staged'].includes(item.state)) },
{ id: 'review', title: 'Manual review', description: 'Potential downgrade or incomparable version; Chimera will not prepare it automatically.', items: inventory.components.filter(item => ['ahead', 'version-different'].includes(item.state)) },
{ id: 'current', title: 'Current', description: 'Installed and upstream versions match.', items: inventory.components.filter(item => item.state === 'current') },
];
const renderRow = item => {
const step = plan.steps.find(entry => entry.component.id === item.id);
const conflicts = ecosystemEvidenceConflictSummary(item);
const summaryLine = `${escHtml(item.kind)} ${'\u00B7'} console ${escHtml(item.installedVersion || 'unknown')}${ecosystemEvidenceSummary(item)} ${'\u00B7'} upstream ${escHtml(item.latestVersion || 'unknown')} ${'\u00B7'} locally staged ${escHtml(item.stagedVersion || 'no')}`;
const details = `${conflicts}${conflicts && (item.dependencies?.length || step?.blockers?.length || item.compatibility?.reason) ? ' \u00B7 ' : ''}` +
`${item.dependencies?.length ? `Depends on ${item.dependencies.map(escHtml).join(', ')}` : ''}${conflicts && item.dependencies?.length ? '. ' : ''}` +
`${step?.blockers?.length ? `Blocked: ${step.blockers.map(escHtml).join(' ')}` : item.compatibility?.reason || ''}`;
const actions = item.assets.length
? `<select class="ecosystem-asset-select" aria-label="Release asset">${item.assets.map(asset => `<option value="${escHtml(asset.name)}">${escHtml(asset.name)}${asset.verified ? ` ${ecosystemBullet} checksum` : ''}${asset.downloaded ? ` ${ecosystemBullet} local` : ''}</option>`).join('')}</select>${['ahead', 'version-different'].includes(item.state) ? '<span class="hint">Downgrade blocked</span>' : '<button class="btn btn-sm btn-accent ecosystem-update-prepare">Prepare safely</button>'}`
: '<span class="hint">Sync to discover assets</span>';
const tracking = window.t(`ecosystem.payloadEvidence.mode.${item.trackingMode}`, item.trackingMode);
const automaticButton = item.manualEvidence ? `<button class="btn btn-sm ecosystem-follow-automatic">${escHtml(window.t('ecosystem.payloadEvidence.followAutomatic', 'Follow automatic'))}</button>` : '';
return `<article class="ecosystem-update-row ${item.risk === 'expert' ? 'expert' : ''}" data-id="${escHtml(item.id)}"><div class="ecosystem-update-main"><div><strong>${escHtml(item.name)}</strong>${ecosystemUpdateState(item)}</div><span>${summaryLine}</span><small>${escHtml(tracking)}${details ? ` ${ecosystemBullet} ${details}` : ''}</small></div><div class="ecosystem-update-controls"><label>${escHtml(window.t('ecosystem.payloadEvidence.manualVersion', 'Manual version override'))}<input class="ecosystem-version-input" value="${escHtml(item.manualEvidence?.version || '')}" placeholder="${escHtml(item.latestVersion || 'v1.0')}"></label><button class="btn btn-sm ecosystem-version-save">${escHtml(window.t('ecosystem.payloadEvidence.useManual', 'Use manual'))}</button>${automaticButton}${actions}</div></article>`;
};
$('ecosystem-update-plan').innerHTML = groups.map(group => `<details class="ecosystem-update-group ecosystem-update-group-${group.id}" ${['ready', 'version'].includes(group.id) ? 'open' : ''}>
<summary><div><h4>${escHtml(group.title)}</h4><span>${escHtml(group.description)}</span></div><b>${group.items.length}</b></summary>
${group.items.length ? `<div class="ecosystem-update-list">${group.items.map(renderRow).join('')}</div>` : '<p class="hint">No components in this section.</p>'}
</details>`).join('');
}
async function loadEcosystemUpdates() {
const [inventory, plan, policy, beaconArtifact] = await Promise.all([
window.pork.ecosystemInventory(),
window.pork.ecosystemUpdatePlan(),
window.pork.ecosystemSupplyPolicy(),
window.pork.chimeraBeaconArtifact(),
]);
$('ecosystem-repository-url').value = `${window.location.origin}/payloads.json`;
renderEcosystemUpdates(inventory, plan);
$('ecosystem-supply-official').checked = policy.officialSourcesOnly;
$('ecosystem-supply-https').checked = policy.requireHttps;
$('ecosystem-supply-hash').checked = policy.requirePublishedHashForExecution;
if (!state.connected) {
renderPayloadRuntimeUnavailable('The PS5 target is offline. Local update planning remains available above.');
return;
}
const timeout = (promise, ms) => Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject(new Error('The PS5 runtime check timed out.')), ms)),
]);
try {
const [watchdog, runtimeUpdates] = await Promise.all([
timeout(window.pork.ecosystemPayloadWatchdog(), 4_000),
timeout(window.pork.ecosystemPayloadUpdates(), 4_000),
]);
renderPayloadRuntime(watchdog, runtimeUpdates, beaconArtifact);
} catch (error) {
renderPayloadRuntimeUnavailable(localizedEcosystemError(error.message));
}
}
function renderPayloadRuntimeUnavailable(message) {
$('ecosystem-watchdog').innerHTML = `<div class="operations-empty"><strong>Runtime status unavailable</strong><span>${escHtml(message)}</span><button class="btn btn-sm" data-route="settings/general" data-focus-target="s-host">Review PS5 target</button></div>`;
$('ecosystem-runtime-updates').innerHTML = '<div class="ecosystem-plan-safety"><strong>Safety boundary</strong><span>Catalog synchronization, version comparison and verified staging remain available locally. Runtime installation is never attempted while the PS5 target is offline.</span></div>';
}
function renderPayloadRuntime(watchdog, updates, beaconArtifact) {
$('ecosystem-watchdog').innerHTML = `<div class="ecosystem-metrics">${[
['Library', watchdog.manager?.installed || 0], ['Running', watchdog.stats?.running || 0], ['Autoload offline', watchdog.stats?.autoloadOffline || 0], ['Issues', watchdog.stats?.issues || 0], ['Updates', updates.stats?.updates || 0],
].map(([label, value]) => `<div class="operations-metric"><strong>${escHtml(value)}</strong><span>${escHtml(label)}</span></div>`).join('')}</div>
<div class="ecosystem-update-list">${watchdog.services.map(service => `<div class="ecosystem-service-row"><div><strong>${escHtml(service.name)}</strong><span>${escHtml(service.filename || 'no payload file')} · port ${escHtml(service.port)}${service.installedVersion ? ` · file ${escHtml(service.installedVersion)}` : ''}${service.runtimeVersion ? ` · runtime ${escHtml(service.runtimeVersion)}` : ''}</span></div>${ecosystemStatus(service.running ? 'online' : service.state === 'autoload-offline' ? 'warning' : 'unknown', service.running ? 'Running' : service.autoload ? 'Autoload · offline' : service.installed ? 'Installed' : 'Not present')}</div>`).join('')}</div>
${watchdog.issues?.length ? `<div class="ecosystem-plan-safety"><strong>Attention required</strong>${watchdog.issues.map(issue => `<span>${escHtml(issue.message)}</span>`).join('')}</div>` : '<div class="operations-empty-ok">✓ No autoload or runtime issues found.</div>'}`;
const actionable = updates.candidates.filter(item => item.status === 'update-available');
$('ecosystem-runtime-updates').innerHTML = `<div class="ecosystem-plan-safety"><strong>Chimera Sentinel ${escHtml(beaconArtifact.version)} · ${beaconArtifact.built ? 'build ready' : 'not built'}</strong><span>Paired read-only UDP ${beaconArtifact.port}: HMAC-SHA256, fixed bounded telemetry and visible legacy Beacon 0.1 detection. No writes, persistence, serial number, memory access or command execution.</span><span>${beaconArtifact.sha256 ? `SHA-256 ${escHtml(beaconArtifact.sha256)}` : 'The ELF appears after a complete container build.'}</span><div class="ecosystem-inline-actions">${beaconArtifact.publicUrl ? `<a class="btn btn-sm" href="${escHtml(beaconArtifact.publicUrl)}">Download verified ELF</a>` : ''}<button class="btn btn-sm chimera-sentinel-pair">Explicitly pair Sentinel</button></div></div><div class="ecosystem-plan-safety"><strong>Safe Payload Manager update flow</strong><span>${updates.stats.current} current · ${updates.stats.review} need manual review · ${updates.stats.newer} locally newer. Installing only adds the selected file; starting and restarting remain disabled.</span></div>${actionable.length ? `<div class="ecosystem-update-list">${actionable.map(item => `<article class="ecosystem-update-row" data-runtime-filename="${escHtml(item.latest.filename)}" data-runtime-source="${escHtml(item.latest.sourceId)}"><div class="ecosystem-update-main"><strong>${escHtml(item.installed.name)}</strong><span>${escHtml(item.installed.version || 'unknown')} → ${escHtml(item.latest.version || 'unknown')} · ${escHtml(item.latest.sourceName)}</span><small>${escHtml(item.latest.description || '')}</small></div><div class="ecosystem-update-controls"><button class="btn btn-sm btn-accent ecosystem-runtime-install">Install selected update</button></div></article>`).join('')}</div>` : '<p class="hint">No safely comparable newer Payload Manager versions found.</p>'}${updates.recommendations?.length ? `<h4>Matching extensions · never installed automatically</h4><div class="ecosystem-update-list">${updates.recommendations.map(item => `<div class="ecosystem-service-row"><div><strong>${escHtml(item.name)}</strong><span>${escHtml(item.version || 'unknown version')} · ${escHtml(item.risk)} · ${escHtml(item.reason)}</span></div>${ecosystemStatus(item.risk === 'expert' ? 'warning' : 'unknown', 'Via Payload Manager')}</div>`).join('')}</div>` : ''}`;
}
function etaPatchFromForm() {
const patch = {};
$('ecosystem-etahen-fields').querySelectorAll('[data-eta-key]').forEach(input => { patch[input.dataset.etaKey] = input.type === 'checkbox' ? (input.checked ? 1 : 0) : Number(input.value); });
return patch;
}
function renderEtaDiff(result) {
_ecosystemEtaProposed = result.proposed;
$('ecosystem-etahen-diff').innerHTML = `${(result.warnings || []).map(warning => `<p class="operations-warning">⚠ ${escHtml(warning)}</p>`).join('')}${result.diff?.length ? result.diff.map(item => `<div class="ecosystem-diff-row ${item.risk}"><strong>${escHtml(item.label)}</strong><code>${escHtml(item.before ?? '—')} → ${escHtml(item.after ?? '—')}</code></div>`).join('') : '<div class="operations-empty-ok">✓ No changes.</div>'}`;
$('ecosystem-etahen-apply').disabled = !result.diff?.length;
}
async function loadEtaHen() {
const [config, history] = await Promise.all([window.pork.ecosystemEtaHenGet(), window.pork.ecosystemEtaHenHistory()]);
_ecosystemEtaHash = config.hash;
_ecosystemEtaProposed = null;
$('ecosystem-etahen-raw').value = config.raw;
const preset = $('ecosystem-etahen-preset');
preset.innerHTML = '<option value="">No preset</option>' + Object.entries(config.presets || {}).map(([id, value]) => `<option value="${escHtml(id)}">${escHtml(value.label)}</option>`).join('');
$('ecosystem-etahen-fields').innerHTML = Object.entries(config.schema || {}).map(([key, definition]) => {
const value = config.values?.[key] ?? '0';
return `<label class="ecosystem-config-field ${definition.risk || ''}"><span>${escHtml(definition.label)}${definition.risk ? ` <small>${escHtml(definition.risk)}</small>` : ''}</span>${definition.type === 'bool' ? `<input type="checkbox" data-eta-key="${escHtml(key)}" ${String(value) === '1' ? 'checked' : ''}>` : `<input type="number" data-eta-key="${escHtml(key)}" min="${definition.min}" max="${definition.max}" value="${escHtml(value)}">`}</label>`;
}).join('');
$('ecosystem-etahen-history').innerHTML = history.length ? `<h4>Restore points</h4>${history.slice(0, 20).map(item => `<div class="ecosystem-history-row" data-id="${escHtml(item.id)}"><div><strong>${escHtml(item.label)}</strong><span>${ecosystemDate(item.createdAt)} · ${item.diff?.length || 0} change(s)</span></div><button class="btn btn-sm ecosystem-etahen-restore">Restore</button></div>`).join('')}` : '<p class="hint">No configuration restore points yet.</p>';
$('ecosystem-etahen-diff').innerHTML = '<p class="hint">Choose a preset or change known fields, then create a preview.</p>';
$('ecosystem-etahen-apply').disabled = true;
}
function renderHomebrew(result) {
$('ecosystem-homebrew').innerHTML = result.items?.length ? result.items.map(item => `<article class="ecosystem-homebrew-card" data-id="${escHtml(item.id)}">${item.iconUrl ? `<img src="${escHtml(item.iconUrl)}" alt="">` : '<div class="ecosystem-homebrew-placeholder">HB</div>'}<div><strong>${escHtml(item.name)}</strong><span>${escHtml(item.location)} · ${escHtml(item.executable)}${item.duplicate ? ' · duplicate' : ''}</span><code>${escHtml(item.path)}</code></div><button class="btn btn-sm btn-accent ecosystem-homebrew-launch">Launch</button></article>`).join('') : '<div class="operations-empty"><strong>No homebrew found</strong><span>Start websrv or FTP and run a new scan.</span></div>';
}
function renderPacbrew(packet) {
_pacbrewPacket = packet;
const query = String($('ecosystem-pacbrew-search').value || '').trim().toLowerCase();
const kind = $('ecosystem-pacbrew-kind').value;
const filtered = (packet.packages || []).filter(item => (!query || item.name.toLowerCase().includes(query)) && (kind === 'all' || item.kind === kind));
const counts = Object.fromEntries(['application', 'service', 'library', 'toolchain'].map(type => [type, (packet.packages || []).filter(item => item.kind === type).length]));
$('ecosystem-pacbrew').innerHTML = `
${packet.warning ? `<div class="operations-warning"><strong>${packet.stale ? 'Cached catalog' : 'Fallback transport active'}</strong><p>${escHtml(packet.warning)}</p></div>` : ''}
<div class="ecosystem-component-summary">
<div><strong>${filtered.length}/${packet.packages?.length || 0}</strong><span>Visible recipes</span></div>
<div><strong>${counts.application || 0}</strong><span>Applications</span></div>
<div><strong>${counts.library || 0}</strong><span>Libraries</span></div>
<div><strong>${counts.service || 0}</strong><span>Services</span></div>
</div>
<p class="hint">Release ${escHtml(packet.release?.tag || 'unknown')} · checked ${ecosystemDate(packet.checkedAt)} · ${escHtml(packet.transport || 'cached')} · recipes are never executed</p>
<div class="ecosystem-cheat-list">${filtered.slice(0, 500).map(item => `<article class="ecosystem-cheat-row" data-pacbrew-name="${escHtml(item.name)}"><div><strong>${escHtml(item.name)}</strong><span>${escHtml(item.kind)} · source ${escHtml(String(item.sourceSha || '').slice(0, 12))}</span></div><div class="ecosystem-inline-actions">${ecosystemStatus('unknown', 'Recipe only')}<button class="btn btn-sm ecosystem-pacbrew-inspect">Inspect metadata</button><button class="btn btn-sm ecosystem-open-upstream" data-url="${escHtml(item.upstream)}">Open recipe</button></div></article>`).join('')}</div>`;
}
async function loadHomebrew() {
const [homebrew, pacbrew] = await Promise.all([window.pork.ecosystemHomebrewList(), window.pork.ecosystemPacbrewCatalog()]);
renderHomebrew(homebrew);
renderPacbrew(pacbrew);
}
function renderWebsrvDiscovery(result) {
const services = result.services || [];
$('ecosystem-websrv-discovery').innerHTML = services.length ? `<div class="ecosystem-discovery"><strong>${services.length} mDNS service(s) discovered by websrv</strong><div>${services.map(service => {
const name = service.name || service.instance || service.hostname || service.host || 'Onbekende dienst';
const detail = [service.type, service.address || service.host, service.port].filter(Boolean).join(' · ');
return `<span><b>${escHtml(name)}</b>${detail ? ` ${escHtml(detail)}` : ''}</span>`;
}).join('')}</div></div>` : '<p class="hint">websrv found no mDNS services on the PS5 network.</p>';
}
function renderSources(sources) {
$('ecosystem-sources').innerHTML = sources.length ? sources.map(source => `<div class="ecosystem-source-row" data-id="${escHtml(source.id)}"><div><strong>${escHtml(source.label)}</strong><span>${escHtml(source.protocol.toUpperCase())} · ${escHtml(source.host)}:${source.port}${escHtml(source.path)} · password ${source.passwordConfigured ? 'configured' : 'not configured'}</span></div><div class="ecosystem-inline-actions"><button class="btn btn-sm ecosystem-source-test">Test</button><button class="btn btn-sm ecosystem-source-edit">Edit</button><button class="btn btn-sm btn-danger ecosystem-source-delete">Delete</button></div></div>`).join('') : '<p class="hint">No remote sources configured yet.</p>';
}
function renderEzRemote(packet) {
$('ezremote-status').innerHTML = packet.reachable
? `<strong>Online · ${escHtml(packet.version || 'unknown version')}</strong><span>${escHtml(packet.host)}:${packet.port} · resume after server restart supported · pause/cancel unsupported</span>`
: `<strong>Offline op ${escHtml(packet.host || 'PS5')}:${packet.port || 6701}</strong><span>${escHtml(packet.error || 'Start ezRemote Server op de PS5.')}</span>`;
const rows = packet.downloads || [];
$('ezremote-downloads').innerHTML = rows.length ? `<div class="ecosystem-update-list">${rows.slice(0, 100).map(item => `<div class="ecosystem-service-row"><div><strong>${escHtml(item.path || 'Unknown destination')}</strong><span>${ecosystemBytes(item.transferred)} / ${item.size ? ecosystemBytes(item.size) : '?'} · ${item.percent}%</span></div>${ecosystemStatus(item.state === 'success' ? 'online' : item.state === 'failed' ? 'offline' : 'warning', item.state)}</div>`).join('')}</div>` : '<p class="hint">ezRemote Server reports no downloads yet.</p>';
}
async function loadSources() {
const [sources, status] = await Promise.all([window.pork.ecosystemSources(), window.pork.ezRemoteStatus()]);
renderSources(sources);
const select = $('ezremote-source'); const selected = select.value;
select.innerHTML = '<option value="">Choose a source</option>' + sources.filter(source => source.enabled !== false).map(source => `<option value="${escHtml(source.id)}">${escHtml(source.label)} · ${escHtml(source.protocol.toUpperCase())}</option>`).join('');
if (sources.some(source => source.id === selected)) select.value = selected;
renderEzRemote(status);
}
function renderPlugins(result, catalog = []) {
const summary = `<div class="ecosystem-plan-safety"><strong>${result.items?.length || 0} plugins · ${(result.roots || []).length} available locations</strong><span>Last scan: ${result.scannedAt ? ecosystemDate(result.scannedAt) : 'not yet'}. ${result.skippedUnmountedUsbRoots || 0} unmounted USB locations were skipped without generating errors.</span></div>`;
const installed = result.items?.length
? result.items.map(item => `<div class="ecosystem-plugin-row ${item.duplicate ? 'conflict' : ''}" data-id="${escHtml(item.id)}"><div><strong>${escHtml(item.name)}</strong><span>${escHtml(item.location)} · ${ecosystemBytes(item.size)}${item.duplicate ? ' · duplicate' : ''}${item.effective ? ' · highest active priority' : ''}</span><code>${escHtml(item.path)}</code></div><div class="ecosystem-inline-actions">${ecosystemStatus(item.enabled ? 'compatible' : 'unknown', item.enabled ? 'Enabled' : 'Disabled')}<button class="btn btn-sm ecosystem-plugin-inspect">Inspect</button><button class="btn btn-sm ecosystem-plugin-toggle">${item.enabled ? 'Disable' : 'Enable'}</button></div></div>`).join('')
: '<div class="operations-empty"><strong>No installed plugins found</strong><span>Choose a reviewed source below or upload a local ELF/PRX through the guarded installer.</span></div>';
const reviewed = catalog.length
? `<section class="ecosystem-component-group"><header><div><h4>Reviewed plugin sources</h4><span>Discovery only · installation remains explicit</span></div></header><div class="ecosystem-component-list">${catalog.map(item => `<article class="ecosystem-component ${item.risk === 'expert' ? 'expert' : ''}"><div><strong>${escHtml(item.name)}</strong><span>${escHtml(item.platform)} · ${escHtml(item.trust)} · ${escHtml(item.release?.tag || 'version unknown')}</span></div><p>${escHtml(item.compatibility?.reason || 'Review the upstream release before installing.')}</p><button class="btn btn-sm ecosystem-open-upstream" data-url="${escHtml(item.release?.url || item.upstream)}">Review upstream</button></article>`).join('')}</div></section>`
: '<p class="hint">No reviewed plugin sources are present in the component catalog.</p>';
$('ecosystem-plugins').innerHTML = `${summary}<h4>Installed plugins</h4>${installed}${reviewed}`;
}
async function loadPlugins() {
const [plugins, components] = await Promise.all([
window.pork.ecosystemPlugins(),
window.pork.ecosystemComponents().catch(() => ({ components: [] })),
]);
const catalog = (components.components || []).filter(item => /plugin/i.test(`${item.kind} ${item.name}`));
renderPlugins(plugins, catalog);
}
async function loadCheatsV2() {
const gameContext = consumeGameWorkspaceContext('cheats');
if (gameContext) { $('ecosystem-cheats-search').value = gameContext; _ecosystemCheatOffset = 0; }
const request = ++_ecosystemCheatRequest;
const filters = { query: $('ecosystem-cheats-search').value, platform: $('ecosystem-cheats-platform').value, kind: $('ecosystem-cheats-kind').value, installedOnly: $('ecosystem-cheats-installed').checked, limit: ECOSYSTEM_CHEAT_PAGE_SIZE, offset: _ecosystemCheatOffset };
const result = await window.pork.ecosystemCheats(filters);
if (request !== _ecosystemCheatRequest) return;
_ecosystemCheatOffset = result.offset || 0;
const first = result.total ? _ecosystemCheatOffset + 1 : 0;
const last = _ecosystemCheatOffset + result.items.length;
const list = result.items.length
? `<div class="ecosystem-cheat-list">${result.items.map(item => {
const match = item.compatibility === 'exact'
? ecosystemStatus('compatible', 'Exact version')
: item.compatibility === 'title-match'
? ecosystemStatus('compatible', 'Title match')
: item.compatibility === 'version-mismatch'
? ecosystemStatus('warning', 'Version mismatch')
: item.compatibility === 'version-unknown'
? ecosystemStatus('unknown', 'Version unknown')
: ecosystemStatus('unknown', 'Not local');
return `<article class="ecosystem-cheat-row ${item.conflict ? 'conflict' : ''}" data-key="${escHtml(item.key)}"><div><strong>${escHtml(item.installedTitle || item.title)}</strong><span>${escHtml(item.gameId || 'Unlinked')} · ${escHtml(item.platform)} · ${escHtml(item.kind)} · ${escHtml(item.format.toUpperCase())} · ${escHtml(item.sourceName)}</span><small>${item.version ? `Target version ${escHtml(item.version)}` : 'No target version supplied'}${item.installedVersion ? ` · installed ${escHtml(item.installedVersion)}` : ''}${item.conflict ? ' · multiple formats available' : ''}</small></div><div class="ecosystem-inline-actions">${match}<button class="btn btn-sm ecosystem-open-upstream" data-url="${escHtml(item.upstream)}">Source</button>${item.deployable ? '<button class="btn btn-sm btn-accent ecosystem-cheat-deploy">Install</button>' : `<span class="hint" title="${escHtml(item.note)}">View only</span>`}</div></article>`;
}).join('')}</div>`
: '<div class="operations-empty"><strong>No cheats or patches match</strong><span>Adjust the filters or synchronize the reviewed sources to refresh the catalog.</span></div>';
const compatibility = result.compatibility || {};
$('ecosystem-cheats').innerHTML = `<div class="ecosystem-page-summary"><p class="hint">${first}–${last} of ${result.total} results · ${Number(compatibility.exact || 0) + Number(compatibility.titleMatch || 0)} recommended · ${Number(compatibility.versionMismatch || 0)} mismatched · synced ${ecosystemDate(result.syncedAt)}</p><div class="ecosystem-inline-actions"><button class="btn btn-sm ecosystem-cheat-page" data-offset="${result.previousOffset ?? ''}" ${result.previousOffset == null ? 'disabled' : ''}>Previous</button><button class="btn btn-sm ecosystem-cheat-page" data-offset="${result.nextOffset ?? ''}" ${result.nextOffset == null ? 'disabled' : ''}>Next</button></div></div>${(result.errors || []).map(error => `<p class="operations-warning">${escHtml(error.sourceId)}: ${escHtml(error.error)}</p>`).join('')}${list}`;
}
function renderRemoteSaves(result) {
const groups = new Map();
for (const save of result.saves || []) { const id = String(save.title_id || 'UNKNOWN'); if (!groups.has(id)) groups.set(id, { titleId: id, title: save.title_name || id, saves: [] }); groups.get(id).saves.push(save); }
$('ecosystem-remote-saves').innerHTML = groups.size ? [...groups.values()].map(group => `<div class="ecosystem-save-row" data-title-id="${escHtml(group.titleId)}"><div><strong>${escHtml(group.title)}</strong><span>${escHtml(group.titleId)} · ${group.saves.length} save(s) · ${group.saves.some(save => save.type === 'ps5') ? 'PS5' : 'PS4'}</span></div><button class="btn btn-sm btn-accent ecosystem-save-snapshot">Snapshot</button></div>`).join('') : '<p class="hint">No saves found.</p>';
}
async function loadSaveVault() {
const gameContext = consumeGameWorkspaceContext('saves');
const [vaultResult, capabilitiesResult, gamesResult] = await Promise.allSettled([
window.pork.ecosystemSaveVault(),
window.pork.ecosystemSaveRestoreCapabilities(),
window.pork.listGames({}),
]);
const allSnapshots = vaultResult.status === 'fulfilled' && Array.isArray(vaultResult.value) ? vaultResult.value : [];
const capabilities = capabilitiesResult.status === 'fulfilled' && capabilitiesResult.value
? capabilitiesResult.value
: { automatedRestore: false, error: capabilitiesResult.reason?.message || 'Restore capabilities could not be verified.' };
const snapshots = gameContext ? allSnapshots.filter(item => item.titleId === gameContext) : allSnapshots;
const gamesById = new Map((gamesResult.status === 'fulfilled' ? gamesResult.value : []).map(game => [game.game_id, game]));
_ecosystemSaveCapabilities = capabilities;
$('ecosystem-save-restore-safety').innerHTML = capabilities.automatedRestore
? '<strong>Protected PS5 restore available</strong><span>PPSA archives only. Mandatory SHA-256 and CRC dry run, target-user validation, pre-restore snapshot, exact confirmation and byte-for-byte post-check. Resign, forced import and automatic rollback remain disabled.</span>'
: `<strong>Restore remains blocked</strong><span>${escHtml(localizedEcosystemError(capabilities.reason || capabilities.error || 'Garlic does not prove the required import capabilities.'))}</span>`;
const vaultFailure = vaultResult.status === 'rejected'
? `<div class="operations-warning"><strong>Local vault unavailable</strong><p>${escHtml(localizedEcosystemError(vaultResult.reason?.message || 'The local snapshot index could not be loaded.'))}</p></div>`
: '';
$('ecosystem-save-vault').innerHTML = `${vaultFailure}${gameContext ? `<div class="ecosystem-plan-safety"><strong>Game workspace ${escHtml(gameContext)}</strong><span>Only snapshots for this game are shown. Refresh the page to see the complete Save Vault.</span></div>` : ''}${snapshots.length ? snapshots.map(item => {
const game = gamesById.get(item.titleId);
const title = game?.prospero_name || game?.title || item.titleId;
return `<div class="ecosystem-vault-row" data-id="${escHtml(item.id)}" data-title-id="${escHtml(item.titleId)}">${game?.prospero_icon_url ? `<img class="ecosystem-vault-cover" src="${escHtml(game.prospero_icon_url)}" alt="">` : ''}<div><strong>${escHtml(title)}</strong><span>${escHtml(item.titleId)} · ${ecosystemDate(item.createdAt)} · ${ecosystemBytes(item.bytes)}${item.purpose ? ` · ${escHtml(item.purpose)}` : ''}</span><code>${escHtml(String(item.sha256).slice(0, 18))}…</code></div><div class="ecosystem-inline-actions"><button class="btn btn-sm ecosystem-save-verify">Verify</button><button class="btn btn-sm btn-accent ecosystem-save-restore" ${capabilities.automatedRestore && /^PPSA\d{5}$/.test(item.titleId) ? '' : 'disabled'}>Restore plan</button><button class="btn btn-sm ecosystem-save-download">Download</button><button class="btn btn-sm btn-danger ecosystem-save-delete">Delete</button></div></div>`;
}).join('') : '<div class="operations-empty"><strong>No snapshots yet</strong><span>Create a verified snapshot after console saves have been discovered.</span></div>'}`;
}
function renderTimeline(result) {
const state = result.state || {};
const options = result.options || {};
$('ecosystem-timeline-persist').checked = options.enabled === true;
$('ecosystem-timeline-retention').value = options.retentionDays || 14;
$('ecosystem-timeline-max').value = options.maxEvents || 5000;
$('ecosystem-timeline-state').innerHTML = `<p class="hint">${state.connected ? `Connected on port ${state.port} since ${ecosystemDate(state.startedAt)}` : state.error ? `Not connected: ${escHtml(state.error)}` : 'Not connected.'} · ${result.filtered || 0}/${result.total || 0} events · persistence ${options.enabled ? 'on' : 'off'}</p>`;
$('ecosystem-timeline-events').innerHTML = (result.events || []).slice().reverse().map(event => `<div class="ecosystem-log-row ${escHtml(event.level)}"><time>${new Date(event.at).toLocaleTimeString()}</time><strong>${escHtml(event.source)}</strong><span>${escHtml(event.message)}</span></div>`).join('') || '<p class="hint">No events yet.</p>';
}
function renderGameUpdateJobs(jobs) {
$('game-updates-jobs').innerHTML = jobs.length ? jobs.map(job => `<div class="ecosystem-update-row" data-id="${escHtml(job.id)}"><div class="ecosystem-update-main"><strong>${escHtml(job.manifest.gameId)} · ${escHtml(job.manifest.filename)}</strong><span>${escHtml(job.status)} · ${Number(job.progress || 0)}% · ${ecosystemBytes(job.bytesDownloaded || job.manifest.originalFileSize)}</span>${job.error ? `<small class="operations-warning">${escHtml(job.error)}</small>` : job.remotePath ? `<code>${escHtml(job.remotePath)}</code>` : job.localPath ? `<code>${escHtml(job.localPath)}</code>` : ''}</div><div class="ecosystem-inline-actions">${ecosystemStatus(job.status === 'completed' ? 'compatible' : job.status === 'failed' ? 'warning' : 'unknown', job.status)}${['queued', 'downloading', 'verifying'].includes(job.status) ? '<button class="btn btn-sm game-update-cancel">Cancel</button>' : ''}${job.status === 'completed' ? '<button class="btn btn-sm game-update-handoff">Create HTTPS link</button><button class="btn btn-sm btn-accent game-update-stage">Stage for PS5</button>' : ''}${!['queued', 'downloading', 'verifying', 'staging'].includes(job.status) ? '<button class="btn btn-sm btn-danger game-update-delete">Delete</button>' : ''}</div></div>`).join('') : '<p class="hint">No game update jobs yet.</p>';
}
async function loadGameUpdates() {
const [catalog, jobs] = await Promise.all([window.pork.gameUpdatesCatalog(), window.pork.gameUpdatesJobs()]);
const discovered = catalog.games.filter(game => game.discovery?.status === 'available').length;
const updateStateLabel = state => window.t(`ecosystem.gameUpdates.state.${state}`, state);
const evidenceRows = catalog.games.map(game => {
const locations = (game.locations || []).map(location => {
const observed = location.observedAt ? new Date(location.observedAt).toLocaleString() : window.t('ecosystem.gameUpdates.evidenceUnknown', 'evidence time unknown');
const label = location.volumeLabel || location.location || location.volumeId || window.t('ecosystem.gameUpdates.locationUnknown', 'unknown location');
return `<small>${escHtml(label)} · ${escHtml(location.version || '?')} · ${escHtml(location.source || 'unknown')} (${escHtml(location.confidence || 'unknown')}) · ${escHtml(observed)} · ${escHtml(updateStateLabel(location.status))}</small>`;
}).join('');
return `<div class="ecosystem-service-row"><div><strong>${escHtml(game.title)}</strong><span>${escHtml(game.gameId)} · ${escHtml(game.latestVersion || '?')} · ${escHtml(game.sourceEvidence?.source || 'unknown')}${game.discovery?.reason ? ` · ${escHtml(game.discovery.reason)}` : ''}</span>${locations || `<small>${escHtml(window.t('ecosystem.gameUpdates.noConfirmedLocations', 'No confirmed console locations'))}</small>`}</div>${ecosystemStatus(game.updateAvailable ? 'warning' : game.status === 'current' ? 'compatible' : 'unknown', updateStateLabel(game.status))}</div>`;
}).join('');
$('game-updates-summary').innerHTML = `<div class="ecosystem-metrics">${[[window.t('ecosystem.gameUpdates.gameCount', 'Games'), catalog.stats.total], [window.t('ecosystem.gameUpdates.officialManifests', 'Official manifests'), discovered], [window.t('ecosystem.gameUpdates.available', 'Possible updates'), catalog.stats.updates]].map(([label, value]) => `<div class="operations-metric"><strong>${escHtml(value)}</strong><span>${escHtml(label)}</span></div>`).join('')}</div><details class="ecosystem-raw-config"><summary>${escHtml(window.t('ecosystem.gameUpdates.discoveryEvidence', 'Library discovery evidence'))} · ${catalog.games.length}</summary><div class="ecosystem-update-list">${evidenceRows}</div></details>`;
$('game-updates-game').innerHTML = `<option value="">${escHtml(window.t('ecosystem.gameUpdates.selectGame', 'Select a library game'))}</option>` + catalog.games.map(game => `<option value="${escHtml(game.gameId)}">${escHtml(game.gameId)} · ${escHtml(game.title)} · ${escHtml(game.discovery?.status || 'not checked')}</option>`).join('');
renderGameUpdateJobs(jobs);
}
function renderGameUpdatePlan(plan) {
_gameUpdatePlan = plan;
$('game-updates-plan').innerHTML = `<strong>${escHtml(plan.manifest.gameId)} · ${escHtml(plan.manifest.filename)}</strong><span>${ecosystemBytes(plan.manifest.originalFileSize)} · ${plan.manifest.pieces.length} part(s) · free space ${ecosystemBytes(plan.availableBytes)} · ${plan.manifest.packageDigest ? 'complete SHA-256 available' : 'no complete SHA-256 in manifest'}</span><button id="game-updates-download-confirm" class="btn btn-accent" ${plan.enoughSpace ? '' : 'disabled'}>Start verified download</button>`;
$('game-updates-download-confirm')?.addEventListener('click', async click => {
if (!await showConfirm('Download this official Sony package to Chimera backup storage? Every part and the complete package will be verified. Nothing is installed on the PS5.')) return;
try { await ecosystemBusy(click.currentTarget, () => window.pork.gameUpdatesDownload(plan.token)); _gameUpdatePlan = null; showToast('Game update job started.', 'ok'); await loadGameUpdates(); }
catch (error) { showToast(error.message, 'error', 10000); }
});
}
function renderReadonlyFiles(targetId, packet, kind) {
const rows = packet.roots?.length ? packet.roots : packet.items || [];
$(targetId).innerHTML = rows.length ? rows.map(item => `<div class="ecosystem-service-row websrv-file-row" data-path="${escHtml(item.path)}" data-directory="${item.isDirectory ? '1' : '0'}"><div><strong>${item.isDirectory ? '📁' : '📄'} ${escHtml(item.name)}</strong><span>${item.isDirectory ? escHtml(item.mode || 'folder') : ecosystemBytes(item.size)}${item.mtime ? ` · ${ecosystemDate(item.mtime * 1000)}` : ''}</span></div><button class="btn btn-sm ${item.isDirectory ? 'websrv-file-open' : 'websrv-file-download'}">${item.isDirectory ? 'Open' : 'Download'}</button></div>`).join('') : '<p class="hint">This folder is empty.</p>';
const pathTarget = kind === 'smb' ? 'websrv-smb-path' : 'websrv-files-path';
$(pathTarget).innerHTML = `<strong>${escHtml(packet.path || (kind === 'smb' ? 'SMB-root' : '/'))}</strong><span>Read-only · ${rows.length} item(s)</span>${packet.parent !== null && packet.parent !== undefined ? `<button class="btn btn-sm ${kind === 'smb' ? 'websrv-smb-back' : 'websrv-files-back'}" data-parent="${escHtml(packet.parent)}">Omhoog</button>` : ''}`;
}
async function loadWebsrvFiles(remotePath = _websrvFsPath) {
const packet = await window.pork.ecosystemWebsrvFiles(remotePath);
_websrvFsPath = packet.path;
renderReadonlyFiles('websrv-files-list', packet, 'fs');
}
async function loadWebsrvSmb(smbPath = _websrvSmbPath) {
const sourceId = $('websrv-smb-source').value;
if (!sourceId) return;
const packet = await window.pork.ecosystemWebsrvSmb(sourceId, smbPath);
_websrvSmbPath = packet.path;
renderReadonlyFiles('websrv-smb-list', packet, 'smb');
}
async function loadFilesTab() {
const sources = (await window.pork.ecosystemSources()).filter(source => source.protocol === 'smb');
const select = $('websrv-smb-source'); const selected = select.value;
select.innerHTML = '<option value="">Choose a source</option>' + sources.map(source => `<option value="${escHtml(source.id)}">${escHtml(source.label)} · ${escHtml(source.host)}</option>`).join('');
if (sources.some(source => source.id === selected)) select.value = selected;
if (!sources.length) {
$('websrv-smb-path').innerHTML = '<strong>No SMB sources configured</strong><span>Add a reviewed network source before browsing it through the PS5.</span>';
$('websrv-smb-list').innerHTML = '<div class="operations-empty"><strong>Connect a storage source</strong><span>Sources are managed centrally so arbitrary hosts never appear here.</span><button class="btn btn-accent" data-route="settings/sources">Configure Sources</button></div>';
}
await loadWebsrvFiles(_websrvFsPath);
}
async function loadLiveSession() {
const packet = await window.pork.ecosystemLiveSession();
$('ecosystem-live-session').innerHTML = packet.online ? `<div class="ecosystem-live-hero"><div><span class="operations-service-dot online"></span><div><strong>Live telemetry available</strong><small>${escHtml(packet.branding || 'ps5debug-NG')} · ${escHtml(packet.host)}:${escHtml(packet.port)} · checked ${ecosystemDate(packet.checkedAt)}</small></div></div>${ecosystemStatus('online', 'Read-only')}</div><div class="ecosystem-metrics ecosystem-live-metrics">${[['Protocol', packet.protocol || '?'], ['Firmware', packet.firmware || '?'], ['Platform', packet.platform === 5 ? 'PS5' : packet.platform || '?']].map(([label, value]) => `<div class="operations-metric"><strong>${escHtml(value)}</strong><span>${escHtml(label)}</span></div>`).join('')}</div><div class="ecosystem-live-game">${packet.foreground ? `<div><span class="ecosystem-section-kicker">Foreground application</span><h3>${escHtml(packet.foreground.game?.title || packet.foreground.titleId)}</h3><p>${escHtml(packet.foreground.titleId)} · v${escHtml(packet.foreground.version || '?')} · PID ${escHtml(packet.foreground.pid)}</p><code>${escHtml(packet.foreground.contentId || packet.foreground.processName)}</code></div>` : '<div><span class="ecosystem-section-kicker">Foreground application</span><h3>No game active</h3><p class="hint">The console service is online and ready to report the next foreground title.</p></div>'}</div><div class="operations-empty-ok">✓ Only five reviewed read-only commands are registered. Memory, attach, kernel, notification and reboot are not exposed.</div>` : `<div class="ecosystem-live-prerequisite"><span class="done">1</span><div><strong>ps5debug-NG 1.3.0 installed</strong><small>Verified official release staged through Payload Manager</small></div><span>2</span><div><strong>Start payload when needed</strong><small>Port ${escHtml(packet.port)} must be reachable</small></div><span>3</span><div><strong>Read session</strong><small>Use the button above to retry</small></div></div><div class="operations-warning"><strong>Live telemetry unavailable</strong><p>${escHtml(packet.error || 'Service offline.')}</p></div>`;
}
function remotePlaySelectedWorker(packet) {
return (packet.workers || []).find(worker => worker.id === ($('remote-play-worker')?.value || packet.selectedWorkerId)) || null;
}
function renderRemotePlay(packet, companion) {
_remotePlayPacket = packet;
const discovery = packet.lastDiscovery || {};
const workers = packet.workers || [];
const selectedId = $('remote-play-worker')?.value || packet.selectedWorkerId || '';
const selected = workers.find(worker => worker.id === selectedId) || workers[0] || null;
const health = selected?.streamHealth || null;
const clientName = packet.clientNickname || discovery.console?.name || '';
const registered = Boolean(selected?.registeredHosts?.includes(clientName));
const consoleReady = discovery.state === 'ready';
const consoleStandby = discovery.state === 'standby';
const checks = [
{ label: 'PS5', ok: consoleReady, partial: consoleStandby, value: consoleReady ? window.t('remotePlay.state.ready', 'Ready') : consoleStandby ? window.t('remotePlay.state.standby', 'Rest mode') : discovery.online ? discovery.state : window.t('remotePlay.state.notDiscovered', 'Not discovered') },
{ label: 'Worker', ok: selected?.online, value: selected?.online ? `${selected.label} online` : selected ? 'Offline' : window.t('remotePlay.state.notPaired', 'Not paired') },
{ label: 'chiaki-ng', ok: selected?.client?.installed, value: selected?.client?.installed ? `${selected.client.version || window.t('remotePlay.state.installed', 'Installed')}${selected.client.managed ? ` · ${window.t('remotePlay.state.managed', 'Chimera-managed')}` : ''}` : window.t('remotePlay.state.notFound', 'Not found') },
{ label: window.t('remotePlay.registration', 'Registration'), ok: registered, value: registered ? clientName : clientName ? window.tf('remotePlay.registrationMissing', '{name} is not registered locally', { name: clientName }) : window.t('remotePlay.consoleNameMissing', 'Console name missing') },
];
$('remote-play-readiness').innerHTML = checks.map(check => `<div class="remote-play-check ${check.ok ? 'ready' : check.partial ? 'standby' : 'blocked'}"><span class="remote-play-check-dot"></span><div><strong>${escHtml(check.label)}</strong><small>${escHtml(check.value)}</small></div></div>`).join('');
if (document.activeElement !== $('remote-play-nickname')) $('remote-play-nickname').value = packet.nickname || '';
if (document.activeElement !== $('remote-play-client-nickname')) $('remote-play-client-nickname').value = clientName;
$('remote-play-key').value = '';
$('remote-play-display-mode').value = packet.displayMode || 'fullscreen';
$('remote-play-worker').innerHTML = workers.length ? workers.map(worker => `<option value="${escHtml(worker.id)}">${escHtml(worker.label)} · ${worker.online ? 'online' : 'offline'}</option>`).join('') : `<option value="">${escHtml(window.t('remotePlay.worker.none', 'No worker yet'))}</option>`;
$('remote-play-worker').value = selected?.id || '';
$('remote-play-status').innerHTML = `<strong>${escHtml(packet.registrationKeyConfigured ? window.t('remotePlay.profile.secured', 'Wake profile secured') : window.t('remotePlay.profile.keyMissing', 'Wake key missing'))}</strong><span>${escHtml(packet.nickname)} · ${escHtml(packet.host || window.t('remotePlay.profile.noHost', 'no PS5 host'))} · UDP ${packet.port} · ${escHtml(window.t('remotePlay.profile.sessionPorts', 'session ports'))} ${(packet.sessionPorts || []).join(', ')}</span><span>${escHtml(window.t('remotePlay.profile.registrationKey', 'registration key'))} ${escHtml(packet.registrationKeyConfigured ? window.t('remotePlay.profile.keyEncrypted', 'AES-256-GCM encrypted and never returned') : window.t('remotePlay.profile.keyWakeOnly', 'needed only to wake from rest mode'))}</span>`;
$('remote-play-workers').innerHTML = workers.length ? workers.map(worker => `<div class="ecosystem-service-row remote-play-worker-row" data-remote-worker-id="${escHtml(worker.id)}"><div><strong>${escHtml(worker.label)}</strong><span>${worker.online ? `${escHtml(worker.hostname || worker.platform || 'worker')} · ${worker.client?.installed ? `chiaki-ng ${escHtml(worker.client.version || 'found')}${worker.client.managed ? ' · managed' : ''}` : 'chiaki-ng missing'}` : worker.enrollmentPending ? 'Waiting for installation and first pairing' : `Offline${worker.lastSeenAt ? ` · last ${ecosystemDate(worker.lastSeenAt)}` : ''}`}</span></div><div class="ecosystem-inline-actions">${ecosystemStatus(worker.online ? 'online' : worker.enrollmentPending ? 'unknown' : 'offline', worker.online ? 'Online' : worker.enrollmentPending ? 'Install' : 'Offline')}<button class="btn btn-sm btn-danger remote-play-worker-remove" type="button">Delete</button></div></div>`).join('') : '<div class="operations-warning"><strong>No local worker yet</strong><p>Download the package above on the device where you want to play.</p></div>';
const healthLabel = health?.status === 'healthy' ? window.t('remotePlay.health.healthy', 'Healthy') : health?.status === 'degraded' ? window.t('remotePlay.health.degraded', 'Degraded') : window.t('remotePlay.health.attention', 'Attention');
const healthTone = health?.status === 'healthy' ? 'online' : health?.status === 'degraded' ? 'error' : 'warning';
const healthMarkup = health ? `<div class="ecosystem-plan-safety"><strong>Last stream · ${escHtml(healthLabel)}</strong><span>${escHtml(health.resolution || 'resolution unknown')} · ${escHtml(health.decoder || 'decoder unknown')}${health.rttMs != null ? ` · RTT ${escHtml(health.rttMs)} ms` : ''} · ${ecosystemDate(health.checkedAt)}</span><span>Packet/FEC errors ${escHtml(health.packetErrors)} · duplicates ${escHtml(health.duplicateUnits)} · decoder buffer drops ${escHtml(health.decoderDrops)}</span>${ecosystemStatus(healthTone, healthLabel)}</div>` : '';
const workflowCopy = {
'Console ontdekken': [window.t('remotePlay.workflow.discovery', 'Discover console'), window.t('remotePlay.workflow.discoveryDetail', 'LAN discovery through the upstream PS5 protocol on UDP 9302.')],
Wakeprofiel: [window.t('remotePlay.workflow.wakeProfile', 'Wake profile'), window.t('remotePlay.workflow.wakeDetail', 'Wake uses only the encrypted rp_regist_key.')],
'Lokale worker': [window.t('remotePlay.workflow.worker', 'Local worker'), window.t('remotePlay.workflow.workerDetail', 'The HMAC-paired worker manages and starts only a hash-verified chiaki-ng client.')],
'chiaki-ng geregistreerd': [window.t('remotePlay.workflow.registered', 'chiaki-ng registered'), window.t('remotePlay.workflow.registeredDetail', 'The PSN Account ID and Link Device PIN remain only in the local chiaki-ng configuration.')],
};
$('remote-play-companion').innerHTML = `<div class="ecosystem-update-list">${(companion.workflow || []).map(step => {
const translated = workflowCopy[step.id] || [step.id, step.detail];
return `<div class="ecosystem-service-row"><div><strong>${escHtml(translated[0])}</strong><span>${escHtml(translated[1])}</span></div>${ecosystemStatus(step.ready ? 'online' : 'unknown', step.ready ? window.t('remotePlay.state.ready', 'Ready') : window.t('remotePlay.state.configure', 'Configure'))}</div>`;
}).join('')}</div>${healthMarkup}<div class="ecosystem-plan-safety"><strong>${escHtml(window.t('remotePlay.native.title', 'Why native?'))}</strong><span>${escHtml(window.t('remotePlay.native.reason', 'Video, audio, controller and haptics run natively on the device where chiaki-ng and the controller are present; Chimera orchestrates and monitors the session.'))}</span><span>${escHtml(window.t('remotePlay.native.detail', 'The worker has no shell, receives no PS5 registration key and can perform only fixed chiaki-ng actions.'))}</span></div>`;
const jobStatuses = { queued: window.t('remotePlay.jobs.queued', 'Queued'), dispatched: window.t('remotePlay.jobs.dispatched', 'Dispatched…'), completed: window.t('remotePlay.jobs.completed', 'Completed'), failed: window.t('remotePlay.jobs.failed', 'Failed'), expired: window.t('remotePlay.jobs.expired', 'Expired'), cancelled: window.t('remotePlay.jobs.cancelled', 'Cancelled') };
$('remote-play-jobs').innerHTML = packet.jobs?.length ? `<div class="ecosystem-update-list">${packet.jobs.map(job => `<div class="ecosystem-service-row"><div><strong>${escHtml(job.action)}</strong><span>${ecosystemDate(job.createdAt)}${job.result?.message ? ` · ${escHtml(job.result.message)}` : job.error ? ` · ${escHtml(job.error)}` : ''}</span></div>${ecosystemStatus(job.status === 'completed' ? 'online' : job.status === 'failed' ? 'error' : job.status === 'expired' ? 'offline' : 'unknown', jobStatuses[job.status] || job.status)}</div>`).join('')}</div>` : `<p class="hint">${escHtml(window.t('remotePlay.sessions.empty', 'No Remote Play action has run yet.'))}</p>`;
const blockers = [];
if (!selected?.online) blockers.push('worker offline');
if (!selected?.client?.installed) blockers.push(window.t('remotePlay.blocker.clientMissing', 'chiaki-ng missing'));
if (!registered) blockers.push(window.t('remotePlay.blocker.notRegistered', 'console not registered locally'));
if (!consoleReady && !(consoleStandby && packet.registrationKeyConfigured)) blockers.push(window.t('remotePlay.blocker.notReady', 'PS5 not ready'));
$('remote-play-start').disabled = blockers.length > 0;
$('remote-play-start-hint').textContent = blockers.length
? window.tf('remotePlay.blocker.prefix', 'Still needed: {items}.', { items: blockers.join(' · ') })
: consoleStandby
? window.t('remotePlay.ready.wakeFirst', 'Chimera will confirm wake first and then start the local stream.')
: window.t('remotePlay.ready.start', 'All checks passed; the stream opens locally on the selected worker.');
$('remote-play-wake').disabled = !packet.registrationKeyConfigured || consoleReady;
$('remote-play-diagnose').disabled = !selected?.online;
$('remote-play-open-client').disabled = !selected?.online || !selected?.client?.installed;
$('remote-play-install-client').disabled = !selected?.online || selected?.client?.installed;
$('remote-play-update-client').disabled = !selected?.online || !selected?.client?.installed;
$('remote-play-stop').disabled = !selected?.online || !selected?.session?.active;
}
async function loadRemotePlay({ discover = false, schedule = true } = {}) {
if (discover) await window.pork.remotePlayDiscover().catch(() => null);
const [packet, companion] = await Promise.all([window.pork.remotePlayStatus(), window.pork.remotePlayCompanion()]);
renderRemotePlay(packet, companion);
clearTimeout(_remotePlayPollTimer);
if (schedule && _ecosystemActiveTab === 'remote-play') _remotePlayPollTimer = setTimeout(() => loadRemotePlay({ schedule: true }).catch(error => showToast(error.message, 'error')), 3_000);
}
async function loadItemzflow() {
_itemzflowPacket = await window.pork.ecosystemItemzflowStatus();
const packet = _itemzflowPacket;
$('itemzflow-summary').innerHTML = `<div class="ecosystem-metrics">${[['Logs', `${packet.health?.available || 0}/${packet.logs?.length || 0}`], ['Errors', packet.health?.errors || 0], ['Warnings', packet.health?.warnings || 0], ['Games', packet.games?.installed || 0], ['Progress', packet.latestProgress?.progress != null ? `${packet.latestProgress.progress}%` : 'None active']].map(([label, value]) => `<div class="operations-metric"><strong>${escHtml(value)}</strong><span>${escHtml(label)}</span></div>`).join('')}</div>${packet.error ? `<p class="operations-warning">${escHtml(localizedEcosystemError(packet.error))}</p>` : `<p class="hint">${packet.lines} lines from ${packet.health?.available || 0} logs · ${ecosystemDate(packet.checkedAt)}</p>`}<div class="ecosystem-update-list">${(packet.logs || []).map(log => `<div class="ecosystem-service-row"><div><strong>${escHtml(log.label)}</strong><span>${escHtml(log.path)} · ${log.lines || 0} lines</span></div>${ecosystemStatus(log.online ? (log.errorCount ? 'warning' : 'online') : 'offline', log.online ? `${log.errorCount || 0} errors · ${log.warningCount || 0} warnings` : 'Not present')}</div>`).join('')}</div>${packet.nfsSources?.length ? `<div class="ecosystem-plan-safety"><strong>NFS profiles</strong><span>${packet.nfsSources.map(source => `${escHtml(source.label)} · ${escHtml(source.host)}:${source.port}${escHtml(source.path)}`).join(' · ')}</span></div>` : ''}`;
const selected = $('itemzflow-log-select').value;
$('itemzflow-log-select').innerHTML = '<option value="all">All logs</option>' + (packet.logs || []).map(log => `<option value="${escHtml(log.id)}">${escHtml(log.label)}${log.online ? '' : ' · missing'}</option>`).join('');
if ((packet.logs || []).some(log => log.id === selected)) $('itemzflow-log-select').value = selected;
renderItemzflowEvents();
}
function renderItemzflowEvents() {
if (!_itemzflowPacket) return;
const source = $('itemzflow-log-select').value;
const level = $('itemzflow-level').value;
const jobsOnly = $('itemzflow-jobs-only').checked;
const pool = jobsOnly ? _itemzflowPacket.events || [] : _itemzflowPacket.recentLogs || [];
const events = pool.filter(event => (source === 'all' || event.source === source) && (level === 'all' || event.level === level));
$('itemzflow-events').innerHTML = events.map(event => `<div class="ecosystem-log-row ${escHtml(event.level)}"><strong>${escHtml(event.sourceLabel || event.source)}</strong><span><b>${escHtml(event.type)}</b> · ${escHtml(event.message)}</span>${event.progress != null ? `<code>${event.progress}%</code>` : `<code>${escHtml(event.level)}</code>`}</div>`).join('') || '<p class="hint">No events match these filters.</p>';
}
async function loadLinuxLab() {
const packet = await window.pork.ecosystemLinuxLab();
const state = packet.status === 'compatible' ? 'Firmware explicitly supported' : packet.status === 'incompatible' ? 'Firmware unsupported' : 'Firmware unknown';
$('ecosystem-linux-summary').innerHTML = `<div class="ecosystem-metrics">${[['Status', state], ['Profile', packet.firmware || 'Not entered'], ['Models', packet.supportedModels.join(' + ')], ['Execution', 'Blocked']].map(([label, value]) => `<div class="operations-metric"><strong>${escHtml(value)}</strong><span>${escHtml(label)}</span></div>`).join('')}</div><p class="hint">Source reviewed ${escHtml(packet.sourceReviewedAt)} · supported firmware: ${packet.supportedFirmwares.map(escHtml).join(', ')}</p>`;
$('ecosystem-linux-checks').innerHTML = (packet.requirements || []).map(check => `<div class="ecosystem-service-row"><div><strong>${escHtml(check.label)}</strong><span>${escHtml(check.detail)}</span></div>${ecosystemStatus(check.status, check.status === 'compatible' ? 'Satisfied' : check.status === 'incompatible' ? 'Incompatible' : check.status === 'warning' ? 'Disable manually' : 'Check manually')}</div>`).join('');
$('ecosystem-linux-components').innerHTML = (packet.components || []).map(component => `<div class="ecosystem-source-row"><div><strong>${escHtml(component.name)}</strong><span>${escHtml(component.release?.tag || 'release unknown')} · ${escHtml(component.risk)}</span></div><button class="btn btn-sm ecosystem-open-upstream" data-url="${escHtml(component.upstream)}">Upstream</button></div>`).join('');
$('ecosystem-linux-policy').innerHTML = '<strong>All dangerous actions are intentionally absent</strong><span>No download, payload sender, exploit, boot, block-device write or automatic HEN/kstuff change. Use the official upstream guide outside Chimera and only after every preflight condition has been verified manually.</span>';
}
async function loadTimeline() { renderTimeline(await window.pork.ecosystemTimeline({ search: $('ecosystem-timeline-search')?.value || '', level: $('ecosystem-timeline-level')?.value || '', limit: 1000 })); }
let _platformLabState = { artifacts: [], launchPlan: null };
function platformLabText(key, fallback) { return window.t(`platformLab.${key}`, fallback); }
function platformLabError(id, error) {
const target = $(id);
if (target) target.innerHTML = `<div class="ecosystem-plan-safety"><strong>${escHtml(platformLabText('unavailable', 'Unavailable'))}</strong><div class="hint">${escHtml(error?.message || String(error))}</div></div>`;
}
function renderPlatformBindings(data) {
$('platform-bindings').innerHTML = (data.bindings || []).map(binding => `<div class="ecosystem-service-row"><div><strong>${escHtml(binding.label)}</strong><div class="hint">${escHtml(binding.repository)} · ${escHtml(binding.latestTag || platformLabText('notChecked', 'not checked'))} · ${binding.assets?.length || 0} assets</div></div><div class="ecosystem-inline-actions"><span class="status-badge ${binding.state === 'current' ? 'online' : binding.state === 'update-available' ? 'warning' : 'unknown'}">${escHtml(binding.state)}</span>${binding.latestTag ? `<button class="btn btn-sm platform-binding-review" data-id="${escHtml(binding.id)}" data-tag="${escHtml(binding.latestTag)}">${escHtml(platformLabText('review', 'Review'))}</button>` : ''}</div></div>`).join('') || `<p class="hint">${escHtml(platformLabText('empty', 'No results.'))}</p>`;
}
function renderAutoloader(data) {
const profiles = [...(data.presets || []), ...(data.profiles || [])];
_platformLabState.profiles = profiles;
$('platform-autoloader-profile').innerHTML = profiles.map(profile => `<option value="${escHtml(profile.id)}">${escHtml(profile.name)} · ${escHtml((profile.entrypoints || []).join('/'))}</option>`).join('');
const selected = profiles[0] || { id: 'custom-profile', name: 'Custom profile', entrypoints: ['y2jb'], titleIds: [], payloads: ['pldmgr.elf'], firmware: null, youtubeVersion: null };
$('platform-profile-json').value = JSON.stringify(selected, null, 2);
$('platform-autoloader-output').innerHTML = `<div class="ecosystem-plan-safety">${escHtml(platformLabText('autoloader.safe', 'Profiles are preview-only; no USB or console writes are performed.'))}</div>`;
}
function renderAutoloaderPreview(data) {
$('platform-autoloader-output').innerHTML = `<div class="ecosystem-service-row"><div><strong>${escHtml(data.profile.name)}</strong><div class="hint">${escHtml(data.directory)} · ${escHtml((data.profile.entrypoints || []).join(', '))}</div></div><span class="status-badge ${data.blocked ? 'warning' : 'unknown'}">${escHtml(data.blocked ? platformLabText('blocked', 'blocked') : platformLabText('previewOnly', 'preview only'))}</span></div><pre class="ecosystem-log">${escHtml(data.autoloadText)}</pre>${(data.compatibility || []).map(item => `<div class="ecosystem-service-row"><div>${escHtml(item.message)}</div><span class="status-badge ${item.state === 'compatible' ? 'online' : item.state === 'blocked' ? 'warning' : 'unknown'}">${escHtml(item.state)}</span></div>`).join('')}${(data.warnings || []).map(warning => `<div class="ecosystem-plan-safety">${escHtml(warning)}</div>`).join('')}`;
}
function discoveryCount(value) { return Array.isArray(value) ? value.length : value && typeof value === 'object' ? Object.keys(value).length : 0; }
function renderPlatformWebsrv(data) {
$('platform-websrv').innerHTML = `<div class="ecosystem-stat-grid"><div><strong>${discoveryCount(data.mdns)}</strong><span>mDNS</span></div><div><strong>${data.capabilities?.smb ? '✓' : '−'}</strong><span>SMB</span></div><div><strong>${data.capabilities?.filesystem ? '✓' : '−'}</strong><span>Filesystem</span></div></div>${(data.errors || []).map(error => `<div class="hint">${escHtml(error)}</div>`).join('')}${(data.blockedExternal || []).map(item => `<div class="ecosystem-plan-safety">${escHtml(item.capability)}: ${escHtml(item.reason)}</div>`).join('')}<div class="ecosystem-plan-safety">${escHtml(platformLabText('readOnly', 'Read-only discovery'))}</div>`;
}
function renderRemoteIntelligence(data) {
const workers = data.workers || [];
$('platform-remote').innerHTML = `<div class="ecosystem-stat-grid"><div><strong>${data.counts?.online || 0}</strong><span>${escHtml(platformLabText('online', 'online'))}</span></div><div><strong>${data.counts?.updates || 0}</strong><span>${escHtml(platformLabText('updates', 'updates'))}</span></div><div><strong>${data.counts?.degraded || 0}</strong><span>${escHtml(platformLabText('attention', 'attention'))}</span></div></div>${workers.map(worker => `<div class="ecosystem-service-row"><div><strong>${escHtml(worker.name || worker.id)}</strong><div class="hint">${escHtml(worker.version || platformLabText('unknownVersion', 'unknown version'))} · ${escHtml((worker.recommendations || []).join(', ') || platformLabText('healthy', 'healthy'))}</div></div><span class="status-badge ${worker.updateState === 'current' ? 'online' : 'warning'}">${escHtml(worker.updateState)}</span></div>`).join('') || `<p class="hint">${escHtml(platformLabText('noWorkers', 'No Remote Play workers paired.'))}</p>`}`;
}
function renderShadps4Roots(data) {
$('platform-shadps4-root').innerHTML = `<option value="">${escHtml(platformLabText('selectRoot', 'Select library root'))}</option>${(data.roots || []).map(root => `<option value="${escHtml(root.id)}" ${root.selected ? 'selected' : ''}>${escHtml(root.label)}</option>`).join('')}`;
}
function renderShadps4Scan(data) {
if (data.status === 'not-configured') { $('platform-shadps4').innerHTML = `<p class="hint">${escHtml(platformLabText('selectRootHint', 'Select a semantic root to scan.'))}</p>`; return; }
const entries = [...(data.copies || []), ...(data.archives || [])];
$('platform-shadps4').innerHTML = `<div class="ecosystem-stat-grid"><div><strong>${(data.copies || []).length}</strong><span>${escHtml(platformLabText('copies', 'copies'))}</span></div><div><strong>${(data.archives || []).length}</strong><span>${escHtml(platformLabText('archiveCandidates', 'archive candidates'))}</span></div></div>${entries.slice(0, 50).map(item => `<div class="ecosystem-service-row"><div><strong>${escHtml(item.titleId || item.name)}</strong><div class="hint">${escHtml(item.relativePath || item.name)} · ${escHtml(item.format)}</div></div><div class="ecosystem-inline-actions"><span class="status-badge unknown">${escHtml(platformLabText('emulatorCopy', 'emulator copy'))}</span>${item.format === 'directory' ? `<button class="btn btn-sm platform-shadps4-plan" data-id="${escHtml(item.id)}">${escHtml(platformLabText('launchPlan', 'Launch plan'))}</button>` : ''}</div></div>`).join('')}<div class="ecosystem-plan-safety">${escHtml(platformLabText('notConsoleInstall', 'These records never imply a console installation.'))}</div>`;
}
function renderArtifactRoots(data) {
const options = `<option value="">${escHtml(platformLabText('selectRoot', 'Select library root'))}</option>${(data.roots || []).map(root => `<option value="${escHtml(root.id)}">${escHtml(root.label)}</option>`).join('')}`;
$('platform-artifact-root').innerHTML = options;
$('platform-stage-root').innerHTML = options;
}
function renderArtifacts(data) {
_platformLabState.artifacts = data.artifacts || [];
$('platform-artifacts').innerHTML = _platformLabState.artifacts.map(item => `<div class="ecosystem-service-row"><label><input class="platform-artifact-select" type="checkbox" value="${escHtml(item.id)}" ${item.lifecycleState === 'reviewed' ? 'checked' : ''}> <strong>${escHtml(item.fileName)}</strong><div class="hint">${escHtml(item.lifecycleState)} · ${escHtml(item.sha256.slice(0, 16))}…</div></label>${item.lifecycleState === 'hashed' ? `<button class="btn btn-sm platform-artifact-review" data-id="${escHtml(item.id)}" data-sha="${escHtml(item.sha256)}">${escHtml(platformLabText('reviewHash', 'Review hash'))}</button>` : `<span class="status-badge online">${escHtml(item.lifecycleState)}</span>`}</div>`).join('') || `<p class="hint">${escHtml(platformLabText('noArtifacts', 'No inspected artifacts.'))}</p>`;
}
function renderContracts(data) {
$('platform-contracts').innerHTML = (data.contracts || []).map(item => `<div class="ecosystem-service-row"><div><strong>${escHtml(item.label)}</strong><div class="hint">${escHtml(item.mode)}${item.missing.length ? ` · ${escHtml(item.missing.join(', '))}` : ''}</div></div><span class="status-badge ${item.state === 'ready' ? 'online' : item.state === 'blocked_external' ? 'warning' : 'unknown'}">${escHtml(item.state)}</span></div>`).join('');
}
function renderNotifications(data) {
$('platform-notifications').innerHTML = (data.items || []).map(item => `<div class="ecosystem-service-row"><div><strong>${escHtml(item.category)}</strong><div class="hint">${escHtml(item.message)}</div></div><span class="status-badge ${item.severity === 'warning' ? 'warning' : 'unknown'}">${escHtml(item.severity)}</span></div>`).join('') || `<p class="hint">${escHtml(platformLabText('noNotifications', 'No current recommendations.'))}</p>`;
}
function renderOperations(data) {
$('platform-operations').innerHTML = (data.jobs || []).slice(0, 10).map(job => `<div class="ecosystem-service-row"><div><strong>${escHtml(job.type)}</strong><div class="hint">${escHtml(job.error || job.updatedAt)}</div></div><span class="status-badge ${job.state === 'completed' ? 'online' : job.state === 'failed' ? 'warning' : 'unknown'}">${escHtml(job.state)} · ${job.progress}%</span></div>`).join('') || `<p class="hint">${escHtml(platformLabText('noOperations', 'No background operations.'))}</p>`;
}
function renderLaunchRoots(data) {
$('platform-shadps4-exe-root').innerHTML = `<option value="">${escHtml(platformLabText('selectExecutableRoot', 'Select executable root'))}</option>${(data.roots || []).map(root => `<option value="${escHtml(root.id)}">${escHtml(root.label)}</option>`).join('')}`;
}
function renderLaunchProbe(data) {
$('platform-shadps4-launch').innerHTML = `<div class="ecosystem-plan-safety">${data.available ? `${escHtml(data.executableLabel)} · ${escHtml(platformLabText('ready', 'ready'))}` : escHtml(platformLabText('launcherNotConfigured', 'Launcher not configured.'))} · shell=false</div>`;
}
function renderSearch(data) {
$('platform-search-results').innerHTML = (data.results || []).map(item => `<div class="ecosystem-service-row"><div><strong>${escHtml(item.title)}</strong><div class="hint">${escHtml(item.kind)} · ${escHtml(item.detail)}</div></div></div>`).join('') || `<p class="hint">${escHtml(platformLabText('empty', 'No results.'))}</p>`;
}
async function loadPlatformLab() {
const jobs = [
['platform-bindings', window.pork.platformBindings(), renderPlatformBindings],
['platform-autoloader-output', window.pork.unifiedAutoloaderList(), renderAutoloader],
['platform-websrv', window.pork.platformWebsrvDiscover(), renderPlatformWebsrv],
['platform-remote', window.pork.remotePlayIntelligence(), renderRemoteIntelligence],
['platform-shadps4', window.pork.shadps4Roots(), renderShadps4Roots],
['platform-shadps4', window.pork.shadps4Scan(), renderShadps4Scan],
['platform-artifacts', window.pork.platformArtifactRoots(), renderArtifactRoots],
['platform-artifacts', window.pork.platformArtifacts(), renderArtifacts],
['platform-contracts', window.pork.platformContracts(), renderContracts],
['platform-notifications', window.pork.platformNotifications(), renderNotifications],
['platform-operations', window.pork.platformOperations(), renderOperations],
['platform-shadps4-launch', window.pork.shadps4LaunchRoots(), renderLaunchRoots],
['platform-shadps4-launch', window.pork.shadps4LaunchProbe(), renderLaunchProbe],
];
const results = await Promise.allSettled(jobs.map(job => job[1]));
results.forEach((result, index) => result.status === 'fulfilled' ? jobs[index][2](result.value) : platformLabError(jobs[index][0], result.reason));
}
async function loadEcosystemTab(tab) {
if (tab === 'overview') return loadEcosystemOverview();
if (tab === 'components') return loadEcosystemComponents();
if (tab === 'updates') return loadEcosystemUpdates();
if (tab === 'game-updates') return loadGameUpdates();
if (tab === 'etahen') return loadEtaHen();
if (tab === 'live') return loadLiveSession();
if (tab === 'remote-play') return loadRemotePlay({ discover: !_remotePlayPacket });
if (tab === 'files') return loadFilesTab();
if (tab === 'homebrew') return loadHomebrew();
if (tab === 'sources') return loadSources();
if (tab === 'plugins') return loadPlugins();
if (tab === 'cheats') return loadCheatsV2();
if (tab === 'saves') return loadSaveVault();
if (tab === 'itemzflow') return loadItemzflow();
if (tab === 'platform-lab') return loadPlatformLab();
if (tab === 'research') return loadLinuxLab();
if (tab === 'timeline') return loadTimeline();
}
function activateEcosystemTab(tab) {
_ecosystemActiveTab = tab;
document.querySelectorAll('.ecosystem-tab').forEach(button => {
const active = button.dataset.ecosystemTab === tab;
button.classList.toggle('active', active);
button.setAttribute('aria-selected', String(active));
button.tabIndex = active ? 0 : -1;
});
document.querySelectorAll('.ecosystem-panel').forEach(panel => { const active = panel.dataset.ecosystemPanel === tab; panel.classList.toggle('active', active); panel.hidden = !active; });
loadEcosystemTab(tab).catch(error => showToast(error.message, 'error'));
}
function navigateEcosystemTab(tab) {
const route = window.ChimeraNavigationContract?.routes?.find(item => item.page === 'ecosystem' && item.tab === tab);
if (route && typeof window.chimeraNavigate === 'function') return window.chimeraNavigate(route.id);
return activateEcosystemTab(tab);
}
function bindEcosystemEvents() {
document.querySelectorAll('.ecosystem-tab').forEach(button => button.addEventListener('click', () => navigateEcosystemTab(button.dataset.ecosystemTab)));
$('ecosystem-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, () => loadEcosystemTab(_ecosystemActiveTab)).catch(error => showToast(error.message, 'error')));
$('ecosystem-health-run').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
await window.pork.ecosystemHealthRun(true);
await loadEcosystemOverview();
showToast('Deep console health scan completed.', 'ok');
}).catch(error => showToast(error.message, 'error', 9000)));
$('ecosystem-overview').addEventListener('click', async event => {
const alert = event.target.closest('.ecosystem-alert');
if (!alert) return;
try {
if (event.target.closest('.ecosystem-alert-ack')) {
await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemHealthAcknowledge(alert.dataset.id));
await loadEcosystemOverview();
}
const open = event.target.closest('.ecosystem-alert-open');
if (open?.dataset.tab) navigateEcosystemTab(open.dataset.tab);
if (open?.dataset.page) navigate(open.dataset.page);
} catch (error) { showToast(error.message, 'error'); }
});
$('ecosystem-overview').addEventListener('change', async event => {
if (event.target.id !== 'ecosystem-health-notifications') return;
try {
const packet = await window.pork.ecosystemHealthOptions(event.target.checked);
event.target.checked = Boolean(packet.notificationsEnabled);
showToast(packet.notificationsEnabled ? 'Health notifications enabled.' : 'Health notifications disabled.', 'ok');
} catch (error) { event.target.checked = !event.target.checked; showToast(error.message, 'error'); }
});
$('ecosystem-components-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => { await window.pork.ecosystemRefreshComponents(true); await loadEcosystemComponents(); showToast('Upstreamcomponenten bijgewerkt.', 'ok'); }).catch(error => showToast(error.message, 'error')));
$('ecosystem-components-search').addEventListener('input', renderEcosystemComponents);
$('ecosystem-components-platform').addEventListener('change', renderEcosystemComponents);
$('ecosystem-components-risk').addEventListener('change', renderEcosystemComponents);
$('ecosystem-components').addEventListener('click', event => { const button = event.target.closest('.ecosystem-open-upstream'); if (button) window.pork.openShell(button.dataset.url); });
$('ecosystem-updates-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
const result = await window.pork.ecosystemUpdatesRefresh();
await loadEcosystemUpdates();
showToast(`${result.stats?.total || 0} upstreamcomponenten en payloadcatalogi gesynchroniseerd.`, result.blocked ? 'info' : 'ok', 9000);
}).catch(error => showToast(error.message, 'error', 9000)));
$('ecosystem-watchdog-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, loadEcosystemUpdates).catch(error => showToast(error.message, 'error', 10000)));
$('ecosystem-manager-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => { await window.pork.ecosystemPayloadRefresh(); await loadEcosystemUpdates(); showToast('Payload Manager sources refreshed; nothing was installed or started.', 'ok', 9000); }).catch(error => showToast(error.message, 'error', 10000)));
$('ecosystem-runtime-updates').addEventListener('click', async event => {
const pairButton = event.target.closest('.chimera-sentinel-pair');
if (pairButton) {
const confirmation = await showExactConfirmation(window.t('ecosystem.sentinel.pairWarning', 'Sentinel pairs only briefly after payload start; the key stays in RAM.'), 'PAIR SENTINEL');
if (!confirmation) return;
try {
await ecosystemBusy(pairButton, () => window.pork.chimeraSentinelPair({ confirmation, rotate: false }));
showToast('Sentinel 0.2 paired. The console keeps the key only until the payload stops.', 'ok', 10000);
await loadEcosystemUpdates();
} catch (error) { showToast(error.message, 'error', 10000); }
return;
}
const button = event.target.closest('.ecosystem-runtime-install');
const row = event.target.closest('[data-runtime-filename]');
if (!button || !row) return;
const required = `UPDATE ${row.dataset.runtimeFilename}`;
const confirmation = await showExactConfirmation(`This installs only ${row.dataset.runtimeFilename} in Payload Manager and starts nothing.`, required);
if (!confirmation) return;
try {
const result = await ecosystemBusy(button, () => window.pork.ecosystemPayloadInstall({ filename: row.dataset.runtimeFilename, sourceId: row.dataset.runtimeSource, confirmation }));
showToast(`${result.filename} installed. The payload was not started.`, 'ok', 10000);
await loadEcosystemUpdates();
} catch (error) { showToast(error.message, 'error', 10000); }
});
$('ecosystem-supply-policy').addEventListener('submit', async event => {
event.preventDefault();
try {
await ecosystemBusy(event.submitter, () => window.pork.ecosystemSupplyPolicySave({ officialSourcesOnly: $('ecosystem-supply-official').checked, requireHttps: $('ecosystem-supply-https').checked, requirePublishedHashForExecution: $('ecosystem-supply-hash').checked }));
showToast('Payload trust policy saved. Automatic execution remains disabled.', 'ok');
} catch (error) { showToast(error.message, 'error'); }
});
$('ecosystem-supply-audit').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
const report = await window.pork.ecosystemSupplyAudit();
const blocked = report.assets.filter(asset => asset.downloaded && asset.issues.length);
$('ecosystem-supply-report').innerHTML = `<strong>${report.stats.verified} verified · ${report.stats.blocked} blocked</strong><span>${report.stats.assets} release assets from ${report.stats.sources} sources · checked ${ecosystemDate(report.checkedAt)}</span>${blocked.length ? `<div>${blocked.slice(0, 30).map(asset => `<p class="operations-warning"><strong>${escHtml(asset.source)} · ${escHtml(asset.name)}</strong> — ${asset.issues.map(escHtml).join(' ')}</p>`).join('')}</div>` : '<div class="operations-empty-ok">✓ Every downloaded payload satisfies the active policy.</div>'}`;
showToast(`Payload Vault: ${report.stats.verified} verified, ${report.stats.blocked} blocked.`, report.stats.blocked ? 'info' : 'ok', 10000);
}).catch(error => showToast(error.message, 'error', 10000)));
$('ecosystem-repository-copy').addEventListener('click', async () => {
const input = $('ecosystem-repository-url');
try {
if (!navigator.clipboard?.writeText) throw new Error('Clipboard API unavailable');
await navigator.clipboard.writeText(input.value);
showToast('Payload Manager source URL copied.', 'ok');
} catch (_) {
input.focus(); input.select(); input.setSelectionRange(0, input.value.length);
const copied = typeof document.execCommand === 'function' && document.execCommand('copy');
showToast(copied ? 'Payload Manager source URL copied.' : 'The URL is selected; copy it with Ctrl+C.', copied ? 'ok' : 'info');
}
});
$('ecosystem-payload-manager-open').addEventListener('click', () => navigate('jailbreak'));
$('game-updates-manifest-form').addEventListener('submit', async event => {
event.preventDefault();
try {
const plan = await ecosystemBusy(event.submitter, () => window.pork.gameUpdatesAutoInspect($('game-updates-game').value, true));
renderGameUpdatePlan(plan);
} catch (error) { _gameUpdatePlan = null; $('game-updates-plan').innerHTML = `<strong>Manifest afgekeurd</strong><span>${escHtml(error.message)}</span>`; showToast(error.message, 'error', 10000); }
});
$('game-updates-manual-inspect').addEventListener('click', async event => {
try {
const url = $('game-updates-manifest-url').value.trim();
if (!url) throw new Error('Paste an official Sony JSON manifest URL first.');
renderGameUpdatePlan(await ecosystemBusy(event.currentTarget, () => window.pork.gameUpdatesInspect(url, $('game-updates-game').value)));
} catch (error) { _gameUpdatePlan = null; $('game-updates-plan').innerHTML = `<strong>Manifest rejected</strong><span>${escHtml(error.message)}</span>`; }
});
$('game-updates-scan-all').addEventListener('click', async event => {
try {
const result = await ecosystemBusy(event.currentTarget, () => window.pork.gameUpdatesRefreshAll());
showToast(`${result.available}/${result.total} official manifests available.`, result.failed ? 'info' : 'ok', 9000);
await loadGameUpdates();
} catch (error) { showToast(error.message, 'error', 10000); }
});
$('game-updates-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, loadGameUpdates).catch(error => showToast(error.message, 'error')));
$('game-updates-jobs').addEventListener('click', async event => {
const row = event.target.closest('.ecosystem-update-row'); const button = event.target.closest('button');
if (!row || !button) return;
try {
if (button.classList.contains('game-update-cancel') && await showConfirm('Cancel this download and remove the incomplete file?')) await window.pork.gameUpdatesCancel(row.dataset.id);
if (button.classList.contains('game-update-stage')) {
if (!await showConfirm('Copy the fully verified package to /data/chimera-updates on the PS5? This installs or starts nothing.')) return;
const result = await ecosystemBusy(button, () => window.pork.gameUpdatesStage(row.dataset.id));
showToast(`Package verified and copied to ${result.remotePath}; installation was not performed.`, 'ok', 12000);
}
if (button.classList.contains('game-update-handoff')) {
const result = await ecosystemBusy(button, () => window.pork.gameUpdatesHttpsHandoff(row.dataset.id));
await navigator.clipboard.writeText(result.url);
showToast('Temporary verified HTTPS package URL copied; it expires in 30 minutes.', 'ok', 10000);
}
if (button.classList.contains('game-update-delete') && await showExactConfirmation('Permanently delete this job and local package file?', 'DELETE')) await window.pork.gameUpdatesDelete(row.dataset.id);
await loadGameUpdates();
} catch (error) { showToast(error.message, 'error', 10000); }
});
$('ecosystem-update-plan').addEventListener('click', async event => {
const row = event.target.closest('.ecosystem-update-row');
if (!row) return;
const button = event.target.closest('button');
try {
if (button?.classList.contains('ecosystem-version-save')) {
const version = row.querySelector('.ecosystem-version-input').value.trim();
await ecosystemBusy(button, () => window.pork.ecosystemTrackVersion(row.dataset.id, version));
await loadEcosystemUpdates();
showToast(window.t(version ? 'ecosystem.payloadEvidence.manualSaved' : 'ecosystem.payloadEvidence.manualCleared', version ? 'Manual override saved.' : 'Manual tracking cleared.'), 'ok');
}
if (button?.classList.contains('ecosystem-follow-automatic')) {
await ecosystemBusy(button, () => window.pork.ecosystemFollowAutomatic(row.dataset.id));
await loadEcosystemUpdates();
showToast(window.t('ecosystem.payloadEvidence.automaticSaved', 'Automatic evidence following enabled.'), 'ok');
}
if (button?.classList.contains('ecosystem-update-prepare')) {
const asset = row.querySelector('.ecosystem-asset-select')?.value;
if (!await showConfirm('This only downloads the selected official release asset to Chimera and verifies available checksums. Nothing executes on the PS5. Continue?')) return;
const result = await ecosystemBusy(button, () => window.pork.ecosystemUpdatePrepare({ componentId: row.dataset.id, assetName: asset }));
await loadEcosystemUpdates();
showToast(`Update voorbereid: ${result.file?.name || result.asset?.name || asset}${result.file?.verified || result.verified ? ' · checksum geverifieerd' : ''}.`, result.file?.verified || result.verified ? 'ok' : 'info', 9000);
}
} catch (error) { showToast(error.message, 'error', 9000); }
});
$('ecosystem-etahen-reload').addEventListener('click', event => ecosystemBusy(event.currentTarget, loadEtaHen).catch(error => showToast(error.message, 'error')));
$('ecosystem-etahen-preview').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderEtaDiff(await window.pork.ecosystemEtaHenPreview({ raw: $('ecosystem-etahen-raw').value, preset: $('ecosystem-etahen-preset').value || null, patch: etaPatchFromForm() }))).catch(error => showToast(error.message, 'error')));
$('ecosystem-etahen-apply').addEventListener('click', async event => {
if (!_ecosystemEtaProposed || !await showExactConfirmation('Apply this etaHEN configuration? Chimera creates a restore point first and verifies the upload.', 'PAS ETAHEN TOE')) return;
try { await ecosystemBusy(event.currentTarget, () => window.pork.ecosystemEtaHenApply({ proposed: _ecosystemEtaProposed, expectedHash: _ecosystemEtaHash, label: $('ecosystem-etahen-preset').selectedOptions[0]?.textContent || 'Manual change' }, 'PAS ETAHEN TOE')); showToast('etaHEN configuration applied safely.', 'ok'); await loadEtaHen(); } catch (error) { showToast(error.message, 'error', 9000); }
});
$('ecosystem-etahen-history').addEventListener('click', async event => { const row = event.target.closest('.ecosystem-history-row'); if (!row || !event.target.closest('.ecosystem-etahen-restore') || !await showExactConfirmation('Restore this etaHEN restore point?', 'HERSTEL')) return; try { await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemEtaHenRestore(row.dataset.id)); showToast('etaHEN configuration restored.', 'ok'); await loadEtaHen(); } catch (error) { showToast(error.message, 'error'); } });
$('ecosystem-live-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, loadLiveSession).catch(error => showToast(error.message, 'error')));
$('remote-play-profile').addEventListener('submit', async event => {
event.preventDefault();
try {
await ecosystemBusy(event.submitter, () => window.pork.remotePlaySave({ nickname: $('remote-play-nickname').value, clientNickname: $('remote-play-client-nickname').value, workerId: $('remote-play-worker').value, displayMode: $('remote-play-display-mode').value, registrationKey: $('remote-play-key').value || undefined }));
$('remote-play-key').value = '';
showToast('Remote Play profile saved safely.', 'ok');
await loadRemotePlay();
} catch (error) { showToast(error.message, 'error', 10000); }
});
$('remote-play-worker').addEventListener('change', () => { if (_remotePlayPacket) loadRemotePlay().catch(error => showToast(error.message, 'error')); });
$('remote-play-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, () => loadRemotePlay({ schedule: true })).catch(error => showToast(error.message, 'error')));
$('remote-play-discover').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => { const result = await window.pork.remotePlayDiscover(); await loadRemotePlay(); showToast(result.online ? `PS5 ${result.console?.name || ''} is ${result.state}.` : 'The PS5 did not answer LAN discovery.', result.online ? 'ok' : 'error'); }).catch(error => showToast(error.message, 'error')));
$('remote-play-wake').addEventListener('click', async event => {
if (!await showExactConfirmation('This sends a wake packet to the PS5 and waits up to 30 seconds for the console to become ready.', 'WEK PS5')) return;
try { const result = await ecosystemBusy(event.currentTarget, () => window.pork.remotePlayWake()); showToast(result.ready ? (result.alreadyReady ? 'The PS5 was already ready.' : 'The PS5 is awake and ready.') : 'Wake packet sent, but the PS5 did not become ready in time.', result.ready ? 'ok' : 'error', 10000); await loadRemotePlay(); } catch (error) { showToast(error.message, 'error', 10000); }
});
$('remote-play-start').addEventListener('click', async event => {
const worker = remotePlaySelectedWorker(_remotePlayPacket || {});
if (!worker || !await showExactConfirmation(`Dit ${_remotePlayPacket?.lastDiscovery?.state === 'standby' ? 'wekt de PS5 en ' : ''}opent chiaki-ng op ${worker.label}.`, 'START REMOTE PLAY')) return;
try {
const result = await ecosystemBusy(event.currentTarget, () => window.pork.remotePlayStart({ workerId: worker.id, clientNickname: $('remote-play-client-nickname').value.trim(), displayMode: $('remote-play-display-mode').value, wakeIfNeeded: _remotePlayPacket?.lastDiscovery?.state === 'standby' }));
showToast(`Remote Play-job ${result.job.id.slice(0, 8)} staat klaar voor ${worker.label}.`, 'ok', 9000);
await loadRemotePlay();
} catch (error) { showToast(error.message, 'error', 12000); }
});
$('remote-play-worker-package-form').addEventListener('submit', async event => {
event.preventDefault();
try { await ecosystemBusy(event.submitter, () => window.pork.remotePlayWorkerPackage({ label: $('remote-play-worker-label').value.trim(), serverOrigin: window.location.origin })); showToast('Worker package created. The enrollment token expires in 30 minutes.', 'ok', 10000); await loadRemotePlay(); }
catch (error) { showToast(error.message, 'error', 10000); }
});
$('remote-play-workers').addEventListener('click', async event => {
const row = event.target.closest('[data-remote-worker-id]');
if (!row || !event.target.closest('.remote-play-worker-remove') || !await showExactConfirmation('Revoke this worker, erase its HMAC key and cancel open jobs?', 'VERWIJDER')) return;
try { await ecosystemBusy(event.target.closest('button'), () => window.pork.remotePlayWorkerRemove(row.dataset.remoteWorkerId)); showToast('Remote Play worker revoked.', 'ok'); await loadRemotePlay(); } catch (error) { showToast(error.message, 'error'); }
});
const runWorkerAction = async (button, action, phrase = null) => {
const worker = remotePlaySelectedWorker(_remotePlayPacket || {});
if (!worker) return showToast('Select a Remote Play worker first.', 'error');
if (phrase && !await showExactConfirmation(`Deze vaste actie wordt uitgevoerd op ${worker.label}.`, phrase)) return;
try { const job = await ecosystemBusy(button, () => window.pork.remotePlayWorkerAction({ workerId: worker.id, action, confirmation: phrase || undefined })); showToast(`${action} is queued securely for the worker (${job.id.slice(0, 8)}).`, 'ok', 9000); await loadRemotePlay(); } catch (error) { showToast(error.message, 'error', 12000); }
};
$('remote-play-diagnose').addEventListener('click', event => runWorkerAction(event.currentTarget, 'diagnose'));
$('remote-play-open-client').addEventListener('click', event => runWorkerAction(event.currentTarget, 'open-client', 'OPEN CHIAKI'));
$('remote-play-install-client').addEventListener('click', event => runWorkerAction(event.currentTarget, 'install-client', 'INSTALL CHIAKI'));
$('remote-play-update-client').addEventListener('click', event => runWorkerAction(event.currentTarget, 'update-client', 'UPDATE CHIAKI'));
$('remote-play-stop').addEventListener('click', event => runWorkerAction(event.currentTarget, 'stop-session', 'STOP REMOTE PLAY'));
$('remote-play-profile-export').addEventListener('click', event => ecosystemBusy(event.currentTarget, () => window.pork.remotePlayProfileExport()).catch(error => showToast(error.message, 'error')));
$('remote-play-upstream').addEventListener('click', () => window.pork.openShell('https://streetpea.github.io/chiaki-ng/setup/configuration/'));
$('websrv-files-root').addEventListener('click', event => ecosystemBusy(event.currentTarget, () => loadWebsrvFiles('/')).catch(error => showToast(error.message, 'error')));
$('websrv-files-path').addEventListener('click', event => { const button = event.target.closest('.websrv-files-back'); if (button) loadWebsrvFiles(button.dataset.parent).catch(error => showToast(error.message, 'error')); });
$('websrv-files-list').addEventListener('click', async event => { const row = event.target.closest('.websrv-file-row'); if (!row) return; try { if (event.target.closest('.websrv-file-open')) await loadWebsrvFiles(row.dataset.path); if (event.target.closest('.websrv-file-download')) await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemWebsrvDownload(row.dataset.path)); } catch (error) { showToast(error.message, 'error', 10000); } });
$('websrv-smb-source').addEventListener('change', () => { _websrvSmbPath = ''; if ($('websrv-smb-source').value) loadWebsrvSmb('').catch(error => showToast(error.message, 'error')); else { $('websrv-smb-list').innerHTML = '<p class="hint">Choose a saved SMB source.</p>'; $('websrv-smb-path').innerHTML = ''; } });
$('websrv-smb-path').addEventListener('click', event => { const button = event.target.closest('.websrv-smb-back'); if (button) loadWebsrvSmb(button.dataset.parent).catch(error => showToast(error.message, 'error')); });
$('websrv-smb-list').addEventListener('click', async event => { const row = event.target.closest('.websrv-file-row'); if (!row) return; try { if (event.target.closest('.websrv-file-open')) await loadWebsrvSmb(row.dataset.path); if (event.target.closest('.websrv-file-download')) await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemWebsrvSmbDownload($('websrv-smb-source').value, row.dataset.path)); } catch (error) { showToast(error.message, 'error', 10000); } });
$('ecosystem-homebrew-scan').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderHomebrew(await window.pork.ecosystemHomebrewScan())).catch(error => showToast(error.message, 'error')));
$('ecosystem-websrv-discover').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
const result = await window.pork.ecosystemWebsrvDiscover();
renderWebsrvDiscovery(result);
showToast(`${result.services?.length || 0} mDNS service(s) found.`, 'ok');
}).catch(error => showToast(error.message, 'error')));
$('ecosystem-homebrew-webui').addEventListener('click', async () => { try { const urls = await window.pork.ecosystemSourceWebUis(); window.pork.openShell(urls.websrv); } catch (error) { showToast(error.message, 'error'); } });
$('ecosystem-homebrew').addEventListener('click', async event => { const row = event.target.closest('.ecosystem-homebrew-card'); if (!row || !event.target.closest('.ecosystem-homebrew-launch')) return; if (!await showExactConfirmation('websrv warns that launching can crash when other homebrew is already active.', 'START HOMEBREW')) return; try { await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemHomebrewLaunch(row.dataset.id, 'START HOMEBREW')); showToast('Homebrew launch sent to websrv.', 'ok'); } catch (error) { showToast(error.message, 'error'); } });
$('ecosystem-pacbrew-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderPacbrew(await window.pork.ecosystemPacbrewRefresh())).catch(error => showToast(error.message, 'error')));
$('ecosystem-pacbrew-search').addEventListener('input', () => { if (_pacbrewPacket) renderPacbrew(_pacbrewPacket); });
$('ecosystem-pacbrew-kind').addEventListener('change', () => { if (_pacbrewPacket) renderPacbrew(_pacbrewPacket); });
$('ecosystem-pacbrew').addEventListener('click', async event => {
const upstream = event.target.closest('.ecosystem-open-upstream');
if (upstream) { window.pork.openShell(upstream.dataset.url); return; }
const button = event.target.closest('.ecosystem-pacbrew-inspect');
const row = event.target.closest('[data-pacbrew-name]');
if (!button || !row) return;
try {
const item = await ecosystemBusy(button, () => window.pork.ecosystemPacbrewPackage(row.dataset.pacbrewName));
$('ecosystem-pacbrew-detail').innerHTML = `<div class="ecosystem-plan-safety"><strong>${escHtml(item.pkgname || item.name)} · ${escHtml(item.version || 'git')}</strong><span>${escHtml(item.description || 'No description in PKGBUILD.')}</span><span>Runtime: ${(item.dependencies || []).map(escHtml).join(', ') || 'none'} · build: ${(item.buildDependencies || []).map(escHtml).join(', ') || 'none'} · license: ${(item.licenses || []).map(escHtml).join(', ') || 'unknown'}</span><span>Recipe ${escHtml(String(item.sourceSha || '').slice(0, 12))} was parsed as text only; nothing was executed.</span></div>`;
} catch (error) { showToast(error.message, 'error', 10000); }
});
$('ecosystem-source-form').addEventListener('submit', async event => { event.preventDefault(); const button = event.submitter; try { const sources = await ecosystemBusy(button, () => window.pork.ecosystemSourceSave({ id: $('ecosystem-source-id').value || undefined, label: $('ecosystem-source-label').value, protocol: $('ecosystem-source-protocol').value, host: $('ecosystem-source-host').value, port: $('ecosystem-source-port').value || undefined, path: $('ecosystem-source-path').value, username: $('ecosystem-source-user').value, password: $('ecosystem-source-password').value || undefined })); renderSources(sources); event.target.reset(); $('ecosystem-source-path').value = '/'; $('ecosystem-source-id').value = ''; showToast('Remote source saved with encryption.', 'ok'); } catch (error) { showToast(error.message, 'error'); } });
$('ecosystem-sources').addEventListener('click', async event => {
const row = event.target.closest('.ecosystem-source-row');
if (!row) return;
const sources = await window.pork.ecosystemSources();
const source = sources.find(item => item.id === row.dataset.id);
if (!source) return;
try {
if (event.target.closest('.ecosystem-source-test')) {
const result = await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemSourceTest(row.dataset.id));
showToast(result.ok ? `Source reachable in ${result.latencyMs} ms.` : `Source unreachable: ${result.error || result.status}`, result.ok ? 'ok' : 'error');
}
if (event.target.closest('.ecosystem-source-edit')) {
$('ecosystem-source-id').value = source.id;
$('ecosystem-source-label').value = source.label;
$('ecosystem-source-protocol').value = source.protocol;
$('ecosystem-source-host').value = source.host;
$('ecosystem-source-port').value = source.port;
$('ecosystem-source-path').value = source.path;
$('ecosystem-source-user').value = source.username || '';
$('ecosystem-source-password').value = '';
}
if (event.target.closest('.ecosystem-source-delete') && await showExactConfirmation(`Delete ${source.label}?`, 'DELETE')) {
renderSources(await window.pork.ecosystemSourceRemove(source.id));
}
} catch (error) { showToast(error.message, 'error'); }
});
$('ezremote-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, loadSources).catch(error => showToast(error.message, 'error')));
$('ezremote-download-form').addEventListener('submit', async event => {
event.preventDefault();
const destinationPath = $('ezremote-destination').value.trim();
const required = `DOWNLOAD ${destinationPath}`;
const confirmation = await showExactConfirmation('ezRemote writes to the PS5 and cannot safely cancel an active download.', required);
if (!confirmation) return;
try {
const result = await ecosystemBusy(event.submitter, () => window.pork.ezRemoteDownload({
sourceId: $('ezremote-source').value,
sourcePath: $('ezremote-source-path').value.trim(),
destinationPath,
size: Number($('ezremote-size').value || 0),
confirmation,
}));
showToast(result.deduplicated ? 'Reused the existing ezRemote task.' : 'ezRemote download safely queued in the durable transfer queue.', 'ok', 9000);
} catch (error) { showToast(error.message, 'error', 10000); }
});
$('ecosystem-ezremote-webui').addEventListener('click', async () => { try { window.pork.openShell((await window.pork.ecosystemSourceWebUis()).ezremote); } catch (error) { showToast(error.message, 'error'); } });
$('ecosystem-dpi-webui').addEventListener('click', async () => { try { window.pork.openShell((await window.pork.ecosystemSourceWebUis()).dpiV2); } catch (error) { showToast(error.message, 'error'); } });
$('ecosystem-plugins-scan').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderPlugins(await window.pork.ecosystemPluginsScan())).catch(error => showToast(error.message, 'error')));
$('ecosystem-plugin-install').addEventListener('click', async event => {
const targetRoot = $('ecosystem-plugin-target').value;
const confirmation = await showExactConfirmation(`Install one selected ELF or PRX into ${targetRoot}? An existing file is backed up and the upload is verified.`, 'INSTALL PLUGIN');
if (!confirmation) return;
try {
const result = await ecosystemBusy(event.currentTarget, () => window.pork.ecosystemPluginInstall(targetRoot, confirmation));
if (!result) return;
renderPlugins(result.inventory);
showToast(`${result.filename} installed and verified · ${String(result.sha256).slice(0, 12)}…`, 'ok', 10000);
} catch (error) { showToast(error.message, 'error', 10000); }
});
$('ecosystem-plugins').addEventListener('click', async event => {
const upstream = event.target.closest('.ecosystem-open-upstream');
if (upstream) {
window.pork.openShell(upstream.dataset.url);
return;
}
const row = event.target.closest('.ecosystem-plugin-row');
if (!row) return;
try {
if (event.target.closest('.ecosystem-plugin-inspect')) {
const info = await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemPluginInspect(row.dataset.id));
let output = row.nextElementSibling?.classList.contains('ecosystem-plugin-inspection') ? row.nextElementSibling : null;
if (!output) {
output = document.createElement('pre');
output.className = 'ecosystem-plugin-inspection';
row.insertAdjacentElement('afterend', output);
}
output.textContent = `SHA-256: ${info.sha256}\nTitle IDs: ${info.titleIds.join(', ') || 'none reliably detected'}\nVersion candidates: ${info.versions.join(', ') || 'none detected'}`;
}
if (event.target.closest('.ecosystem-plugin-toggle')) {
const currentEnabled = event.target.closest('button').textContent.includes('Disable');
if (!await showExactConfirmation(`${currentEnabled ? 'Disable' : 'Enable'} this plugin? Chimera creates a backup first and etaHEN must then be reloaded.`, 'WIJZIG PLUGIN')) return;
const result = await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemPluginToggle(row.dataset.id, !currentEnabled, 'WIJZIG PLUGIN'));
renderPlugins(result.inventory);
}
} catch (error) { showToast(error.message, 'error'); }
});
const scheduleCheatLoad = () => { _ecosystemCheatOffset = 0; clearTimeout(_ecosystemCheatTimer); _ecosystemCheatTimer = setTimeout(() => loadCheatsV2().catch(error => showToast(error.message, 'error')), 250); };
$('ecosystem-cheats-search').addEventListener('input', scheduleCheatLoad); $('ecosystem-cheats-platform').addEventListener('change', scheduleCheatLoad); $('ecosystem-cheats-kind').addEventListener('change', scheduleCheatLoad); $('ecosystem-cheats-installed').addEventListener('change', scheduleCheatLoad);
$('ecosystem-cheats-sync').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => { await window.pork.ecosystemCheatsSync(); _ecosystemCheatOffset = 0; await loadCheatsV2(); showToast('Cheat and patch sources synchronized.', 'ok'); }).catch(error => showToast(error.message, 'error', 9000)));
$('ecosystem-cheats').addEventListener('click', async event => { const pager = event.target.closest('.ecosystem-cheat-page'); if (pager && !pager.disabled) { _ecosystemCheatOffset = Number(pager.dataset.offset) || 0; await loadCheatsV2(); return; } const source = event.target.closest('.ecosystem-open-upstream'); if (source) return window.pork.openShell(source.dataset.url); const row = event.target.closest('.ecosystem-cheat-row'); if (!row || !event.target.closest('.ecosystem-cheat-deploy')) return; const versionWarning = row.querySelector('.operations-status-warning')?.textContent.includes('Version mismatch'); const conflict = row.classList.contains('conflict'); if (!await showExactConfirmation(`Send this cheat or patch to the documented PS5 folder?${versionWarning ? '\n\nThe game version differs; you explicitly confirm that you want to continue.' : ''}${conflict ? '\n\nOther formats for the same version will be replaced safely.' : ''}`, 'INSTALLEER CHEAT')) return; try { await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemCheatDeploy({ key: row.dataset.key, forceVersion: versionWarning, replaceConflicts: conflict, confirmation: 'INSTALLEER CHEAT' })); showToast('Cheat or patch installed with backup and checksum verification.', 'ok'); } catch (error) { showToast(error.message, 'error', 9000); } });
$('ecosystem-saves-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderRemoteSaves(await window.pork.ecosystemRemoteSaves())).catch(error => showToast(error.message, 'error')));
$('ecosystem-saves-all').addEventListener('click', async event => { if (!await showConfirm('Export every discovered title to Save Vault one by one? This can take a long time and use substantial storage.')) return; try { const result = await ecosystemBusy(event.currentTarget, () => window.pork.ecosystemSaveSnapshotAll()); showToast(`${result.succeeded}/${result.total} save backups created.`, result.failed ? 'info' : 'ok', 9000); await loadSaveVault(); } catch (error) { showToast(error.message, 'error'); } });
$('ecosystem-remote-saves').addEventListener('click', async event => { const row = event.target.closest('.ecosystem-save-row'); if (!row || !event.target.closest('.ecosystem-save-snapshot')) return; try { await ecosystemBusy(event.target.closest('button'), () => window.pork.ecosystemSaveSnapshot(row.dataset.titleId)); showToast('Save Vault-snapshot gemaakt en geverifieerd.', 'ok'); await loadSaveVault(); } catch (error) { showToast(error.message, 'error'); } });
$('ecosystem-save-vault').addEventListener('click', async event => {
const row = event.target.closest('.ecosystem-vault-row');
if (!row) return;
const button = event.target.closest('button');
try {
if (button?.classList.contains('ecosystem-save-verify')) {
const result = await ecosystemBusy(button, () => window.pork.ecosystemSaveVerify(row.dataset.id));
showToast(result.ok ? `${result.titleId}: SHA-256, ZIP CRC and ${result.archive.entries.length} save file(s) are valid.` : `Snapshot rejected: ${result.error}`, result.ok ? 'ok' : 'error', 10000);
}
if (button?.classList.contains('ecosystem-save-restore')) {
if (!_ecosystemSaveCapabilities?.automatedRestore) throw new Error('Restore capability is not active.');
let plan = await ecosystemBusy(button, () => window.pork.ecosystemSaveRestorePlan(row.dataset.id));
if (plan.requiresTargetUser) {
const choices = plan.users.map(user => `${user.uid} (${user.name || 'name unknown'})`).join('\n');
const targetUid = await showPrompt(`Choose the exact target UID for ${plan.titleId}:\n\n${choices}`, plan.users[0]?.uid || '');
if (!targetUid) return;
plan = await ecosystemBusy(button, () => window.pork.ecosystemSaveRestorePlan(row.dataset.id, targetUid.trim()));
}
const incoming = plan.impact.incomingSaves.map(save => `• ${save.name} — ${ecosystemBytes(save.bytes)} — ${String(save.sha256).slice(0, 12)}…`).join('\n');
const approved = await showConfirm(
`DRY RUN — nothing has changed on the PS5 yet.\n\n` +
`Title: ${plan.titleId}\nTarget user: ${plan.targetUid}\nExisting saves: ${plan.impact.existingSaves.length}\nIncoming saves: ${plan.impact.incomingSaves.length}\n\n${incoming}\n\n` +
'If you continue, Chimera first creates a new verified snapshot. Account mismatch, zeroed param.sfo or changed source or destination saves stop the process. A byte-for-byte export check follows. Automatic rollback is intentionally not performed. Continue to the exact confirmation?'
);
if (!approved) return;
const phrase = await showExactConfirmation('The dry run was approved. Now confirm the actual restore.', plan.confirmationPhrase);
if (!phrase) return;
const result = await ecosystemBusy(button, () => window.pork.ecosystemSaveRestore(plan.token, phrase));
showToast(`${result.titleId} restored safely: ${result.verifiedEntries} file(s) verified byte-for-byte. Pre- and post-snapshots were retained.`, 'ok', 15000);
await loadSaveVault();
}
if (button?.classList.contains('ecosystem-save-download')) await ecosystemBusy(button, () => window.pork.ecosystemSaveDownload(row.dataset.id));
if (button?.classList.contains('ecosystem-save-delete') && await showExactConfirmation('Permanently delete this local save backup?', 'DELETE')) { await window.pork.ecosystemSaveDelete(row.dataset.id); await loadSaveVault(); }
} catch (error) { showToast(error.message, 'error', 15000); }
});
$('itemzflow-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, loadItemzflow).catch(error => showToast(error.message, 'error')));
$('itemzflow-homebrew-open').addEventListener('click', () => navigateEcosystemTab('homebrew'));
$('itemzflow-log-select').addEventListener('change', renderItemzflowEvents);
$('itemzflow-level').addEventListener('change', renderItemzflowEvents);
$('itemzflow-jobs-only').addEventListener('change', renderItemzflowEvents);
$('platform-lab-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, loadPlatformLab).catch(error => showToast(error.message, 'error')));
$('platform-bindings-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderPlatformBindings(await window.pork.platformBindingsRefresh())).catch(error => showToast(error.message, 'error')));
$('platform-bindings').addEventListener('click', event => {
const button = event.target.closest('.platform-binding-review');
if (!button) return;
ecosystemBusy(button, async () => { await window.pork.platformBindingReview({ id: button.dataset.id, tag: button.dataset.tag }); renderPlatformBindings(await window.pork.platformBindings()); }).then(() => showToast(platformLabText('reviewed', 'Upstream evidence reviewed.'), 'ok')).catch(error => showToast(error.message, 'error'));
});
$('platform-autoloader-preview').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderAutoloaderPreview(await window.pork.unifiedAutoloaderPreview({ id: $('platform-autoloader-profile').value }))).catch(error => showToast(error.message, 'error')));
$('platform-autoloader-profile').addEventListener('change', () => {
const profile = (_platformLabState.profiles || []).find(item => item.id === $('platform-autoloader-profile').value);
if (profile) $('platform-profile-json').value = JSON.stringify(profile, null, 2);
});
$('platform-profile-save').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
const profile = JSON.parse($('platform-profile-json').value);
await window.pork.unifiedAutoloaderSave(profile);
renderAutoloader(await window.pork.unifiedAutoloaderList());
}).then(() => showToast(platformLabText('profileSaved', 'Profile saved.'), 'ok')).catch(error => showToast(error.message, 'error')));
$('platform-profile-delete').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
await window.pork.unifiedAutoloaderRemove({ id: $('platform-autoloader-profile').value });
renderAutoloader(await window.pork.unifiedAutoloaderList());
}).then(() => showToast(platformLabText('profileDeleted', 'Profile deleted.'), 'ok')).catch(error => showToast(error.message, 'error')));
$('platform-profile-recovery').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
const result = await window.pork.unifiedAutoloaderRecovery({ id: $('platform-autoloader-profile').value });
$('platform-profile-output').innerHTML = `<pre class="ecosystem-log">${escHtml(JSON.stringify(result, null, 2))}</pre>`;
}).catch(error => showToast(error.message, 'error')));
$('platform-websrv-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderPlatformWebsrv(await window.pork.platformWebsrvDiscover())).catch(error => showToast(error.message, 'error')));
$('platform-remote-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderRemoteIntelligence(await window.pork.remotePlayIntelligence())).catch(error => showToast(error.message, 'error')));
$('platform-shadps4-scan').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => { const rootId = $('platform-shadps4-root').value; if (rootId) await window.pork.shadps4Select({ rootId }); renderShadps4Scan(await window.pork.shadps4Scan()); }).catch(error => showToast(error.message, 'error')));
$('platform-artifact-inspect').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
await window.pork.platformArtifactInspect({ rootId: $('platform-artifact-root').value, relativePath: $('platform-artifact-relative').value });
renderArtifacts(await window.pork.platformArtifacts());
}).then(() => showToast(platformLabText('artifactHashed', 'Artifact hashed.'), 'ok')).catch(error => showToast(error.message, 'error')));
$('platform-artifacts').addEventListener('click', event => {
const button = event.target.closest('.platform-artifact-review');
if (!button) return;
ecosystemBusy(button, async () => { await window.pork.platformArtifactReview({ artifactId: button.dataset.id, sha256: button.dataset.sha }); renderArtifacts(await window.pork.platformArtifacts()); }).then(() => showToast(platformLabText('hashReviewed', 'Exact hash reviewed.'), 'ok')).catch(error => showToast(error.message, 'error'));
});
$('platform-autoloader-stage').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
const artifactIds = [...document.querySelectorAll('.platform-artifact-select:checked')].map(input => input.value);
const result = await window.pork.platformAutoloaderStage({ profileId: $('platform-autoloader-profile').value, targetRootId: $('platform-stage-root').value, artifactIds });
$('platform-stage-output').innerHTML = `<div class="ecosystem-plan-safety"><strong>${escHtml(result.lifecycleState)}</strong> · ${escHtml(result.destinationLabel)}</div>`;
renderArtifacts(await window.pork.platformArtifacts());
}).then(() => showToast(platformLabText('staged', 'Profile staged atomically.'), 'ok')).catch(error => showToast(error.message, 'error')));
$('platform-search-form').addEventListener('submit', event => { event.preventDefault(); ecosystemBusy(event.submitter, async () => renderSearch(await window.pork.platformSearch({ query: $('platform-search-query').value }))).catch(error => showToast(error.message, 'error')); });
$('platform-shadps4-configure').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => {
await window.pork.shadps4LaunchConfigure({ rootId: $('platform-shadps4-exe-root').value, relativePath: $('platform-shadps4-exe-relative').value });
renderLaunchProbe(await window.pork.shadps4LaunchProbe());
}).then(() => showToast(platformLabText('launcherConfigured', 'Launcher configured.'), 'ok')).catch(error => showToast(error.message, 'error')));
$('platform-shadps4').addEventListener('click', event => {
const button = event.target.closest('.platform-shadps4-plan');
if (!button) return;
ecosystemBusy(button, async () => {
_platformLabState.launchPlan = await window.pork.shadps4LaunchPlan({ itemId: button.dataset.id });
$('platform-shadps4-launch').innerHTML = `<div class="ecosystem-plan-safety"><strong>${escHtml(_platformLabState.launchPlan.itemLabel)}</strong><div class="hint">${escHtml(platformLabText('exactConfirmation', 'Enter the exact confirmation shown below.'))}</div><input id="platform-shadps4-confirmation" value="" placeholder="${escHtml(_platformLabState.launchPlan.confirmation)}"><button id="platform-shadps4-launch-confirm" class="btn btn-accent">${escHtml(platformLabText('launch', 'Launch'))}</button></div>`;
}).catch(error => showToast(error.message, 'error'));
});
$('platform-shadps4-launch').addEventListener('click', event => {
const button = event.target.closest('#platform-shadps4-launch-confirm');
if (!button || !_platformLabState.launchPlan) return;
ecosystemBusy(button, async () => {
const result = await window.pork.shadps4Launch({ token: _platformLabState.launchPlan.token, confirmation: $('platform-shadps4-confirmation').value });
_platformLabState.launchPlan = null;
$('platform-shadps4-launch').innerHTML = `<div class="ecosystem-plan-safety"><strong>${escHtml(platformLabText('started', 'Started'))}</strong> · PID ${result.pid}</div>`;
}).catch(error => showToast(error.message, 'error'));
});
$('ecosystem-linux-refresh').addEventListener('click', event => ecosystemBusy(event.currentTarget, loadLinuxLab).catch(error => showToast(error.message, 'error')));
$('ecosystem-linux-components').addEventListener('click', event => { const button = event.target.closest('.ecosystem-open-upstream'); if (button) window.pork.openShell(button.dataset.url); });
$('ecosystem-timeline-start').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderTimeline(await window.pork.ecosystemTimelineStart(Number($('ecosystem-timeline-port').value)))).catch(error => showToast(error.message, 'error')));
$('ecosystem-timeline-stop').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderTimeline(await window.pork.ecosystemTimelineStop())).catch(error => showToast(error.message, 'error')));
$('ecosystem-timeline-import').addEventListener('click', event => ecosystemBusy(event.currentTarget, async () => renderTimeline(await window.pork.ecosystemTimelineImport())).catch(error => showToast(error.message, 'error')));
$('ecosystem-timeline-options').addEventListener('submit', event => { event.preventDefault(); ecosystemBusy(event.submitter, async () => renderTimeline(await window.pork.ecosystemTimelineConfigure({ enabled: $('ecosystem-timeline-persist').checked, retentionDays: Number($('ecosystem-timeline-retention').value), maxEvents: Number($('ecosystem-timeline-max').value) }))).then(() => showToast('Timeline policy saved; exports remain redacted by default.', 'ok')).catch(error => showToast(error.message, 'error')); });
$('ecosystem-timeline-export').addEventListener('click', event => ecosystemBusy(event.currentTarget, () => window.pork.ecosystemTimelineExport({ search: $('ecosystem-timeline-search').value, level: $('ecosystem-timeline-level').value })).catch(error => showToast(error.message, 'error')));
const scheduleTimelineFilter = () => { clearTimeout(_timelineFilterTimer); _timelineFilterTimer = setTimeout(() => loadTimeline().catch(error => showToast(error.message, 'error')), 250); };
$('ecosystem-timeline-search').addEventListener('input', scheduleTimelineFilter);
$('ecosystem-timeline-level').addEventListener('change', scheduleTimelineFilter);
window.pork?.on?.('ecosystem:timeline', () => { if (_ecosystemActiveTab === 'timeline' && document.querySelector('#page-ecosystem.active')) loadTimeline().catch(() => {}); });
window.pork?.on?.('ecosystem:health', () => { if (_ecosystemActiveTab === 'overview' && document.querySelector('#page-ecosystem.active')) loadEcosystemOverview().catch(() => {}); });
window.pork?.on?.('platform:operation', () => { if (_ecosystemActiveTab === 'platform-lab') window.pork.platformOperations().then(renderOperations).catch(() => {}); });
window.pork?.on?.('game-updates:progress', () => {
if (_ecosystemActiveTab !== 'game-updates' || !document.querySelector('#page-ecosystem.active')) return;
clearTimeout(_gameUpdateRefreshTimer); _gameUpdateRefreshTimer = setTimeout(() => loadGameUpdates().catch(() => {}), 500);
});
}
async function loadEcosystem({ background = false } = {}) {
buildEcosystemPage();
if (background) return loadEcosystemOverview();
const requestedTab = window.chimeraRequestedEcosystemTab;
if (requestedTab) {
_ecosystemActiveTab = requestedTab;
window.chimeraRequestedEcosystemTab = null;
document.querySelectorAll('.ecosystem-tab').forEach(button => {
const active = button.dataset.ecosystemTab === requestedTab;
button.classList.toggle('active', active);
button.setAttribute('aria-selected', String(active));
button.tabIndex = active ? 0 : -1;
});
document.querySelectorAll('.ecosystem-panel').forEach(panel => {
const active = panel.dataset.ecosystemPanel === requestedTab;
panel.classList.toggle('active', active);
panel.hidden = !active;
});
}
const tabLoad = loadEcosystemTab(_ecosystemActiveTab);
if (_ecosystemActiveTab !== 'overview' && !_ecosystemLoaded) {
const headerLoad = loadEcosystemOverview().catch(error => {
$('ecosystem-health').textContent = localizedEcosystemError(error.message || 'Status unavailable');
$('ecosystem-health').className = 'operations-health warning';
});
await Promise.all([tabLoad, headerLoad]);
return;
}
await tabLoad;
}
// end