8fc2b4a1a9
gates / gates (push) Successful in 26s
The guarded update gains a folder copy of the app's named volumes, taken after the pull where the app stops anyway (decision 19, chosen by the 2026-09-23 bake-off). On a failed health check the box undoes: every copy validated by its finished-marker first, volumes refilled, definition and pin from the job's own pre-update copies, the old version checked with the OLD .felhom.yml probe. It holds only if the undo fails, and the hold sentence says so and what state the data is in. Bind-mounted folders are never touched. - R-637 built; R-638/R-640/R-641 do not arise with a folder copy; R-639 (pre-update copies incl. .felhom.yml kept until the undo is over). - journal phases copying/undoing with power-cut recovery. - app.yaml last_update_undone + one line on the app page (hu/en). - R-642: start/restart never answer "completed". - Removal deletes kept undo copies. MinAgent unchanged (0.131.0). Nine red-proofs in REPORT.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
288 lines
15 KiB
HTML
288 lines
15 KiB
HTML
{{define "app_info"}}
|
|
{{template "layout_start" .}}
|
|
|
|
<div class="page-header">
|
|
<div style="display:flex;align-items:center;gap:1rem">
|
|
<a href="/stacks" class="btn btn-sm btn-outline">{{T "app_info.alkalmazasok"}}</a>
|
|
<h2>{{.Meta.DisplayName}}</h2>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:.5rem">
|
|
{{if .Stack.Deployed}}
|
|
<span class="stack-state-badge state-{{stateColor .Stack.State}}">{{stateLabel .Stack.State}}</span>
|
|
{{if .Stack.Orphaned}}<span class="badge badge-orphaned">{{T "common.elavult"}}</span>{{end}}
|
|
{{template "meta_badge" (lifecycleBadge .Meta)}}
|
|
{{template "meta_badge" (updateBadge .Stack)}}
|
|
{{if .EffectiveSubdomain}}<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline">{{T "app_info.megnyitas"}}</a>{{end}}
|
|
<a href="/stacks/{{.Stack.Name}}/logs" class="btn btn-sm btn-outline">{{T "app_info.naplo"}}</a>
|
|
{{if .Stack.Orphaned}}
|
|
<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('{{.Stack.Name}}')">{{T "common.torles"}}</button>
|
|
{{else}}
|
|
<a href="/stacks/{{.Stack.Name}}/export" class="btn btn-sm btn-outline">{{T "app_info.exportalas"}}</a>
|
|
<a href="/stacks/{{.Stack.Name}}/deploy" class="btn btn-sm btn-outline">{{T "app_info.beallitasok"}}</a>
|
|
{{end}}
|
|
{{else}}
|
|
{{/* Server-side the deploy endpoint refuses a non-installable template; hide the button
|
|
so the page never offers an action that would be rejected. */}}
|
|
{{if canInstall .Meta}}<a href="/stacks/{{.Stack.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Stack.Name}}')">{{T "app_info.telepites"}}</a>{{end}}
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
{{if .Stack.Updating}}
|
|
<div class="alert alert-info" style="margin-top:1rem" data-update-phase="{{.Stack.UpdatePhase}}">{{.Stack.UpdatePhaseLabel}}</div>
|
|
{{else if .Stack.HoldReason}}
|
|
<div class="alert alert-error" style="margin-top:1rem" data-held="true">{{.Stack.HoldReason}} <a href="/backups/apps" class="btn btn-sm btn-outline">{{T "app_info.mentesek"}}</a></div>
|
|
{{else if .Stack.UpdateError}}
|
|
<div class="alert alert-warning" style="margin-top:1rem" data-update-error="true">{{.Stack.UpdateError}}</div>
|
|
{{else if .UpdateUndoneLine}}
|
|
<div class="alert alert-info" style="margin-top:1rem" data-update-undone="true">{{.UpdateUndoneLine}}</div>
|
|
{{end}}
|
|
|
|
{{if .MissingStorageLabel}}
|
|
<div class="alert alert-warning" style="margin-top:1rem">
|
|
<strong><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "app_info.hianyzo_tarhely"}} {{.MissingStorageLabel}}</strong><br>
|
|
{{T "app_info.ennek_az_alkalmazasnak_az_adattaroloja"}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Hero section -->
|
|
<div class="app-info-hero">
|
|
<img class="app-info-logo" src="{{logoURL .Meta.Slug}}"
|
|
alt="{{.Meta.DisplayName}}" data-fallback="/static/app-placeholder.svg"
|
|
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Meta.Slug}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
|
<div class="app-info-hero-text">
|
|
{{if .AppInfo.Tagline}}
|
|
<p class="app-info-tagline">{{.AppInfo.Tagline}}</p>
|
|
{{else}}
|
|
<p class="app-info-tagline">{{.Meta.Description}}</p>
|
|
{{end}}
|
|
<div class="stack-meta-badges">
|
|
<span class="meta-badge">~{{.Meta.Resources.MemRequest}} RAM</span>
|
|
<span class="meta-badge">{{.Meta.Category}}</span>
|
|
{{if .Meta.Resources.NeedsHDD}}<span class="meta-badge meta-badge-warn">{{T "common.hdd_szukseges"}}</span>{{end}}
|
|
{{if .Meta.Resources.PiCompatible}}<span class="meta-badge meta-badge-ok">{{T "common.pi_kompatibilis"}}</span>{{else}}<span class="meta-badge meta-badge-warn">{{T "app_info.csak_x86"}}</span>{{end}}
|
|
{{if .Meta.Resources.HungarianUI}}<span class="meta-badge meta-badge-ok">{{T "common.magyar_felulet"}}</span>{{end}}
|
|
</div>
|
|
{{if .Meta.IsAbandoned}}
|
|
<div class="alert alert-warning" style="margin-top:.75rem">
|
|
{{T "app_info.az_alkalmazas_fejlesztoje_felhagyott_a"}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Screenshots (graceful — hidden if assets don't exist) -->
|
|
<div class="app-screenshots" id="screenshots">
|
|
<img src="{{screenshotURL .Meta.Slug 1}}" alt="" class="app-screenshot"
|
|
onerror="this.style.display='none'">
|
|
<img src="{{screenshotURL .Meta.Slug 2}}" alt="" class="app-screenshot"
|
|
onerror="this.style.display='none'">
|
|
<img src="{{screenshotURL .Meta.Slug 3}}" alt="" class="app-screenshot"
|
|
onerror="this.style.display='none'">
|
|
</div>
|
|
|
|
{{if .DataPathCards}}
|
|
<div class="app-info-card" style="margin-top:1rem">
|
|
<h3>{{T "app_info.hova_tegyem_a_fajlokat"}}</h3>
|
|
<p class="form-hint">{{T "app_info.ezeket_a_mappakat_a_fajlkezeloben"}}</p>
|
|
<div class="datapath-list">
|
|
{{range .DataPathCards}}
|
|
<div class="datapath-row">
|
|
<div class="datapath-head">
|
|
<a href="{{.Link}}" target="_blank" rel="noopener" class="datapath-link">{{.Label}} ↗</a>
|
|
{{if .FreeSpace}}<span class="datapath-space">{{.FreeSpace}}</span>{{end}}
|
|
</div>
|
|
{{if .Consequence}}<p class="datapath-note">{{.Consequence}}</p>{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if and .Stack.Deployed .MigrateTargets}}
|
|
<div class="app-info-card" style="margin-top:1rem">
|
|
<h3>{{T "app_info.athelyezes_masik_tarhelyre"}}</h3>
|
|
<p class="form-hint">{{T "app_info.ennek_az_alkalmazasnak_az_adatait"}}</p>
|
|
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.5rem">
|
|
<select id="app-migrate-target" class="btn btn-sm btn-outline">
|
|
<option value="">{{T "app_info.valassz_celtarhelyet"}}</option>
|
|
{{range .MigrateTargets}}<option value="{{.Path}}">{{.Label}} ({{.Path}})</option>{{end}}
|
|
</select>
|
|
<button class="btn btn-sm btn-outline" onclick="appMigrate(this,'{{.Stack.Name}}','{{.Meta.DisplayName}}')">{{T "app_info.athelyezes"}}</button>
|
|
</div>
|
|
<div id="app-migrate-progress" style="display:none;margin-top:.75rem;padding:.75rem;border:1px solid var(--blue);border-radius:6px;background:rgba(0,136,204,0.06)"></div>
|
|
</div>
|
|
<script>
|
|
function appMigFmtGB(b){ return (Number(b||0)/1e9).toFixed(1)+' GB'; }
|
|
function appMigRender(job){
|
|
var names={stop:'{{T "app_info.leallitas"}}',copy:'{{T "app_info.adatok_masolasa"}}',verify:'{{T "app_info.ellenorzes"}}',flip:'{{T "app_info.ujratelepites"}}',redeploy:'{{T "app_info.ujratelepites"}}',cleanup:'{{T "app_info.regi_adatok_torlese"}}'};
|
|
var s=names[job.phase]||job.phase;
|
|
if(job.phase==='copy'&&job.bytes_total>0){ s+=' ('+Math.floor(100*job.bytes_done/job.bytes_total)+'% — '+appMigFmtGB(job.bytes_done)+'/'+appMigFmtGB(job.bytes_total)+')'; }
|
|
return s+'…';
|
|
}
|
|
function appMigWatch(){
|
|
var panel=document.getElementById('app-migrate-progress');
|
|
if(panel) panel.style.display='block';
|
|
function tick(){
|
|
fetch('/api/storage/migrate/status').then(function(r){return r.json();}).then(function(d){
|
|
var job=d.data&&d.data.job;
|
|
if(!job){ if(panel) panel.textContent='{{T "app_info.nincs_folyamatban_athelyezes"}}'; return; }
|
|
if(panel) panel.innerHTML=appMigRender(job);
|
|
if(job.phase==='done'){ if(panel) panel.innerHTML+='<br><strong>{{T "app_info.kesz"}}</strong>'; setTimeout(function(){location.reload();},1500); return; }
|
|
if(job.phase==='aborted'){ if(panel) panel.innerHTML+='<br><strong style="color:var(--danger,#c0392b)">{{T "app_info.megszakadt"}} '+(job.error||'')+'</strong><br>{{T "app_info.a_regi_adatok_erintetlenek"}}'; return; }
|
|
setTimeout(tick,1500);
|
|
}).catch(function(){ setTimeout(tick,2000); });
|
|
}
|
|
tick();
|
|
}
|
|
function appMigrate(btn,app,label){
|
|
var sel=document.getElementById('app-migrate-target');
|
|
var target=sel?sel.value:'';
|
|
if(!target){ showAlert('{{T "app_info.valassz_celtarhelyet_2"}}'); return; }
|
|
felhomConfirm(btn,'{{T "app_info.athelyezed_a_z"}} '+label+' {{T "app_info.adatait_ide"}} '+target+'{{T "app_info.az_alkalmazas_rovid_idore_leall"}}',function(){
|
|
fetch('/api/storage/migrate-app',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify({app:app,target:target})})
|
|
.then(function(r){return r.json();}).then(function(d){ if(d.ok){ appMigWatch(); } else { showAlert('{{T "common.hiba_kettospont"}} '+(d.error||'{{T "app_info.ismeretlen"}}')); } })
|
|
.catch(function(e){ showAlert('{{T "common.hiba_kettospont"}} '+e); });
|
|
});
|
|
}
|
|
(function(){ fetch('/api/storage/migrate/status').then(function(r){return r.json();}).then(function(d){ if(d.data&&d.data.job){ appMigWatch(); } }).catch(function(){}); })();
|
|
</script>
|
|
{{end}}
|
|
|
|
{{if .HasAppInfo}}
|
|
<div class="app-info-grid">
|
|
{{if .AppInfo.UseCases}}
|
|
<div class="app-info-card">
|
|
<h3>{{T "app_info.mire_hasznalhato"}}</h3>
|
|
<ul class="app-info-list">
|
|
{{range .AppInfo.UseCases}}<li>{{.}}</li>{{end}}
|
|
</ul>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .AppInfo.FirstSteps}}
|
|
<div class="app-info-card">
|
|
<h3>{{T "app_info.elso_lepesek"}}</h3>
|
|
<ol class="app-info-list">
|
|
{{range .AppInfo.FirstSteps}}<li>{{.}}</li>{{end}}
|
|
</ol>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .AppInfo.Prerequisites}}
|
|
<div class="app-info-card">
|
|
<h3>{{T "app_info.elofeltetelek"}}</h3>
|
|
<ul class="app-info-list">
|
|
{{range .AppInfo.Prerequisites}}<li>{{.}}</li>{{end}}
|
|
</ul>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .InitialCreds}}
|
|
<div class="app-info-card">
|
|
<h3>{{T "app_info.kezdeti_belepesi_adatok"}}</h3>
|
|
<table class="initcred-table">
|
|
{{if .InitialCreds.Username}}
|
|
<tr>
|
|
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">{{T "app_info.felhasznalonev"}}</td>
|
|
<td><code class="initcred-user" style="user-select:all">{{.InitialCreds.Username}}</code></td>
|
|
</tr>
|
|
{{end}}
|
|
{{if .InitialCredsOperatorSet}}
|
|
<tr>
|
|
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">{{T "common.jelszo"}}</td>
|
|
<td>{{T "app_info.az_uzemelteto_allitotta_be"}}</td>
|
|
</tr>
|
|
{{else}}
|
|
<tr>
|
|
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap;vertical-align:middle">{{T "common.jelszo"}}</td>
|
|
<!-- R-254: the value is NOT in this page. It used to be rendered into a `hidden`
|
|
span, which stops a browser drawing it and nothing else — a fetch of this page
|
|
returned a real per-install password. Both buttons now ask the server. -->
|
|
<td style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
|
<code id="initcred-pw">••••••••••••</code>
|
|
<button type="button" class="btn btn-sm btn-outline" id="initcred-reveal" onclick="icRevealPw(this)">{{T "app_info.megjelenites"}}</button>
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="icCopyPw(this)">{{T "app_info.masolas"}}</button>
|
|
<span id="initcred-err" class="form-hint" style="display:none;color:var(--red)"></span>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</table>
|
|
{{if .InitialCreds.Note}}<p class="app-info-creds-warn">{{.InitialCreds.Note}}</p>{{end}}
|
|
{{if not .InitialCredsOperatorSet}}<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal"}}</p>{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .AppInfo.DefaultCreds}}
|
|
<div class="app-info-card">
|
|
<h3>{{T "app_info.alapertelmezett_belepes"}}</h3>
|
|
<p class="app-info-creds">{{.AppInfo.DefaultCreds}}</p>
|
|
<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal_2"}}</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .AppInfo.DocsURL}}
|
|
<div class="app-info-card">
|
|
<h3>{{T "app_info.dokumentacio"}}</h3>
|
|
<p><a href="{{.AppInfo.DocsURL}}" target="_blank" class="app-info-link">{{T "app_info.hivatalos_dokumentacio"}}</a></p>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .InitialCreds}}
|
|
<script>
|
|
// R-254: the password is NOT in this page. It is fetched on demand from the server, which re-reads it
|
|
// live from the running container — so this is the only way it reaches a browser, and every fetch is
|
|
// recorded server-side. Nothing caches it in a variable between presses: each act asks again.
|
|
function icFetchPw() {
|
|
return fetch('/apps/{{.Meta.Slug}}/initial-credentials/reveal', {
|
|
method: 'POST',
|
|
headers: {'X-CSRF-Token': '{{.CSRFToken}}'},
|
|
credentials: 'same-origin'
|
|
}).then(function (r) { return r.json(); }).then(function (j) {
|
|
if (!j.ok) { throw new Error(j.error || '{{T "app_info.a_kezdeti_jelszo_beolvasasa_nem"}}'); }
|
|
return j.data.password;
|
|
});
|
|
}
|
|
function icErr(msg) {
|
|
var e = document.getElementById('initcred-err');
|
|
e.textContent = msg;
|
|
e.style.display = 'inline';
|
|
}
|
|
function icRevealPw(btn) {
|
|
var code = document.getElementById('initcred-pw');
|
|
if (!code) return;
|
|
document.getElementById('initcred-err').style.display = 'none';
|
|
if (code.dataset.shown === '1') { // hide: drop the value out of the DOM again
|
|
code.textContent = '••••••••••••';
|
|
code.dataset.shown = '0';
|
|
btn.textContent = '{{T "app_info.megjelenites"}}';
|
|
return;
|
|
}
|
|
btn.disabled = true;
|
|
icFetchPw().then(function (pw) {
|
|
btn.disabled = false;
|
|
code.textContent = pw;
|
|
code.dataset.shown = '1';
|
|
btn.textContent = '{{T "app_info.elrejtes"}}';
|
|
}).catch(function (e) { btn.disabled = false; icErr(e.message); });
|
|
}
|
|
function icCopyPw(btn) {
|
|
document.getElementById('initcred-err').style.display = 'none';
|
|
btn.disabled = true;
|
|
icFetchPw().then(function (pw) {
|
|
return navigator.clipboard.writeText(pw).then(function () {
|
|
btn.disabled = false;
|
|
var orig = btn.textContent;
|
|
btn.textContent = '{{T "app_info.masolva"}}';
|
|
setTimeout(function () { btn.textContent = orig; }, 1500);
|
|
});
|
|
}).catch(function (e) { btn.disabled = false; icErr(e.message); });
|
|
}
|
|
</script>
|
|
{{end}}
|
|
|
|
{{template "layout_end" .}}
|
|
{{end}}
|