feat: add service worker route with cache-control headers and update PWA manifest icons for proper maskable support

Add /sw.js route with send_from_directory to serve service worker from root scope with Service-Worker-Allowed:/ and Cache-Control:no-cache headers. Add navigator.serviceWorker.register() script to index.html with load event listener and scope:/ parameter. Update service worker cache to v4 with media-mark-192.svg/media-mark-512.svg additions to SHELL array.

Update manifest.json icons
This commit is contained in:
2026-08-16 17:29:21 +02:00
parent cef785361f
commit 5723a5d2a0
5 changed files with 23 additions and 7 deletions
+10 -1
View File
@@ -13,7 +13,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
import requests import requests
from flask import Flask, jsonify, render_template, request from flask import Flask, jsonify, render_template, request, send_from_directory
RADARR_URL = os.environ.get("RADARR_URL", "http://radarr:7878").rstrip("/") RADARR_URL = os.environ.get("RADARR_URL", "http://radarr:7878").rstrip("/")
RADARR_API_KEY = os.environ.get("RADARR_API_KEY", "") RADARR_API_KEY = os.environ.get("RADARR_API_KEY", "")
@@ -57,6 +57,15 @@ try:
except OSError as exc: except OSError as exc:
log.warning("Datei-Logging nicht verfügbar (%s): %s", LOG_FILE, exc) log.warning("Datei-Logging nicht verfügbar (%s): %s", LOG_FILE, exc)
app = Flask(__name__) app = Flask(__name__)
@app.get("/sw.js")
def service_worker():
"""Serve the worker from the origin root so it can control the whole app."""
response = send_from_directory(app.static_folder, "sw.js", mimetype="application/javascript")
response.headers["Service-Worker-Allowed"] = "/"
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
return response
executor = ThreadPoolExecutor(max_workers=int(os.environ.get("SCAN_WORKERS", "2"))) executor = ThreadPoolExecutor(max_workers=int(os.environ.get("SCAN_WORKERS", "2")))
scan_executor = ThreadPoolExecutor(max_workers=int(os.environ.get("SCAN_WORKERS", "4"))) scan_executor = ThreadPoolExecutor(max_workers=int(os.environ.get("SCAN_WORKERS", "4")))
jobs = {"radarr": {"state": "idle", "total": 0, "done": 0, "error": None}, "sonarr": {"state": "idle", "total": 0, "done": 0, "error": None}} jobs = {"radarr": {"state": "idle", "total": 0, "done": 0, "error": None}, "sonarr": {"state": "idle", "total": 0, "done": 0, "error": None}}
+3 -2
View File
@@ -13,7 +13,8 @@
"theme_color": "#1f252d", "theme_color": "#1f252d",
"orientation": "any", "orientation": "any",
"icons": [ "icons": [
{"src": "/static/media-mark-192.svg", "sizes": "192x192", "type": "image/svg+xml", "purpose": "any maskable"}, {"src": "/static/media-mark-192.svg", "sizes": "192x192", "type": "image/svg+xml", "purpose": "any"},
{"src": "/static/media-mark-512.svg", "sizes": "512x512", "type": "image/svg+xml", "purpose": "any maskable"} {"src": "/static/media-mark-512.svg", "sizes": "512x512", "type": "image/svg+xml", "purpose": "any"},
{"src": "/static/media-mark-512.svg", "sizes": "512x512", "type": "image/svg+xml", "purpose": "maskable"}
] ]
} }
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="512" height="512"><defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#293542"/><stop offset="1" stop-color="#15191f"/></linearGradient><filter id="glow"><feGaussianBlur stdDeviation="2.2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect x="5" y="5" width="54" height="54" rx="15" fill="url(#bg)" stroke="#60a9e6" stroke-width="2.5"/><path d="M17 21h30M17 31h19M17 41h30" fill="none" stroke="#f4c430" stroke-width="4" stroke-linecap="round"/><circle cx="46" cy="31" r="7" fill="#159447" stroke="#c9ffe0" stroke-width="2" filter="url(#glow)"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="192" height="192"><defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#293542"/><stop offset="1" stop-color="#15191f"/></linearGradient><filter id="glow"><feGaussianBlur stdDeviation="2.2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect x="5" y="5" width="54" height="54" rx="15" fill="url(#bg)" stroke="#60a9e6" stroke-width="2.5"/><path d="M17 21h30M17 31h19M17 41h30" fill="none" stroke="#f4c430" stroke-width="4" stroke-linecap="round"/><circle cx="46" cy="31" r="7" fill="#159447" stroke="#c9ffe0" stroke-width="2" filter="url(#glow)"/></svg>

