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

1072 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
(function initChimeraNavigation() {
const contract = window.ChimeraNavigationContract;
if (!contract) {
console.error('[navigation] ChimeraNavigationContract is missing.');
return;
}
const routeById = new Map(contract.routes.map(route => [route.id, route]));
const groupById = new Map(contract.groups.map(group => [group.id, group]));
const expandedGroups = new Set();
const iconPaths = {
home: '<path d="M3 9.5 10 3l7 6.5V17a1 1 0 0 1-1 1h-4v-5H8v5H4a1 1 0 0 1-1-1z"/>',
library: '<path d="M3 4.5h5.5A2.5 2.5 0 0 1 11 7v10H5a2 2 0 0 0-2 1.2zM17 4.5h-3.5A2.5 2.5 0 0 0 11 7v10h4a2 2 0 0 1 2 1.2z"/>',
console: '<rect x="2.5" y="5" width="15" height="10" rx="4"/><path d="M6 10h4M8 8v4M13.5 9.5h.01M15.5 11.5h.01"/>',
payload: '<path d="M11 2 5 11h4l-1 7 7-10h-4z"/>',
shield: '<path d="M10 2.5 16 5v4.5c0 4.1-2.4 6.8-6 8.5-3.6-1.7-6-4.4-6-8.5V5z"/><path d="m7 10 2 2 4-4"/>',
tools: '<path d="m12.5 3.5 4 4-9 9H3.5v-4zM10.5 5.5l4 4"/><path d="M13 15h4M15 13v4"/>',
settings: '<circle cx="10" cy="10" r="3"/><path d="M10 2v2M10 16v2M2 10h2M16 10h2M4.3 4.3l1.4 1.4M14.3 14.3l1.4 1.4M4.3 15.7l1.4-1.4M14.3 5.7l1.4-1.4"/>',
action: '<path d="M4 5.5h12M4 10h8M4 14.5h6"/><circle cx="15.5" cy="14.5" r="2"/>',
more: '<circle cx="4" cy="10" r="1"/><circle cx="10" cy="10" r="1"/><circle cx="16" cy="10" r="1"/>',
};
let currentRouteId = null;
let activePayloadStage = 'discover';
let navigationSequence = 0;
let commandGameCache = null;
let commandGameCacheAt = 0;
let commandSearchSequence = 0;
let currentTransferState = { jobs: [], activeCount: 0, queuedCount: 0, paused: false };
let currentActionCount = 0;
let expertMode = false;
const tr = (key, fallback) => typeof window.t === 'function' ? window.t(key, fallback) : fallback;
const routeKey = route => `navigation.routes.${route.id.replace(/\//g, '.')}`;
const routeLabel = route => tr(`${routeKey(route)}.label`, route.label);
const routeDescription = route => tr(`${routeKey(route)}.description`, route.description);
const groupLabel = group => tr(`navigation.groups.${group.id}`, group.label);
const activeLocale = () => window.i18n?.current === 'nl' ? 'nl-BE' : 'en-GB';
const escapeText = value => String(value ?? '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
function icon(name) {
return `<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.45" stroke-linecap="round" stroke-linejoin="round">${iconPaths[name] || iconPaths.home}</svg>`;
}
function normalizeRoute(value) {
let routeId = String(value || '').trim().replace(/^#\/?/, '').replace(/^\/+|\/+$/g, '');
try { routeId = decodeURIComponent(routeId); } catch (_) {}
routeId = contract.legacyAliases[routeId] || routeId;
if (!routeById.has(routeId)) return contract.defaultRoute;
const route = routeById.get(routeId);
if (route.expert && !expertMode) return groupById.get(route.group)?.defaultRoute || contract.defaultRoute;
return routeId;
}
async function syncExpertPermission() {
if (typeof window.pork?.sessionExpertMode !== 'function') return expertMode;
try {
const result = await window.pork.sessionExpertMode(expertMode, expertMode ? 'ENABLE EXPERT MODE' : undefined);
expertMode = result?.enabled === true;
} catch (_) {
expertMode = false;
}
document.body.classList.toggle('chimera-expert-mode', expertMode);
const toggle = document.getElementById('chimera-expert-mode');
if (toggle) toggle.checked = expertMode;
return expertMode;
}
setTimeout(syncExpertPermission);
function routeFromLocation() {
return normalizeRoute(window.location.hash);
}
function routeUrl(routeId) {
return `#/${encodeURI(routeId)}`;
}
function mountSidebar() {
const navList = document.querySelector('#sidebar .nav-list');
if (!navList) return;
navList.className = 'nav-list chimera-nav-root';
navList.setAttribute('aria-label', 'Chimera main navigation');
navList.innerHTML = contract.groups.map(group => {
const isHome = group.routes.length === 1 && group.routes[0].id === 'home';
const routeLinks = group.routes.map(route => `
<li>
<a href="${routeUrl(route.id)}" class="chimera-route-link${route.expert ? ' chimera-expert-route' : ''}" data-route="${escapeText(route.id)}" title="${escapeText(routeDescription(route))}">
<span data-i18n="${escapeText(`${routeKey(route)}.label`)}">${escapeText(routeLabel(route))}</span>
${route.risk === 'confirm' ? `<span class="chimera-risk-dot" title="${escapeText(tr('navigation.confirmationRequired', 'Action requires explicit confirmation'))}">!</span>` : ''}
${route.badge === 'transfer' ? '<span id="nav-transfer-badge" class="nav-badge" hidden></span>' : ''}
</a>
</li>`).join('');
return `
<li class="chimera-domain" data-nav-group="${escapeText(group.id)}">
${isHome ? `
<a href="${routeUrl(group.defaultRoute)}" class="chimera-domain-button chimera-domain-home" data-route="${escapeText(group.defaultRoute)}">
${icon(group.icon)}<span data-i18n="${escapeText(`navigation.groups.${group.id}`)}">${escapeText(groupLabel(group))}</span>
</a>` : `
<button class="chimera-domain-button" type="button" data-group-toggle="${escapeText(group.id)}" aria-expanded="false">
${icon(group.icon)}<span data-i18n="${escapeText(`navigation.groups.${group.id}`)}">${escapeText(groupLabel(group))}</span><span class="chimera-domain-chevron">›</span>
</button>
<ul class="chimera-domain-routes" data-group-routes="${escapeText(group.id)}" hidden>${routeLinks}</ul>`}
</li>`;
}).join('') + `<li class="chimera-expert-toggle"><label><input id="chimera-expert-mode" type="checkbox" ${expertMode ? 'checked' : ''}><span><strong data-i18n="navigation.expert.label">Expert tools</strong><small data-i18n="navigation.expert.description">Database, logs and low-level tools</small></span></label></li>`;
document.body.classList.toggle('chimera-expert-mode', expertMode);
const aboutLabel = document.querySelector('#sidebar-donate .donate-label');
if (aboutLabel) {
aboutLabel.removeAttribute('data-i18n');
aboutLabel.textContent = tr('navigation.about', 'About Chimera');
aboutLabel.dataset.i18n = 'navigation.about';
}
}
function mountShell() {
document.body.classList.add('chimera-shell-v2');
const content = document.getElementById('content');
if (!content) return;
const toolbar = document.createElement('div');
toolbar.id = 'chimera-shell-toolbar';
toolbar.innerHTML = `
<div id="chimera-shell-health" class="chimera-shell-health is-offline" role="status" aria-live="polite" aria-label="${escapeText(tr('connection.consoleState', 'PS5 connection status'))}" data-i18n-aria-label="connection.consoleState">
<span class="chimera-shell-health-dot" aria-hidden="true"></span>
<span class="chimera-shell-health-copy">
<small data-i18n="connection.console">Console</small>
<strong id="chimera-shell-health-label" data-i18n="connection.disconnected">Disconnected</strong>
</span>
</div>
<span class="chimera-toolbar-spacer" aria-hidden="true"></span>
<button id="chimera-command-open" class="chimera-toolbar-button" type="button" aria-haspopup="dialog">
<span data-i18n="navigation.search.open">Search or navigate</span><kbd>Ctrl K</kbd>
</button>
<button id="chimera-jobs-open" class="chimera-toolbar-button chimera-jobs-button" type="button" aria-haspopup="dialog" aria-label="${escapeText(tr('navigation.actionCenter.open', 'Action Center'))}" data-i18n-aria-label="navigation.actionCenter.open">
<span class="chimera-jobs-pulse"></span><span data-i18n="navigation.actionCenter.open">Action Center</span><span id="chimera-job-count" class="nav-badge" hidden></span>
</button>`;
const mobileToggle = document.getElementById('mobile-nav-toggle');
if (mobileToggle) toolbar.prepend(mobileToggle);
const header = document.createElement('header');
header.id = 'chimera-route-header';
header.innerHTML = `
<div class="chimera-route-copy">
<nav id="chimera-breadcrumbs" aria-label="Breadcrumb"></nav>
<h1 id="chimera-route-title">Chimera</h1>
<p id="chimera-route-description"></p>
</div>
<div class="chimera-route-actions">
<span id="chimera-route-availability" class="chimera-route-availability" role="status" hidden></span>
<span id="chimera-route-risk" class="chimera-route-risk" hidden data-i18n="navigation.confirmationRequired">Explicit confirmation required</span>
<button id="chimera-route-refresh" class="btn btn-sm" type="button">↻ <span data-i18n="common.refresh">Refresh</span></button>
</div>`;
content.prepend(header);
content.prepend(toolbar);
const palette = document.createElement('div');
palette.id = 'chimera-command-overlay';
palette.className = 'chimera-overlay';
palette.hidden = true;
palette.innerHTML = `
<section id="chimera-command-dialog" class="chimera-command-dialog" role="dialog" aria-modal="true" aria-labelledby="chimera-command-title">
<div class="chimera-command-head">
<div><span class="chimera-command-kicker">Chimera</span><h2 id="chimera-command-title" data-i18n="navigation.search.title">Go to…</h2></div>
<button class="chimera-icon-button" data-command-close type="button" aria-label="Close">×</button>
</div>
<div class="chimera-command-search" role="search">
<span class="chimera-command-search-icon" aria-hidden="true">⌕</span>
<input id="chimera-command-input" type="search" autocomplete="off" spellcheck="false" aria-label="Search Chimera" aria-controls="chimera-command-results" aria-describedby="chimera-command-help" placeholder="Search feature, tool, game or title ID…" data-i18n-placeholder="navigation.search.placeholder"/>
<button id="chimera-command-clear" class="chimera-command-clear" type="button" aria-label="Clear search" title="Clear search" hidden>×</button>
</div>
<div class="chimera-command-meta">
<span id="chimera-command-help" data-i18n="navigation.search.help">Type to filter features; game search starts after 2 characters.</span>
<span id="chimera-command-status" role="status" aria-live="polite"></span>
</div>
<div id="chimera-command-results" class="chimera-command-results" role="listbox" aria-label="Search results"></div>
<footer><span><kbd>↑</kbd><kbd>↓</kbd> <span data-i18n="navigation.search.select">select</span></span><span><kbd>Enter</kbd> <span data-i18n="navigation.search.openResult">open</span></span><span><kbd>Esc</kbd> <span data-i18n="navigation.search.close">close</span></span></footer>
</section>`;
document.body.appendChild(palette);
const drawer = document.createElement('div');
drawer.id = 'chimera-jobs-overlay';
drawer.className = 'chimera-overlay chimera-drawer-overlay';
drawer.hidden = true;
drawer.innerHTML = `
<aside id="chimera-jobs-drawer" class="chimera-jobs-drawer" role="dialog" aria-modal="true" aria-labelledby="chimera-jobs-title">
<div class="chimera-drawer-head"><div><span class="chimera-command-kicker" data-i18n="navigation.actionCenter.kicker">System attention</span><h2 id="chimera-jobs-title" data-i18n="navigation.actionCenter.title">Action Center</h2></div><button class="chimera-icon-button" data-jobs-close type="button" aria-label="Close">×</button></div>
<section class="chimera-action-section" aria-labelledby="chimera-action-title">
<div class="chimera-drawer-section-title"><h3 id="chimera-action-title" data-i18n="navigation.actionCenter.attention">Needs attention</h3><button class="btn btn-sm" data-route="console/status" type="button" data-i18n="navigation.actionCenter.health">Console health</button></div>
<div id="chimera-action-list" class="chimera-action-list"><p class="hint" data-i18n="common.loading">Loading…</p></div>
</section>
<div class="chimera-drawer-section-title"><h3 data-i18n="navigation.actionCenter.transfers">Transfers</h3></div>
<div id="chimera-jobs-summary" class="chimera-jobs-summary"></div>
<div id="chimera-jobs-list" class="chimera-jobs-list"></div>
<button class="btn btn-accent" data-route="tools/transfers" type="button" data-i18n="navigation.jobs.open">Open full Transfer Manager</button>
</aside>`;
document.body.appendChild(drawer);
}
function mountCompanionDock() {
if (document.getElementById('chimera-companion-dock')) return;
const dock = document.createElement('nav');
dock.id = 'chimera-companion-dock';
dock.className = 'chimera-companion-dock';
dock.setAttribute('aria-label', tr('navigation.companion.label', 'Companion shortcuts'));
dock.dataset.i18nAriaLabel = 'navigation.companion.label';
dock.innerHTML = `
<a href="${routeUrl('home')}" data-route="home" data-companion-group="home">
${icon('home')}<span data-i18n="navigation.groups.home">Home</span>
</a>
<a href="${routeUrl('library/games')}" data-route="library/games" data-companion-group="library">
${icon('library')}<span data-i18n="nav.games">Games</span>
</a>
<a href="${routeUrl('console/status')}" data-route="console/status" data-companion-group="console">
${icon('console')}<span data-i18n="navigation.groups.console">Console</span>
</a>
<button id="chimera-companion-actions" type="button" aria-haspopup="dialog">
${icon('action')}<span data-i18n="navigation.companion.actions">Actions</span>
<b id="chimera-companion-count" hidden></b>
</button>
<button id="chimera-companion-more" type="button" aria-controls="sidebar" aria-expanded="false">
${icon('more')}<span data-i18n="navigation.companion.more">More</span>
</button>`;
document.body.appendChild(dock);
}
function mountHomeCommandCenter() {
const page = document.getElementById('page-dashboard');
const template = document.getElementById('chimera-home-command-center-template');
if (!page || !template || document.getElementById('chimera-home-command-center')) return;
const launchpad = document.createElement('section');
launchpad.id = 'chimera-home-command-center';
launchpad.setAttribute('aria-label', 'Chimera status and next steps');
launchpad.append(template.content.cloneNode(true));
const pageHeader = page.querySelector(':scope > .page-header');
if (pageHeader) pageHeader.insertAdjacentElement('afterend', launchpad);
else page.prepend(launchpad);
}
function mountSettingsNavigator() {
const page = document.getElementById('page-settings');
if (!page || document.getElementById('chimera-settings-navigator')) return;
const cards = [...page.querySelectorAll(':scope > .card')];
if (!cards.length) return;
const categoryFor = (card, label) => {
const id = String(card.id || '');
const value = `${id} ${label}`.toLowerCase();
if (/appearance|language/.test(value)) return 'setup';
if (/console profile|console identity|ftp connection|payload manager|cheats manager|save manager/.test(value)) return 'console';
if (/game source|conversion|remote game|metadata|ps4 package|package preservation/.test(value)) return 'storage';
if (/automation advisor/.test(value)) return 'automation';
if (/two-factor|passkey|security/.test(value)) return 'security';
if (/payload remote|integration|notification|companion/.test(value)) return 'integrations';
return 'advanced';
};
const sections = cards.map((card, index) => {
const heading = card.querySelector('h2, h3, h4');
const label = heading?.textContent?.trim() || `${tr('settingsNavigator.section', 'Section')} ${index + 1}`;
if (!card.id) card.id = `settings-section-${index + 1}`;
card.style.scrollMarginTop = '108px';
const category = categoryFor(card, label);
card.dataset.settingsCategory = category;
return { card, id: card.id, label, category };
});
const categories = [
['setup', tr('settingsNavigator.setup', 'Setup')],
['console', tr('settingsNavigator.console', 'Console')],
['storage', tr('settingsNavigator.storage', 'Storage')],
['automation', tr('settingsNavigator.automation', 'Automation')],
['integrations', tr('settingsNavigator.integrations', 'Integrations')],
['security', tr('settingsNavigator.security', 'Security')],
['advanced', tr('settingsNavigator.advanced', 'Advanced')],
];
let activeCategory = 'setup';
const navigator = document.createElement('section');
navigator.id = 'chimera-settings-navigator';
navigator.className = 'chimera-settings-navigator';
navigator.setAttribute('aria-label', 'Search and navigate settings');
navigator.innerHTML = `
<div class="chimera-settings-navigator-copy">
<strong data-i18n="settingsNavigator.title">Find a setting quickly</strong>
<span id="chimera-settings-filter-status" role="status" aria-live="polite">${sections.length} ${tr('settingsNavigator.sections', 'sections')}</span>
</div>
<div class="chimera-settings-controls">
<div class="chimera-settings-search" role="search">
<span aria-hidden="true">⌕</span>
<input id="chimera-settings-search" type="search" autocomplete="off" spellcheck="false" aria-label="Search settings" placeholder="Search FTP, paths, notifications…" data-i18n-placeholder="settingsNavigator.placeholder"/>
<button id="chimera-settings-clear" type="button" aria-label="Clear settings filter" title="Clear filter" hidden>×</button>
</div>
<label class="chimera-settings-jump">
<span data-i18n="settingsNavigator.jump">Jump to</span>
<select id="chimera-settings-jump" aria-label="Jump to settings section" data-i18n-aria-label="settingsNavigator.jumpAria">
<option value="" data-i18n="settingsNavigator.choose">Choose a section…</option>
${sections.map(section => `<option value="${escapeText(section.id)}">${escapeText(section.label)}</option>`).join('')}
</select>
</label>
</div>
<div class="chimera-settings-categories" role="tablist" aria-label="Settings categories">
${categories.map(([id, label], index) => `<button type="button" role="tab" data-settings-category="${id}" aria-selected="${index === 0 ? 'true' : 'false'}">${label}<span>${sections.filter(section => section.category === id).length}</span></button>`).join('')}
</div>`;
const pageHeader = page.querySelector(':scope > .page-header');
if (pageHeader) pageHeader.insertAdjacentElement('afterend', navigator);
else page.prepend(navigator);
const input = navigator.querySelector('#chimera-settings-search');
const clearButton = navigator.querySelector('#chimera-settings-clear');
const status = navigator.querySelector('#chimera-settings-filter-status');
const jump = navigator.querySelector('#chimera-settings-jump');
const syncSections = () => {
sections.forEach((section, index) => {
const liveLabel = section.card.querySelector('h2, h3, h4')?.textContent?.trim() || section.label;
section.label = liveLabel;
if (jump.options[index + 1]) jump.options[index + 1].textContent = liveLabel;
});
};
const applyFilter = value => {
const query = String(value || '').trim().toLocaleLowerCase(activeLocale());
let visible = 0;
sections.forEach(section => {
const searchText = section.card.textContent.toLocaleLowerCase(activeLocale());
const matchesCategory = activeCategory === 'all' || section.category === activeCategory;
const matches = matchesCategory && (!query || searchText.includes(query));
section.card.toggleAttribute('data-settings-filter-hidden', !matches);
if (matches) visible += 1;
});
clearButton.hidden = !query;
status.textContent = query || activeCategory !== 'all'
? `${visible} ${tr('settingsNavigator.of', 'of')} ${sections.length} ${tr('settingsNavigator.sections', 'sections')}`
: `${sections.length} ${tr('settingsNavigator.sections', 'sections')}`;
};
applyFilter('');
navigator.querySelector('.chimera-settings-categories').addEventListener('click', event => {
const button = event.target.closest('[data-settings-category]');
if (!button) return;
activeCategory = button.dataset.settingsCategory;
navigator.querySelectorAll('[data-settings-category]').forEach(candidate => candidate.setAttribute('aria-selected', String(candidate === button)));
applyFilter(input.value);
});
input.addEventListener('input', event => applyFilter(event.target.value));
clearButton.addEventListener('click', () => {
input.value = '';
applyFilter('');
input.focus();
});
jump.addEventListener('focus', syncSections);
jump.addEventListener('pointerdown', syncSections);
jump.addEventListener('change', event => {
const section = sections.find(item => item.id === event.target.value);
if (!section) return;
input.value = '';
activeCategory = section.category;
navigator.querySelectorAll('[data-settings-category]').forEach(candidate => candidate.setAttribute('aria-selected', String(candidate.dataset.settingsCategory === activeCategory)));
applyFilter('');
section.card.scrollIntoView({
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
block: 'center',
});
event.target.value = '';
});
window.addEventListener('chimera:language-changed', () => {
syncSections();
applyFilter(input.value);
});
window.setTimeout(syncSections, 0);
}
function setExpandedGroup(groupId, expanded) {
if (expanded) {
for (const openGroup of [...expandedGroups]) {
if (openGroup !== groupId) setExpandedGroup(openGroup, false);
}
}
if (expanded) expandedGroups.add(groupId); else expandedGroups.delete(groupId);
const domain = [...document.querySelectorAll('[data-nav-group]')].find(item => item.dataset.navGroup === groupId);
if (!domain) return;
const button = domain.querySelector('[data-group-toggle]');
const routes = domain.querySelector('[data-group-routes]');
if (button) button.setAttribute('aria-expanded', String(expanded));
if (routes) routes.hidden = !expanded;
domain.classList.toggle('expanded', expanded);
}
function updateSidebar(route) {
const group = groupById.get(route.group);
if (group && group.id !== 'home') setExpandedGroup(group.id, true);
document.querySelectorAll('[data-route]:not([data-payload-stage])').forEach(element => {
const active = element.dataset.route === route.id;
element.classList.toggle('active', active);
if (element.matches('a,button')) {
if (active) element.setAttribute('aria-current', 'page');
else element.removeAttribute('aria-current');
}
});
updatePayloadWorkspaceStage(route.id);
document.querySelectorAll('[data-nav-group]').forEach(element => element.classList.toggle('active', element.dataset.navGroup === route.group));
document.querySelectorAll('#chimera-companion-dock [data-companion-group]').forEach(element => {
const active = element.dataset.route === 'library/games'
? route.id === 'library/games'
: element.dataset.companionGroup === route.group;
element.classList.toggle('active', active);
if (active) element.setAttribute('aria-current', 'page');
else element.removeAttribute('aria-current');
});
document.getElementById('chimera-companion-more')?.classList.toggle(
'active',
!['home', 'library', 'console'].includes(route.group),
);
}
function defaultPayloadStage(routeId) {
if (routeId === 'payloads/updates') return 'review';
if (routeId === 'payloads/boot') return 'boot';
if (routeId === 'payloads/installed') return 'discover';
return null;
}
function updatePayloadWorkspaceStage(routeId, requestedStage = null) {
const currentButton = document.querySelector(`[data-payload-stage="${activePayloadStage}"]`);
const stage = requestedStage
|| (currentButton?.dataset.route === routeId ? activePayloadStage : defaultPayloadStage(routeId));
if (!stage) return;
activePayloadStage = stage;
const stages = ['discover', 'review', 'stage', 'boot'];
const currentIndex = stages.indexOf(stage);
document.querySelectorAll('[data-payload-stage]').forEach(button => {
const buttonIndex = stages.indexOf(button.dataset.payloadStage);
const current = button.dataset.payloadStage === stage;
button.classList.toggle('is-current', current);
button.classList.toggle('is-complete', buttonIndex >= 0 && buttonIndex < currentIndex);
button.setAttribute('aria-pressed', String(current));
if (current) button.setAttribute('aria-current', 'step');
else button.removeAttribute('aria-current');
});
}
function localizeSidebar() {
contract.routes.forEach(route => {
document.querySelectorAll(`.chimera-route-link[data-route="${route.id}"]`).forEach(link => {
const label = link.querySelector(':scope > span:first-child');
if (label) label.textContent = routeLabel(route);
link.title = routeDescription(route);
});
});
contract.groups.forEach(group => {
const label = document.querySelector(`[data-nav-group="${group.id}"] .chimera-domain-button > span:not(.chimera-domain-chevron)`);
if (label) label.textContent = groupLabel(group);
});
const aboutLabel = document.querySelector('#sidebar-donate .donate-label');
if (aboutLabel) aboutLabel.textContent = tr('navigation.about', 'About Chimera');
}
function updateRouteHeader(route) {
const group = groupById.get(route.group);
const localizedGroup = groupLabel(group);
const localizedLabel = routeLabel(route);
const localizedDescription = routeDescription(route);
const breadcrumbs = document.getElementById('chimera-breadcrumbs');
if (breadcrumbs) breadcrumbs.innerHTML = route.id === 'home'
? '<span>Home</span>'
: `<button type="button" data-route="${escapeText(group.defaultRoute)}">${escapeText(localizedGroup)}</button><span aria-hidden="true">/</span><span>${escapeText(localizedLabel)}</span>`;
const title = document.getElementById('chimera-route-title');
const description = document.getElementById('chimera-route-description');
const risk = document.getElementById('chimera-route-risk');
if (title) title.textContent = localizedLabel;
if (description) description.textContent = localizedDescription;
if (risk) risk.hidden = route.risk !== 'confirm';
document.title = `${localizedLabel} — Chimera`;
}
function clearRouteFocus() {
document.querySelectorAll('[data-route-hidden="true"]').forEach(element => element.removeAttribute('data-route-hidden'));
document.querySelectorAll('[data-route-focus]').forEach(element => element.removeAttribute('data-route-focus'));
}
function hideElement(element) {
if (element) element.setAttribute('data-route-hidden', 'true');
}
function showOnlyPageChildren(pageId, allowedIds) {
const page = document.getElementById(pageId);
if (!page) return;
const allowed = new Set(allowedIds);
[...page.children].forEach(child => {
if (child.classList.contains('page-header') || child.hasAttribute('data-route-persistent')) return;
if (!child.id || !allowed.has(child.id)) hideElement(child);
});
}
function applyJailbreakFocus(focus) {
if (!focus) return;
const profiles = {
payloads: ['ps5-payload-manager-card', 'console-services-card', 'payload-update-card', 'payload-manager-card'],
boot: ['payload-manager-card', 'y2jb-generator-card', 'bdjb-generator-card'],
power: ['system-state-card'],
};
showOnlyPageChildren('page-jailbreak', profiles[focus] || profiles.payloads);
const workspaceNav = document.querySelector('#page-jailbreak > .payload-workspace-nav');
if (workspaceNav && focus === 'power') hideElement(workspaceNav);
const sectionVisibility = {
payloads: ['payload-local-section', 'payload-sender-section'],
boot: ['payload-autoloader-section', 'autoloader-snapshots-section'],
};
if (sectionVisibility[focus]) {
const visible = new Set(sectionVisibility[focus]);
document.querySelectorAll('#payload-manager-card > .jb-section').forEach(section => {
if (!visible.has(section.id)) hideElement(section);
});
}
document.getElementById('page-jailbreak')?.setAttribute('data-route-focus', focus);
}
function applyReliabilityFocus(focus) {
if (!focus) return;
const visibleIds = {
replication: ['operations-reliability-warning', 'operations-replication-card'],
monitoring: ['operations-monitoring-card'],
security: ['operations-sessions-card', 'operations-password-card'],
}[focus];
if (!visibleIds) return;
const visible = new Set(visibleIds);
document.querySelectorAll('[data-operations-panel="reliability"] .card[id]').forEach(card => {
if (!visible.has(card.id)) hideElement(card);
});
document.querySelectorAll('[data-operations-panel="reliability"] .operations-two-column').forEach(column => {
const cards = [...column.querySelectorAll(':scope > .card')];
if (cards.length && cards.every(card => card.hasAttribute('data-route-hidden'))) hideElement(column);
});
document.querySelector('[data-operations-panel="reliability"]')?.setAttribute('data-route-focus', focus);
}
function applyRouteFocus(route) {
if (route.page === 'jailbreak') return applyJailbreakFocus(route.focus);
if (route.page === 'operations' && route.panel === 'reliability') return applyReliabilityFocus(route.focus);
}
function activatePage(page) {
document.querySelectorAll('.page').forEach(element => element.classList.toggle('active', element.id === `page-${page}`));
const isEmbed = page === 'savemgr';
document.getElementById('content')?.classList.toggle('embed-page-active', isEmbed);
if (!isEmbed) {
const content = document.getElementById('content');
if (content) content.scrollTop = 0;
}
closeMobileNav();
if (typeof syncPinStates === 'function') syncPinStates();
if (typeof scheduleBackgroundWarm === 'function') scheduleBackgroundWarm(800);
}
function selectEcosystemPanel(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;
});
}
async function loadEcosystemRoute(route, force) {
const wasStale = force || pageIsStale('ecosystem');
window.chimeraRequestedEcosystemTab = route.tab;
await ensurePageReady('ecosystem', { force: !!force });
selectEcosystemPanel(route.tab);
if (!wasStale && typeof loadEcosystemTab === 'function') await loadEcosystemTab(route.tab);
}
async function loadOperationsRoute(route, force) {
await ensurePageReady('operations', { force: !!force });
document.querySelectorAll('.operations-tab').forEach(button => {
const active = button.dataset.operationsTab === route.panel;
button.classList.toggle('active', active);
button.setAttribute('aria-selected', String(active));
button.tabIndex = active ? 0 : -1;
});
document.querySelectorAll('.operations-panel').forEach(panel => {
const active = panel.dataset.operationsPanel === route.panel;
panel.classList.toggle('active', active);
panel.hidden = !active;
});
if (route.panel === 'shadowmount' && typeof loadOperationsShadowMount === 'function') await loadOperationsShadowMount();
if (route.panel === 'packages' && typeof loadOperationsPackages === 'function') await loadOperationsPackages();
if (route.panel === 'reliability' && typeof loadOperationsReliability === 'function') await loadOperationsReliability({ focus: route.focus });
if (route.panel === 'audit' && typeof loadOperationsAudit === 'function') await loadOperationsAudit();
if (route.panel === 'diagnostics' && typeof loadKlog === 'function') await loadKlog();
}
async function hydrateRoute(route, force) {
if (route.page === 'ecosystem') await loadEcosystemRoute(route, force);
else if (route.page === 'operations') await loadOperationsRoute(route, force);
else await ensurePageReady(route.page, { force: !!force });
}
function writeHistory(routeId, mode) {
const url = routeUrl(routeId);
if (mode === 'push') window.history.pushState({ chimeraRoute: routeId }, '', url);
if (mode === 'replace') window.history.replaceState({ chimeraRoute: routeId }, '', url);
}
async function navigateRoute(value, options = {}) {
const routeId = normalizeRoute(value);
const route = routeById.get(routeId);
const historyMode = options.history ?? 'push';
if (!route) return;
if (currentRouteId === routeId && !options.force) {
closeMobileNav();
if (historyMode === 'replace') writeHistory(routeId, 'replace');
return;
}
const sequence = ++navigationSequence;
clearRouteFocus();
currentRouteId = routeId;
if (historyMode !== 'none') writeHistory(routeId, historyMode);
updateSidebar(route);
updateRouteHeader(route);
// Apply deep-route disclosure while the destination page is still hidden.
// This prevents unrelated Reliability cards from flashing for one frame.
applyRouteFocus(route);
activatePage(route.page);
document.body.setAttribute('data-chimera-route', route.id);
const refresh = document.getElementById('chimera-route-refresh');
const availability = document.getElementById('chimera-route-availability');
if (availability) availability.hidden = true;
if (refresh) { refresh.disabled = true; refresh.textContent = tr('common.loading', 'Loading…'); }
try {
await window.chimeraI18nReady;
if (sequence !== navigationSequence) return;
await hydrateRoute(route, options.force);
if (sequence !== navigationSequence) return;
applyRouteFocus(route);
} catch (error) {
if (typeof isExpectedUnavailable === 'function' && isExpectedUnavailable(error)) {
console.info(`[navigation] ${route.id} temporarily unavailable: ${error.message}`);
if (availability) {
availability.textContent = `${tr('navigation.unavailable', 'Unavailable')}: ${friendlyMessage(error?.message || error)}`;
availability.hidden = false;
}
} else {
console.error(`[navigation] ${route.id}`, error);
const failure = `${tr('navigation.loadFailed', 'Could not fully load')} ${routeLabel(route)}: ${friendlyMessage(error?.message || error)}`;
if (availability) {
availability.textContent = `${failure}. ${tr('navigation.retryHint', 'Use Refresh to retry.')}`;
availability.hidden = false;
} else if (typeof showToast === 'function') {
showToast(failure, 'error', 9000);
}
}
} finally {
if (sequence === navigationSequence && refresh) { refresh.disabled = false; refresh.innerHTML = `↻ <span data-i18n="common.refresh">${escapeText(tr('common.refresh', 'Refresh'))}</span>`; }
}
}
function openCommandPalette() {
const overlay = document.getElementById('chimera-command-overlay');
if (!overlay) return;
document.body.classList.add('chimera-modal-open');
const input = document.getElementById('chimera-command-input');
input.value = '';
document.getElementById('chimera-command-clear')?.setAttribute('hidden', '');
renderCommandResults('');
if (typeof openAccessibleDialog === 'function') openAccessibleDialog(overlay, input);
else { overlay.hidden = false; requestAnimationFrame(() => input.focus()); }
}
function closeCommandPalette() {
const overlay = document.getElementById('chimera-command-overlay');
if (typeof closeAccessibleDialog === 'function') closeAccessibleDialog(overlay);
else if (overlay) overlay.hidden = true;
if (document.getElementById('chimera-jobs-overlay')?.hidden !== false) document.body.classList.remove('chimera-modal-open');
}
function routeMatches(route, query) {
const group = groupById.get(route.group);
return [route.label, routeLabel(route), route.description, routeDescription(route), route.id, group?.label, groupLabel(group), ...(route.keywords || [])].join(' ').toLocaleLowerCase(activeLocale()).includes(query);
}
function commandRouteMarkup(routes) {
if (!routes.length) return '';
return `<div class="chimera-command-section-label">${escapeText(tr('navigation.search.features', 'Features'))}</div>${routes.map(route => {
const group = groupById.get(route.group);
return `<button class="chimera-command-result" type="button" role="option" aria-selected="false" data-command-route="${escapeText(route.id)}"><span class="chimera-command-result-icon">${icon(group.icon)}</span><span><strong>${escapeText(routeLabel(route))}</strong><small>${escapeText(groupLabel(group))} · ${escapeText(routeDescription(route))}</small></span><span class="chimera-command-arrow">↵</span></button>`;
}).join('')}`;
}
function commandGameMarkup(games) {
if (!games.length) return '';
return `<div class="chimera-command-section-label">Games</div>${games.map(game => {
const name = game.prospero_name || game.title || game.game_id;
return `<button class="chimera-command-result" type="button" role="option" aria-selected="false" data-command-game="${escapeText(game.game_id)}"><span class="chimera-command-game-cover">${game.prospero_icon_url ? `<img src="${escapeText(game.prospero_icon_url)}" alt="" loading="lazy"/>` : '◫'}</span><span><strong>${escapeText(name)}</strong><small>${escapeText(game.game_id)}${game.prospero_publisher ? ` · ${escapeText(game.prospero_publisher)}` : ''}</small></span><span class="chimera-command-arrow">↵</span></button>`;
}).join('')}`;
}
function recentCommandQueries() {
try {
const value = JSON.parse(localStorage.getItem('chimera.search.recent') || '[]');
return Array.isArray(value) ? value.filter(item => typeof item === 'string' && item.trim()).slice(0, 6) : [];
} catch (_) { return []; }
}
function rememberCommandQuery(value) {
const query = String(value || '').trim();
if (query.length < 2) return;
const recent = [query, ...recentCommandQueries().filter(item => item.toLocaleLowerCase(activeLocale()) !== query.toLocaleLowerCase(activeLocale()))].slice(0, 6);
localStorage.setItem('chimera.search.recent', JSON.stringify(recent));
}
function recentCommandMarkup() {
const recent = recentCommandQueries();
if (!recent.length) return '';
return `<div class="chimera-command-section-label">${escapeText(tr('navigation.search.recent', 'Recent searches'))}</div><div class="chimera-command-recent">${recent.map(query => `<button type="button" data-command-query="${escapeText(query)}">${escapeText(query)}</button>`).join('')}</div>`;
}
function resetCommandSelection() {
const first = document.querySelector('#chimera-command-results .chimera-command-result');
if (first) {
first.classList.add('selected');
first.setAttribute('aria-selected', 'true');
}
}
function updateCommandStatus(message, busy = false) {
const results = document.getElementById('chimera-command-results');
const status = document.getElementById('chimera-command-status');
if (results) results.setAttribute('aria-busy', String(busy));
if (status) status.textContent = message;
}
async function getCommandGames(query = '') {
if (!window.pork.isWebMode && Array.isArray(state?.games) && state.games.length) return state.games;
const cacheKey = String(query || '').trim().toLocaleLowerCase(activeLocale());
if (commandGameCache?.key === cacheKey && Date.now() - commandGameCacheAt < 60_000) return commandGameCache.games;
const result = await window.pork.listGames(cacheKey ? { search: cacheKey, limit: 24 } : { limit: 24 });
commandGameCache = { key: cacheKey, games: Array.isArray(result) ? result : (result?.games || []) };
commandGameCacheAt = Date.now();
return commandGameCache.games;
}
async function renderCommandResults(value) {
const results = document.getElementById('chimera-command-results');
if (!results) return;
const query = String(value || '').trim().toLocaleLowerCase(activeLocale());
const sequence = ++commandSearchSequence;
const clearButton = document.getElementById('chimera-command-clear');
if (clearButton) clearButton.hidden = !query;
const availableRoutes = contract.routes.filter(route => expertMode || !route.expert);
const routes = (query ? availableRoutes.filter(route => routeMatches(route, query)) : availableRoutes.filter(route => ['home', 'library/games', 'console/status', 'payloads/installed', 'tools/transfers', 'settings/general'].includes(route.id))).slice(0, 12);
const recentMarkup = query ? '' : recentCommandMarkup();
results.innerHTML = `${recentMarkup}${commandRouteMarkup(routes)}` || `<p class="chimera-command-empty">${escapeText(tr('navigation.search.noFeature', 'No feature found. Keep typing to search games.'))}</p>`;
resetCommandSelection();
if (query.length < 2) {
updateCommandStatus(query
? `${routes.length} ${tr('navigation.search.featuresLower', 'features')} · ${tr('navigation.search.oneMore', 'type 1 more character for games')}`
: `${routes.length} ${tr('navigation.search.quickRoutes', 'quick routes')}`);
return;
}
updateCommandStatus(`${routes.length} ${tr('navigation.search.featuresLower', 'features')} · ${tr('navigation.search.searchingGames', 'searching games…')}`, true);
try {
const games = (await getCommandGames(query)).filter(game => [game.prospero_name, game.title, game.game_id, game.prospero_publisher].filter(Boolean).join(' ').toLocaleLowerCase(activeLocale()).includes(query)).slice(0, 8);
if (sequence !== commandSearchSequence) return;
results.innerHTML = `${commandRouteMarkup(routes)}${commandGameMarkup(games)}` || `<p class="chimera-command-empty">${escapeText(tr('navigation.search.noMatches', 'No matches found.'))}</p>`;
resetCommandSelection();
updateCommandStatus(`${routes.length + games.length} ${tr('navigation.search.results', 'results')}`);
} catch (_) {
if (!routes.length && sequence === commandSearchSequence) results.innerHTML = `<p class="chimera-command-empty">${escapeText(tr('navigation.search.gamesUnavailable', 'Game search is currently unavailable.'))}</p>`;
if (sequence === commandSearchSequence) updateCommandStatus(routes.length
? `${routes.length} ${tr('navigation.search.featuresLower', 'features')} · ${tr('navigation.search.gamesUnavailableShort', 'games unavailable')}`
: tr('navigation.search.noResults', 'No results'));
}
}
async function activateCommandResult(button) {
if (!button) return;
rememberCommandQuery(document.getElementById('chimera-command-input')?.value);
closeCommandPalette();
if (button.dataset.commandRoute) return navigateRoute(button.dataset.commandRoute);
if (button.dataset.commandGame) {
await navigateRoute('library/games');
const input = document.getElementById('games-search');
if (input) {
input.value = button.dataset.commandGame;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}
}
function moveCommandSelection(direction) {
const buttons = [...document.querySelectorAll('#chimera-command-results .chimera-command-result')];
if (!buttons.length) return;
let index = buttons.findIndex(button => button.classList.contains('selected'));
buttons.forEach(button => {
button.classList.remove('selected');
button.setAttribute('aria-selected', 'false');
});
index = (index + direction + buttons.length) % buttons.length;
buttons[index].classList.add('selected');
buttons[index].setAttribute('aria-selected', 'true');
buttons[index].scrollIntoView({ block: 'nearest' });
}
function transferStatusLabel(status) {
const fallback = {
queued: 'Queued', active: 'Active', done: 'Completed', error: 'Error',
cancelled: 'Cancelled', interrupted: 'Interrupted', retrying: 'Retry scheduled',
}[status] || status || 'Unknown';
return tr(`navigation.jobs.status.${status || 'unknown'}`, fallback);
}
function renderJobs(stateValue) {
currentTransferState = stateValue || currentTransferState;
const active = Number(currentTransferState.activeCount || 0);
const queued = Number(currentTransferState.queuedCount || 0);
const count = active + queued;
const homeTransferTruth = document.getElementById('home-truth-transfers');
if (homeTransferTruth) {
homeTransferTruth.textContent = active
? `${active} active`
: queued
? `${queued} queued`
: currentTransferState.paused
? 'Queue paused'
: 'No active work';
}
const badge = document.getElementById('chimera-job-count');
const navBadge = document.getElementById('nav-transfer-badge');
if (badge) {
const total = count + currentActionCount;
badge.hidden = total === 0;
badge.textContent = String(total);
}
if (navBadge) {
navBadge.hidden = count === 0;
navBadge.textContent = String(count);
}
const companionBadge = document.getElementById('chimera-companion-count');
if (companionBadge) {
const total = count + currentActionCount;
companionBadge.hidden = total === 0;
companionBadge.textContent = String(total);
}
document.querySelector('.chimera-jobs-pulse')?.classList.toggle('active', active > 0);
const summary = document.getElementById('chimera-jobs-summary');
const list = document.getElementById('chimera-jobs-list');
if (summary) summary.innerHTML = `<div><strong>${active}</strong><span>${escapeText(tr('navigation.jobs.active', 'active'))}</span></div><div><strong>${queued}</strong><span>${escapeText(tr('navigation.jobs.queued', 'queued'))}</span></div><div><strong>${escapeText(currentTransferState.paused ? tr('common.yes', 'Yes') : tr('common.no', 'No'))}</strong><span>${escapeText(tr('navigation.jobs.paused', 'paused'))}</span></div>`;
if (!list) return;
const jobs = (currentTransferState.jobs || []).filter(job => job.status !== 'cancelled').slice(-20).reverse();
list.innerHTML = jobs.length ? jobs.map(job => {
const progress = Math.max(0, Math.min(100, Number(job.progress?.percent || (job.status === 'done' ? 100 : 0))));
return `<article class="chimera-job chimera-job-${escapeText(job.status)}"><div><strong>${escapeText(job.label || job.progress?.file || 'Transfer')}</strong><span>${escapeText(transferStatusLabel(job.status))}${job.progress?.file ? ` · ${escapeText(job.progress.file)}` : ''}</span></div><b>${Math.round(progress)}%</b><div class="chimera-job-progress"><span style="width:${progress}%"></span></div>${job.error ? `<small>${escapeText(job.error)}</small>` : ''}</article>`;
}).join('') : `<div class="chimera-drawer-empty"><strong>${escapeText(tr('navigation.jobs.emptyTitle', 'No transfers'))}</strong><span>${escapeText(tr('navigation.jobs.emptyDetail', 'New uploads and downloads will appear here automatically.'))}</span></div>`;
}
function actionRoute(action = {}) {
if (action.tab) return contract.routes.find(route => route.page === 'ecosystem' && route.tab === action.tab)?.id || 'console/status';
if (action.panel) return contract.routes.find(route => route.page === 'operations' && route.panel === action.panel)?.id || 'settings/automation';
return contract.legacyAliases[action.page] || (routeById.has(action.page) ? action.page : 'console/status');
}
function renderActions(packet) {
const list = document.getElementById('chimera-action-list');
if (!list) return;
const report = packet?.report || null;
const alerts = (report?.alerts || []).filter(alert => !alert.acknowledged).slice(0, 12);
currentActionCount = alerts.length;
const badge = document.getElementById('chimera-job-count');
if (badge) {
const total = Number(currentTransferState.activeCount || 0) + Number(currentTransferState.queuedCount || 0) + currentActionCount;
badge.hidden = total === 0;
badge.textContent = String(total);
}
const companionBadge = document.getElementById('chimera-companion-count');
if (companionBadge) {
const total = Number(currentTransferState.activeCount || 0) + Number(currentTransferState.queuedCount || 0) + currentActionCount;
companionBadge.hidden = total === 0;
companionBadge.textContent = String(total);
}
if (!report) {
list.innerHTML = `<div class="chimera-drawer-empty"><strong>${escapeText(tr('navigation.actionCenter.notScanned', 'No health scan yet'))}</strong><span>${escapeText(tr('navigation.actionCenter.notScannedDetail', 'Open Console health to run the first bounded scan.'))}</span></div>`;
return;
}
list.innerHTML = alerts.length ? alerts.map(alert => {
const count = String(alert.title || '').match(/\d+(?:\/\d+)?/)?.[0] || '';
const values = { count };
const translatedTitle = window.tf?.(`navigation.alerts.${alert.id}.title`, alert.title, values) || alert.title;
const translatedDetail = window.tf?.(`navigation.alerts.${alert.id}.detail`, alert.detail, values) || alert.detail;
return `
<article class="chimera-action chimera-action-${escapeText(alert.severity)}">
<span class="chimera-action-dot" aria-hidden="true"></span>
<div><strong>${escapeText(translatedTitle)}</strong><span>${escapeText(translatedDetail)}</span></div>
<button class="btn btn-sm" type="button" data-route="${escapeText(actionRoute(alert.action || {}))}">${escapeText(tr('common.open', 'Open'))}</button>
</article>`;
}).join('') : `<div class="chimera-action-clear"><span aria-hidden="true">✓</span><div><strong>${escapeText(tr('navigation.actionCenter.clear', 'Nothing needs attention'))}</strong><span>${escapeText(tr('navigation.actionCenter.clearDetail', 'No open alerts were found in the latest health scan.'))}</span></div></div>`;
}
async function openJobsDrawer() {
const overlay = document.getElementById('chimera-jobs-overlay');
if (!overlay) return;
document.body.classList.add('chimera-modal-open');
const [transfers, health] = await Promise.allSettled([
window.pork.transferState(),
window.pork.ecosystemHealthLatest(),
]);
renderJobs(transfers.status === 'fulfilled' ? transfers.value : currentTransferState);
renderActions(health.status === 'fulfilled' ? health.value : null);
const closeButton = overlay.querySelector('[data-jobs-close]');
if (typeof openAccessibleDialog === 'function') openAccessibleDialog(overlay, closeButton);
else { overlay.hidden = false; requestAnimationFrame(() => closeButton?.focus()); }
}
function closeJobsDrawer() {
const overlay = document.getElementById('chimera-jobs-overlay');
if (typeof closeAccessibleDialog === 'function') closeAccessibleDialog(overlay);
else if (overlay) overlay.hidden = true;
if (document.getElementById('chimera-command-overlay')?.hidden !== false) document.body.classList.remove('chimera-modal-open');
}
function openAboutDialog() {
const overlay = document.getElementById('chimera-about-overlay');
if (!overlay) return;
const closeButton = overlay.querySelector('[data-about-close]');
if (typeof openAccessibleDialog === 'function') openAccessibleDialog(overlay, closeButton);
else { overlay.hidden = false; requestAnimationFrame(() => closeButton?.focus()); }
}
function closeAboutDialog() {
const overlay = document.getElementById('chimera-about-overlay');
if (typeof closeAccessibleDialog === 'function') closeAccessibleDialog(overlay);
else if (overlay) overlay.hidden = true;
}
function bindEvents() {
document.addEventListener('click', async event => {
const routeTarget = event.target.closest('[data-route]');
if (routeTarget) {
event.preventDefault();
closeCommandPalette();
closeJobsDrawer();
closeAboutDialog();
const requestedStage = routeTarget.dataset.payloadStage
|| (!routeTarget.closest('.payload-workspace-nav') ? defaultPayloadStage(routeTarget.dataset.route) : null);
if (requestedStage) activePayloadStage = requestedStage;
await navigateRoute(routeTarget.dataset.route);
const focusTarget = routeTarget.dataset.focusTarget
? document.getElementById(routeTarget.dataset.focusTarget)
: null;
if (focusTarget) {
focusTarget.scrollIntoView({
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
block: 'center',
});
requestAnimationFrame(() => focusTarget.focus({ preventScroll: true }));
}
if (requestedStage) updatePayloadWorkspaceStage(routeTarget.dataset.route, requestedStage);
const payloadTarget = routeTarget.dataset.payloadTarget
? document.getElementById(routeTarget.dataset.payloadTarget)
: null;
if (payloadTarget) {
payloadTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
payloadTarget.classList.remove('payload-workspace-highlight');
requestAnimationFrame(() => payloadTarget.classList.add('payload-workspace-highlight'));
window.setTimeout(() => payloadTarget.classList.remove('payload-workspace-highlight'), 1800);
}
return;
}
const groupToggle = event.target.closest('[data-group-toggle]');
if (groupToggle) {
const groupId = groupToggle.dataset.groupToggle;
setExpandedGroup(groupId, !expandedGroups.has(groupId));
}
const commandResult = event.target.closest('[data-command-route], [data-command-game]');
if (commandResult) activateCommandResult(commandResult);
const commandQuery = event.target.closest('[data-command-query]');
if (commandQuery) {
const input = document.getElementById('chimera-command-input');
if (input) {
input.value = commandQuery.dataset.commandQuery;
renderCommandResults(input.value);
input.focus();
}
}
if (event.target.closest('[data-command-close]') || event.target.id === 'chimera-command-overlay') closeCommandPalette();
if (event.target.closest('[data-jobs-close]') || event.target.id === 'chimera-jobs-overlay') closeJobsDrawer();
if (event.target.closest('[data-about-close]') || event.target.id === 'chimera-about-overlay') closeAboutDialog();
});
document.getElementById('chimera-command-open')?.addEventListener('click', openCommandPalette);
document.getElementById('chimera-jobs-open')?.addEventListener('click', openJobsDrawer);
document.getElementById('chimera-companion-actions')?.addEventListener('click', openJobsDrawer);
document.getElementById('chimera-companion-more')?.addEventListener('click', event => {
document.getElementById('mobile-nav-toggle')?.click();
event.currentTarget.setAttribute('aria-expanded', String(document.body.classList.contains('mobile-nav-open')));
});
document.getElementById('chimera-route-refresh')?.addEventListener('click', () => navigateRoute(currentRouteId, { history: 'none', force: true }));
document.getElementById('chimera-expert-mode')?.addEventListener('change', async event => {
expertMode = event.target.checked;
document.body.classList.toggle('chimera-expert-mode', expertMode);
await syncExpertPermission();
window.dispatchEvent(new CustomEvent('chimera:expert-mode-changed', {
detail: { enabled: expertMode },
}));
if (!expertMode && routeById.get(currentRouteId)?.expert) navigateRoute(currentRouteId, { history: 'replace', force: true });
});
document.getElementById('chimera-command-input')?.addEventListener('input', event => renderCommandResults(event.target.value));
document.getElementById('chimera-command-clear')?.addEventListener('click', () => {
const input = document.getElementById('chimera-command-input');
if (!input) return;
input.value = '';
renderCommandResults('');
input.focus();
});
document.getElementById('chimera-command-input')?.addEventListener('keydown', event => {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); moveCommandSelection(event.key === 'ArrowDown' ? 1 : -1); }
if (event.key === 'Enter') { event.preventDefault(); activateCommandResult(document.querySelector('#chimera-command-results .chimera-command-result.selected')); }
});
document.addEventListener('keydown', event => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); openCommandPalette(); }
if (event.key === 'Escape') { closeCommandPalette(); closeJobsDrawer(); closeAboutDialog(); }
});
const syncFromLocation = () => navigateRoute(routeFromLocation(), { history: 'none' });
window.addEventListener('popstate', syncFromLocation);
window.addEventListener('hashchange', syncFromLocation);
window.addEventListener('chimera:language-changed', () => {
localizeSidebar();
const route = routeById.get(currentRouteId);
if (route) updateRouteHeader(route);
const input = document.getElementById('chimera-command-input');
if (input && document.getElementById('chimera-command-overlay')?.hidden === false) renderCommandResults(input.value);
renderJobs(currentTransferState);
if (document.getElementById('chimera-jobs-overlay')?.hidden === false) {
window.pork.ecosystemHealthLatest().then(renderActions).catch(() => renderActions(null));
}
});
window.pork?.on?.('transfer:update', renderJobs);
}
mountSidebar();
mountShell();
mountCompanionDock();
mountHomeCommandCenter();
mountSettingsNavigator();
bindEvents();
window.chimeraOpenAbout = openAboutDialog;
const compatibleNavigate = page => navigateRoute(contract.legacyAliases[page] || page);
navigate = compatibleNavigate;
window.navigate = compatibleNavigate;
window.chimeraNavigate = navigateRoute;
window.chimeraNavigationContract = contract;
const initialRoute = routeFromLocation();
const initialGroup = routeById.get(initialRoute)?.group;
if (initialGroup && initialGroup !== 'home') expandedGroups.add(initialGroup);
requestAnimationFrame(() => navigateRoute(initialRoute, { history: 'replace' }));
window.pork?.transferState?.().then(renderJobs).catch(() => {});
window.pork?.ecosystemHealthLatest?.().then(renderActions).catch(() => {});
})();