1497 lines
175 KiB
JavaScript
1497 lines
175 KiB
JavaScript
'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
|