Before

Width:  |  Height:  |  Size: 709 B

After

Width:  |  Height:  |  Size: 709 B

+2 -2
View File
@@ -1,5 +1,5 @@
const CACHE_NAME = 'media-max-shell-v3'; const CACHE_NAME = 'media-max-shell-v4';
const SHELL = ['/static/media-mark.svg', '/static/manifest.json']; const SHELL = ['/static/media-mark.svg', '/static/media-mark-192.svg', '/static/media-mark-512.svg', '/static/manifest.json'];
self.addEventListener('install', event => { self.addEventListener('install', event => {
event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(SHELL))); event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(SHELL)));
+7 -1
View File
@@ -1,4 +1,11 @@
<!doctype html> <!doctype html>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js?v=4', {scope: '/'}).catch(function () {});
});
}
</script>
<style>@media(max-width:700px){header .nav,main>.nav{display:none!important}#movies td:nth-child(4) .badge:nth-child(n+2),#movies td:nth-child(5) .badge:nth-child(n+2),#sonarr td:nth-child(4) .badge:nth-child(n+2),#sonarr td:nth-child(5) .badge:nth-child(n+2),#missing td:nth-child(4) .badge:nth-child(n+2),#missing td:nth-child(5) .badge:nth-child(n+2){display:none!important}.year{display:none!important}.title{max-width:100%!important}}</style> <style>@media(max-width:700px){header .nav,main>.nav{display:none!important}#movies td:nth-child(4) .badge:nth-child(n+2),#movies td:nth-child(5) .badge:nth-child(n+2),#sonarr td:nth-child(4) .badge:nth-child(n+2),#sonarr td:nth-child(5) .badge:nth-child(n+2),#missing td:nth-child(4) .badge:nth-child(n+2),#missing td:nth-child(5) .badge:nth-child(n+2){display:none!important}.year{display:none!important}.title{max-width:100%!important}}</style>
<script>document.addEventListener('DOMContentLoaded',()=>{const applyPreferred=()=>{const audio=localStorage.getItem('preferred-audio')||'',subs=localStorage.getItem('preferred-subs')||'';document.querySelectorAll('#movies tbody tr,#sonarr tbody tr,#missing tbody tr').forEach(row=>{[[4,audio,'audio-preferred'],[5,subs,'subtitle-preferred']].forEach(([column,preference,className])=>{const badges=[...row.querySelectorAll(`td:nth-child(${column}) .badge`)];if(!badges.length)return;badges.forEach(badge=>badge.classList.remove(className));const preferred=preference?badges.find(badge=>badge.textContent.includes(preference)):null;if(preferred){preferred.classList.add(className);preferred.parentElement.prepend(preferred)}})})};applyPreferred();setInterval(applyPreferred,1000)});</script> <script>document.addEventListener('DOMContentLoaded',()=>{const applyPreferred=()=>{const audio=localStorage.getItem('preferred-audio')||'',subs=localStorage.getItem('preferred-subs')||'';document.querySelectorAll('#movies tbody tr,#sonarr tbody tr,#missing tbody tr').forEach(row=>{[[4,audio,'audio-preferred'],[5,subs,'subtitle-preferred']].forEach(([column,preference,className])=>{const badges=[...row.querySelectorAll(`td:nth-child(${column}) .badge`)];if(!badges.length)return;badges.forEach(badge=>badge.classList.remove(className));const preferred=preference?badges.find(badge=>badge.textContent.includes(preference)):null;if(preferred){preferred.classList.add(className);preferred.parentElement.prepend(preferred)}})})};applyPreferred();setInterval(applyPreferred,1000)});</script>
<script>document.addEventListener('DOMContentLoaded',()=>{const nav=document.querySelector('.nav');if(nav)nav.classList.add('legacy-mobile-nav');const closeAll=()=>{document.body.classList.remove('mobile-nav-open');document.querySelectorAll('.mobile-drawer-clone,.mobile-drawer-shade,.mobile-nav-backdrop').forEach(node=>node.classList.remove('open'));document.querySelectorAll('.nav').forEach(node=>node.classList.remove('mobile-drawer-open'))};document.addEventListener('click',event=>{if(!window.matchMedia('(max-width:700px)').matches||!document.body.classList.contains('mobile-nav-open'))return;if(!event.target.closest('.mobile-drawer-clone,.mobile-nav,.brand'))closeAll()});document.addEventListener('keydown',event=>{if(event.key==='Escape')closeAll()});window.addEventListener('resize',()=>{if(!window.matchMedia('(max-width:700px)').matches)closeAll()})});</script> <script>document.addEventListener('DOMContentLoaded',()=>{const nav=document.querySelector('.nav');if(nav)nav.classList.add('legacy-mobile-nav');const closeAll=()=>{document.body.classList.remove('mobile-nav-open');document.querySelectorAll('.mobile-drawer-clone,.mobile-drawer-shade,.mobile-nav-backdrop').forEach(node=>node.classList.remove('open'));document.querySelectorAll('.nav').forEach(node=>node.classList.remove('mobile-drawer-open'))};document.addEventListener('click',event=>{if(!window.matchMedia('(max-width:700px)').matches||!document.body.classList.contains('mobile-nav-open'))return;if(!event.target.closest('.mobile-drawer-clone,.mobile-nav,.brand'))closeAll()});document.addEventListener('keydown',event=>{if(event.key==='Escape')closeAll()});window.addEventListener('resize',()=>{if(!window.matchMedia('(max-width:700px)').matches)closeAll()})});</script>
@@ -29,7 +36,6 @@
</style><style>.scan-control{display:flex;align-items:center;gap:5px}.scan-control select{height:36px;padding:0 8px;font-size:11px}.scan-control button{white-space:nowrap}.scan-running{position:relative;overflow:hidden;background:linear-gradient(90deg,rgba(244,196,48,.14) var(--scan-progress,0%),rgba(244,196,48,.04) var(--scan-progress,0%))!important}.scan-running::after{content:'';position:absolute;left:0;bottom:0;width:var(--scan-progress,0%);height:2px;background:#f4c430;transition:width .35s ease}@media(max-width:700px){.scan-control select{max-width:108px;font-size:10px}.scan-control button{font-size:0}.scan-control button::after{content:'↻';font-size:16px}}</style></head><body> </style><style>.scan-control{display:flex;align-items:center;gap:5px}.scan-control select{height:36px;padding:0 8px;font-size:11px}.scan-control button{white-space:nowrap}.scan-running{position:relative;overflow:hidden;background:linear-gradient(90deg,rgba(244,196,48,.14) var(--scan-progress,0%),rgba(244,196,48,.04) var(--scan-progress,0%))!important}.scan-running::after{content:'';position:absolute;left:0;bottom:0;width:var(--scan-progress,0%);height:2px;background:#f4c430;transition:width .35s ease}@media(max-width:700px){.scan-control select{max-width:108px;font-size:10px}.scan-control button{font-size:0}.scan-control button::after{content:'↻';font-size:16px}}</style></head><body>
<header><div class="brand"><div class="brand-mark" aria-label="Media Max"><svg viewBox="0 0 48 48" role="img"><path d="M10 8h28a4 4 0 0 1 4 4v24a4 4 0 0 1-4 4H10a4 4 0 0 1-4-4V12a4 4 0 0 1 4-4Z" fill="#20252d" stroke="#60a9e6" stroke-width="2"/><path d="M14 17h20M14 24h13M14 31h20" stroke="#f4c430" stroke-width="3" stroke-linecap="round"/><circle cx="34" cy="24" r="5" fill="#159447" stroke="#c9ffe0" stroke-width="1.5"/></svg></div><div class="brand-name">MEDIA</div></div><div><div class="head-title" data-de="Media Max" data-en="Media Max">Media Max</div><div class="head-sub" data-de="Audio- und Untertitelspuren aus den Mediendateien" data-en="Audio and subtitle tracks from media files">Audio- und Untertitelspuren aus den Mediendateien</div></div><div class="actions"><button class="lang-switch" id="languageToggle" title="Sprache / Language">DE</button><button class="settings-button" id="settingsOpen" title="Einstellungen"></button><div class="scan-control"><select id="scanSource" aria-label="Scan-Quelle"><option value="all">Alles scannen</option><option value="radarr">Nur Radarr</option><option value="sonarr">Nur Sonarr</option></select><button id="rescan" data-de="↻ Neu scannen" data-en="↻ Rescan">↻ Neu scannen</button></div></div></header> <header><div class="brand"><div class="brand-mark" aria-label="Media Max"><svg viewBox="0 0 48 48" role="img"><path d="M10 8h28a4 4 0 0 1 4 4v24a4 4 0 0 1-4 4H10a4 4 0 0 1-4-4V12a4 4 0 0 1 4-4Z" fill="#20252d" stroke="#60a9e6" stroke-width="2"/><path d="M14 17h20M14 24h13M14 31h20" stroke="#f4c430" stroke-width="3" stroke-linecap="round"/><circle cx="34" cy="24" r="5" fill="#159447" stroke="#c9ffe0" stroke-width="1.5"/></svg></div><div class="brand-name">MEDIA</div></div><div><div class="head-title" data-de="Media Max" data-en="Media Max">Media Max</div><div class="head-sub" data-de="Audio- und Untertitelspuren aus den Mediendateien" data-en="Audio and subtitle tracks from media files">Audio- und Untertitelspuren aus den Mediendateien</div></div><div class="actions"><button class="lang-switch" id="languageToggle" title="Sprache / Language">DE</button><button class="settings-button" id="settingsOpen" title="Einstellungen"></button><div class="scan-control"><select id="scanSource" aria-label="Scan-Quelle"><option value="all">Alles scannen</option><option value="radarr">Nur Radarr</option><option value="sonarr">Nur Sonarr</option></select><button id="rescan" data-de="↻ Neu scannen" data-en="↻ Rescan">↻ Neu scannen</button></div></div></header>
<main> <main>
{% if error %}<div class="banner"><strong>Fehler:</strong> {{ error }}</div>{% endif %}
<nav class="nav"><button class="active" data-view="dashboard"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:view-dashboard-outline.svg?color=%238bc9f2" alt="" aria-hidden="true">Dashboard</button><button data-view="radarr"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:movie-open-outline.svg?color=%23cc7b19" alt="" aria-hidden="true">RADARR · Filme</button>{% if sonarr_enabled %}<button data-view="sonarr"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:television-classic.svg?color=%2335c5f4" alt="" aria-hidden="true">SONARR · Serien</button>{% endif %}<button data-view="missing"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:alert-outline.svg?color=%23ff9e9e" alt="" aria-hidden="true">Ohne Deutsch <span class="nav-count" id="missingNavCount">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }}</span></button>{% if sab_enabled %}<button data-view="downloads"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:download-box-outline.svg?color=%23f4c430" alt="" aria-hidden="true">SABNZBD · Downloads <span class="nav-count download-nav-count hidden" id="downloadNavCount">0</span></button>{% endif %}<button data-view="datastore"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:harddisk.svg?color=%2360a9e6" alt="" aria-hidden="true">Datastore</button></nav> <nav class="nav"><button class="active" data-view="dashboard"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:view-dashboard-outline.svg?color=%238bc9f2" alt="" aria-hidden="true">Dashboard</button><button data-view="radarr"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:movie-open-outline.svg?color=%23cc7b19" alt="" aria-hidden="true">RADARR · Filme</button>{% if sonarr_enabled %}<button data-view="sonarr"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:television-classic.svg?color=%2335c5f4" alt="" aria-hidden="true">SONARR · Serien</button>{% endif %}<button data-view="missing"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:alert-outline.svg?color=%23ff9e9e" alt="" aria-hidden="true">Ohne Deutsch <span class="nav-count" id="missingNavCount">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }}</span></button>{% if sab_enabled %}<button data-view="downloads"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:download-box-outline.svg?color=%23f4c430" alt="" aria-hidden="true">SABNZBD · Downloads <span class="nav-count download-nav-count hidden" id="downloadNavCount">0</span></button>{% endif %}<button data-view="datastore"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:harddisk.svg?color=%2360a9e6" alt="" aria-hidden="true">Datastore</button></nav>
<section id="dashboard" class="view active" data-source="dashboard"><div class="dashboard-hero"><div><div class="eyebrow">MEDIA MAX</div><h1>Dashboard</h1><p>Deine gesamte Medienbibliothek und Systemauslastung auf einen Blick.</p></div><div class="live-pill"><span class="live-dot"></span> Live</div></div><div class="overview-grid"><div class="overview-card"><span class="overview-icon movie"></span><div><span>Filme</span><strong>{{ rows|length }}</strong><small>Radarr Library</small></div></div><div class="overview-card"><span class="overview-icon series-icon"></span><div><span>Serien</span><strong>{{ series|length }}</strong><small>Sonarr Shows</small></div></div><div class="overview-card"><span class="overview-icon warning"></span><div><span>Ohne Deutsch</span><strong id="dashboardMissing">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }}</strong><small>Audio oder Untertitel prüfen</small></div></div><div class="overview-card"><span class="overview-icon download-icon"></span><div><span>Downloads</span><strong id="dashboardDownloads"></strong><small id="dashboardRemaining">SABnzbd wird geladen …</small></div></div></div><div class="dashboard-columns"><section class="system-panel"><div class="panel-heading"><div><h2>Systemauslastung</h2><p>Aktuelle Werte aus dem Dashboard-Container</p></div><span id="systemUpdated" class="muted">Wird geladen …</span></div><div class="system-meters"><div class="meter"><div class="meter-top"><span>CPU</span><strong id="cpuValue"></strong></div><div class="meter-track"><span id="cpuBar"></span></div></div><div class="meter"><div class="meter-top"><span>RAM</span><strong id="ramValue"></strong></div><div class="meter-track"><span id="ramBar"></span></div></div></div><div id="diskList" class="disk-list"><div class="disk-row muted">Laufwerke werden geladen …</div></div></section><section class="service-panel"><div class="panel-heading"><div><h2>Dienste</h2><p>Verbindung und Bibliotheksstatus</p></div></div><div class="service-row"><span class="service-logo radarr-dot">R</span><div><strong>Radarr</strong><small>Filme & Qualität</small></div><span class="service-status online">Aktiv</span></div><div class="service-row"><span class="service-logo sonarr-dot">S</span><div><strong>Sonarr</strong><small>Serien & Episoden</small></div><span class="service-status {% if sonarr_enabled %}online{% else %}offline{% endif %}">{% if sonarr_enabled %}Aktiv{% else %}Nicht konfiguriert{% endif %}</span></div><div class="service-row"><span class="service-logo sab-dot"></span><div><strong>SABnzbd</strong><small>Downloads & Queue</small></div><span id="dashboardSabStatus" class="service-status {% if sab_enabled %}online{% else %}offline{% endif %}">{% if sab_enabled %}Wird geprüft …{% else %}Nicht konfiguriert{% endif %}</span></div></section></div></section> <section id="dashboard" class="view active" data-source="dashboard"><div class="dashboard-hero"><div><div class="eyebrow">MEDIA MAX</div><h1>Dashboard</h1><p>Deine gesamte Medienbibliothek und Systemauslastung auf einen Blick.</p></div><div class="live-pill"><span class="live-dot"></span> Live</div></div><div class="overview-grid"><div class="overview-card"><span class="overview-icon movie"></span><div><span>Filme</span><strong>{{ rows|length }}</strong><small>Radarr Library</small></div></div><div class="overview-card"><span class="overview-icon series-icon"></span><div><span>Serien</span><strong>{{ series|length }}</strong><small>Sonarr Shows</small></div></div><div class="overview-card"><span class="overview-icon warning"></span><div><span>Ohne Deutsch</span><strong id="dashboardMissing">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }}</strong><small>Audio oder Untertitel prüfen</small></div></div><div class="overview-card"><span class="overview-icon download-icon"></span><div><span>Downloads</span><strong id="dashboardDownloads"></strong><small id="dashboardRemaining">SABnzbd wird geladen …</small></div></div></div><div class="dashboard-columns"><section class="system-panel"><div class="panel-heading"><div><h2>Systemauslastung</h2><p>Aktuelle Werte aus dem Dashboard-Container</p></div><span id="systemUpdated" class="muted">Wird geladen …</span></div><div class="system-meters"><div class="meter"><div class="meter-top"><span>CPU</span><strong id="cpuValue"></strong></div><div class="meter-track"><span id="cpuBar"></span></div></div><div class="meter"><div class="meter-top"><span>RAM</span><strong id="ramValue"></strong></div><div class="meter-track"><span id="ramBar"></span></div></div></div><div id="diskList" class="disk-list"><div class="disk-row muted">Laufwerke werden geladen …</div></div></section><section class="service-panel"><div class="panel-heading"><div><h2>Dienste</h2><p>Verbindung und Bibliotheksstatus</p></div></div><div class="service-row"><span class="service-logo radarr-dot">R</span><div><strong>Radarr</strong><small>Filme & Qualität</small></div><span class="service-status online">Aktiv</span></div><div class="service-row"><span class="service-logo sonarr-dot">S</span><div><strong>Sonarr</strong><small>Serien & Episoden</small></div><span class="service-status {% if sonarr_enabled %}online{% else %}offline{% endif %}">{% if sonarr_enabled %}Aktiv{% else %}Nicht konfiguriert{% endif %}</span></div><div class="service-row"><span class="service-logo sab-dot"></span><div><strong>SABnzbd</strong><small>Downloads & Queue</small></div><span id="dashboardSabStatus" class="service-status {% if sab_enabled %}online{% else %}offline{% endif %}">{% if sab_enabled %}Wird geprüft …{% else %}Nicht konfiguriert{% endif %}</span></div></section></div></section>
<section id="radarr" class="view" data-source="radarr"> <section id="radarr" class="view" data-source="radarr">