${escHtml(entry.sourcePath)}${entry.warnings?.length ? `${entry.warnings.map(escHtml).join(' ')}` : ''}'use strict'; let _operationsLoaded = false; let _operationsStatus = null; let _operationsBusy = new Set(); let _operationsStorageReport = null; let _operationsStorageTopology = null; function operationsDate(value) { if (!value) return '—'; try { return new Date(Number(value) || value).toLocaleString(window.i18n?.current || 'en'); } catch (_) { return '—'; } } function operationsDuration(value) { const ms = Number(value || 0); if (!ms) return '—'; if (ms < 1000) return `${ms} ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)} s`; return `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`; } function operationsBytes(value) { const bytes = Number(value || 0); if (!Number.isFinite(bytes) || bytes <= 0) return '—'; 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 || unit === 0 ? amount.toFixed(0) : amount.toFixed(1)} ${units[unit]}`; } function operationsRunSummary(run) { const raw = String(run.error || run.summary || '').trim(); if (!raw) return run.status === 'completed' ? 'Completed' : ''; if (run.error) return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw; let result; try { result = JSON.parse(raw); } catch (_) { return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw; } if (result == null) return 'Completed'; if (typeof result === 'string' || typeof result === 'number') return String(result); if (run.task_id === 'library-scan') { const total = Number(result.total ?? result.found?.length ?? result.found ?? 0); const added = Number(result.added || 0); return `${total} game${total === 1 ? '' : 's'} found · ${added} new`; } if (run.task_id === 'metadata-refresh') { const queued = Number(result.queued ?? (typeof result === 'number' ? result : 0)); return `${queued} metadata item${queued === 1 ? '' : 's'} queued`; } if (run.task_id === 'payload-sync' && Array.isArray(result)) { const failed = result.filter(item => item?.error).length; return `${result.length} source${result.length === 1 ? '' : 's'} checked${failed ? ` · ${failed} error${failed === 1 ? '' : 's'}` : ''}`; } if (run.task_id === 'cheat-sync') { const indexed = Number(result.ecosystem?.total || 0); const cached = Number(result.legacy?.total || 0); const failed = Number(result.legacy?.failed || 0) + Number(result.ecosystem?.errors?.length || 0); return `${indexed} modern cheats or patches indexed · ${cached} legacy items cached${failed ? ` · ${failed} error${failed === 1 ? '' : 's'}` : ''}`; } if (run.task_id === 'storage-audit') { const summary = result.summary || {}; const issues = Number(summary.missingBackups || 0) + Number(summary.partialFiles || 0) + Number(summary.orphanGameFolders || 0); return `${Number(summary.games || 0)} games · ${Number(summary.backups || 0)} backups · ${issues} attention item${issues === 1 ? '' : 's'}`; } if (run.task_id === 'recovery-snapshot') { const localFiles = Array.isArray(result.files) ? result.files.length : 0; const remoteFiles = Array.isArray(result.remote) ? result.remote.filter(item => item.included).length : 0; return `${localFiles} local and ${remoteFiles} PS5 files secured`; } const values = Object.entries(result).filter(([, value]) => ['string', 'number', 'boolean'].includes(typeof value)).slice(0, 3); return values.length ? values.map(([key, value]) => `${key}: ${value}`).join(' · ') : 'Completed'; } function operationsStatusBadge(status) { const labels = { completed: 'Succeeded', failed: 'Failed', running: 'Running', compatible: 'Compatible', warning: 'Warning', unknown: 'Unknown' }; return `${escHtml(labels[status] || status || 'Unknown')}`; } async function operationsAction(key, button, task) { if (_operationsBusy.has(key)) return null; _operationsBusy.add(key); const prior = button?.textContent; if (button) { button.disabled = true; button.textContent = window.t('common.loading', 'Loading…'); } try { return await task(); } finally { _operationsBusy.delete(key); if (button) { button.disabled = false; button.textContent = prior; } } } function renderOperationsAutomation(data) { _operationsStatus = data; const scheduler = $('operations-scheduler-state'); scheduler.textContent = data.running ? 'Active' : 'Stopped'; scheduler.className = `badge ${data.running ? 'badge-green' : 'badge-red'}`; const tasks = data.tasks || []; $('operations-task-list').innerHTML = tasks.map(task => `
No tasks configured.
'; const runs = (data.recentRuns || []).slice(0, 25); $('operations-run-list').innerHTML = runs.map(run => { const summary = operationsRunSummary(run); return `No runs yet.
'; const failed = runs.filter(run => run.status === 'failed').length; const health = $('operations-health'); health.textContent = failed ? window.tf('operations.status.failures', '{count} recent failure(s)', { count: failed }) : window.t('operations.status.healthy', 'Automation healthy'); health.className = `operations-health ${failed ? 'warning' : 'healthy'}`; const badge = $('nav-operations-badge'); if (badge) { badge.hidden = failed === 0; badge.textContent = failed || ''; } } function renderOperationsStorage(report) { _operationsStorageReport = report; if (!report) { $('operations-storage-summary').innerHTML = ''; $('operations-storage-details').innerHTML = 'No storage report available yet.
'; return; } const summary = report.summary || {}; const metrics = [ [window.t('operations.storage.metrics.mounts', 'Mounts'), summary.roots], [window.t('operations.storage.metrics.games', 'Games'), summary.games], [window.t('operations.storage.metrics.backups', 'Backups'), summary.backups], [window.t('operations.storage.metrics.missing', 'Missing'), summary.missingBackups], [window.t('operations.storage.metrics.duplicates', 'Duplicate games'), summary.duplicateGames], [window.t('operations.storage.metrics.partial', 'Partial files'), summary.partialFiles], [window.t('operations.storage.metrics.orphaned', 'Orphaned folders'), summary.orphanGameFolders], ]; $('operations-storage-summary').innerHTML = metrics.map(([label, value]) => `${escHtml(root.path)}${root.error ? escHtml(root.error) : `${operationsBytes(root.availableBytes)} ${escHtml(window.t('operations.storage.freeOf', 'free of'))} ${operationsBytes(root.totalBytes)}`}${escHtml(window.t('operations.storage.first100', 'Only the first 100 items are shown.'))}
` : ''}${escHtml(item.path)}${escHtml(item)}${escHtml(item.path)}${escHtml(window.t('operations.storage.reportAt', 'Report from'))} ${operationsDate(report.generatedAt)}
${sections.join('')}`; } function renderStorageTopology(packet) { const target = $('operations-storage-topology'); if (!target || !packet) return; _operationsStorageTopology = packet; target.innerHTML = `${escHtml(window.t('operations.storage.consoleSource', 'Console source'))}: ${escHtml(packet.provenance?.console || 'unknown')} · ${escHtml(window.t('operations.storage.serverSource', 'server source'))}: ${escHtml(packet.provenance?.server || 'unknown')}
${escHtml(snapshot.id)}No checks available.
'; } function renderReplicationStatus(status) { const targetState = status.available && status.writable ? 'Available and writable' : status.error || 'Unavailable'; const free = status.storage ? `${operationsBytes(status.storage.availableBytes)} free` : 'free space unknown'; const protection = status.protection || {}; const rpo = protection.effectiveRpoSeconds == null ? 'not established' : protection.effectiveRpoSeconds < 3600 ? `${Math.ceil(protection.effectiveRpoSeconds / 60)} min` : `${Math.round(protection.effectiveRpoSeconds / 360) / 10} h`; const rto = protection.lastRestoreDrillDurationMs == null ? 'not measured' : `${Math.max(1, Math.round(protection.lastRestoreDrillDurationMs / 1000))} s drill`; $('operations-replication-status').innerHTML = `${escHtml(status.target || '/replication')}${escHtml(free)}${status.running ? ' · replication running' : ''}Atomic: ${status.targetContract?.atomicPublish ? 'yes' : 'unknown'} · hashes: ${status.targetContract?.hashVerified ? 'yes' : 'unknown'} · immutable: ${status.targetContract?.immutable ? 'yes' : 'no'} · off-host: ${escHtml(status.targetContract?.offHost || 'unknown')}
${protection.blockers?.length ? `${escHtml(snapshot.id)}${escHtml(snapshot.error)}${escHtml(snapshot.id)}No replication snapshots yet.
'; } function renderMonitoring(value) { const metrics = [ ['Uptime', `${Math.round(Number(value.uptimeSeconds || 0) / 60)} min.`], ['Memory', operationsBytes(value.memory?.rssBytes)], ['Games', value.library?.totalGames || 0], ['Backups', value.library?.totalBackups || 0], ['FTP', value.ftpConnected ? 'Connected' : 'Offline'], ['Task failures', value.automation?.recentFailures || 0], ['Replications', value.replication?.snapshots || 0], ['API p95', `${Number(value.api?.p95DurationMs || 0)} ms`], ['Last scan', value.scans?.lastDurationMs == null ? 'not measured' : `${Number(value.scans.lastDurationMs)} ms`], ['Transfer / 24h', operationsBytes(value.transfers?.bytes)], ['Cache hit', `${Math.round(Number(value.transport?.staticCacheHitRatio || 0) * 100)}%`], ['WebSocket disconnects', Number(value.transport?.wsDisconnects || 0)], ]; $('operations-monitoring-summary').innerHTML = metrics.map(([label, result]) => `${escHtml(session.id)}No active sessions.
'; } async function loadOperationsReliability({ focus = '' } = {}) { if (focus === 'security') { if (!window.pork.authSessions) { $('operations-session-list').innerHTML = 'Session management is available only in server mode.
'; return; } renderSessions(await window.pork.authSessions()); return; } if (!window.pork.backupReplicationStatus || !window.pork.authSessions) { $('operations-replication-status').innerHTML = 'Session and replication management are available only in server mode.
'; return; } const [replication, monitoring, token, sessions] = await Promise.all([ window.pork.backupReplicationStatus(), window.pork.monitoringSnapshot(), window.pork.monitoringTokenStatus(), window.pork.authSessions(), ]); renderReplicationStatus(replication); renderMonitoring(monitoring); $('operations-monitoring-token-state').textContent = token.configured ? window.t('operations.monitoring.tokenActive', 'Metrics token active · endpoint /metrics') : window.t('operations.monitoring.tokenDisabled', 'Metrics disabled until a token is created'); renderSessions(sessions); } async function loadOperations({ background = false } = {}) { const [status, storage, snapshots, compatibility, topology] = await Promise.all([ window.pork.operationsStatus(), window.pork.storageStatus().catch(() => null), window.pork.recoveryList().catch(() => []), window.pork.compatibilityGet().catch(() => ({ profile: {}, checks: [] })), window.pork.consoleStorageTopology?.().catch(() => null) || Promise.resolve(null), ]); renderOperationsAutomation(status); renderOperationsStorage(storage); renderOperationsRecovery(snapshots); renderOperationsCompatibility(compatibility); renderStorageTopology(topology); _operationsLoaded = true; if (!background) $('operations-health').title = window.tf('operations.status.updated', 'Updated {time}', { time: new Date().toLocaleTimeString(window.i18n?.current || 'en'), }); } async function loadOperationsShadowMount() { const result = await window.pork.shadowMountStatus(); const config = result.files?.['shadowmount-config'] || {}; const manual = result.files?.['shadowmount-manual'] || {}; $('operations-shadow-config').value = config.text || ''; $('operations-shadow-manual').value = manual.text || ''; for (const [element, file] of [[$('operations-shadow-config-state'), config], [$('operations-shadow-manual-state'), manual]]) { element.textContent = file.ok ? 'Loaded' : 'Unavailable'; element.className = `badge ${file.ok ? 'badge-green' : 'badge-red'}`; element.title = file.error || ''; } renderOperationsShadowMountModel(result.model || { summary: {}, entries: [], unknownStatusLines: [] }); renderOperationsShadowPreflight(result.preflight); } function renderOperationsShadowPreflight(preflight) { const target = $('operations-shadow-preflight'); if (!target) return; if (!preflight) { target.innerHTML = 'Configuration preflight unavailableLoad a reachable config.ini or run the check again while saving.
'; return; } const counts = preflight.counts || {}; const headline = !preflight.ok ? 'Save blocked' : preflight.requiresRiskConfirmation ? 'Explicit risk confirmation required' : 'Preflight passed'; const findings = (preflight.findings || []).map(item => `${Number(counts.invalid || 0)} invalid · ${Number(counts.critical || 0)} critical · ${Number(counts.warning || 0)} warning(s) · ${Number(counts.unknown || 0)} unknown future key(s)
${findings ? `No safety issues in the recognized configuration.
'}`; } function renderOperationsShadowMountModel(model) { const summary = model.summary || {}; const entries = model.entries || []; const statusBadge = $('operations-shadow-status-state'); statusBadge.textContent = summary.invalid || summary.unknownStatusLines ? 'review needed' : `${summary.installed || 0} installed`; statusBadge.className = `badge ${summary.invalid || summary.unknownStatusLines ? 'badge-yellow' : 'badge-green'}`; $('operations-shadow-summary').innerHTML = [ ['Active sources', summary.active || 0], ['Installed', summary.installed || 0], ['Pending', summary.pending || 0], ['Deleted', summary.deleted || 0], ['Experimental', summary.experimental || 0], ['Library matches', summary.libraryMatches || 0], ].map(([label, value]) => `${escHtml(entry.sourcePath)}${entry.warnings?.length ? `${entry.warnings.map(escHtml).join(' ')}` : ''}${escHtml(model.unknownStatusLines.map(item => `line ${item.line}: ${item.text}`).join('\n'))}manual.lst and manual.status do not contain game sources yet.
'; } async function loadOperationsPackages() { const result = await window.pork.packagesStatus(); $('operations-package-services').innerHTML = (result.services || []).map(service => `No kernel log records match these filters.
'; } async function loadKlog() { const [status, result] = await Promise.all([window.pork.klogStatus(), window.pork.klogQuery(klogFilters())]); renderKlogStatus(status); renderKlogEntries(result); } document.querySelectorAll('.operations-tab').forEach(button => button.addEventListener('click', async () => { const tab = button.dataset.operationsTab; document.querySelectorAll('.operations-tab').forEach(item => item.classList.toggle('active', item === button)); document.querySelectorAll('.operations-panel').forEach(panel => { const active = panel.dataset.operationsPanel === tab; panel.classList.toggle('active', active); panel.hidden = !active; }); try { if (tab === 'shadowmount') await loadOperationsShadowMount(); if (tab === 'storage') await loadStorageTopology(); if (tab === 'packages') await loadOperationsPackages(); if (tab === 'reliability') await loadOperationsReliability(); if (tab === 'audit') await loadOperationsAudit(); if (tab === 'diagnostics') await loadKlog(); } catch (error) { showToast(error.message, 'error'); } })); $('operations-refresh')?.addEventListener('click', event => operationsAction('refresh', event.currentTarget, () => loadOperations()).catch(error => showToast(error.message, 'error'))); $('operations-task-list')?.addEventListener('click', async event => { const row = event.target.closest('.operations-task'); if (!row) return; const taskId = row.dataset.taskId; try { if (event.target.closest('.operations-task-run')) { await operationsAction(`run-${taskId}`, event.target.closest('button'), () => window.pork.operationsRunTask(taskId)); showToast('Task completed.', 'ok'); await loadOperations(); } if (event.target.closest('.operations-task-save')) { await operationsAction(`save-${taskId}`, event.target.closest('button'), () => window.pork.operationsConfigureTask(taskId, { enabled: row.querySelector('.operations-task-enabled').checked, intervalMinutes: Number(row.querySelector('.operations-task-interval').value), })); showToast('Schedule saved.', 'ok'); await loadOperations(); } } catch (error) { showToast(error.message, 'error'); } }); $('operations-storage-run')?.addEventListener('click', async event => { try { const report = await operationsAction('storage', event.currentTarget, () => window.pork.storageAudit()); renderOperationsStorage(report); showToast('Storage audit completed. Nothing was deleted.', 'ok'); } catch (error) { showToast(error.message, 'error'); } }); $('operations-storage-topology-refresh')?.addEventListener('click', event => operationsAction('storage-topology', event.currentTarget, loadStorageTopology).catch(error => showToast(error.message, 'error'))); $('operations-recovery-create')?.addEventListener('click', async event => { const label = await showPrompt('Name for this restore point:', 'Manual restore point'); if (label == null) return; try { await operationsAction('recovery-create', event.currentTarget, () => window.pork.recoveryCreate({ label, includeRemote: true })); renderOperationsRecovery(await window.pork.recoveryList()); showToast('Restore point saved safely.', 'ok'); } catch (error) { showToast(error.message, 'error'); } }); $('operations-recovery-import')?.addEventListener('click', async event => { try { const result = await operationsAction('recovery-import', event.currentTarget, () => window.pork.recoveryImport()); if (!result) return; renderOperationsRecovery(await window.pork.recoveryList()); showToast(`Archive imported as ${result.id} and verified with SHA-256. Nothing was restored.`, 'ok', 9000); } catch (error) { showToast(error.message, 'error', 10000); } }); $('operations-recovery-list')?.addEventListener('click', async event => { const row = event.target.closest('.operations-recovery-item'); if (!row) return; const id = row.dataset.snapshotId; try { if (event.target.closest('.operations-recovery-verify')) { const verified = await operationsAction(`verify-${id}`, event.target.closest('button'), () => window.pork.recoveryVerify(id)); showToast(`${verified.files.length} files verified (${verified.integrity}). Nothing was restored.`, 'ok'); } if (event.target.closest('.operations-recovery-export')) await operationsAction(`export-${id}`, event.target.closest('button'), () => window.pork.recoveryExport(id)); if (event.target.closest('.operations-recovery-restore')) { if (!await showExactConfirmation('This restores the database, settings and available PS5 configuration. A new safety recovery point is created first.', 'RESTORE')) return; await operationsAction(`restore-${id}`, event.target.closest('button'), () => window.pork.recoveryRestore(id, true)); showToast('Restore completed. Refresh the page if settings are not immediately visible.', 'ok', 8000); await loadOperations(); } if (event.target.closest('.operations-recovery-delete')) { if (!await showExactConfirmation('Permanently delete this recovery point?', 'DELETE')) return; await operationsAction(`delete-${id}`, event.target.closest('button'), () => window.pork.recoveryDelete(id)); renderOperationsRecovery(await window.pork.recoveryList()); } } catch (error) { showToast(error.message, 'error'); } }); $('operations-replication-refresh')?.addEventListener('click', event => operationsAction('replication-refresh', event.currentTarget, loadOperationsReliability).catch(error => showToast(error.message, 'error'))); $('operations-replication-run')?.addEventListener('click', async event => { const includeGameBackups = $('operations-replication-games').checked; const warning = includeGameBackups ? '\n\nFull game backups are selected and may use a large amount of storage.' : ''; if (!await showConfirm(`Create a new hash-verified snapshot on the secondary backup target? Nothing is removed from the source.${warning}`)) return; try { const label = `Manual replication ${new Date().toLocaleString(window.i18n?.current || 'en')}`; await operationsAction('replication-run', event.currentTarget, () => window.pork.backupReplicationRun({ includeGameBackups, label })); await loadOperationsReliability(); showToast('Replication completed and every copied file was verified.', 'ok', 8000); } catch (error) { showToast(error.message, 'error', 10000); } }); $('operations-replication-list')?.addEventListener('click', async event => { const row = event.target.closest('[data-replication-id]'); if (!row) return; try { const button = event.target.closest('button'); if (button?.classList.contains('operations-replication-verify')) { await operationsAction(`replication-verify-${row.dataset.replicationId}`, button, () => window.pork.backupReplicationVerify(row.dataset.replicationId)); showToast('Snapshot fully verified with SHA-256.', 'ok'); } else if (button?.classList.contains('operations-replication-drill')) { if (!await showConfirm('Copy every snapshot file to a temporary local destination, verify its hash, then remove the temporary copies? The PS5 is not modified.')) return; const result = await operationsAction(`replication-drill-${row.dataset.replicationId}`, button, () => window.pork.backupReplicationRestoreDrill(row.dataset.replicationId)); showToast(`Restore drill succeeded: ${result.files} files, PS5 unchanged.`, 'ok', 8000); } else return; await loadOperationsReliability(); } catch (error) { showToast(error.message, 'error', 10000); } }); $('operations-replication-retention')?.addEventListener('submit', async event => { event.preventDefault(); try { const preview = await operationsAction('replication-retention', event.submitter, () => window.pork.backupReplicationRetentionSave({ enabled: $('operations-replication-retention-enabled').checked, maxSnapshots: Number($('operations-replication-retention-count').value), maxAgeDays: Number($('operations-replication-retention-days').value) })); showToast(`Retention saved. ${preview.removable?.length || 0} snapshot(s) will be eligible after the next successful replication.`, 'ok', 9000); await loadOperationsReliability(); } catch (error) { showToast(error.message, 'error'); } }); $('operations-monitoring-refresh')?.addEventListener('click', event => operationsAction('monitoring-refresh', event.currentTarget, async () => renderMonitoring(await window.pork.monitoringSnapshot())).catch(error => showToast(error.message, 'error'))); $('operations-monitoring-token')?.addEventListener('click', async event => { if (!await showConfirm('Create a new metrics token? An existing token becomes invalid immediately.')) return; try { const result = await operationsAction('metrics-token', event.currentTarget, () => window.pork.monitoringTokenRotate()); $('operations-monitoring-token-state').textContent = 'New token active. Save it now; Chimera will not show it again.'; showOperationsJson($('operations-monitoring-token-output'), `Endpoint: ${location.origin}${result.endpoint}\nAuthorization: Bearer ${result.token}\n\nThis token is shown only once.`); } catch (error) { showToast(error.message, 'error'); } }); $('operations-sessions-refresh')?.addEventListener('click', event => operationsAction('sessions-refresh', event.currentTarget, async () => renderSessions(await window.pork.authSessions())).catch(error => showToast(error.message, 'error'))); $('operations-session-list')?.addEventListener('click', async event => { const row = event.target.closest('[data-session-id]'); if (!row || !event.target.closest('.operations-session-revoke') || !await showExactConfirmation('Revoke this session immediately?', 'REVOKE')) return; try { const result = await window.pork.authSessionRevoke(row.dataset.sessionId); if (result.current) { location.replace('/server-login.html'); return; } renderSessions(await window.pork.authSessions()); showToast('Session revoked.', 'ok'); } catch (error) { showToast(error.message, 'error'); } }); $('operations-password-form')?.addEventListener('submit', async event => { event.preventDefault(); const currentPassword = $('operations-password-current').value; const newPassword = $('operations-password-new').value; if (newPassword !== $('operations-password-repeat').value) { showToast('The new passwords do not match.', 'error'); return; } if (!await showConfirm('Change the password and revoke all other active sessions?')) return; try { const result = await operationsAction('password-change', event.submitter, () => window.pork.authPasswordChange(currentPassword, newPassword)); event.currentTarget.reset(); showToast(`Password changed; ${result.otherSessionsRevoked} other session(s) revoked.`, 'ok', 8000); renderSessions(await window.pork.authSessions()); } catch (error) { showToast(error.message, 'error'); } }); $('operations-profile-save')?.addEventListener('click', async event => { try { const result = await operationsAction('profile', event.currentTarget, () => window.pork.compatibilitySet({ firmware: $('operations-profile-firmware').value, hen: $('operations-profile-hen').value, henVersion: $('operations-profile-hen-version').value, kstuffVersion: $('operations-profile-kstuff').value, loader: $('operations-profile-loader').value, notes: $('operations-profile-notes').value, })); renderOperationsCompatibility(result); showToast('PS5 profile saved.', 'ok'); } catch (error) { showToast(error.message, 'error'); } }); $('operations-shadow-reload')?.addEventListener('click', event => operationsAction('shadow-load', event.currentTarget, loadOperationsShadowMount).catch(error => showToast(error.message, 'error'))); $('operations-shadow-propose')?.addEventListener('click', async event => { try { const proposal = await operationsAction('shadow-proposal', event.currentTarget, () => window.pork.shadowMountProposal()); $('operations-shadow-manual').value = proposal.proposedManual; $('operations-shadow-preflight').innerHTML = `Library proposal · ${proposal.additions.length} addition(s)${proposal.unchanged.length} unchanged · ${proposal.unresolved.length} unresolved. Nothing was written to the PS5.
${proposal.unresolved.length ? `