mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 07:00:26 +02:00
Add source-aware update policy
This commit is contained in:
1 parent
9c2822f367
commit
9e89d3a342
5 files changed
+274
-7
No files matched your search
@@ -10,6 +10,23 @@ The target design is deny-by-default:
|
|||||||
- Games must be explicitly enabled before PatchDL checks or downloads updates.
|
- Games must be explicitly enabled before PatchDL checks or downloads updates.
|
||||||
- Patch selection is capped to the highest update compatible with the current
|
- Patch selection is capped to the highest update compatible with the current
|
||||||
firmware.
|
firmware.
|
||||||
|
- Install actions are source-aware: shadowmounted and unknown titles are never
|
||||||
|
installed by PatchDL.
|
||||||
|
|
||||||
|
## Source Classification
|
||||||
|
|
||||||
|
PatchDL treats the title source as part of the safety policy:
|
||||||
|
|
||||||
|
```text
|
||||||
|
official check/download/install allowed
|
||||||
|
external check/download/install allowed when detected as a real install
|
||||||
|
shadowmount check/download allowed, install blocked
|
||||||
|
unknown check allowed, download/install blocked
|
||||||
|
```
|
||||||
|
|
||||||
|
The future ELF scanner should classify titles by app metadata plus mount table
|
||||||
|
inspection. Shadowmounts should be detected through `statfs()` / `getfsstat()`
|
||||||
|
and `nullfs` mount origins instead of trusting title IDs alone.
|
||||||
|
|
||||||
## Current Contents
|
## Current Contents
|
||||||
|
|
||||||
@@ -20,4 +37,3 @@ web/
|
|||||||
|
|
||||||
Open `web/index.html` directly for the mock UI, or serve `web/` from a local
|
Open `web/index.html` directly for the mock UI, or serve `web/` from a local
|
||||||
HTTP server.
|
HTTP server.
|
||||||
|
|
||||||
@@ -28,6 +28,12 @@ The UI assumes deny-by-default behavior:
|
|||||||
"download_dir": "/mnt/usb0/patches",
|
"download_dir": "/mnt/usb0/patches",
|
||||||
"install_after_download": false,
|
"install_after_download": false,
|
||||||
"delete_pkg_after_install": 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": [
|
"cdn_allowlist": [
|
||||||
"sgst.prod.dl.playstation.net",
|
"sgst.prod.dl.playstation.net",
|
||||||
"gst.prod.dl.playstation.net",
|
"gst.prod.dl.playstation.net",
|
||||||
@@ -45,3 +51,30 @@ latest_compatible
|
|||||||
pin
|
pin
|
||||||
check_only
|
check_only
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Per-title source fields:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title_id": "PPSA90001_00",
|
||||||
|
"name": "Shadowmounted Test Title",
|
||||||
|
"source_type": "shadowmount",
|
||||||
|
"source_path": "/system_ex/app/PPSA90001_00",
|
||||||
|
"mount_from": "/mnt/usb0/itemzflow/Shadowmounted Test Title",
|
||||||
|
"enabled": true,
|
||||||
|
"mode": "download_only"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported `source_type` values:
|
||||||
|
|
||||||
|
```text
|
||||||
|
official
|
||||||
|
external
|
||||||
|
shadowmount
|
||||||
|
unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
The frontend treats `shadowmount` as download-only and `unknown` as blocked for
|
||||||
|
downloads and installs. Backend code should enforce the same policy even if a
|
||||||
|
client sends a forged request.
|
||||||
+153
-4
@@ -6,6 +6,33 @@ const API = {
|
|||||||
action: (titleId, action) => `/api/titles/${encodeURIComponent(titleId)}/${action}`,
|
action: (titleId, action) => `/api/titles/${encodeURIComponent(titleId)}/${action}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SOURCE_TYPES = {
|
||||||
|
official: {
|
||||||
|
label: "Official",
|
||||||
|
className: "ok",
|
||||||
|
installAllowed: true,
|
||||||
|
description: "Echte Installation",
|
||||||
|
},
|
||||||
|
external: {
|
||||||
|
label: "External",
|
||||||
|
className: "external",
|
||||||
|
installAllowed: true,
|
||||||
|
description: "Echte externe Installation",
|
||||||
|
},
|
||||||
|
shadowmount: {
|
||||||
|
label: "Shadowmount",
|
||||||
|
className: "shadow",
|
||||||
|
installAllowed: false,
|
||||||
|
description: "Download only",
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
label: "Unknown",
|
||||||
|
className: "blocked",
|
||||||
|
installAllowed: false,
|
||||||
|
description: "Blockiert",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const fallback = {
|
const fallback = {
|
||||||
status: {
|
status: {
|
||||||
firmware: "11.60",
|
firmware: "11.60",
|
||||||
@@ -20,6 +47,12 @@ const fallback = {
|
|||||||
download_dir: "/mnt/usb0/patches",
|
download_dir: "/mnt/usb0/patches",
|
||||||
install_after_download: false,
|
install_after_download: false,
|
||||||
delete_pkg_after_install: 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: [
|
cdn_allowlist: [
|
||||||
"sgst.prod.dl.playstation.net",
|
"sgst.prod.dl.playstation.net",
|
||||||
"gst.prod.dl.playstation.net",
|
"gst.prod.dl.playstation.net",
|
||||||
@@ -35,6 +68,9 @@ const fallback = {
|
|||||||
compatible_version: "01.041.000",
|
compatible_version: "01.041.000",
|
||||||
latest_version: "01.041.000",
|
latest_version: "01.041.000",
|
||||||
latest_required_fw: "11.60",
|
latest_required_fw: "11.60",
|
||||||
|
source_type: "official",
|
||||||
|
source_path: "/system_ex/app/PPSA01628_00",
|
||||||
|
mount_from: "/dev/ssd0.system_ex",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
mode: "latest_compatible",
|
mode: "latest_compatible",
|
||||||
queued: false,
|
queued: false,
|
||||||
@@ -48,6 +84,9 @@ const fallback = {
|
|||||||
compatible_version: "01.004.000",
|
compatible_version: "01.004.000",
|
||||||
latest_version: "01.004.000",
|
latest_version: "01.004.000",
|
||||||
latest_required_fw: "10.01",
|
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,
|
enabled: true,
|
||||||
mode: "pin",
|
mode: "pin",
|
||||||
max_content_ver: "01.004.000",
|
max_content_ver: "01.004.000",
|
||||||
@@ -62,11 +101,30 @@ const fallback = {
|
|||||||
compatible_version: null,
|
compatible_version: null,
|
||||||
latest_version: "01.012.000",
|
latest_version: "01.012.000",
|
||||||
latest_required_fw: "12.50",
|
latest_required_fw: "12.50",
|
||||||
|
source_type: "unknown",
|
||||||
|
source_path: "/system_ex/app/PPSA08329_00",
|
||||||
|
mount_from: "",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
mode: "disabled",
|
mode: "disabled",
|
||||||
queued: false,
|
queued: false,
|
||||||
status: "blocked",
|
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: [
|
downloads: [
|
||||||
{
|
{
|
||||||
@@ -116,6 +174,7 @@ function bindElements() {
|
|||||||
queueList: document.getElementById("queueList"),
|
queueList: document.getElementById("queueList"),
|
||||||
logOutput: document.getElementById("logOutput"),
|
logOutput: document.getElementById("logOutput"),
|
||||||
allowlistHosts: document.getElementById("allowlistHosts"),
|
allowlistHosts: document.getElementById("allowlistHosts"),
|
||||||
|
sourcePolicyList: document.getElementById("sourcePolicyList"),
|
||||||
searchInput: document.getElementById("searchInput"),
|
searchInput: document.getElementById("searchInput"),
|
||||||
defaultPolicy: document.getElementById("defaultPolicy"),
|
defaultPolicy: document.getElementById("defaultPolicy"),
|
||||||
downloadDir: document.getElementById("downloadDir"),
|
downloadDir: document.getElementById("downloadDir"),
|
||||||
@@ -207,6 +266,7 @@ function renderSettings() {
|
|||||||
chip.textContent = host;
|
chip.textContent = host;
|
||||||
return chip;
|
return chip;
|
||||||
}));
|
}));
|
||||||
|
renderSourcePolicies();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGames() {
|
function renderGames() {
|
||||||
@@ -227,6 +287,7 @@ function renderGames() {
|
|||||||
function matchesFilter(game) {
|
function matchesFilter(game) {
|
||||||
if (state.filter === "enabled") return game.enabled;
|
if (state.filter === "enabled") return game.enabled;
|
||||||
if (state.filter === "available") return game.status === "available";
|
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 === "blocked") return game.status === "blocked";
|
||||||
if (state.filter === "queued") return game.queued || game.status === "queued";
|
if (state.filter === "queued") return game.queued || game.status === "queued";
|
||||||
return true;
|
return true;
|
||||||
@@ -235,6 +296,7 @@ function matchesFilter(game) {
|
|||||||
function matchesQuery(game) {
|
function matchesQuery(game) {
|
||||||
if (!state.query) return true;
|
if (!state.query) return true;
|
||||||
return [game.name, game.title_id, game.content_id]
|
return [game.name, game.title_id, game.content_id]
|
||||||
|
.concat([game.source_type, game.source_path, game.mount_from])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.some((value) => value.toLowerCase().includes(state.query));
|
.some((value) => value.toLowerCase().includes(state.query));
|
||||||
}
|
}
|
||||||
@@ -245,6 +307,7 @@ function createGameCard(game) {
|
|||||||
|
|
||||||
const title = document.createElement("div");
|
const title = document.createElement("div");
|
||||||
title.className = "game-title";
|
title.className = "game-title";
|
||||||
|
const installBlocked = isInstallBlocked(game);
|
||||||
title.innerHTML = `
|
title.innerHTML = `
|
||||||
<div class="cover">${initials(game.name)}</div>
|
<div class="cover">${initials(game.name)}</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -255,8 +318,11 @@ function createGameCard(game) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="subline" style="margin-top:8px">
|
<div class="subline" style="margin-top:8px">
|
||||||
${statusPill(game)}
|
${statusPill(game)}
|
||||||
|
${sourcePill(game)}
|
||||||
|
${installBlocked ? `<span class="pill blocked">Install gesperrt</span>` : `<span class="pill ok">Install erlaubt</span>`}
|
||||||
<span class="pill">${game.enabled ? "Aktiv" : "Aus"}</span>
|
<span class="pill">${game.enabled ? "Aktiv" : "Aus"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="source-path">${escapeHtml(sourceDetail(game))}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -306,7 +372,7 @@ function createGameCard(game) {
|
|||||||
<svg><use href="#icon-refresh"></use></svg>
|
<svg><use href="#icon-refresh"></use></svg>
|
||||||
Pruefen
|
Pruefen
|
||||||
</button>
|
</button>
|
||||||
<button class="row-button is-primary" data-action="download" ${!game.compatible_version ? "disabled" : ""}>
|
<button class="row-button is-primary" data-action="download" ${!isDownloadAllowed(game) ? "disabled" : ""} title="${escapeHtml(downloadTitle(game))}">
|
||||||
<svg><use href="#icon-download"></use></svg>
|
<svg><use href="#icon-download"></use></svg>
|
||||||
Laden
|
Laden
|
||||||
</button>
|
</button>
|
||||||
@@ -331,12 +397,39 @@ function versionBox(label, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusPill(game) {
|
function statusPill(game) {
|
||||||
if (game.status === "blocked") return `<span class="pill blocked">FW blockiert</span>`;
|
if (game.status === "blocked") return `<span class="pill blocked">${escapeHtml(blockedLabel(game))}</span>`;
|
||||||
if (game.status === "queued") return `<span class="pill warn">In Queue</span>`;
|
if (game.status === "queued") return `<span class="pill warn">In Queue</span>`;
|
||||||
if (game.status === "available") return `<span class="pill ok">Update verfuegbar</span>`;
|
if (game.status === "available") return `<span class="pill ok">Update verfuegbar</span>`;
|
||||||
return `<span class="pill">Aktuell</span>`;
|
return `<span class="pill">Aktuell</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sourcePill(game) {
|
||||||
|
const info = sourceInfo(game);
|
||||||
|
return `<span class="pill ${info.className}">${escapeHtml(info.label)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceInfo(game) {
|
||||||
|
return SOURCE_TYPES[sourceType(game)] || SOURCE_TYPES.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceType(game) {
|
||||||
|
return game.source_type || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceDetail(game) {
|
||||||
|
const src = sourceType(game);
|
||||||
|
if (src === "shadowmount") return `Mount: ${game.mount_from || "unbekannte Quelle"}`;
|
||||||
|
if (src === "external") return `External: ${game.source_path || "externe App-Metadaten"}`;
|
||||||
|
if (src === "official") return game.source_path || "Offizielle Installation";
|
||||||
|
return "Quelle nicht eindeutig erkannt";
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockedLabel(game) {
|
||||||
|
if (sourceType(game) === "unknown") return "Quelle unbekannt";
|
||||||
|
if (!game.compatible_version) return "FW blockiert";
|
||||||
|
return "Blockiert";
|
||||||
|
}
|
||||||
|
|
||||||
function renderQueue() {
|
function renderQueue() {
|
||||||
els.queueList.replaceChildren();
|
els.queueList.replaceChildren();
|
||||||
if (!state.downloads.length) {
|
if (!state.downloads.length) {
|
||||||
@@ -362,6 +455,22 @@ function renderQueue() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSourcePolicies() {
|
||||||
|
const rows = Object.keys(SOURCE_TYPES).map((type) => {
|
||||||
|
const info = SOURCE_TYPES[type];
|
||||||
|
const policy = sourcePolicyForType(type);
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "policy-row";
|
||||||
|
row.innerHTML = `
|
||||||
|
<span class="pill ${info.className}">${escapeHtml(info.label)}</span>
|
||||||
|
<strong>${policy.allow_install ? "Install erlaubt" : "Install gesperrt"}</strong>
|
||||||
|
<em>${policy.allow_download ? "Download erlaubt" : "Download gesperrt"} - ${escapeHtml(info.description)}</em>
|
||||||
|
`;
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
els.sourcePolicyList.replaceChildren(...rows);
|
||||||
|
}
|
||||||
|
|
||||||
function renderLogs() {
|
function renderLogs() {
|
||||||
els.logOutput.textContent = state.logs.join("\n");
|
els.logOutput.textContent = state.logs.join("\n");
|
||||||
}
|
}
|
||||||
@@ -391,6 +500,11 @@ async function runTitleAction(titleId, action) {
|
|||||||
const game = state.titles.find((item) => item.title_id === titleId);
|
const game = state.titles.find((item) => item.title_id === titleId);
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
|
if (action === "download" && !isDownloadAllowed(game)) {
|
||||||
|
showToast(downloadTitle(game));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await postJson(API.action(titleId, action), {});
|
await postJson(API.action(titleId, action), {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -398,6 +512,9 @@ async function runTitleAction(titleId, action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "download" && game.compatible_version) {
|
if (action === "download" && game.compatible_version) {
|
||||||
|
const detail = isInstallBlocked(game)
|
||||||
|
? "Download only - Install durch Source-Policy gesperrt"
|
||||||
|
: "Wartet auf Start";
|
||||||
game.queued = true;
|
game.queued = true;
|
||||||
game.status = "queued";
|
game.status = "queued";
|
||||||
if (!state.downloads.some((item) => item.title_id === titleId)) {
|
if (!state.downloads.some((item) => item.title_id === titleId)) {
|
||||||
@@ -406,10 +523,10 @@ async function runTitleAction(titleId, action) {
|
|||||||
name: game.name,
|
name: game.name,
|
||||||
version: game.compatible_version,
|
version: game.compatible_version,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
detail: "Wartet auf Start",
|
detail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
state.logs.push(`[${timeNow()}] Queued ${game.title_id} ${game.compatible_version}`);
|
state.logs.push(`[${timeNow()}] Queued ${game.title_id} ${game.compatible_version} (${sourceType(game)}, install=${isInstallBlocked(game) ? "blocked" : "allowed"})`);
|
||||||
showToast(`${game.title_id} wurde zur Queue hinzugefuegt.`);
|
showToast(`${game.title_id} wurde zur Queue hinzugefuegt.`);
|
||||||
} else {
|
} else {
|
||||||
state.logs.push(`[${timeNow()}] Check requested for ${game.title_id}`);
|
state.logs.push(`[${timeNow()}] Check requested for ${game.title_id}`);
|
||||||
@@ -424,6 +541,11 @@ async function runTitleAction(titleId, action) {
|
|||||||
function updateGame(titleId, patch) {
|
function updateGame(titleId, patch) {
|
||||||
const game = state.titles.find((item) => item.title_id === titleId);
|
const game = state.titles.find((item) => item.title_id === titleId);
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
if (sourceType(game) === "unknown" && patch.mode && !["disabled", "check_only"].includes(patch.mode)) {
|
||||||
|
patch.mode = "check_only";
|
||||||
|
patch.enabled = true;
|
||||||
|
showToast(`${titleId}: Quelle unbekannt, nur Check only erlaubt.`);
|
||||||
|
}
|
||||||
Object.assign(game, patch);
|
Object.assign(game, patch);
|
||||||
if (patch.mode === "disabled") game.enabled = false;
|
if (patch.mode === "disabled") game.enabled = false;
|
||||||
state.logs.push(`[${timeNow()}] Policy updated for ${titleId}`);
|
state.logs.push(`[${timeNow()}] Policy updated for ${titleId}`);
|
||||||
@@ -431,6 +553,33 @@ function updateGame(titleId, patch) {
|
|||||||
renderLogs();
|
renderLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDownloadAllowed(game) {
|
||||||
|
const policy = sourcePolicy(game);
|
||||||
|
return Boolean(game.enabled && game.compatible_version && policy.allow_download);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInstallBlocked(game) {
|
||||||
|
return !sourcePolicy(game).allow_install;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourcePolicy(game) {
|
||||||
|
return sourcePolicyForType(sourceType(game));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourcePolicyForType(type) {
|
||||||
|
const fallbackPolicy = fallback.config.source_policy[type] || fallback.config.source_policy.unknown;
|
||||||
|
const configuredPolicy = (state.config.source_policy || {})[type] || {};
|
||||||
|
return { ...fallbackPolicy, ...configuredPolicy };
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTitle(game) {
|
||||||
|
if (!game.enabled) return "Titel ist deaktiviert.";
|
||||||
|
if (!game.compatible_version) return "Keine kompatible Update-Version verfuegbar.";
|
||||||
|
if (!sourcePolicy(game).allow_download) return "Download ist fuer diese Quelle gesperrt.";
|
||||||
|
if (isInstallBlocked(game)) return "Download erlaubt, Installation bleibt fuer diese Quelle gesperrt.";
|
||||||
|
return "Kompatibles Update laden.";
|
||||||
|
}
|
||||||
|
|
||||||
async function postJson(url, body) {
|
async function postJson(url, body) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -127,6 +127,7 @@
|
|||||||
<button class="is-selected" data-filter="all">Alle</button>
|
<button class="is-selected" data-filter="all">Alle</button>
|
||||||
<button data-filter="enabled">Aktiv</button>
|
<button data-filter="enabled">Aktiv</button>
|
||||||
<button data-filter="available">Updates</button>
|
<button data-filter="available">Updates</button>
|
||||||
|
<button data-filter="shadowmount">Shadow</button>
|
||||||
<button data-filter="blocked">Blockiert</button>
|
<button data-filter="blocked">Blockiert</button>
|
||||||
<button data-filter="queued">Queue</button>
|
<button data-filter="queued">Queue</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,6 +190,11 @@
|
|||||||
<span>Interne CDN Allowlist</span>
|
<span>Interne CDN Allowlist</span>
|
||||||
<div id="allowlistHosts"></div>
|
<div id="allowlistHosts"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="source-policy">
|
||||||
|
<span>Install Schutz</span>
|
||||||
|
<div id="sourcePolicyList"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+65
-2
@@ -377,12 +377,34 @@ h2 {
|
|||||||
color: #81500b;
|
color: #81500b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pill.external {
|
||||||
|
border-color: #b7d8e8;
|
||||||
|
background: var(--blue-soft);
|
||||||
|
color: #1e547a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill.shadow {
|
||||||
|
border-color: #d0c4eb;
|
||||||
|
background: #eee8fb;
|
||||||
|
color: #584080;
|
||||||
|
}
|
||||||
|
|
||||||
.pill.blocked {
|
.pill.blocked {
|
||||||
border-color: #f0b8b8;
|
border-color: #f0b8b8;
|
||||||
background: var(--red-soft);
|
background: var(--red-soft);
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-path {
|
||||||
|
margin-top: 8px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.version-grid {
|
.version-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -521,7 +543,8 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.field span,
|
.field span,
|
||||||
.allowlist > span {
|
.allowlist > span,
|
||||||
|
.source-policy > span {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -576,12 +599,47 @@ h2 {
|
|||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-policy {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.allowlist div {
|
.allowlist div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-policy > div {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 110px minmax(100px, 0.8fr) minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
background: #f8f9f6;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-row strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-row em {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.host-chip {
|
.host-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -701,10 +759,15 @@ pre {
|
|||||||
.toolbar,
|
.toolbar,
|
||||||
.split-band,
|
.split-band,
|
||||||
.status-strip,
|
.status-strip,
|
||||||
.version-grid {
|
.version-grid,
|
||||||
|
.policy-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.policy-row em {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.segmented {
|
.segmented {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user