Media build part 2 (unreleased): drive wiring in every app (R-1024/R-1037), old-install repair (R-1047), drive events notice + real restart now/later + stream check, file browser lets go of dead/removed drives and recreates on return (R-1038), safe removal keeps the drive (R-1044), Plex claim on the running server + probe accept (R-1028/R-1045)
gates / gates (push) Successful in 45s

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
admin committed 2026-10-15 09:54:49 +02:00
1 parent 78c894234c
commit f580d88a14
46 files changed
+3700 -8

No files matched your search

@@ -112,6 +112,16 @@
<h3>{{T "app_info.setup_gate_title"}}</h3>
<p>{{T "app_info.setup_gate_closed"}}</p>{{- if .SignupNative}}
<p class="form-hint" data-restarts-once="true">{{T "app_info.restarts_once"}}</p>{{- end}}
{{- if .PlexClaim}}
<div class="plex-claim" data-plex-claim="true">
<p>{{T "app_info.plex_claim_intro"}}</p>
<div class="input-with-button">
<input type="text" id="plex-claim-code" class="form-control" placeholder="claim-xxxxxxxxxxxxxxxxxxxx" autocomplete="off" spellcheck="false">
<button type="button" class="btn btn-sm btn-primary" id="plex-claim-btn" onclick="plexClaim(this)">{{T "app_info.plex_claim_btn"}}</button>
</div>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
{{- end}}
{{- if .SetupGateHasProbe}}
<p class="form-hint">{{T "app_info.setup_gate_probe"}}</p>
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/{{.Meta.Slug}}/setup-gate/open')">{{T "app_info.setup_gate_done_btn"}}</button>
@@ -368,6 +378,16 @@ function icCopyPw(btn) {
{{- if or .SetupGateClosed .SignupClosed .CloseSignupOffered (and .Stack.Deployed .KnownLoginLine) .LostFiles}}
<script>
// v0.280.0: the household's presses on this page — "Done, I set it up" (decision 46), "I changed it" (R-710).
// R-1028: the household's Plex code, given to the RUNNING server; the gate opens when Plex says it is claimed.
function plexClaim(btn) {
var box = btn.closest('.plex-claim'), err = box.querySelector('.app-press-err');
var code = (document.getElementById('plex-claim-code') || {}).value || '';
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/plex/plex-claim', {method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), credentials: 'same-origin', body: JSON.stringify({code: code.trim()})})
.then(function (r) { return r.json(); })
.then(function (j) { if (!j.ok) { throw new Error(j.error || ''); } window.location.reload(); })
.catch(function (e) { btn.disabled = false; err.textContent = e.message; err.style.display = 'inline'; });
}
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
@@ -5,6 +5,7 @@
<h2>{{T "dashboard.vezerlopult"}}</h2>
<span class="domain-badge">{{.Domain}}</span>
</div>
{{template "media_notice" .}}
<div class="stats-grid">
<a href="/stacks?filter=running" class="stat-card">
@@ -0,0 +1,74 @@
{{/*
media_notice (v0.313.0, `09` §3 decisions 238/239/245) — the media drive notice: a drive was added, removed or came back,
so the media apps must restart to see it; „restart now" (after a stream check) or „later" (tonight 03:30); per drive
„apps restarted since this change: yes/no"; and the result of the drive wiring. Rendered empty and filled from
GET /api/storage/media/status, so every page that includes it shows the same truth. Hidden when there is nothing to say.
*/}}
{{define "media_notice"}}
<div id="media-notice" class="settings-card media-notice" hidden>
<h3>{{T "media.notice_title"}}</h3>
<p class="settings-card-desc">{{T "media.notice_body"}}</p>
<ul id="media-notice-drives" class="media-notice-list"></ul>
<p id="media-notice-apps" class="form-hint"></p>
<p id="media-notice-stale" class="form-hint" hidden></p>
<p id="media-notice-later" class="form-hint" hidden></p>
<div class="media-notice-actions">
<button type="button" class="btn btn-sm btn-primary" id="media-restart-now">{{T "media.restart_now"}}</button>
<button type="button" class="btn btn-sm btn-outline" id="media-restart-later">{{T "media.restart_later"}}</button>
</div>
<div id="media-notice-out"></div>
<div id="media-wiring" hidden>
<h4>{{T "media.wiring_title"}}</h4>
<ul id="media-wiring-list" class="media-notice-list"></ul>
<p id="media-wiring-hint" class="form-hint" hidden>{{T "media.jellyfin_key_hint"}}</p>
<button type="button" class="btn btn-sm btn-outline" id="media-wire-now">{{T "media.wire_now"}}</button>
</div>
</div>
<script>
(function(){
var box=document.getElementById('media-notice'); if(!box) return;
var csrf=(document.querySelector('meta[name="csrf-token"]')||{}).content||'';
var L={added:"{{T "media.change_added"}}",removed:"{{T "media.change_removed"}}",returned:"{{T "media.change_returned"}}",
yes:"{{T "media.restarted_yes"}}",no:"{{T "media.restarted_no"}}",apps:"{{T "media.apps"}}",stale:"{{T "media.stale"}}",
later:"{{T "media.later_at"}}",streams:"{{T "media.streams_playing"}}",done:"{{T "media.restart_done"}}",
err:"{{T "media.error"}}",household:"{{T "media.state_household"}}"};
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
function li(t){var e=document.createElement('li'); e.textContent=t; return e;}
async function load(){
try{
var r=await fetch('/api/storage/media/status',{credentials:'same-origin'}); var j=await r.json(); if(!j.ok||!j.data) return;
var d=j.data; if(!d.apps||!d.apps.length) return;
var ul=document.getElementById('media-notice-drives'); ul.innerHTML='';
(d.drives||[]).forEach(function(x){ ul.appendChild(li(x.label+' — '+(L[x.change]||x.change)+' — '+(x.restarted?L.yes:L.no))); });
document.getElementById('media-notice-apps').textContent=L.apps+' '+d.apps.join(', ');
var st=document.getElementById('media-notice-stale'); if(d.stale&&d.stale.length){st.textContent=L.stale+' '+d.stale.join(', '); st.hidden=false;}
var la=document.getElementById('media-notice-later'); if(d.later_at){la.textContent=L.later+' '+d.later_at; la.hidden=false;}
if(d.wiring&&d.wiring.length){
var wl=document.getElementById('media-wiring-list'); wl.innerHTML=''; var hh=false;
d.wiring.forEach(function(x){ if(x.app==='folders') return; var t=x.app+': '+(x.state==='household'?L.household:x.state)+(x.detail&&x.state==='error'?' ('+x.detail+')':''); wl.appendChild(li(t)); if(x.state==='household'&&x.app==='jellyfin') hh=true; });
document.getElementById('media-wiring-hint').hidden=!hh; document.getElementById('media-wiring').hidden=false;
}
var show=d.pending||!!(d.wiring&&d.wiring.some(function(x){return x.state==='household'||x.state==='error';}));
document.querySelector('.media-notice-actions').hidden=!d.pending;
box.hidden=!show;
}catch(e){}
}
async function post(url,body){
var r=await fetch(url,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify(body||{})});
var j={}; try{j=await r.json();}catch(e){} return {status:r.status,j:j};
}
var out=document.getElementById('media-notice-out');
async function restart(force){
out.innerHTML='';
var res=await post('/api/storage/media/restart',{when:'now',force:!!force});
if(res.status===409){ felhomConfirm(document.getElementById('media-restart-now'), L.streams, function(){ restart(true); }); return; }
out.innerHTML=res.j.ok?'<div class="alert alert-info">'+esc(L.done)+'</div>':'<div class="alert alert-error">'+esc(L.err)+' '+esc(res.j.error||((res.j.data||{}).failed||[]).join(', '))+'</div>';
load();
}
document.getElementById('media-restart-now').addEventListener('click',function(){restart(false);});
document.getElementById('media-restart-later').addEventListener('click',async function(){ await post('/api/storage/media/restart',{when:'later'}); load(); });
document.getElementById('media-wire-now').addEventListener('click',async function(){ await post('/api/storage/media/wire',{}); load(); });
load();
})();
</script>
{{end}}
@@ -4,6 +4,7 @@
<div class="page-header">
<h2>{{T "storage.tarhely_meghajtok"}}</h2>
</div>
{{template "media_notice" .}}
<!-- Section: Storage Paths -->
<div class="settings-card">
@@ -3756,3 +3756,8 @@ html::-webkit-scrollbar-track { background: var(--bg-0); }
.kept-delete-form { display: flex; flex-direction: column; gap: .4rem; margin-top: .5rem; max-width: 32rem; }
.kept-delete-text { color: var(--warn); margin: 0; }
.storage-kept-link { margin-top: .75rem; }
/* media drive notice (v0.313.0) */
.media-notice-list { margin: .4rem 0 .6rem 1.1rem; padding: 0; }
.media-notice-list li { margin: .15rem 0; }
.media-notice-actions { display: flex; gap: .5rem; flex-wrap: wrap; margin: .6rem 0; }