feat: add non-blocking RAID status refresh with dedicated lock and remove offline navigation fallback from service worker
Add raid_refresh_lock to prevent concurrent RAID refresh operations. Extract refresh_raid_status() function with non-blocking lock acquisition using acquire(blocking=False). Update datastore_snapshot() to spawn background thread for RAID refresh when cache is stale but return immediately with cached data. Remove offline navigation fallback from service worker to prevent masking backend errors. Bump service worker cache version to v7
This commit is contained in:
@@ -83,6 +83,7 @@ service_status_lock = threading.Lock()
|
|||||||
service_check_executor = ThreadPoolExecutor(max_workers=16)
|
service_check_executor = ThreadPoolExecutor(max_workers=16)
|
||||||
datastore_cache_lock = threading.Lock()
|
datastore_cache_lock = threading.Lock()
|
||||||
datastore_refresh_lock = threading.Lock()
|
datastore_refresh_lock = threading.Lock()
|
||||||
|
raid_refresh_lock = threading.Lock()
|
||||||
datastore_cache = {"stored_at": 0.0, "snapshot": None, "raid": None, "raid_at": 0.0}
|
datastore_cache = {"stored_at": 0.0, "snapshot": None, "raid": None, "raid_at": 0.0}
|
||||||
raid_log_state = None
|
raid_log_state = None
|
||||||
hardware_log_once = set()
|
hardware_log_once = set()
|
||||||
@@ -549,6 +550,25 @@ def refresh_datastore_storage():
|
|||||||
finally:
|
finally:
|
||||||
datastore_refresh_lock.release()
|
datastore_refresh_lock.release()
|
||||||
|
|
||||||
|
def refresh_raid_status():
|
||||||
|
if not raid_refresh_lock.acquire(blocking=False):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
current = raid_status()
|
||||||
|
with datastore_cache_lock:
|
||||||
|
if current.get("status") == "failed" and current.get("error") and datastore_cache.get("raid"):
|
||||||
|
cached = dict(datastore_cache["raid"])
|
||||||
|
cached["stale"] = True
|
||||||
|
cached["warning"] = "Vorübergehend keine aktuelle RAID-Antwort."
|
||||||
|
datastore_cache["raid"] = cached
|
||||||
|
else:
|
||||||
|
datastore_cache["raid"] = current
|
||||||
|
datastore_cache["raid_at"] = time.time()
|
||||||
|
except Exception as exc:
|
||||||
|
log.error("RAID-Refresh fehlgeschlagen: %s", error_summary(exc))
|
||||||
|
finally:
|
||||||
|
raid_refresh_lock.release()
|
||||||
|
|
||||||
def datastore_snapshot():
|
def datastore_snapshot():
|
||||||
now = time.time()
|
now = time.time()
|
||||||
with datastore_cache_lock:
|
with datastore_cache_lock:
|
||||||
@@ -560,15 +580,14 @@ def datastore_snapshot():
|
|||||||
threading.Thread(target=refresh_datastore_storage, name="datastore-refresh", daemon=True).start()
|
threading.Thread(target=refresh_datastore_storage, name="datastore-refresh", daemon=True).start()
|
||||||
with datastore_cache_lock:
|
with datastore_cache_lock:
|
||||||
storage = datastore_cache["snapshot"]
|
storage = datastore_cache["snapshot"]
|
||||||
current_raid = raid_status()
|
|
||||||
with datastore_cache_lock:
|
with datastore_cache_lock:
|
||||||
if current_raid.get("status") == "failed" and current_raid.get("error") and datastore_cache.get("raid") and now - datastore_cache.get("raid_at", 0) < 60:
|
current_raid = datastore_cache.get("raid")
|
||||||
current_raid = dict(datastore_cache["raid"])
|
raid_age = now - datastore_cache.get("raid_at", 0)
|
||||||
current_raid["stale"] = True
|
if current_raid is None:
|
||||||
current_raid["warning"] = "Vorübergehend keine aktuelle RAID-Antwort; letzter gültiger Stand wird angezeigt."
|
threading.Thread(target=refresh_raid_status, name="raid-refresh", daemon=True).start()
|
||||||
elif current_raid.get("status") != "failed":
|
current_raid = {"device": RAID_DEVICE, "status": "unknown", "label": "WIRD GELADEN", "error": "RAID-Status wird geladen"}
|
||||||
datastore_cache["raid"] = dict(current_raid)
|
elif raid_age >= 5:
|
||||||
datastore_cache["raid_at"] = now
|
threading.Thread(target=refresh_raid_status, name="raid-refresh", daemon=True).start()
|
||||||
return {"storage": storage, "raid": current_raid, "io_pressure": io_pressure(), "cache_seconds": STORAGE_CACHE_SECONDS}
|
return {"storage": storage, "raid": current_raid, "io_pressure": io_pressure(), "cache_seconds": STORAGE_CACHE_SECONDS}
|
||||||
|
|
||||||
def cpu_usage_percent():
|
def cpu_usage_percent():
|
||||||
|
|||||||
+5
-4
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = 'media-max-shell-v6';
|
const CACHE_NAME = 'media-max-shell-v7';
|
||||||
const SHELL = ['/static/media-mark.svg', '/static/media-mark-192.png', '/static/media-mark-512.png', '/static/manifest.json'];
|
const SHELL = ['/static/media-mark.svg', '/static/media-mark-192.png', '/static/media-mark-512.png', '/static/manifest.json'];
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
@@ -16,10 +16,11 @@ self.addEventListener('fetch', event => {
|
|||||||
if (request.method !== 'GET') return;
|
if (request.method !== 'GET') return;
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
if (url.pathname.startsWith('/api/')) return;
|
if (url.pathname.startsWith('/api/')) return;
|
||||||
|
// Navigation requests must stay real network requests. Returning an
|
||||||
|
// offline document here can mask backend errors and make the app appear
|
||||||
|
// disconnected after a scan/reload.
|
||||||
|
if (request.mode === 'navigate') return;
|
||||||
event.respondWith(fetch(request).catch(() => {
|
event.respondWith(fetch(request).catch(() => {
|
||||||
if (request.mode === 'navigate') {
|
|
||||||
return new Response('<!doctype html><meta charset="utf-8"><title>Media Max</title><style>body{margin:0;padding:2rem;background:#181818;color:#ddd;font:16px system-ui}main{max-width:36rem;margin:auto}h1{color:#f4c430}</style><main><h1>Media Max</h1><p>Die Verbindung ist momentan unterbrochen. Bitte erneut laden.</p></main>', {headers: {'Content-Type': 'text/html; charset=utf-8'}});
|
|
||||||
}
|
|
||||||
return caches.match(request).then(response => response || new Response('', {status: 503}));
|
return caches.match(request).then(response => response || new Response('', {status: 503}));
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ document.addEventListener('DOMContentLoaded',()=>{const grid=document.querySelec
|
|||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
navigator.serviceWorker.register('/sw.js?v=6', {scope: '/'}).catch(function () {});
|
navigator.serviceWorker.register('/sw.js?v=7', {scope: '/'}).catch(function () {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -50,7 +50,7 @@ if ('serviceWorker' in navigator) {
|
|||||||
<script>document.addEventListener('DOMContentLoaded',()=>{const badge=document.querySelector('#raidStatusBadge');if(!badge)return;const side=document.createElement('div');side.className='raid-status-side';const countdown=document.createElement('small');countdown.id='raidRefreshCountdown';countdown.textContent='Update in 5s';badge.replaceWith(side);side.append(badge,countdown);const interval=5000;let next=Date.now()+interval;const reset=()=>{next=Date.now()+interval};window.resetRaidRefreshCountdown=reset;const update=()=>{const seconds=Math.max(0,Math.ceil((next-Date.now())/1000));countdown.textContent=`Update in ${seconds}s`;if(seconds===0)reset()};update();setInterval(update,250);const updated=document.querySelector('#datastoreUpdated');if(updated)new MutationObserver(reset).observe(updated,{childList:true,characterData:true,subtree:true})});</script>
|
<script>document.addEventListener('DOMContentLoaded',()=>{const badge=document.querySelector('#raidStatusBadge');if(!badge)return;const side=document.createElement('div');side.className='raid-status-side';const countdown=document.createElement('small');countdown.id='raidRefreshCountdown';countdown.textContent='Update in 5s';badge.replaceWith(side);side.append(badge,countdown);const interval=5000;let next=Date.now()+interval;const reset=()=>{next=Date.now()+interval};window.resetRaidRefreshCountdown=reset;const update=()=>{const seconds=Math.max(0,Math.ceil((next-Date.now())/1000));countdown.textContent=`Update in ${seconds}s`;if(seconds===0)reset()};update();setInterval(update,250);const updated=document.querySelector('#datastoreUpdated');if(updated)new MutationObserver(reset).observe(updated,{childList:true,characterData:true,subtree:true})});</script>
|
||||||
<script>document.addEventListener('DOMContentLoaded',()=>{const icons={total:'mdi:harddisk',used:'mdi:database',free:'mdi:database-outline',percent:'mdi:chart-donut'};document.querySelectorAll('.storage-card .storage-icon').forEach((node,index)=>{const key=['total','used','free','percent'][index];if(!key)return;const image=document.createElement('img');image.src=`https://api.iconify.design/${icons[key]}.svg?color=${key==='used'?'%23f4d267':key==='free'?'%236ae29a':key==='percent'?'%23c59cff':'%2360a9e6'}`;image.alt='';image.setAttribute('aria-hidden','true');node.textContent='';node.appendChild(image)})});</script>
|
<script>document.addEventListener('DOMContentLoaded',()=>{const icons={total:'mdi:harddisk',used:'mdi:database',free:'mdi:database-outline',percent:'mdi:chart-donut'};document.querySelectorAll('.storage-card .storage-icon').forEach((node,index)=>{const key=['total','used','free','percent'][index];if(!key)return;const image=document.createElement('img');image.src=`https://api.iconify.design/${icons[key]}.svg?color=${key==='used'?'%23f4d267':key==='free'?'%236ae29a':key==='percent'?'%23c59cff':'%2360a9e6'}`;image.alt='';image.setAttribute('aria-hidden','true');node.textContent='';node.appendChild(image)})});</script>
|
||||||
<script>document.addEventListener('DOMContentLoaded',()=>{if(!document.querySelector('link[rel="shortcut icon"]')){const favicon=document.createElement('link');favicon.rel='shortcut icon';favicon.type='image/svg+xml';favicon.href="{{ url_for('static', filename='media-mark.svg') }}?v=media-max";document.head.appendChild(favicon)}});</script>
|
<script>document.addEventListener('DOMContentLoaded',()=>{if(!document.querySelector('link[rel="shortcut icon"]')){const favicon=document.createElement('link');favicon.rel='shortcut icon';favicon.type='image/svg+xml';favicon.href="{{ url_for('static', filename='media-mark.svg') }}?v=media-max";document.head.appendChild(favicon)}});</script>
|
||||||
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#1f252d"><meta name="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-capable" content="yes"><link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}?v=6"><link rel="apple-touch-icon" href="{{ url_for('static', filename='media-mark-192.png') }}"><link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='media-mark.svg') }}"><title>Media Max</title>
|
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#1f252d"><meta name="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-capable" content="yes"><link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}?v=7"><link rel="apple-touch-icon" href="{{ url_for('static', filename='media-mark-192.png') }}"><link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='media-mark.svg') }}"><title>Media Max</title>
|
||||||
<style>
|
<style>
|
||||||
:root{color-scheme:dark;--bg:#181818;--panel:#222;--line:#3a3a3a;--text:#dedede;--muted:#929292;--yellow:#f4c430;--blue:#60a9e6}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:13px Inter,system-ui,sans-serif}header{height:58px;display:flex;align-items:center;gap:15px;padding:0 20px;background:#242424;border-bottom:1px solid #343434}.logo{color:var(--yellow);font-size:22px;font-weight:900;letter-spacing:1px}.head-title{font-weight:700}.head-sub{font-size:12px;color:var(--muted);margin-top:2px}.actions{margin-left:auto}button,input,select{font:inherit;color:var(--text);background:#292929;border:1px solid #444;border-radius:5px}button{padding:7px 10px;cursor:pointer}main{padding:14px 20px 30px}.nav{display:flex;gap:4px;margin-bottom:12px;border-bottom:1px solid var(--line)}.nav button{border:0;border-radius:5px 5px 0 0;background:transparent;color:#aaa}.nav button.active{background:#303030;color:#fff}.view{display:none}.view.active{display:block}.toolbar{display:grid;grid-template-columns:minmax(280px,1fr) 190px 190px 170px;gap:8px;margin-bottom:10px}input,select{padding:7px 10px;height:34px}.stats{display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap}.stat{background:#242424;border:1px solid #363636;padding:5px 9px;border-radius:5px;color:#bbb}.scan{color:#f4c430}.banner{padding:9px 11px;border:1px solid #693b3b;background:#372424;border-radius:5px;margin-bottom:10px;color:#ffd1d1}.tablewrap{border:1px solid #353535;border-radius:6px;overflow:hidden;background:var(--panel)}table{width:100%;border-collapse:collapse;table-layout:fixed}th{padding:8px 10px;background:#282828;border-bottom:1px solid #444;text-align:left;font-size:12px}td{padding:7px 10px;border-bottom:1px solid #343434;vertical-align:middle}.col-title{width:28%}.col-quality{width:12%}.col-state{width:10%}.col-audio{width:17%}.col-subs{width:17%}.col-video{width:8%}.col-size{width:8%}.title{color:var(--blue);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;max-width:90%;vertical-align:bottom}.year{color:var(--muted);font-size:11px;margin-left:5px}.badges{display:flex;gap:4px;flex-wrap:wrap}.badge{display:inline-flex;gap:4px;align-items:center;padding:2px 6px;border-radius:999px;border:1px solid #484848;background:#303030;white-space:nowrap;font-size:11px}.quality{background:#0e7138;border-color:#19894b}.state{font-size:11px;font-weight:700;padding:3px 7px;border-radius:4px;display:inline-block}.state.ok{background:#126f39;color:#d9ffe8}.state.warn{background:#77561e;color:#ffebc4}.state.bad{background:#763030;color:#ffd9d9}.state.neutral{background:#3b3b3b;color:#ccc}.muted{color:var(--muted)}.hidden{display:none}.series{border-bottom:1px solid var(--line)}.series:last-child{border:0}.series summary{padding:12px;cursor:pointer;background:#282828;font-weight:700}.series summary:hover{background:#303030}.series table{border-radius:0}.series td:first-child{padding-left:28px}@media(max-width:1100px){.toolbar{grid-template-columns:1fr 1fr}.col-video,.col-size{display:none}}
|
:root{color-scheme:dark;--bg:#181818;--panel:#222;--line:#3a3a3a;--text:#dedede;--muted:#929292;--yellow:#f4c430;--blue:#60a9e6}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:13px Inter,system-ui,sans-serif}header{height:58px;display:flex;align-items:center;gap:15px;padding:0 20px;background:#242424;border-bottom:1px solid #343434}.logo{color:var(--yellow);font-size:22px;font-weight:900;letter-spacing:1px}.head-title{font-weight:700}.head-sub{font-size:12px;color:var(--muted);margin-top:2px}.actions{margin-left:auto}button,input,select{font:inherit;color:var(--text);background:#292929;border:1px solid #444;border-radius:5px}button{padding:7px 10px;cursor:pointer}main{padding:14px 20px 30px}.nav{display:flex;gap:4px;margin-bottom:12px;border-bottom:1px solid var(--line)}.nav button{border:0;border-radius:5px 5px 0 0;background:transparent;color:#aaa}.nav button.active{background:#303030;color:#fff}.view{display:none}.view.active{display:block}.toolbar{display:grid;grid-template-columns:minmax(280px,1fr) 190px 190px 170px;gap:8px;margin-bottom:10px}input,select{padding:7px 10px;height:34px}.stats{display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap}.stat{background:#242424;border:1px solid #363636;padding:5px 9px;border-radius:5px;color:#bbb}.scan{color:#f4c430}.banner{padding:9px 11px;border:1px solid #693b3b;background:#372424;border-radius:5px;margin-bottom:10px;color:#ffd1d1}.tablewrap{border:1px solid #353535;border-radius:6px;overflow:hidden;background:var(--panel)}table{width:100%;border-collapse:collapse;table-layout:fixed}th{padding:8px 10px;background:#282828;border-bottom:1px solid #444;text-align:left;font-size:12px}td{padding:7px 10px;border-bottom:1px solid #343434;vertical-align:middle}.col-title{width:28%}.col-quality{width:12%}.col-state{width:10%}.col-audio{width:17%}.col-subs{width:17%}.col-video{width:8%}.col-size{width:8%}.title{color:var(--blue);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;max-width:90%;vertical-align:bottom}.year{color:var(--muted);font-size:11px;margin-left:5px}.badges{display:flex;gap:4px;flex-wrap:wrap}.badge{display:inline-flex;gap:4px;align-items:center;padding:2px 6px;border-radius:999px;border:1px solid #484848;background:#303030;white-space:nowrap;font-size:11px}.quality{background:#0e7138;border-color:#19894b}.state{font-size:11px;font-weight:700;padding:3px 7px;border-radius:4px;display:inline-block}.state.ok{background:#126f39;color:#d9ffe8}.state.warn{background:#77561e;color:#ffebc4}.state.bad{background:#763030;color:#ffd9d9}.state.neutral{background:#3b3b3b;color:#ccc}.muted{color:var(--muted)}.hidden{display:none}.series{border-bottom:1px solid var(--line)}.series:last-child{border:0}.series summary{padding:12px;cursor:pointer;background:#282828;font-weight:700}.series summary:hover{background:#303030}.series table{border-radius:0}.series td:first-child{padding-left:28px}@media(max-width:1100px){.toolbar{grid-template-columns:1fr 1fr}.col-video,.col-size{display:none}}
|
||||||
.brand-mark{width:34px;height:34px;display:grid;place-items:center;filter:drop-shadow(0 0 10px rgba(96,169,230,.24))}.brand-mark svg{width:34px;height:34px}.brand-name{color:var(--yellow);font-size:16px;font-weight:900;letter-spacing:1.5px}.brand{display:flex;align-items:center;gap:9px}
|
.brand-mark{width:34px;height:34px;display:grid;place-items:center;filter:drop-shadow(0 0 10px rgba(96,169,230,.24))}.brand-mark svg{width:34px;height:34px}.brand-name{color:var(--yellow);font-size:16px;font-weight:900;letter-spacing:1.5px}.brand{display:flex;align-items:center;gap:9px}
|
||||||
|
|||||||
Reference in New Issue
Block a user