206b0357d1
gates / gates (push) Successful in 27s
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 and (gt .Stack.LadderStepsLeft 0) (not .Stack.HoldReason)}}<small class="text-muted" data-ladder-steps="{{.Stack.LadderStepsLeft}}">{{T "app_info.ladder_steps_left"}} {{.Stack.LadderStepsLeft}}</small>{{end}}
|
|
{{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}}">{{updatePhaseText .Stack}}</div>
|
|
{{else if .Stack.HoldReason}}
|
|
<div class="alert alert-error" style="margin-top:1rem" data-held="true">{{holdText .Stack}}{{if not .Stack.HoldNoWholeCopy}} <a href="/backups/apps" class="btn btn-sm btn-outline">{{T "app_info.mentesek"}}</a>{{end}}</div>
|
|
{{else if .Stack.UpdateError}}
|
|
<div class="alert alert-warning" style="margin-top:1rem" data-update-error="true">{{updateErrorText .Stack}}</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}}
|