const API = { status: "/api/status", titles: "/api/titles", config: "/api/config", downloads: "/api/downloads", action: (titleId, action) => `/api/titles/${encodeURIComponent(titleId)}/${action}`, }; const SOURCE_TYPES = { official: { label: "Official", className: "ok", installAllowed: true, description: "Native console installation", }, external: { label: "External", className: "external", installAllowed: true, description: "Real external installation", }, shadowmount: { label: "Shadowmount", className: "shadow", installAllowed: false, description: "Download only", }, unknown: { label: "Unknown", className: "blocked", installAllowed: false, description: "Blocked", }, }; const fallback = { status: { firmware: "11.60", firmware_build: "0x11600000", dns_guard: "Active", resolver: "Internal allowlist", free_space_mb: 64218, download_dir: "/mnt/usb0/patches", }, config: { default_policy: "deny", download_dir: "/mnt/usb0/patches", install_after_download: false, delete_pkg_after_install: false, source_policy: { official: { allow_check: true, allow_download: true, allow_install: true }, external: { allow_check: true, allow_download: true, allow_install: true }, shadowmount: { allow_check: true, allow_download: true, allow_install: false }, unknown: { allow_check: true, allow_download: false, allow_install: false }, }, cdn_allowlist: [ "sgst.prod.dl.playstation.net", "gst.prod.dl.playstation.net", "gs2.ww.prod.dl.playstation.net", ], }, titles: [ { title_id: "PPSA01628_00", name: "Call of Duty Black Ops Cold War", content_id: "UP0002-PPSA01628_00-CODCWTHEGAME0001", installed_version: "01.032.000", compatible_version: "01.041.000", latest_version: "01.041.000", latest_required_fw: "11.60", source_type: "official", source_path: "/system_ex/app/PPSA01628_00", mount_from: "/dev/ssd0.system_ex", enabled: true, mode: "latest_compatible", queued: false, status: "available", }, { title_id: "PPSA01284_00", name: "Demon's Souls", content_id: "UP9000-PPSA01284_00-DEMONSSOULS00001", installed_version: "01.002.000", compatible_version: "01.004.000", latest_version: "01.004.000", latest_required_fw: "10.01", source_type: "external", source_path: "/system_data/priv/appmeta/external/PPSA01284_00", mount_from: "/mnt/ext0/user/app/PPSA01284_00", enabled: true, mode: "pin", max_content_ver: "01.004.000", queued: true, status: "queued", }, { title_id: "PPSA08329_00", name: "Example Future Game", content_id: "EP0000-PPSA08329_00-FUTUREPATCH00001", installed_version: "01.000.000", compatible_version: null, latest_version: "01.012.000", latest_required_fw: "12.50", source_type: "unknown", source_path: "/system_ex/app/PPSA08329_00", mount_from: "", enabled: false, mode: "disabled", queued: false, status: "blocked", }, { title_id: "PPSA90001_00", name: "Shadowmounted Test Title", content_id: "UP0000-PPSA90001_00-SHADOWMOUNT0001", installed_version: "01.000.000", compatible_version: "01.006.000", latest_version: "01.009.000", latest_required_fw: "12.00", source_type: "shadowmount", source_path: "/system_ex/app/PPSA90001_00", mount_from: "/mnt/usb0/itemzflow/Shadowmounted Test Title", enabled: true, mode: "download_only", queued: false, status: "available", }, ], downloads: [ { title_id: "PPSA01284_00", name: "Demon's Souls", version: "01.004.000", progress: 64, detail: "4.8 GB of 7.5 GB", }, ], logs: [ "[12:41:02] nanoDNS guard detected: Sony domains blocked", "[12:41:04] Internal resolver ready for 3 CDN hosts", "[12:41:07] PPSA01284_00 selected: pin 01.004.000", "[12:41:09] Download started: PPSA01284_00 01.004.000", ], }; let state = { status: fallback.status, config: fallback.config, titles: fallback.titles, downloads: fallback.downloads, logs: fallback.logs, filter: "all", query: "", usingFallback: false, }; const els = {}; document.addEventListener("DOMContentLoaded", () => { bindElements(); bindEvents(); loadInitialData(); }); function bindElements() { Object.assign(els, { firmwareValue: document.getElementById("firmwareValue"), firmwareBuild: document.getElementById("firmwareBuild"), dnsValue: document.getElementById("dnsValue"), resolverValue: document.getElementById("resolverValue"), spaceValue: document.getElementById("spaceValue"), downloadDirValue: document.getElementById("downloadDirValue"), gameGrid: document.getElementById("gameGrid"), queueList: document.getElementById("queueList"), logOutput: document.getElementById("logOutput"), allowlistHosts: document.getElementById("allowlistHosts"), sourcePolicyList: document.getElementById("sourcePolicyList"), searchInput: document.getElementById("searchInput"), defaultPolicy: document.getElementById("defaultPolicy"), downloadDir: document.getElementById("downloadDir"), installAfterDownload: document.getElementById("installAfterDownload"), deleteAfterInstall: document.getElementById("deleteAfterInstall"), refreshBtn: document.getElementById("refreshBtn"), saveBtn: document.getElementById("saveBtn"), pauseAllBtn: document.getElementById("pauseAllBtn"), toast: document.getElementById("toast"), }); } function bindEvents() { els.searchInput.addEventListener("input", (event) => { state.query = event.target.value.trim().toLowerCase(); renderGames(); }); document.querySelectorAll(".segmented button").forEach((button) => { button.addEventListener("click", () => { document.querySelectorAll(".segmented button").forEach((item) => item.classList.remove("is-selected")); button.classList.add("is-selected"); state.filter = button.dataset.filter; renderGames(); }); }); els.refreshBtn.addEventListener("click", loadInitialData); els.saveBtn.addEventListener("click", saveConfig); els.pauseAllBtn.addEventListener("click", () => showToast("Queue paused.")); } async function loadInitialData() { const [status, config, titles, downloads] = await Promise.all([ getJson(API.status, fallback.status), getJson(API.config, fallback.config), getJson(API.titles, fallback.titles), getJson(API.downloads, fallback.downloads), ]); state = { ...state, status, config, titles, downloads, }; render(); showToast(state.usingFallback ? "Demo data loaded. API is not reachable yet." : "Data refreshed."); } async function getJson(url, fallbackValue) { try { const response = await fetch(url, { cache: "no-store" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { state.usingFallback = true; return clone(fallbackValue); } } function render() { renderStatus(); renderSettings(); renderGames(); renderQueue(); renderLogs(); } function renderStatus() { els.firmwareValue.textContent = state.status.firmware || "--"; els.firmwareBuild.textContent = state.status.firmware_build || "System version"; els.dnsValue.textContent = state.status.dns_guard || "--"; els.resolverValue.textContent = state.status.resolver || "--"; els.spaceValue.textContent = formatMb(state.status.free_space_mb); els.downloadDirValue.textContent = state.status.download_dir || state.config.download_dir || "Download target"; } function renderSettings() { els.defaultPolicy.value = state.config.default_policy || "deny"; els.downloadDir.value = state.config.download_dir || ""; els.installAfterDownload.checked = Boolean(state.config.install_after_download); els.deleteAfterInstall.checked = Boolean(state.config.delete_pkg_after_install); els.allowlistHosts.replaceChildren(...(state.config.cdn_allowlist || []).map((host) => { const chip = document.createElement("span"); chip.className = "host-chip"; chip.textContent = host; return chip; })); renderSourcePolicies(); } function renderGames() { const visible = state.titles.filter(matchesFilter).filter(matchesQuery); els.gameGrid.replaceChildren(); if (!visible.length) { const empty = document.createElement("div"); empty.className = "empty-state"; empty.textContent = "No titles match this filter."; els.gameGrid.appendChild(empty); return; } visible.forEach((game) => els.gameGrid.appendChild(createGameCard(game))); } function matchesFilter(game) { if (state.filter === "enabled") return game.enabled; if (state.filter === "available") return game.status === "available"; if (state.filter === "shadowmount") return sourceType(game) === "shadowmount"; if (state.filter === "blocked") return game.status === "blocked"; if (state.filter === "queued") return game.queued || game.status === "queued"; return true; } function matchesQuery(game) { if (!state.query) return true; return [game.name, game.title_id, game.content_id] .concat([game.source_type, game.source_path, game.mount_from]) .filter(Boolean) .some((value) => value.toLowerCase().includes(state.query)); } function createGameCard(game) { const card = document.createElement("article"); card.className = "game-card"; const title = document.createElement("div"); title.className = "game-title"; const installBlocked = isInstallBlocked(game); title.innerHTML = `