v0.315.0: one app page with four tabs; the guide the catalog can carry; no Raspberry Pi badge
gates / gates (push) Failing after 47s

An app was six pages — details, install/settings, logs, export, the per-app backup panel and a
details card for moving data — and every "Vissza" on them went to the app LIST, so there was no way
back to the app you were looking at. It is now ONE page with four tab URLs under /apps/{slug}/,
one shared header and one set of banners.

  settings  Mukodes (Start/Restart/Stop, Stop confirms) + the cards that want something from the
            household + the install-time and optional settings. The default for an installed app,
            RENDERED at the bare URL and never redirected to.
  data      the data folders, move to another drive, the nightly copy, the 2nd-copy setting, export
  guide     tagline, screenshots, use cases, first steps, the catalog's guide, the FAQ, the docs
            link — and the WHOLE page for an app that is not installed
  logs      the log, and the only tab whose poller runs

The tabs are links, not panels: a link in an alert has to land ON a tab, and the log poller must not
run on the other three. Start/Restart/Stop left the page header, where they read as menu buttons.

The old /stacks/{name}/{deploy,logs,export,backup} GETs answer 302 — never 301, which a browser
caches beyond anyone's reach — to the tab that now holds them, query string carried through, with
name->slug resolved through the stack list rather than assumed equal. Every POST on those paths is
untouched: the backup form, the app-email toggle and the reveal endpoints keep their URLs.

A value asked AFTER the app starts is now generic (D2). The catalog marks a field `after_start: true`
and supplies its label; the ACTION is Go, in a registry keyed by slug + env var. The catalog can
never declare what runs — a declared field with no registered action renders nothing, POSTs 404 and
logs one WARN with the env var KEY, never the value. A red-proof found the second half of that guard
unpinned: deleting the "the catalog must have DECLARED this field" check left the suite green, and a
POST for an undeclared field then reached Plex and opened the setup gate.
TestAfterStart_RegistryAloneIsNotAnInvitation pins it.

New catalog block app_info.guide (sections + faq + checked_version), parsed, localised field by
field and rendered with the household's own address filled in. It renders only when checked_version
names the image tag its author clicked through. No template carries one yet.

The Pi and "csak x86" badges are gone; PiCompatible and the catalog key stay this release, or a box
on 0.314.0 would read "csak x86" on every app (R-1067).

template_id_gate.py now follows {{template}} includes, with a scope decoy proving the walk did not
become vacuous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
This commit is contained in:
2026-10-15 18:37:50 +02:00
parent 85f65c70d8
commit e66d4669d6
120 changed files with 16239 additions and 9866 deletions
File diff suppressed because it is too large Load Diff
@@ -174,11 +174,80 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">&larr; Alkalmazások</a>
<h2>Private Bin &mdash; Exportálás</h2>
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/data" data-tab="data"
aria-current="page" aria-selected="true">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-data-tab">
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<h3 style="margin-top:2rem">Private Bin &mdash; Exportálás</h3>
<div class="card" style="max-width:700px">
<h3>Mentés helye</h3>
<select id="destDrive" onchange="loadEstimate()" style="width:100%;padding:.5rem;margin-bottom:1rem">
@@ -486,6 +555,40 @@ function showDone(data) {
</script>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
<script>
@@ -0,0 +1,654 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Private Bin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/data" data-tab="data"
aria-current="page" aria-selected="true">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-data-tab">
<div class="monitoring-banner monitoring-banner-green">A beállítás elmentve rendben.</div>
<div class="monitoring-banner monitoring-banner-red">A mentés nem sikerült most.</div>
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<h3 style="margin-top:1.5rem">2. mentés beállítása — Private Bin</h3>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
A 2. mentés egy <strong>másik fizikai meghajtóra</strong> készít másolatot az alkalmazás
helyreállítási csomagjáról és adatairól. Ez az egyetlen off-drive védelem a böngészhető
felhasználói fájlokhoz (a teljes rendszermentés/PBS nem éri el ezeket).
</p>
<div class="monitoring-banner monitoring-banner-yellow" style="margin-top:1rem" data-system-backup="unknown">
Ennek az alkalmazásnak az adatai a belső rendszerlemezen vannak, és a teljes rendszermentés része.
Hogy a rendszermentés most hová készül, azt nem tudtuk lekérdezni — a Mentések oldalon láthatod.
</div>
</div>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calibre-Web — Felhom.eu</title>
<title>Private Bin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
@@ -175,7 +175,7 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Calibre-Web</h2>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
@@ -184,78 +184,121 @@
<a href="/stacks/calibre-web/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/calibre-web/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/calibre-web/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/calibre-web-logo.svg"
alt="Calibre-Web" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/calibre-web-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/calibre-web-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/calibre-web-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/calibre-web-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/data" data-tab="data"
aria-current="page" aria-selected="true">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-info-grid">
<div class="app-data-tab">
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<h3 style="margin-top:1.5rem">2. mentés beállítása — Private Bin</h3>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
A 2. mentés egy <strong>másik fizikai meghajtóra</strong> készít másolatot az alkalmazás
helyreállítási csomagjáról és adatairól. Ez az egyetlen off-drive védelem a böngészhető
felhasználói fájlokhoz (a teljes rendszermentés/PBS nem éri el ezeket).
</p>
<div class="app-info-card">
<h3>Alapértelmezett belépés</h3>
<p class="app-info-creds">admin / admin123</p>
<p class="app-info-creds-warn" id="known-login-line">Ez az alkalmazás egy ismert, közös jelszóval indul: admin / admin123. Telepítés után azonnal változtasd meg.</p>
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/calibre-web/default-login/changed')">Megváltoztattam</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
<div class="monitoring-banner monitoring-banner-red" style="margin-top:1rem" data-system-backup="absent">
Ennek az alkalmazásnak az adatai a belső rendszerlemezen vannak. A teljes rendszermentés meghajtója
<strong>most nem érhető el</strong> — amíg vissza nem csatlakoztatod, erről az alkalmazásról sem készül friss rendszermentés.
</div>
</div>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
@@ -609,28 +652,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plex — Felhom.eu</title>
<title>Private Bin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
@@ -175,7 +175,7 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Plex</h2>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
@@ -184,71 +184,123 @@
<a href="/stacks/plex/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/plex/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/plex/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/plex-logo.svg"
alt="Plex" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/plex-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/plex-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/plex-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/plex-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<div class="app-info-card" style="margin-top:1rem" id="setup-gate-card">
<h3>Első beállítás</h3>
<p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p>
<div class="plex-claim" data-plex-claim="true">
<p>Nyisd meg a https://plex.tv/claim oldalt a Plex-fiókoddal, másold ki a kódot, és illeszd be ide. A kód 4 percig érvényes, de a Plex már fut, így nem kell sietned a telepítéssel.</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)">Plex összekapcsolása</button>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/data" data-tab="data"
aria-current="page" aria-selected="true">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-data-tab">
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
<p class="form-hint">Ha kész, a doboz magától észreveszi, és mindenkinek megnyitja az alkalmazást.</p>
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/plex/setup-gate/open')">Kész, beállítottam</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
<h3 style="margin-top:1.5rem">2. mentés beállítása — Private Bin</h3>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
A 2. mentés egy <strong>másik fizikai meghajtóra</strong> készít másolatot az alkalmazás
helyreállítási csomagjáról és adatairól. Ez az egyetlen off-drive védelem a böngészhető
felhasználói fájlokhoz (a teljes rendszermentés/PBS nem éri el ezeket).
</p>
<div class="monitoring-banner monitoring-banner-yellow" style="margin-top:1rem" data-system-backup="protected">
Ennek az alkalmazásnak az adatai a belső rendszerlemezen vannak, amelyek
<strong>szerepelnek a teljes rendszermentésben</strong>, és az egy külön meghajtóra készül. A 2. (off-drive) másolat
kiegészítő, és elsősorban a külső adatmeghajtón tárolt alkalmazásokhoz készül — ehhez az
alkalmazáshoz nincs külön teendő.
</div>
</div>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
@@ -602,28 +654,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opengist — Felhom.eu</title>
<title>Private Bin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
@@ -175,7 +175,7 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Opengist</h2>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
@@ -184,63 +184,122 @@
<a href="/stacks/opengist/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/opengist/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/opengist/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/opengist-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<div class="app-info-card" style="margin-top:1rem" id="close-signup-card">
<h3>Regisztráció</h3>
<p>Ennél az alkalmazásnál bárki regisztrálhat, aki megtalálja a címét. Zárd le a regisztrációt most — a családtagjaidat utána is fel tudod venni.</p>
<p class="form-hint" data-restarts-once="true">Ehhez az alkalmazás egyszer újraindul, és közben rövid ideig nem érhető el.</p>
<button type="button" class="btn btn-sm btn-primary" onclick="appPress(this, '/apps/opengist/close-signup')">Regisztráció lezárása most</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/data" data-tab="data"
aria-current="page" aria-selected="true">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-data-tab">
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<h3 style="margin-top:1.5rem">2. mentés beállítása — Private Bin</h3>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
A 2. mentés egy <strong>másik fizikai meghajtóra</strong> készít másolatot az alkalmazás
helyreállítási csomagjáról és adatairól. Ez az egyetlen off-drive védelem a böngészhető
felhasználói fájlokhoz (a teljes rendszermentés/PBS nem éri el ezeket).
</p>
<div class="monitoring-banner monitoring-banner-yellow" style="margin-top:1rem" data-system-backup="same_disk">
Ennek az alkalmazásnak az adatai a belső rendszerlemezen vannak. A teljes rendszermentés
<strong>jelenleg ugyanerre a lemezre készül</strong> — így hibás fájlok ellen véd, lemezhiba ellen nem.
Ha csatlakoztatsz egy második meghajtót, és a Mentések oldalon kijelölöd a rendszermentés helyéül, ez az alkalmazás is védett lesz.
</div>
</div>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
@@ -594,28 +653,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Második mentés beállítása — Felhom.eu</title>
<title>Private Bin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
@@ -115,7 +115,7 @@
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
@@ -125,10 +125,10 @@
</ul>
</li>
<li class="nav-group is-open">
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="active">Áttekintés</a></li>
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
@@ -173,13 +173,79 @@
<div class="page-header">
<h2>2. mentés beállítása — Immich Photos</h2>
<a href="/backups/apps" class="btn btn-outline btn-sm">← Vissza a mentésekhez</a>
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/data" data-tab="data"
aria-current="page" aria-selected="true">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-data-tab">
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<h3 style="margin-top:1.5rem">2. mentés beállítása — Immich Photos</h3>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
@@ -237,6 +303,41 @@
</div>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
<script>
@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Második mentés beállítása — Felhom.eu</title>
<title>Private Bin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
@@ -115,7 +115,7 @@
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
@@ -125,10 +125,10 @@
</ul>
</li>
<li class="nav-group is-open">
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="active">Áttekintés</a></li>
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
@@ -173,13 +173,79 @@
<div class="page-header">
<h2>2. mentés beállítása — Immich Photos</h2>
<a href="/backups/apps" class="btn btn-outline btn-sm">← Vissza a mentésekhez</a>
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/data" data-tab="data"
aria-current="page" aria-selected="true">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-data-tab">
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<h3 style="margin-top:1.5rem">2. mentés beállítása — Immich Photos</h3>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
@@ -237,6 +303,41 @@
</div>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
<script>
@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Második mentés beállítása — Felhom.eu</title>
<title>Private Bin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
@@ -115,7 +115,7 @@
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
@@ -125,10 +125,10 @@
</ul>
</li>
<li class="nav-group is-open">
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="active">Áttekintés</a></li>
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
@@ -173,13 +173,79 @@
<div class="page-header">
<h2>2. mentés beállítása — Immich Photos</h2>
<a href="/backups/apps" class="btn btn-outline btn-sm">← Vissza a mentésekhez</a>
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/data" data-tab="data"
aria-current="page" aria-selected="true">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-data-tab">
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<h3 style="margin-top:1.5rem">2. mentés beállítása — Immich Photos</h3>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
@@ -240,6 +306,41 @@
</div>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
<script>
@@ -179,42 +179,37 @@
</div>
<div style="display:flex;align-items:center;gap:.5rem">
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/privatebin-logo.svg"
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="PrivateBin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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">
<p class="app-info-tagline">Titkosított jegyzetek.</p>
<div class="app-hero-text">
<p class="app-info-tagline">Titkosított jegyzetek.</p>
<div class="stack-meta-badges">
<span class="meta-badge">~64M RAM</span>
<span class="meta-badge">Hasznos eszközök</span>
<span class="meta-badge meta-badge-ok">Pi kompatibilis</span>
</div>
<div class="alert alert-warning" style="margin-top:.75rem">
Az alkalmazás fejlesztője felhagyott a fejlesztéssel. A telepített verzió továbbra is használható,
de frissítések és biztonsági javítások már nem érkeznek hozzá.
</div>
</div>
</div>
<div class="alert alert-warning" style="margin-top:.75rem">Az alkalmazás fejlesztője felhagyott a fejlesztéssel. A telepített verzió továbbra is használható,
de frissítések és biztonsági javítások már nem érkeznek hozzá.</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
@@ -226,11 +221,59 @@
<div class="app-info-card" style="margin-top:1rem">
<p class="form-hint" data-not-installable="1">Ez az alkalmazás most nem telepíthető.</p>
</div>
<div class="app-info-grid">
</div>
<p class="form-hint" style="margin-top:1rem" data-guide-missing="1">Ehhez az alkalmazáshoz még nem készült útmutató.</p>
<div class="app-info-grid">
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opengist — Felhom.eu</title>
<title>PrivateBin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
@@ -175,7 +175,7 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Opengist</h2>
<h2>PrivateBin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
@@ -184,65 +184,157 @@
<a href="/stacks/opengist/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/opengist/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/opengist/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="PrivateBin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Titkosított szövegmegosztás.</p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab" href="/apps/privatebin/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/guide" data-tab="guide"
aria-current="page" aria-selected="true">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/opengist-screenshot-1.webp" alt="" class="app-screenshot"
<img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-2.webp" alt="" class="app-screenshot"
<img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-3.webp" alt="" class="app-screenshot"
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<div class="app-info-card" style="margin-top:1rem" id="signup-card">
<h3>Regisztráció</h3>
<p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p>
<p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
<p class="form-hint" id="signup-native-open-failed">Az alkalmazás saját regisztrációs kapcsolóját nem sikerült megnyitni a 15 percre, ezért zárva maradt. Új családtag így az alkalmazásban nem tud regisztrálni. Próbáld újra később.</p>
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/opengist/signup-window')">Regisztráció megnyitása 15 percre</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
<div class="app-info-grid">
<div class="app-info-card">
<h3>Mire használható?</h3>
<ul class="app-info-list">
<li>Jelszó átadása</li>
</ul>
</div>
<div class="app-info-card">
<h3>Első lépések</h3>
<ol class="app-info-list">
<li>Nyisd meg a címet</li>
</ol>
</div>
<div class="app-info-card">
<h3>Előfeltételek</h3>
<ul class="app-info-list">
<li>Semmi különös</li>
</ul>
</div>
</div>
<div class="app-guide" data-guide="1" data-guide-checked="2.0.6">
<div class="app-info-card" style="margin-top:1rem" data-guide-section="1">
<h3>Első bekapcsolás</h3>
<p>Nyisd meg a böngészőben.</p>
<ol class="app-info-list"><li>Kattints a Fiók menüre</li><li>Add meg a nevedet</li>
</ol>
</div>
<div class="app-info-card" style="margin-top:1rem" data-guide-section="1">
<h3>Megosztás másokkal</h3>
<ol class="app-info-list"><li>Válaszd ki a mappát</li>
</ol>
</div>
<div class="app-info-card" style="margin-top:1rem" data-guide-faq="1">
<h3>Gyakori kérdések</h3>
<dl class="app-faq">
<dt>Hol vannak a fájlok?</dt>
<dd>A megadott mappában.</dd>
<dt>Lehet több felhasználó?</dt>
<dd>Igen, meghívóval.</dd>
</dl>
</div>
</div>
<div class="app-info-grid">
<div class="app-info-card">
<h3>Dokumentáció</h3>
<p><a href="https://privatebin.info" target="_blank" class="app-info-link">Hivatalos dokumentáció ↗</a></p>
</div>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
<script>
@@ -595,28 +687,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -179,37 +179,35 @@
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/stacks/privatebin/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, 'privatebin')">Telepítés</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/privatebin-logo.svg"
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="PrivateBin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~64M RAM</span>
<span class="meta-badge">Hasznos eszközök</span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
@@ -221,11 +219,60 @@
<div class="app-info-card" style="margin-top:1rem">
<a href="/apps/privatebin/install" class="btn btn-primary" data-install-cta="1"
onclick="return checkBeforeDeploy(event, 'privatebin')">Telepítés</a>
</div>
<div class="app-info-grid">
</div>
<p class="form-hint" style="margin-top:1rem" data-guide-missing="1">Ehhez az alkalmazáshoz még nem készült útmutató.</p>
<div class="app-info-grid">
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opengist — Felhom.eu</title>
<title>Grocy — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
@@ -175,72 +175,105 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Opengist</h2>
<h2>Grocy</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
<a href="/stacks/opengist/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/opengist/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/opengist/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/grocy-logo.svg"
alt="Grocy" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/grocy-logo.png';}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-hero-text">
<p class="app-info-tagline">Házi készletnyilvántartó.</p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/opengist-screenshot-1.webp" alt="" class="app-screenshot"
<img src="/static/assets/grocy-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-2.webp" alt="" class="app-screenshot"
<img src="/static/assets/grocy-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-3.webp" alt="" class="app-screenshot"
<img src="/static/assets/grocy-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<div class="app-info-card" style="margin-top:1rem" id="close-signup-card">
<h3>Regisztráció</h3>
<p>Ennél az alkalmazásnál bárki regisztrálhat, aki megtalálja a címét. Zárd le a regisztrációt most — a családtagjaidat utána is fel tudod venni.</p>
<button type="button" class="btn btn-sm btn-primary" onclick="appPress(this, '/apps/opengist/close-signup')">Regisztráció lezárása most</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
<div class="app-info-card" style="margin-top:1rem">
<a href="/apps/grocy/install" class="btn btn-primary" data-install-cta="1"
onclick="return checkBeforeDeploy(event, 'grocy')">Telepítés</a>
</div>
<div class="app-info-grid">
</div>
<p class="form-hint" style="margin-top:1rem" data-guide-missing="1">Ehhez az alkalmazáshoz még nem készült útmutató.</p>
<div class="app-info-grid">
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
</main>
<script>
@@ -593,28 +626,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -1,589 +0,0 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RomM — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>RomM</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
<small class="text-muted" data-ladder-steps="2">Hátralévő frissítési lépések: 2</small>
<a href="/stacks/romm/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/romm/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/romm/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/romm-logo.svg"
alt="RomM" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/romm-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/romm-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/romm-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/romm-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -1,590 +0,0 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Karakeep — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Karakeep</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-warn">Részlegesen leállt</span>
<a href="/stacks/karakeep/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/karakeep/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/karakeep/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<p class="form-hint" style="margin-top:0.5rem" data-health-not-checked="true">Nem futott egészségellenőrzés: nincs hozzá tartozó konténer.</p>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/karakeep-logo.svg"
alt="Karakeep" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/karakeep-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~64M RAM</span>
<span class="meta-badge">Hasznos eszközök</span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/karakeep-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/karakeep-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/karakeep-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -1,594 +0,0 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opengist — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Opengist</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
<a href="/stacks/opengist/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/opengist/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/opengist/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/opengist-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<div class="app-info-card" style="margin-top:1rem" id="signup-card">
<h3>Regisztráció</h3>
<p>A regisztráció most nyitva, eddig: 14:05</p>
<p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
</div>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -1,591 +0,0 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PrivateBin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>PrivateBin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-progress">Telepítés...</span>
<a href="/stacks/privatebin/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/privatebin/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/privatebin/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="alert alert-info" style="margin-top:1rem" data-update-phase="pull">Letöltés folyamatban…</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/privatebin-logo.svg"
alt="PrivateBin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -171,10 +171,60 @@
<div class="page-header">
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<h2>Private Bin — Naplók</h2>
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab" href="/apps/privatebin/settings" data-tab="settings"
aria-selected="false">Beállítások</a>
<a class="app-tab" href="/apps/privatebin/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab app-tab-active" href="/apps/privatebin/logs" data-tab="logs"
aria-current="page" aria-selected="true">Napló</a>
</nav>
<div data-tab-panel="logs">
<div class="logs-container" id="logs-container">
<pre class="logs-output" id="logs-output">2026-09-14 line one
line two</pre>
@@ -194,14 +244,14 @@ line two</pre>
var toggleBtn = document.getElementById('logs-toggle');
var live = true;
var timer = null;
var stackName = 'privatebin';
var appSlug = 'privatebin';
function isAtBottom() {
return container.scrollHeight - container.scrollTop - container.clientHeight < 50;
}
window.fetchLogs = function() {
fetch('/stacks/' + stackName + '/logs?raw=1')
fetch('/apps/' + appSlug + '/logs?raw=1')
.then(function(r) { return r.text(); })
.then(function(text) {
var wasAtBottom = isAtBottom();
@@ -239,7 +289,8 @@ line two</pre>
container.scrollTop = container.scrollHeight;
startPolling();
})();
</script>
</div>
</main>
<script>
@@ -0,0 +1,794 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plex — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Plex</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/plex-logo.svg"
alt="Plex" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/plex-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/plex/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/plex/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/plex/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/plex/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'plex', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'plex', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="setup-gate-card">
<h3>Első beállítás</h3>
<p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p>
<p class="form-hint">Ha kész, a doboz magától észreveszi, és mindenkinek megnyitja az alkalmazást.</p>
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/plex/setup-gate/open')">Kész, beállítottam</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
<div class="app-info-card after-start-card" data-afterstart="PLEX_CLAIM">
<h3>Még egy lépés</h3>
<p>Illeszd be a plex.tv-n kapott kódot.</p>
<div class="input-with-button">
<input type="text" id="after-start-PLEX_CLAIM" class="form-control"
placeholder="claim-xxxxxxxxxxxxxxxxxxxx" autocomplete="off" spellcheck="false"
aria-label="Plex igénylő kód">
<button type="button" class="btn btn-sm btn-primary"
onclick="appAfterStart(this, 'plex', 'PLEX_CLAIM')">Mentés</button>
</div>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -0,0 +1,781 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opengist — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Opengist</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/opengist/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/opengist/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/opengist/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/opengist/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'opengist', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'opengist', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="close-signup-card">
<h3>Regisztráció</h3>
<p>Ennél az alkalmazásnál bárki regisztrálhat, aki megtalálja a címét. Zárd le a regisztrációt most — a családtagjaidat utána is fel tudod venni.</p>
<button type="button" class="btn btn-sm btn-primary" onclick="appPress(this, '/apps/opengist/close-signup')">Regisztráció lezárása most</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -0,0 +1,782 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opengist — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Opengist</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/opengist/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/opengist/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/opengist/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/opengist/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'opengist', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'opengist', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="close-signup-card">
<h3>Regisztráció</h3>
<p>Ennél az alkalmazásnál bárki regisztrálhat, aki megtalálja a címét. Zárd le a regisztrációt most — a családtagjaidat utána is fel tudod venni.</p>
<p class="form-hint" data-restarts-once="true">Ehhez az alkalmazás egyszer újraindul, és közben rövid ideig nem érhető el.</p>
<button type="button" class="btn btn-sm btn-primary" onclick="appPress(this, '/apps/opengist/close-signup')">Regisztráció lezárása most</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -183,21 +183,32 @@
<span class="tag tag-error" title="A frissítés megállt: nincs friss mentés.">Megállítva — visszaállítás szükséges</span>
<a href="https://paste.example.hu" target="_blank" class="btn btn-sm btn-outline">Megnyitás ↗</a>
<a href="/stacks/privatebin/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/privatebin/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/privatebin/deploy" class="btn btn-sm btn-outline">Beállítások</a>
<a href="https://paste.example.hu" target="_blank" class="btn btn-sm btn-primary" data-app-open="1">Megnyitás ↗</a>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="PrivateBin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Titkosított szövegmegosztás.</p>
<div class="stack-meta-badges">
<span class="meta-badge">~64M RAM</span>
<span class="meta-badge">Hasznos eszközök</span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<div class="alert alert-error" style="margin-top:1rem" data-held="true">A frissítés megállt: nincs friss mentés. <a href="/backups/apps" class="btn btn-sm btn-outline">Mentések</a></div>
<div class="alert alert-warning" style="margin-top:1rem">
<strong><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Hiányzó tárhely: HDD 1</strong><br>
Ennek az alkalmazásnak az adattárolója jelenleg nem elérhető, ezért le van állítva. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre.
@@ -205,133 +216,44 @@
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/privatebin-logo.svg"
alt="PrivateBin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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">
<p class="app-info-tagline">Titkosított szövegmegosztás.</p>
<div class="stack-meta-badges">
<span class="meta-badge">~64M RAM</span>
<span class="meta-badge">Hasznos eszközök</span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/privatebin/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/privatebin/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'privatebin', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'privatebin', 'stop'); })">Leállítás</button>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<div class="app-info-card" style="margin-top:1rem">
<h3>Hova tegyem a fájlokat?</h3>
<p class="form-hint">Ezeket a mappákat a Fájlkezelőben éred el. Előfordulhat, hogy először be kell jelentkezned a Fájlkezelőbe.</p>
<div class="datapath-list">
<div class="datapath-row">
<div class="datapath-head">
<a href="/files/x" target="_blank" rel="noopener" class="datapath-link">Feltöltött fájlok ↗</a>
<span class="datapath-space">12 GB szabad</span>
</div>
<p class="datapath-note">Mentve minden éjjel.</p>
</div>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem">
<h3>Áthelyezés másik tárhelyre</h3>
<p class="form-hint">Ennek az alkalmazásnak az adatait másik csatlakoztatott tárhelyre helyezheted át. Az alkalmazás az áthelyezés alatt rövid időre leáll, az adatok pedig csak az ellenőrzés és a sikeres újraindítás után törlődnek a régi helyről.</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="">Válassz céltárhelyet…</option>
<option value="/mnt/hdd2">HDD 2 (/mnt/hdd2)</option>
</select>
<button class="btn btn-sm btn-outline" onclick="appMigrate(this,'privatebin','PrivateBin')">Áthelyezés</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 fhNum(Number(b||0)/1e9, 1)+' GB'; }
function appMigRender(job){
var names={stop:'Leállítás',copy:'Adatok másolása',verify:'Ellenőrzés',flip:'Újratelepítés',redeploy:'Újratelepítés',cleanup:'Régi adatok törlése'};
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='Nincs folyamatban áthelyezés.'; return; }
if(panel) panel.innerHTML=appMigRender(job);
if(job.phase==='done'){ if(panel) panel.innerHTML+='<br><strong>Kész</strong>'; setTimeout(function(){location.reload();},1500); return; }
if(job.phase==='aborted'){ if(panel) panel.innerHTML+='<br><strong style="color:var(--danger,#c0392b)">Megszakadt: '+(job.error||'')+'</strong><br>A régi adatok érintetlenek.'; 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('Válassz céltárhelyet.'); return; }
felhomConfirm(btn,'Áthelyezed a(z) '+label+' adatait ide: '+target+'? Az alkalmazás rövid időre leáll. A régi adatok csak sikeres áthelyezés után törlődnek.',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('Hiba: '+(d.error||'ismeretlen')); } })
.catch(function(e){ showAlert('Hiba: '+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>
<div class="app-info-grid">
<div class="app-info-card">
<h3>Mire használható?</h3>
<ul class="app-info-list">
<li>Jelszó átadása</li>
</ul>
</div>
<div class="app-info-card">
<h3>Első lépések</h3>
<ol class="app-info-list">
<li>Nyisd meg</li>
</ol>
</div>
<div class="app-info-card">
<h3>Előfeltételek</h3>
<ul class="app-info-list">
<li>Semmi</li>
</ul>
</div>
<div class="app-info-card">
<h3>Kezdeti belépési adatok</h3>
<table class="initcred-table">
@@ -365,16 +287,29 @@ function appMigrate(btn,app,label){
<p class="app-info-creds">nincs</p>
<p class="app-info-creds-warn">Az első bejelentkezés után azonnal változtasd meg!</p>
</div>
</div>
<div class="app-info-card">
<h3>Dokumentáció</h3>
<p><a href="https://privatebin.info" target="_blank" class="app-info-link">Hivatalos dokumentáció ↗</a></p>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
@@ -430,6 +365,153 @@ function icCopyPw(btn) {
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
@@ -184,58 +184,243 @@
<span class="tag tag-error" title="Az AdventureLog frissítése nem sikerült, az alkalmazás leállítva marad.">Megállítva — visszaállítás szükséges</span>
<a href="/stacks/adventurelog/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/adventurelog/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/adventurelog/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="alert alert-error" style="margin-top:1rem" data-held="true">Az AdventureLog frissítése nem sikerült, az alkalmazás leállítva marad. <a href="/backups/apps" class="btn btn-sm btn-outline">Mentések</a><br><span data-hold-log="true">Az alkalmazás saját naplója megmaradt a leállítás előttről — ebből látszik, miért nem indult el az új változat.</span> <a href="/stacks/adventurelog/logs" class="btn btn-sm btn-outline">Napló</a></div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/adventurelog-logo.svg"
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/adventurelog-logo.svg"
alt="AdventureLog" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/adventurelog-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/adventurelog-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/adventurelog-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/adventurelog-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<div class="alert alert-error" style="margin-top:1rem" data-held="true">Az AdventureLog frissítése nem sikerült, az alkalmazás leállítva marad. <a href="/backups/apps" class="btn btn-sm btn-outline">Mentések</a><br><span data-hold-log="true">Az alkalmazás saját naplója megmaradt a leállítás előttről — ebből látszik, miért nem indult el az új változat.</span> <a href="/apps/adventurelog/logs" class="btn btn-sm btn-outline">Napló</a></div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/adventurelog/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/adventurelog/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/adventurelog/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/adventurelog/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="stopped">Leállítva</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-success" data-run-action="start" onclick="stackAction(event, 'adventurelog', 'start')">Indítás</button>
</div>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
@@ -184,61 +184,247 @@
<a href="/stacks/calibre-web/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/calibre-web/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/calibre-web/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/calibre-web-logo.svg"
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/calibre-web-logo.svg"
alt="Calibre" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/calibre-web-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/calibre-web-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/calibre-web-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/calibre-web-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/calibre-web/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/calibre-web/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/calibre-web/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/calibre-web/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'calibre-web', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'calibre-web', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="install-hold-card">
<h3>Még csak te éred el</h3>
<p>A doboz most cseréli le az alkalmazás ismert első jelszavát egy saját, generált jelszóra. Addig csak te éred el, amíg be vagy jelentkezve a vezérlőpultba – más nem léphet be az ismert jelszóval. Ha a csere nem sikerül, változtasd meg a jelszót kézzel, és jelezd itt, hogy megtetted.</p>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
@@ -0,0 +1,783 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calibre-Web — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Calibre-Web</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/calibre-web-logo.svg"
alt="Calibre-Web" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/calibre-web-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/calibre-web/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/calibre-web/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/calibre-web/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/calibre-web/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'calibre-web', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'calibre-web', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-grid">
<div class="app-info-card">
<h3>Alapértelmezett belépés</h3>
<p class="app-info-creds">admin / admin123</p>
<p class="app-info-creds-warn" id="known-login-line">Ez az alkalmazás egy ismert, közös jelszóval indul: admin / admin123. Telepítés után azonnal változtasd meg.</p>
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/calibre-web/default-login/changed')">Megváltoztattam</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -0,0 +1,775 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RomM — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>RomM</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
<small class="text-muted" data-ladder-steps="2">Hátralévő frissítési lépések: 2</small>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/romm-logo.svg"
alt="RomM" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/romm-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/romm/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/romm/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/romm/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/romm/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'romm', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'romm', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -184,14 +184,27 @@
<a href="/stacks/opengist/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/opengist/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/opengist/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<div class="app-info-card alert-warning" style="margin-top:1rem" id="lost-files-card">
<h3>Elveszett fotók egy váratlan leállás miatt</h3>
<p>A gép váratlanul leállt (például áramszünet miatt), és ezek a fájlok elvesztek. A gép újraindulásának ideje: <strong data-lost-boot-at>2026-10-14 14:07</strong></p>
@@ -206,44 +219,217 @@
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/opengist/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/opengist/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/opengist/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/opengist/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<span class="meta-badge meta-badge-warn">Csak x86</span>
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'opengist', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'opengist', 'stop'); })">Leállítás</button>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/opengist-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
@@ -597,28 +783,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -0,0 +1,776 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Karakeep — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Karakeep</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-warn">Részlegesen leállt</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/karakeep-logo.svg"
alt="Karakeep" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/karakeep-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~64M RAM</span>
<span class="meta-badge">Hasznos eszközök</span>
</div>
</div>
</div>
<p class="form-hint" style="margin-top:0.5rem" data-health-not-checked="true">Nem futott egészségellenőrzés: nincs hozzá tartozó konténer.</p>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/karakeep/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/karakeep/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/karakeep/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/karakeep/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="degraded">Részlegesen leállt</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'karakeep', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'karakeep', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -184,63 +184,70 @@
<a href="/stacks/filebrowser/logs" class="btn btn-sm btn-outline">Napló</a>
<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('filebrowser')">Törlés</button>
<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('filebrowser')">Törlés</button>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/filebrowser-logo.svg"
alt="Fájlkezelő app" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/filebrowser-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<div class="alert alert-warning" style="margin-top:1rem" data-update-error="true">A frissítés sikertelen.</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/filebrowser/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/filebrowser/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/filebrowser/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/filebrowser/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/filebrowser-logo.svg"
alt="Fájlkezelő app" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/filebrowser-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="stopped">Leállítva</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<span class="meta-badge meta-badge-warn">Csak x86</span>
<button class="btn btn-sm btn-success" data-run-action="start" onclick="stackAction(event, 'filebrowser', 'start')">Indítás</button>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/filebrowser-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/filebrowser-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/filebrowser-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<div class="app-info-grid">
<div class="app-info-card">
<h3>Kezdeti belépési adatok</h3>
<table class="initcred-table">
@@ -262,13 +269,31 @@
</div>
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
@@ -322,6 +347,153 @@ function icCopyPw(btn) {
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
@@ -173,46 +173,90 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<h2>Private Bin — Beállítások</h2>
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<button class="btn btn-sm btn-warning" onclick="stackAction(event, 'privatebin', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, 'privatebin', 'stop')">Leállítás</button>
<span class="stack-state-badge state-run">Fut</span>
<a href="https://paste.example.hu" target="_blank" class="btn btn-sm btn-outline">Megnyitás ↗</a>
<a href="https://paste.example.hu" target="_blank" class="btn btn-sm btn-primary" data-app-open="1">Megnyitás ↗</a>
<a href="/apps/privatebin" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-info"/></svg> Részletek</a>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/privatebin/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/privatebin/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="flash flash-success">A mentés sikeresen elkészült.</div>
<div class="flash flash-error">A művelet most nem sikerült.</div>
<div class="deploy-info">
<img class="deploy-logo" src="/logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/logo.png';}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>
<h3>Private Bin</h3>
<p>Egy teszt alkalmazás leírása</p>
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
</div>
<a href="/apps/privatebin" class="btn btn-sm btn-outline" style="margin-top:0.5rem">
Részletes leírás, képernyőképek
</a>
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'privatebin', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'privatebin', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-grid">
<div class="app-info-card">
<h3>Alapértelmezett belépés</h3>
<p class="app-info-creds">nincs ilyen</p>
<p class="app-info-creds-warn">Az első bejelentkezés után azonnal változtasd meg!</p>
</div>
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
@@ -275,21 +319,6 @@
</div>
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<div class="app-optional-config">
<h3>Integrációk</h3>
<p class="config-group-desc">
@@ -673,16 +702,8 @@
</form>
</div>
<div id="kept-choice" class="kept-choice card" hidden></div>
<form id="deploy-form" class="deploy-form">
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
@@ -888,152 +909,43 @@
</div>
</form>
<div id="deploy-progress" class="deploy-progress" style="display:none">
<h3>Telepítés folyamatban...</h3>
<div class="deploy-steps">
<div class="deploy-step active" id="step-config">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konfiguráció mentése...</span>
</div>
<div class="deploy-step" id="step-containers">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konténer(ek) indítása...</span>
</div>
<div class="deploy-step" id="step-health">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Alkalmazás inicializálása...</span>
</div>
</div>
<div id="deploy-warning" class="alert alert-warning" style="display:none"></div>
<div id="deploy-result" style="display:none"></div>
<p class="deploy-elapsed" id="deploy-elapsed"></p>
</div>
</div>
<script>
var postDeployInfo = {
firstSteps: ["Nyisd meg a címet"],
defaultCreds: "nincs ilyen",
docsURL: "https://example.org/docs",
domain: "example.hu",
displayName: "Private Bin",
deployFields: null
};
function showKeptChoice(d, form) {
var box = document.getElementById('kept-choice');
box.textContent = '';
function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; }
box.appendChild(el('h3', 'kept-choice-title', d.title));
box.appendChild(el('p', 'kept-choice-body', d.body));
function option(label, desc, choice, enabled, extra) {
var o = el('div', 'kept-choice-option');
var b = el('button', enabled ? 'btn btn-primary' : 'btn btn-outline', label);
b.type = 'button';
b.disabled = !enabled;
b.setAttribute('data-kept-choice', choice);
b.addEventListener('click', function () { window.keptChoice = choice; box.hidden = true; form.requestSubmit(); });
o.appendChild(b);
o.appendChild(el('p', 'kept-choice-desc', desc));
if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
return o;
}
box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, ''));
box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up));
box.hidden = false;
box.scrollIntoView({behavior: 'smooth'});
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function buildPostDeployCard(stackName) {
var subdomain = '';
var sdField = document.getElementById('field-SUBDOMAIN');
if (sdField) subdomain = sdField.value.trim();
if (!subdomain && 'paste') subdomain = 'paste';
var html = '';
if (subdomain && postDeployInfo.domain) {
var appURL = 'https://' + subdomain + '.' + postDeployInfo.domain;
html += '<div style="text-align:center;margin:1.5rem 0">' +
'<a href="' + appURL + '" target="_blank" class="btn btn-primary btn-lg">Alkalmazás megnyitása ↗</a>' +
'</div>';
}
if (postDeployInfo.firstSteps && postDeployInfo.firstSteps.length > 0) {
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Első lépések</h4><ol class="app-info-list">';
for (var i = 0; i < postDeployInfo.firstSteps.length; i++) {
var step = postDeployInfo.firstSteps[i].replace(/DOMAIN/g, postDeployInfo.domain);
html += '<li>' + step + '</li>';
}
html += '</ol></div>';
}
var credRows = '';
var hasUserCredFields = false;
if (postDeployInfo.deployFields) {
for (var i = 0; i < postDeployInfo.deployFields.length; i++) {
var f = postDeployInfo.deployFields[i];
var isPassword = f.type === 'secret' || f.type === 'password';
var isUsername = false;
if (!isPassword && f.type === 'text') {
var ev = f.env_var.toUpperCase();
isUsername = ev.indexOf('USER') >= 0 || ev.indexOf('ADMIN') >= 0 || ev.indexOf('LOGIN') >= 0 || ev.indexOf('EMAIL') >= 0;
}
if (!isPassword && !isUsername) continue;
var evUp = f.env_var.toUpperCase();
if (evUp.indexOf('DB_PASSWORD') >= 0 || evUp.indexOf('MYSQL_ROOT') >= 0 || evUp.indexOf('SECRET_KEY') >= 0) continue;
if (isPassword || isUsername) hasUserCredFields = true;
var el = document.getElementById('auto-field-' + f.env_var) || document.getElementById('field-' + f.env_var);
var val = el ? el.value : '';
if (!val) continue;
if (isPassword) {
var credId = 'postdeploy-cred-' + i;
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;display:flex;align-items:center;gap:.5rem">' +
'<code id="' + credId + '" style="font-family:var(--font-data);letter-spacing:1px">••••••••••••</code>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="togglePostDeployCred(this,\'' + credId + '\',\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Megjelenítés</button>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="copyPostDeployCred(this,\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Másolás</button>' +
'</td></tr>';
} else {
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;font-family:var(--font-data);user-select:all">' + val + '</td></tr>';
}
}
}
if (credRows) {
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Bejelentkezés</h4>' +
'<table style="margin:.5rem 0">' + credRows + '</table></div>';
} else if (postDeployInfo.defaultCreds) {
var creds = postDeployInfo.defaultCreds.replace(/DOMAIN/g, postDeployInfo.domain);
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Bejelentkezés</h4>' +
'<p class="app-info-creds">' + creds + '</p></div>';
}
if (postDeployInfo.docsURL) {
html += '<div style="margin-top:1rem"><a href="' + postDeployInfo.docsURL + '" target="_blank" class="btn btn-sm btn-outline">Dokumentáció ↗</a></div>';
}
html += '<div style="display:flex;gap:.75rem;margin-top:1.5rem;flex-wrap:wrap">' +
'<a href="/stacks/' + stackName + '/deploy" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>' +
'</div>';
return html;
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
@@ -1100,72 +1012,6 @@ function revealAutoField(stackName, envVar, btn, elId) {
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function toggleAutoField(fieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
el.type = el.type === 'password' ? 'text' : 'password';
btn.textContent = el.type === 'password' ? 'Megjelenítés' : 'Elrejtés';
}
function generatePassword(fieldId, confirmFieldId, spec) {
const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const SPECIAL = '-_.!@#%+=';
var parts = (spec || '').split(':');
var n = parseInt(parts[1], 10);
if (parts[0] !== 'password' || !(n >= 12 && n <= 64)) { n = 16; }
var special = parts[2] === 'special';
var chars = special ? letters + SPECIAL : letters;
let pass = '';
for (;;) {
pass = '';
const arr = new Uint32Array(n);
crypto.getRandomValues(arr);
for (let i = 0; i < n; i++) {
pass += chars[arr[i] % chars.length];
}
if (!special || (/[a-z]/.test(pass) && /[A-Z]/.test(pass) && /[0-9]/.test(pass) && /[-_.!@#%+=]/.test(pass) && /^[A-Za-z0-9]/.test(pass))) {
break;
}
}
document.getElementById(fieldId).value = pass;
if (confirmFieldId) {
var ce = document.getElementById(confirmFieldId);
if (ce) ce.value = pass;
}
}
function togglePasswordField(fieldId, confirmFieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
var newType = el.type === 'password' ? 'text' : 'password';
el.type = newType;
var ce = document.getElementById(confirmFieldId);
if (ce) ce.type = newType;
btn.title = newType === 'password' ? 'Megjelenítés' : 'Elrejtés';
}
function togglePostDeployCred(btn, credId, val) {
var el = document.getElementById(credId);
if (!el) return;
if (el.textContent === val) {
el.textContent = '••••••••••••';
btn.textContent = 'Megjelenítés';
} else {
el.textContent = val;
btn.textContent = 'Elrejtés';
}
}
function copyPostDeployCred(btn, val) {
if (navigator.clipboard) {
navigator.clipboard.writeText(val).then(function() {
var orig = btn.textContent;
btn.textContent = 'Másolva!';
setTimeout(function() { btn.textContent = orig; }, 1500);
});
}
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
@@ -1213,203 +1059,6 @@ function deleteStaleDataRun(stackName, stalePath, btn) {
});
}
document.getElementById('deploy-form').addEventListener('submit', async function(e) {
e.preventDefault();
const passwordFields = e.target.querySelectorAll('input[data-field-type="password"]');
for (const pf of passwordFields) {
if (!pf.disabled && pf.value.trim() === '') {
const label = pf.closest('.form-group').querySelector('label').textContent.trim();
showAlert('Kötelező mező: ' + label + '\nHasználd a Generálás gombot vagy írj be egy jelszót.');
pf.focus();
return;
}
}
const confirmInputs = e.target.querySelectorAll('input[data-confirm-for]');
for (const ci of confirmInputs) {
const mainEl = document.getElementById(ci.getAttribute('data-confirm-for'));
if (mainEl && !mainEl.disabled && ci.value !== mainEl.value) {
const label = mainEl.closest('.form-group').querySelector('label').textContent.trim();
showAlert('A két jelszó nem egyezik: ' + label);
ci.focus();
return;
}
}
const subdomainField = e.target.querySelector('.subdomain-input');
if (subdomainField && !subdomainField.disabled) {
const sd = subdomainField.value.trim().toLowerCase();
if (!sd || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sd)) {
showAlert('Az aldomain csak kisbetűket, számokat és kötőjelet tartalmazhat, és nem kezdődhet/végződhet kötőjellel.');
subdomainField.focus();
return;
}
}
const requiredFields = e.target.querySelectorAll('input[required], select[required]');
for (const rf of requiredFields) {
if (!rf.disabled && rf.value.trim() === '') {
const label = rf.closest('.form-group').querySelector('label').textContent.trim();
showAlert('Kötelező mező: ' + label);
rf.focus();
return;
}
}
const btn = e.target.querySelector('[type=submit]');
btn.disabled = true;
btn.textContent = 'Telepítés folyamatban...';
const values = {};
const inputs = e.target.querySelectorAll('input, select');
inputs.forEach(function(el) {
if (el.name && !el.disabled) {
if (el.type === 'checkbox') {
values[el.name] = el.checked ? 'true' : 'false';
} else {
values[el.name] = el.value;
}
}
});
var stackName = 'privatebin';
var progressEl = document.getElementById('deploy-progress');
var formEl = document.getElementById('deploy-form');
var stepConfig = document.getElementById('step-config');
var stepContainers = document.getElementById('step-containers');
var stepHealth = document.getElementById('step-health');
var warningEl = document.getElementById('deploy-warning');
var resultEl = document.getElementById('deploy-result');
var elapsedEl = document.getElementById('deploy-elapsed');
function setStep(el, status, text) {
el.className = 'deploy-step ' + status;
if (text) el.querySelector('.step-text').textContent = text;
var icon = el.querySelector('.step-icon');
if (status === 'done') icon.innerHTML = '<svg class="ico"><use href="#i-check"/></svg>';
else if (status === 'error') icon.innerHTML = '<svg class="ico"><use href="#i-x"/></svg>';
else if (status === 'warn') icon.innerHTML = '<svg class="ico"><use href="#i-triangle-alert"/></svg>';
else if (status === 'active') icon.innerHTML = '<svg class="ico ico-spin"><use href="#i-rotate-cw"/></svg>';
}
try {
var resp = await fetch('/api/stacks/' + stackName + '/deploy', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({values: values, kept_data: window.keptChoice || ''})
});
var data = await resp.json();
if (!data.ok && data.data && data.data.code === 'kept_data_choice') {
showKeptChoice(data.data, e.target);
btn.textContent = 'Telepítés indítása';
btn.disabled = false;
return;
}
if (!data.ok) {
showAlert('Hiba: ' + data.error);
btn.textContent = 'Telepítés indítása';
btn.disabled = false;
return;
}
formEl.style.display = 'none';
progressEl.style.display = 'block';
setStep(stepConfig, 'done', 'Konfiguráció mentve');
setStep(stepContainers, 'active', 'Konténer(ek) indítása...');
if (data.data && data.data.warning) {
warningEl.textContent = data.data.warning;
warningEl.style.display = 'block';
}
var startTime = Date.now();
var pollTimeout = window.keptChoice === 'use' ? 900000 : 120000;
var pollTimer = setInterval(async function() {
var elapsed = Math.round((Date.now() - startTime) / 1000);
elapsedEl.textContent = elapsed + ' másodperce...';
if (Date.now() - startTime > pollTimeout) {
clearInterval(pollTimer);
setStep(stepHealth, 'warn', 'Időtúllépés — az alkalmazás még indulhat');
resultEl.innerHTML = '<div class="alert alert-warning" style="margin-top:1rem">' +
'A telepítés időtúllépésbe futott. Az alkalmazás még indulhat.' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
return;
}
try {
var sr = await fetch('/api/stacks/' + stackName);
var sd = await sr.json();
if (!sd.ok || !sd.data) return;
var state = sd.data.state;
var deployError = sd.data.deploy_error;
if (deployError) {
clearInterval(pollTimer);
setStep(stepContainers, 'error', 'Telepítés sikertelen');
setStep(stepHealth, 'error');
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A telepítés nem sikerült: ' + deployError +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
} else if (state === 'deploying') {
setStep(stepContainers, 'active', 'Képek letöltése, konténerek indítása...');
} else if (state === 'running') {
clearInterval(pollTimer);
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'done', 'Alkalmazás kész!');
progressEl.querySelector('h3').textContent = 'Telepítés sikeres!';
resultEl.innerHTML = '<div class="alert alert-info" style="margin-top:1rem">' +
'Az alkalmazás sikeresen telepítve és fut!' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
} else if (state === 'starting') {
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'active', 'Alkalmazás inicializálása...');
} else if (state === 'unhealthy') {
clearInterval(pollTimer);
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'warn', 'Állapotjelző: még inicializál');
progressEl.querySelector('h3').textContent = 'Telepítés sikeres!';
resultEl.innerHTML = '<div class="alert alert-warning" style="margin-top:1rem">' +
'Az alkalmazás elindult, de még inicializálódik. ' +
'Ez normális az első percekben — az alkalmazás már elérhető lehet.' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
} else if (state === 'exited' || state === 'stopped') {
clearInterval(pollTimer);
setStep(stepContainers, 'error', 'A konténer leállt');
setStep(stepHealth, 'error');
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A konténer leállt. Ellenőrizd a naplókat.' +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
}
} catch(pollErr) {}
}, 3000);
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = 'Telepítés indítása';
btn.disabled = false;
}
});
</script>
@@ -173,46 +173,90 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<h2>Private Bin — Beállítások</h2>
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<button class="btn btn-sm btn-warning" onclick="stackAction(event, 'privatebin', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, 'privatebin', 'stop')">Leállítás</button>
<span class="stack-state-badge state-run">Fut</span>
<a href="https://paste.example.hu" target="_blank" class="btn btn-sm btn-outline">Megnyitás ↗</a>
<a href="https://paste.example.hu" target="_blank" class="btn btn-sm btn-primary" data-app-open="1">Megnyitás ↗</a>
<a href="/apps/privatebin" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-info"/></svg> Részletek</a>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/privatebin/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/privatebin/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="flash flash-success">A mentés sikeresen elkészült.</div>
<div class="flash flash-error">A művelet most nem sikerült.</div>
<div class="deploy-info">
<img class="deploy-logo" src="/logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/logo.png';}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>
<h3>Private Bin</h3>
<p>Egy teszt alkalmazás leírása</p>
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
</div>
<a href="/apps/privatebin" class="btn btn-sm btn-outline" style="margin-top:0.5rem">
Részletes leírás, képernyőképek
</a>
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'privatebin', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'privatebin', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-grid">
<div class="app-info-card">
<h3>Alapértelmezett belépés</h3>
<p class="app-info-creds">nincs ilyen</p>
<p class="app-info-creds-warn">Az első bejelentkezés után azonnal változtasd meg!</p>
</div>
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
@@ -275,21 +319,6 @@
</div>
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<div class="app-optional-config">
<h3>Integrációk</h3>
<p class="config-group-desc">
@@ -673,16 +702,8 @@
</form>
</div>
<div id="kept-choice" class="kept-choice card" hidden></div>
<form id="deploy-form" class="deploy-form">
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
@@ -891,152 +912,43 @@
</div>
</form>
<div id="deploy-progress" class="deploy-progress" style="display:none">
<h3>Telepítés folyamatban...</h3>
<div class="deploy-steps">
<div class="deploy-step active" id="step-config">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konfiguráció mentése...</span>
</div>
<div class="deploy-step" id="step-containers">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konténer(ek) indítása...</span>
</div>
<div class="deploy-step" id="step-health">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Alkalmazás inicializálása...</span>
</div>
</div>
<div id="deploy-warning" class="alert alert-warning" style="display:none"></div>
<div id="deploy-result" style="display:none"></div>
<p class="deploy-elapsed" id="deploy-elapsed"></p>
</div>
</div>
<script>
var postDeployInfo = {
firstSteps: ["Nyisd meg a címet"],
defaultCreds: "nincs ilyen",
docsURL: "https://example.org/docs",
domain: "example.hu",
displayName: "Private Bin",
deployFields: null
};
function showKeptChoice(d, form) {
var box = document.getElementById('kept-choice');
box.textContent = '';
function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; }
box.appendChild(el('h3', 'kept-choice-title', d.title));
box.appendChild(el('p', 'kept-choice-body', d.body));
function option(label, desc, choice, enabled, extra) {
var o = el('div', 'kept-choice-option');
var b = el('button', enabled ? 'btn btn-primary' : 'btn btn-outline', label);
b.type = 'button';
b.disabled = !enabled;
b.setAttribute('data-kept-choice', choice);
b.addEventListener('click', function () { window.keptChoice = choice; box.hidden = true; form.requestSubmit(); });
o.appendChild(b);
o.appendChild(el('p', 'kept-choice-desc', desc));
if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
return o;
}
box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, ''));
box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up));
box.hidden = false;
box.scrollIntoView({behavior: 'smooth'});
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function buildPostDeployCard(stackName) {
var subdomain = '';
var sdField = document.getElementById('field-SUBDOMAIN');
if (sdField) subdomain = sdField.value.trim();
if (!subdomain && 'paste') subdomain = 'paste';
var html = '';
if (subdomain && postDeployInfo.domain) {
var appURL = 'https://' + subdomain + '.' + postDeployInfo.domain;
html += '<div style="text-align:center;margin:1.5rem 0">' +
'<a href="' + appURL + '" target="_blank" class="btn btn-primary btn-lg">Alkalmazás megnyitása ↗</a>' +
'</div>';
}
if (postDeployInfo.firstSteps && postDeployInfo.firstSteps.length > 0) {
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Első lépések</h4><ol class="app-info-list">';
for (var i = 0; i < postDeployInfo.firstSteps.length; i++) {
var step = postDeployInfo.firstSteps[i].replace(/DOMAIN/g, postDeployInfo.domain);
html += '<li>' + step + '</li>';
}
html += '</ol></div>';
}
var credRows = '';
var hasUserCredFields = false;
if (postDeployInfo.deployFields) {
for (var i = 0; i < postDeployInfo.deployFields.length; i++) {
var f = postDeployInfo.deployFields[i];
var isPassword = f.type === 'secret' || f.type === 'password';
var isUsername = false;
if (!isPassword && f.type === 'text') {
var ev = f.env_var.toUpperCase();
isUsername = ev.indexOf('USER') >= 0 || ev.indexOf('ADMIN') >= 0 || ev.indexOf('LOGIN') >= 0 || ev.indexOf('EMAIL') >= 0;
}
if (!isPassword && !isUsername) continue;
var evUp = f.env_var.toUpperCase();
if (evUp.indexOf('DB_PASSWORD') >= 0 || evUp.indexOf('MYSQL_ROOT') >= 0 || evUp.indexOf('SECRET_KEY') >= 0) continue;
if (isPassword || isUsername) hasUserCredFields = true;
var el = document.getElementById('auto-field-' + f.env_var) || document.getElementById('field-' + f.env_var);
var val = el ? el.value : '';
if (!val) continue;
if (isPassword) {
var credId = 'postdeploy-cred-' + i;
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;display:flex;align-items:center;gap:.5rem">' +
'<code id="' + credId + '" style="font-family:var(--font-data);letter-spacing:1px">••••••••••••</code>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="togglePostDeployCred(this,\'' + credId + '\',\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Megjelenítés</button>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="copyPostDeployCred(this,\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Másolás</button>' +
'</td></tr>';
} else {
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;font-family:var(--font-data);user-select:all">' + val + '</td></tr>';
}
}
}
if (credRows) {
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Bejelentkezés</h4>' +
'<table style="margin:.5rem 0">' + credRows + '</table></div>';
} else if (postDeployInfo.defaultCreds) {
var creds = postDeployInfo.defaultCreds.replace(/DOMAIN/g, postDeployInfo.domain);
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Bejelentkezés</h4>' +
'<p class="app-info-creds">' + creds + '</p></div>';
}
if (postDeployInfo.docsURL) {
html += '<div style="margin-top:1rem"><a href="' + postDeployInfo.docsURL + '" target="_blank" class="btn btn-sm btn-outline">Dokumentáció ↗</a></div>';
}
html += '<div style="display:flex;gap:.75rem;margin-top:1.5rem;flex-wrap:wrap">' +
'<a href="/stacks/' + stackName + '/deploy" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>' +
'</div>';
return html;
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
@@ -1103,72 +1015,6 @@ function revealAutoField(stackName, envVar, btn, elId) {
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function toggleAutoField(fieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
el.type = el.type === 'password' ? 'text' : 'password';
btn.textContent = el.type === 'password' ? 'Megjelenítés' : 'Elrejtés';
}
function generatePassword(fieldId, confirmFieldId, spec) {
const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const SPECIAL = '-_.!@#%+=';
var parts = (spec || '').split(':');
var n = parseInt(parts[1], 10);
if (parts[0] !== 'password' || !(n >= 12 && n <= 64)) { n = 16; }
var special = parts[2] === 'special';
var chars = special ? letters + SPECIAL : letters;
let pass = '';
for (;;) {
pass = '';
const arr = new Uint32Array(n);
crypto.getRandomValues(arr);
for (let i = 0; i < n; i++) {
pass += chars[arr[i] % chars.length];
}
if (!special || (/[a-z]/.test(pass) && /[A-Z]/.test(pass) && /[0-9]/.test(pass) && /[-_.!@#%+=]/.test(pass) && /^[A-Za-z0-9]/.test(pass))) {
break;
}
}
document.getElementById(fieldId).value = pass;
if (confirmFieldId) {
var ce = document.getElementById(confirmFieldId);
if (ce) ce.value = pass;
}
}
function togglePasswordField(fieldId, confirmFieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
var newType = el.type === 'password' ? 'text' : 'password';
el.type = newType;
var ce = document.getElementById(confirmFieldId);
if (ce) ce.type = newType;
btn.title = newType === 'password' ? 'Megjelenítés' : 'Elrejtés';
}
function togglePostDeployCred(btn, credId, val) {
var el = document.getElementById(credId);
if (!el) return;
if (el.textContent === val) {
el.textContent = '••••••••••••';
btn.textContent = 'Megjelenítés';
} else {
el.textContent = val;
btn.textContent = 'Elrejtés';
}
}
function copyPostDeployCred(btn, val) {
if (navigator.clipboard) {
navigator.clipboard.writeText(val).then(function() {
var orig = btn.textContent;
btn.textContent = 'Másolva!';
setTimeout(function() { btn.textContent = orig; }, 1500);
});
}
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
@@ -1216,203 +1062,6 @@ function deleteStaleDataRun(stackName, stalePath, btn) {
});
}
document.getElementById('deploy-form').addEventListener('submit', async function(e) {
e.preventDefault();
const passwordFields = e.target.querySelectorAll('input[data-field-type="password"]');
for (const pf of passwordFields) {
if (!pf.disabled && pf.value.trim() === '') {
const label = pf.closest('.form-group').querySelector('label').textContent.trim();
showAlert('Kötelező mező: ' + label + '\nHasználd a Generálás gombot vagy írj be egy jelszót.');
pf.focus();
return;
}
}
const confirmInputs = e.target.querySelectorAll('input[data-confirm-for]');
for (const ci of confirmInputs) {
const mainEl = document.getElementById(ci.getAttribute('data-confirm-for'));
if (mainEl && !mainEl.disabled && ci.value !== mainEl.value) {
const label = mainEl.closest('.form-group').querySelector('label').textContent.trim();
showAlert('A két jelszó nem egyezik: ' + label);
ci.focus();
return;
}
}
const subdomainField = e.target.querySelector('.subdomain-input');
if (subdomainField && !subdomainField.disabled) {
const sd = subdomainField.value.trim().toLowerCase();
if (!sd || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sd)) {
showAlert('Az aldomain csak kisbetűket, számokat és kötőjelet tartalmazhat, és nem kezdődhet/végződhet kötőjellel.');
subdomainField.focus();
return;
}
}
const requiredFields = e.target.querySelectorAll('input[required], select[required]');
for (const rf of requiredFields) {
if (!rf.disabled && rf.value.trim() === '') {
const label = rf.closest('.form-group').querySelector('label').textContent.trim();
showAlert('Kötelező mező: ' + label);
rf.focus();
return;
}
}
const btn = e.target.querySelector('[type=submit]');
btn.disabled = true;
btn.textContent = 'Telepítés folyamatban...';
const values = {};
const inputs = e.target.querySelectorAll('input, select');
inputs.forEach(function(el) {
if (el.name && !el.disabled) {
if (el.type === 'checkbox') {
values[el.name] = el.checked ? 'true' : 'false';
} else {
values[el.name] = el.value;
}
}
});
var stackName = 'privatebin';
var progressEl = document.getElementById('deploy-progress');
var formEl = document.getElementById('deploy-form');
var stepConfig = document.getElementById('step-config');
var stepContainers = document.getElementById('step-containers');
var stepHealth = document.getElementById('step-health');
var warningEl = document.getElementById('deploy-warning');
var resultEl = document.getElementById('deploy-result');
var elapsedEl = document.getElementById('deploy-elapsed');
function setStep(el, status, text) {
el.className = 'deploy-step ' + status;
if (text) el.querySelector('.step-text').textContent = text;
var icon = el.querySelector('.step-icon');
if (status === 'done') icon.innerHTML = '<svg class="ico"><use href="#i-check"/></svg>';
else if (status === 'error') icon.innerHTML = '<svg class="ico"><use href="#i-x"/></svg>';
else if (status === 'warn') icon.innerHTML = '<svg class="ico"><use href="#i-triangle-alert"/></svg>';
else if (status === 'active') icon.innerHTML = '<svg class="ico ico-spin"><use href="#i-rotate-cw"/></svg>';
}
try {
var resp = await fetch('/api/stacks/' + stackName + '/deploy', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({values: values, kept_data: window.keptChoice || ''})
});
var data = await resp.json();
if (!data.ok && data.data && data.data.code === 'kept_data_choice') {
showKeptChoice(data.data, e.target);
btn.textContent = 'Telepítés indítása';
btn.disabled = false;
return;
}
if (!data.ok) {
showAlert('Hiba: ' + data.error);
btn.textContent = 'Telepítés indítása';
btn.disabled = false;
return;
}
formEl.style.display = 'none';
progressEl.style.display = 'block';
setStep(stepConfig, 'done', 'Konfiguráció mentve');
setStep(stepContainers, 'active', 'Konténer(ek) indítása...');
if (data.data && data.data.warning) {
warningEl.textContent = data.data.warning;
warningEl.style.display = 'block';
}
var startTime = Date.now();
var pollTimeout = window.keptChoice === 'use' ? 900000 : 120000;
var pollTimer = setInterval(async function() {
var elapsed = Math.round((Date.now() - startTime) / 1000);
elapsedEl.textContent = elapsed + ' másodperce...';
if (Date.now() - startTime > pollTimeout) {
clearInterval(pollTimer);
setStep(stepHealth, 'warn', 'Időtúllépés — az alkalmazás még indulhat');
resultEl.innerHTML = '<div class="alert alert-warning" style="margin-top:1rem">' +
'A telepítés időtúllépésbe futott. Az alkalmazás még indulhat.' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
return;
}
try {
var sr = await fetch('/api/stacks/' + stackName);
var sd = await sr.json();
if (!sd.ok || !sd.data) return;
var state = sd.data.state;
var deployError = sd.data.deploy_error;
if (deployError) {
clearInterval(pollTimer);
setStep(stepContainers, 'error', 'Telepítés sikertelen');
setStep(stepHealth, 'error');
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A telepítés nem sikerült: ' + deployError +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
} else if (state === 'deploying') {
setStep(stepContainers, 'active', 'Képek letöltése, konténerek indítása...');
} else if (state === 'running') {
clearInterval(pollTimer);
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'done', 'Alkalmazás kész!');
progressEl.querySelector('h3').textContent = 'Telepítés sikeres!';
resultEl.innerHTML = '<div class="alert alert-info" style="margin-top:1rem">' +
'Az alkalmazás sikeresen telepítve és fut!' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
} else if (state === 'starting') {
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'active', 'Alkalmazás inicializálása...');
} else if (state === 'unhealthy') {
clearInterval(pollTimer);
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'warn', 'Állapotjelző: még inicializál');
progressEl.querySelector('h3').textContent = 'Telepítés sikeres!';
resultEl.innerHTML = '<div class="alert alert-warning" style="margin-top:1rem">' +
'Az alkalmazás elindult, de még inicializálódik. ' +
'Ez normális az első percekben — az alkalmazás már elérhető lehet.' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
} else if (state === 'exited' || state === 'stopped') {
clearInterval(pollTimer);
setStep(stepContainers, 'error', 'A konténer leállt');
setStep(stepHealth, 'error');
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A konténer leállt. Ellenőrizd a naplókat.' +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
}
} catch(pollErr) {}
}, 3000);
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = 'Telepítés indítása';
btn.disabled = false;
}
});
</script>
@@ -184,48 +184,63 @@
<a href="/stacks/immich/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/immich/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/immich/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/immich-logo.svg"
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/immich-logo.svg"
alt="Immich" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/immich-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/immich-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/immich-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/immich-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/immich/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/immich/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/immich/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/immich/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'immich', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'immich', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="setup-gate-card">
<h3>Első beállítás</h3>
<p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p>
@@ -234,14 +249,185 @@
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
@@ -594,28 +780,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -184,48 +184,63 @@
<a href="/stacks/immich/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/immich/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/immich/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/immich-logo.svg"
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/immich-logo.svg"
alt="Immich" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/immich-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/immich-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/immich-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/immich-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/immich/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/immich/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/immich/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/immich/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'immich', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'immich', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="setup-gate-card">
<h3>Első beállítás</h3>
<p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p>
@@ -235,14 +250,185 @@
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
@@ -595,28 +781,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -184,48 +184,63 @@
<a href="/stacks/immich/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/immich/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/immich/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/immich-logo.svg"
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/immich-logo.svg"
alt="Immich" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/immich-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/immich-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/immich-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/immich-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/immich/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/immich/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/immich/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/immich/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'immich', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'immich', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="setup-gate-card">
<h3>Első beállítás</h3>
<p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p>
@@ -234,14 +249,185 @@
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
@@ -594,28 +780,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -184,48 +184,65 @@
<a href="/stacks/opengist/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/opengist/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/opengist/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/opengist-logo.svg"
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/opengist-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/opengist/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/opengist/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/opengist/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/opengist/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'opengist', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'opengist', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="signup-card">
<h3>Regisztráció</h3>
<p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p>
@@ -235,12 +252,181 @@
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
@@ -594,28 +780,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -184,48 +184,65 @@
<a href="/stacks/opengist/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/stacks/opengist/export" class="btn btn-sm btn-outline">Exportálás</a>
<a href="/stacks/opengist/deploy" class="btn btn-sm btn-outline">Beállítások</a>
</div>
</div>
<div class="app-info-hero">
<img class="app-info-logo" src="/static/assets/opengist-logo.svg"
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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">
<p class="app-info-tagline"></p>
<div class="app-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">Csak x86</span>
</div>
</div>
</div>
<div class="app-screenshots" id="screenshots">
<img src="/static/assets/opengist-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
<img src="/static/assets/opengist-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/opengist/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/opengist/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/opengist/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/opengist/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'opengist', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'opengist', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="signup-card">
<h3>Regisztráció</h3>
<p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p>
@@ -237,12 +254,181 @@
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
@@ -596,28 +782,3 @@
</body>
</html>
<script>
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'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
</script>
@@ -0,0 +1,783 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opengist — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Opengist</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/opengist/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/opengist/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/opengist/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/opengist/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'opengist', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'opengist', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="signup-card">
<h3>Regisztráció</h3>
<p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p>
<p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
<p class="form-hint" id="signup-native-open-failed">Az alkalmazás saját regisztrációs kapcsolóját nem sikerült megnyitni a 15 percre, ezért zárva maradt. Új családtag így az alkalmazásban nem tud regisztrálni. Próbáld újra később.</p>
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/opengist/signup-window')">Regisztráció megnyitása 15 percre</button>
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -0,0 +1,780 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opengist — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Opengist</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-run">Fut</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/opengist-logo.svg"
alt="Opengist" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/opengist/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/opengist/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/opengist/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/opengist/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="running">Fut</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, 'opengist', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, 'Leállítod az alkalmazást? Amíg áll, a házban senki nem tudja használni.', function(){ stackAction(event, 'opengist', 'stop'); })">Leállítás</button>
</div>
</div>
<div class="app-info-card" style="margin-top:1rem" id="signup-card">
<h3>Regisztráció</h3>
<p>A regisztráció most nyitva, eddig: 14:05</p>
<p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -173,43 +173,89 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<h2>Private Bin — Beállítások</h2>
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>Private Bin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-neutral">Leállítva</span>
<button class="btn btn-sm btn-success" onclick="stackAction(event, 'privatebin', 'start')">Indítás</button>
<a href="/apps/privatebin" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-info"/></svg> Részletek</a>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="Private Bin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline">Egy teszt alkalmazás leírása</p>
<div class="stack-meta-badges">
<span class="meta-badge">~128M RAM</span>
<span class="meta-badge"></span>
<span class="meta-badge meta-badge-warn">HDD szükséges</span>
<span class="meta-badge meta-badge-ok">Magyar felület</span>
</div>
</div>
</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/privatebin/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/privatebin/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="flash flash-success">A mentés sikeresen elkészült.</div>
<div class="flash flash-error">A művelet most nem sikerült.</div>
<div class="deploy-info">
<img class="deploy-logo" src="/logo.svg" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/logo.png';}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>
<h3>Private Bin</h3>
<p>Egy teszt alkalmazás leírása</p>
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
</div>
<a href="/apps/privatebin" class="btn btn-sm btn-outline" style="margin-top:0.5rem">
Részletes leírás, képernyőképek
</a>
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="stopped">Leállítva</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-success" data-run-action="start" onclick="stackAction(event, 'privatebin', 'start')">Indítás</button>
</div>
</div>
<div class="app-info-grid">
<div class="app-info-card">
<h3>Alapértelmezett belépés</h3>
<p class="app-info-creds">nincs ilyen</p>
<p class="app-info-creds-warn">Az első bejelentkezés után azonnal változtasd meg!</p>
</div>
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
@@ -229,21 +275,6 @@
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
<div class="cross-drive-nightly">
<span class="form-hint" style="display:block;margin-top:.25rem">
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a> · <a href="/stacks/privatebin/backup" style="color:var(--blue)">Mentési beállítások →</a>
</span>
</div>
</div>
<div class="app-optional-config">
<h3>Integrációk</h3>
<p class="config-group-desc">
@@ -622,22 +653,8 @@
</form>
</div>
<div id="kept-choice" class="kept-choice card" hidden></div>
<form id="deploy-form" class="deploy-form">
<div class="alert alert-warning" style="margin-bottom:1rem">
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Nincs elég szabad tárhely a telepítéshez. Jelenleg 1 GB szabad, és a rendszer
5 GB tartalékot tart fenn az alapszolgáltatások (vezérlő, proxy) védelmében.
A telepítés ezért átmenetileg le van tiltva — szabadíts fel helyet, vagy bővítsd a tárhelyet.
</div>
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
@@ -818,152 +835,43 @@
</div>
</form>
<div id="deploy-progress" class="deploy-progress" style="display:none">
<h3>Telepítés folyamatban...</h3>
<div class="deploy-steps">
<div class="deploy-step active" id="step-config">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konfiguráció mentése...</span>
</div>
<div class="deploy-step" id="step-containers">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konténer(ek) indítása...</span>
</div>
<div class="deploy-step" id="step-health">
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Alkalmazás inicializálása...</span>
</div>
</div>
<div id="deploy-warning" class="alert alert-warning" style="display:none"></div>
<div id="deploy-result" style="display:none"></div>
<p class="deploy-elapsed" id="deploy-elapsed"></p>
</div>
</div>
<script>
var postDeployInfo = {
firstSteps: ["Nyisd meg a címet"],
defaultCreds: "nincs ilyen",
docsURL: "https://example.org/docs",
domain: "example.hu",
displayName: "Private Bin",
deployFields: null
};
function showKeptChoice(d, form) {
var box = document.getElementById('kept-choice');
box.textContent = '';
function el(tag, cls, text) { var x = document.createElement(tag); if (cls) x.className = cls; if (text) x.textContent = text; return x; }
box.appendChild(el('h3', 'kept-choice-title', d.title));
box.appendChild(el('p', 'kept-choice-body', d.body));
function option(label, desc, choice, enabled, extra) {
var o = el('div', 'kept-choice-option');
var b = el('button', enabled ? 'btn btn-primary' : 'btn btn-outline', label);
b.type = 'button';
b.disabled = !enabled;
b.setAttribute('data-kept-choice', choice);
b.addEventListener('click', function () { window.keptChoice = choice; box.hidden = true; form.requestSubmit(); });
o.appendChild(b);
o.appendChild(el('p', 'kept-choice-desc', desc));
if (extra) o.appendChild(el('p', 'kept-choice-note', extra));
return o;
}
box.appendChild(option(d.use_label, d.use_offered ? d.use_desc : d.use_off, 'use', d.use_offered, ''));
box.appendChild(option(d.fresh_label, d.fresh_desc, 'fresh', true, d.not_backed_up));
box.hidden = false;
box.scrollIntoView({behavior: 'smooth'});
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function buildPostDeployCard(stackName) {
var subdomain = '';
var sdField = document.getElementById('field-SUBDOMAIN');
if (sdField) subdomain = sdField.value.trim();
if (!subdomain && 'paste') subdomain = 'paste';
var html = '';
if (subdomain && postDeployInfo.domain) {
var appURL = 'https://' + subdomain + '.' + postDeployInfo.domain;
html += '<div style="text-align:center;margin:1.5rem 0">' +
'<a href="' + appURL + '" target="_blank" class="btn btn-primary btn-lg">Alkalmazás megnyitása ↗</a>' +
'</div>';
}
if (postDeployInfo.firstSteps && postDeployInfo.firstSteps.length > 0) {
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Első lépések</h4><ol class="app-info-list">';
for (var i = 0; i < postDeployInfo.firstSteps.length; i++) {
var step = postDeployInfo.firstSteps[i].replace(/DOMAIN/g, postDeployInfo.domain);
html += '<li>' + step + '</li>';
}
html += '</ol></div>';
}
var credRows = '';
var hasUserCredFields = false;
if (postDeployInfo.deployFields) {
for (var i = 0; i < postDeployInfo.deployFields.length; i++) {
var f = postDeployInfo.deployFields[i];
var isPassword = f.type === 'secret' || f.type === 'password';
var isUsername = false;
if (!isPassword && f.type === 'text') {
var ev = f.env_var.toUpperCase();
isUsername = ev.indexOf('USER') >= 0 || ev.indexOf('ADMIN') >= 0 || ev.indexOf('LOGIN') >= 0 || ev.indexOf('EMAIL') >= 0;
}
if (!isPassword && !isUsername) continue;
var evUp = f.env_var.toUpperCase();
if (evUp.indexOf('DB_PASSWORD') >= 0 || evUp.indexOf('MYSQL_ROOT') >= 0 || evUp.indexOf('SECRET_KEY') >= 0) continue;
if (isPassword || isUsername) hasUserCredFields = true;
var el = document.getElementById('auto-field-' + f.env_var) || document.getElementById('field-' + f.env_var);
var val = el ? el.value : '';
if (!val) continue;
if (isPassword) {
var credId = 'postdeploy-cred-' + i;
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;display:flex;align-items:center;gap:.5rem">' +
'<code id="' + credId + '" style="font-family:var(--font-data);letter-spacing:1px">••••••••••••</code>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="togglePostDeployCred(this,\'' + credId + '\',\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Megjelenítés</button>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="copyPostDeployCred(this,\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Másolás</button>' +
'</td></tr>';
} else {
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;font-family:var(--font-data);user-select:all">' + val + '</td></tr>';
}
}
}
if (credRows) {
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Bejelentkezés</h4>' +
'<table style="margin:.5rem 0">' + credRows + '</table></div>';
} else if (postDeployInfo.defaultCreds) {
var creds = postDeployInfo.defaultCreds.replace(/DOMAIN/g, postDeployInfo.domain);
html += '<div class="app-info-card" style="margin-top:1rem"><h4>Bejelentkezés</h4>' +
'<p class="app-info-creds">' + creds + '</p></div>';
}
if (postDeployInfo.docsURL) {
html += '<div style="margin-top:1rem"><a href="' + postDeployInfo.docsURL + '" target="_blank" class="btn btn-sm btn-outline">Dokumentáció ↗</a></div>';
}
html += '<div style="display:flex;gap:.75rem;margin-top:1.5rem;flex-wrap:wrap">' +
'<a href="/stacks/' + stackName + '/deploy" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>' +
'</div>';
return html;
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
@@ -1030,72 +938,6 @@ function revealAutoField(stackName, envVar, btn, elId) {
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function toggleAutoField(fieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
el.type = el.type === 'password' ? 'text' : 'password';
btn.textContent = el.type === 'password' ? 'Megjelenítés' : 'Elrejtés';
}
function generatePassword(fieldId, confirmFieldId, spec) {
const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const SPECIAL = '-_.!@#%+=';
var parts = (spec || '').split(':');
var n = parseInt(parts[1], 10);
if (parts[0] !== 'password' || !(n >= 12 && n <= 64)) { n = 16; }
var special = parts[2] === 'special';
var chars = special ? letters + SPECIAL : letters;
let pass = '';
for (;;) {
pass = '';
const arr = new Uint32Array(n);
crypto.getRandomValues(arr);
for (let i = 0; i < n; i++) {
pass += chars[arr[i] % chars.length];
}
if (!special || (/[a-z]/.test(pass) && /[A-Z]/.test(pass) && /[0-9]/.test(pass) && /[-_.!@#%+=]/.test(pass) && /^[A-Za-z0-9]/.test(pass))) {
break;
}
}
document.getElementById(fieldId).value = pass;
if (confirmFieldId) {
var ce = document.getElementById(confirmFieldId);
if (ce) ce.value = pass;
}
}
function togglePasswordField(fieldId, confirmFieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
var newType = el.type === 'password' ? 'text' : 'password';
el.type = newType;
var ce = document.getElementById(confirmFieldId);
if (ce) ce.type = newType;
btn.title = newType === 'password' ? 'Megjelenítés' : 'Elrejtés';
}
function togglePostDeployCred(btn, credId, val) {
var el = document.getElementById(credId);
if (!el) return;
if (el.textContent === val) {
el.textContent = '••••••••••••';
btn.textContent = 'Megjelenítés';
} else {
el.textContent = val;
btn.textContent = 'Elrejtés';
}
}
function copyPostDeployCred(btn, val) {
if (navigator.clipboard) {
navigator.clipboard.writeText(val).then(function() {
var orig = btn.textContent;
btn.textContent = 'Másolva!';
setTimeout(function() { btn.textContent = orig; }, 1500);
});
}
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
@@ -1143,203 +985,6 @@ function deleteStaleDataRun(stackName, stalePath, btn) {
});
}
document.getElementById('deploy-form').addEventListener('submit', async function(e) {
e.preventDefault();
const passwordFields = e.target.querySelectorAll('input[data-field-type="password"]');
for (const pf of passwordFields) {
if (!pf.disabled && pf.value.trim() === '') {
const label = pf.closest('.form-group').querySelector('label').textContent.trim();
showAlert('Kötelező mező: ' + label + '\nHasználd a Generálás gombot vagy írj be egy jelszót.');
pf.focus();
return;
}
}
const confirmInputs = e.target.querySelectorAll('input[data-confirm-for]');
for (const ci of confirmInputs) {
const mainEl = document.getElementById(ci.getAttribute('data-confirm-for'));
if (mainEl && !mainEl.disabled && ci.value !== mainEl.value) {
const label = mainEl.closest('.form-group').querySelector('label').textContent.trim();
showAlert('A két jelszó nem egyezik: ' + label);
ci.focus();
return;
}
}
const subdomainField = e.target.querySelector('.subdomain-input');
if (subdomainField && !subdomainField.disabled) {
const sd = subdomainField.value.trim().toLowerCase();
if (!sd || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sd)) {
showAlert('Az aldomain csak kisbetűket, számokat és kötőjelet tartalmazhat, és nem kezdődhet/végződhet kötőjellel.');
subdomainField.focus();
return;
}
}
const requiredFields = e.target.querySelectorAll('input[required], select[required]');
for (const rf of requiredFields) {
if (!rf.disabled && rf.value.trim() === '') {
const label = rf.closest('.form-group').querySelector('label').textContent.trim();
showAlert('Kötelező mező: ' + label);
rf.focus();
return;
}
}
const btn = e.target.querySelector('[type=submit]');
btn.disabled = true;
btn.textContent = 'Telepítés folyamatban...';
const values = {};
const inputs = e.target.querySelectorAll('input, select');
inputs.forEach(function(el) {
if (el.name && !el.disabled) {
if (el.type === 'checkbox') {
values[el.name] = el.checked ? 'true' : 'false';
} else {
values[el.name] = el.value;
}
}
});
var stackName = 'privatebin';
var progressEl = document.getElementById('deploy-progress');
var formEl = document.getElementById('deploy-form');
var stepConfig = document.getElementById('step-config');
var stepContainers = document.getElementById('step-containers');
var stepHealth = document.getElementById('step-health');
var warningEl = document.getElementById('deploy-warning');
var resultEl = document.getElementById('deploy-result');
var elapsedEl = document.getElementById('deploy-elapsed');
function setStep(el, status, text) {
el.className = 'deploy-step ' + status;
if (text) el.querySelector('.step-text').textContent = text;
var icon = el.querySelector('.step-icon');
if (status === 'done') icon.innerHTML = '<svg class="ico"><use href="#i-check"/></svg>';
else if (status === 'error') icon.innerHTML = '<svg class="ico"><use href="#i-x"/></svg>';
else if (status === 'warn') icon.innerHTML = '<svg class="ico"><use href="#i-triangle-alert"/></svg>';
else if (status === 'active') icon.innerHTML = '<svg class="ico ico-spin"><use href="#i-rotate-cw"/></svg>';
}
try {
var resp = await fetch('/api/stacks/' + stackName + '/deploy', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({values: values, kept_data: window.keptChoice || ''})
});
var data = await resp.json();
if (!data.ok && data.data && data.data.code === 'kept_data_choice') {
showKeptChoice(data.data, e.target);
btn.textContent = 'Telepítés indítása';
btn.disabled = false;
return;
}
if (!data.ok) {
showAlert('Hiba: ' + data.error);
btn.textContent = 'Telepítés indítása';
btn.disabled = false;
return;
}
formEl.style.display = 'none';
progressEl.style.display = 'block';
setStep(stepConfig, 'done', 'Konfiguráció mentve');
setStep(stepContainers, 'active', 'Konténer(ek) indítása...');
if (data.data && data.data.warning) {
warningEl.textContent = data.data.warning;
warningEl.style.display = 'block';
}
var startTime = Date.now();
var pollTimeout = window.keptChoice === 'use' ? 900000 : 120000;
var pollTimer = setInterval(async function() {
var elapsed = Math.round((Date.now() - startTime) / 1000);
elapsedEl.textContent = elapsed + ' másodperce...';
if (Date.now() - startTime > pollTimeout) {
clearInterval(pollTimer);
setStep(stepHealth, 'warn', 'Időtúllépés — az alkalmazás még indulhat');
resultEl.innerHTML = '<div class="alert alert-warning" style="margin-top:1rem">' +
'A telepítés időtúllépésbe futott. Az alkalmazás még indulhat.' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
return;
}
try {
var sr = await fetch('/api/stacks/' + stackName);
var sd = await sr.json();
if (!sd.ok || !sd.data) return;
var state = sd.data.state;
var deployError = sd.data.deploy_error;
if (deployError) {
clearInterval(pollTimer);
setStep(stepContainers, 'error', 'Telepítés sikertelen');
setStep(stepHealth, 'error');
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A telepítés nem sikerült: ' + deployError +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
} else if (state === 'deploying') {
setStep(stepContainers, 'active', 'Képek letöltése, konténerek indítása...');
} else if (state === 'running') {
clearInterval(pollTimer);
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'done', 'Alkalmazás kész!');
progressEl.querySelector('h3').textContent = 'Telepítés sikeres!';
resultEl.innerHTML = '<div class="alert alert-info" style="margin-top:1rem">' +
'Az alkalmazás sikeresen telepítve és fut!' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
} else if (state === 'starting') {
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'active', 'Alkalmazás inicializálása...');
} else if (state === 'unhealthy') {
clearInterval(pollTimer);
setStep(stepContainers, 'done', 'Konténerek elindultak');
setStep(stepHealth, 'warn', 'Állapotjelző: még inicializál');
progressEl.querySelector('h3').textContent = 'Telepítés sikeres!';
resultEl.innerHTML = '<div class="alert alert-warning" style="margin-top:1rem">' +
'Az alkalmazás elindult, de még inicializálódik. ' +
'Ez normális az első percekben — az alkalmazás már elérhető lehet.' +
'</div>' + buildPostDeployCard(stackName);
resultEl.style.display = 'block';
} else if (state === 'exited' || state === 'stopped') {
clearInterval(pollTimer);
setStep(stepContainers, 'error', 'A konténer leállt');
setStep(stepHealth, 'error');
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A konténer leállt. Ellenőrizd a naplókat.' +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
}
} catch(pollErr) {}
}, 3000);
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = 'Telepítés indítása';
btn.disabled = false;
}
});
</script>
@@ -0,0 +1,776 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PrivateBin — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<div style="display:flex;align-items:center;gap:1rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>
<h2>PrivateBin</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<span class="stack-state-badge state-progress">Telepítés...</span>
</div>
</div>
<div class="app-hero">
<img class="app-hero-logo" src="/static/assets/privatebin-logo.svg"
alt="PrivateBin" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}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-hero-text">
<p class="app-info-tagline"></p>
<div class="stack-meta-badges">
<span class="meta-badge">~ RAM</span>
<span class="meta-badge"></span>
</div>
</div>
</div>
<div class="alert alert-info" style="margin-top:1rem" data-update-phase="pull">Letöltés folyamatban…</div>
<nav class="app-tabs" aria-label="Alkalmazás lapjai">
<a class="app-tab app-tab-active" href="/apps/privatebin/settings" data-tab="settings"
aria-current="page" aria-selected="true">Beállítások</a>
<a class="app-tab" href="/apps/privatebin/data" data-tab="data"
aria-selected="false">Adatok és mentés</a>
<a class="app-tab" href="/apps/privatebin/guide" data-tab="guide"
aria-selected="false">Útmutató</a>
<a class="app-tab" href="/apps/privatebin/logs" data-tab="logs"
aria-selected="false">Napló</a>
</nav>
<div class="deploy-container">
<div class="app-info-card" id="app-run-card" data-run-card="1">
<h3>Működés</h3>
<p class="form-hint" data-run-state="deploying">Telepítés...</p>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
<button class="btn btn-sm btn-success" data-run-action="start" onclick="stackAction(event, 'privatebin', 'start')">Indítás</button>
</div>
</div>
<div class="app-info-grid">
</div>
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
<form id="deploy-form" class="deploy-form">
</form>
</div>
<script>
function appPress(btn, url) {
var err = btn.parentNode.querySelector('.app-press-err');
if (err) { err.style.display = 'none'; }
btn.disabled = true;
fetch(url, {method: 'POST', headers: csrfHeaders(), credentials: 'same-origin'})
.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; if (err) { err.textContent = e.message; err.style.display = 'inline'; } });
}
function appAfterStart(btn, slug, envVar) {
var box = btn.closest('.after-start-card'), err = box.querySelector('.app-press-err');
var input = box.querySelector('input');
err.style.display = 'none'; btn.disabled = true;
fetch('/apps/' + slug + '/after-start/' + envVar, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
credentials: 'same-origin',
body: JSON.stringify({value: (input ? input.value : '').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'; });
}
</script>
<script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
</script>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -373,7 +373,7 @@
<span class="layer-unconfigured">2. mentés kikapcsolva</span>
<div class="layer-actions">
<a href="/stacks/disconnected/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/disconnected/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -447,7 +447,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/hdd/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/hdd/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -509,7 +509,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/volume/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/volume/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -569,7 +569,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/confonly/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/confonly/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -630,7 +630,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact1/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact1/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -691,7 +691,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -752,7 +752,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact3/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact3/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -835,7 +835,7 @@
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="Biztosan visszaállítod a teljes egységet?">Teljes visszaállítás a másolatból</button>
</form>
<a href="/stacks/okrun/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/okrun/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -913,7 +913,7 @@
<a href="/stacks/oksucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/oksucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -986,7 +986,7 @@
<a href="/stacks/nosucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1053,7 +1053,7 @@
<a href="/stacks/nosucc2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1112,7 +1112,7 @@
<span class="layer-reason" style="opacity:.85" title="A 2. mentés automatikus — külön beállítás nem kell">Nincs második meghajtó csatlakoztatva</span>
<div class="layer-actions">
<a href="/stacks/t2err/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/t2err/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -404,7 +404,7 @@
<span class="layer-unconfigured">2. mentés kikapcsolva</span>
<div class="layer-actions">
<a href="/stacks/disconnected/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/disconnected/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -478,7 +478,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/hdd/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/hdd/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -540,7 +540,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/volume/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/volume/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -600,7 +600,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/confonly/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/confonly/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -661,7 +661,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact1/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact1/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -722,7 +722,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -783,7 +783,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact3/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact3/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -866,7 +866,7 @@
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="Biztosan visszaállítod a teljes egységet?">Teljes visszaállítás a másolatból</button>
</form>
<a href="/stacks/okrun/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/okrun/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -944,7 +944,7 @@
<a href="/stacks/oksucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/oksucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1017,7 +1017,7 @@
<a href="/stacks/nosucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1084,7 +1084,7 @@
<a href="/stacks/nosucc2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1143,7 +1143,7 @@
<span class="layer-reason" style="opacity:.85" title="A 2. mentés automatikus — külön beállítás nem kell">Nincs második meghajtó csatlakoztatva</span>
<div class="layer-actions">
<a href="/stacks/t2err/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/t2err/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -408,7 +408,7 @@
<span class="layer-unconfigured">2. mentés kikapcsolva</span>
<div class="layer-actions">
<a href="/stacks/disconnected/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/disconnected/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -482,7 +482,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/hdd/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/hdd/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -544,7 +544,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/volume/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/volume/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -604,7 +604,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/confonly/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/confonly/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -665,7 +665,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact1/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact1/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -726,7 +726,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -787,7 +787,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact3/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact3/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -870,7 +870,7 @@
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="Biztosan visszaállítod a teljes egységet?">Teljes visszaállítás a másolatból</button>
</form>
<a href="/stacks/okrun/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/okrun/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -948,7 +948,7 @@
<a href="/stacks/oksucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/oksucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1021,7 +1021,7 @@
<a href="/stacks/nosucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1088,7 +1088,7 @@
<a href="/stacks/nosucc2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1147,7 +1147,7 @@
<span class="layer-reason" style="opacity:.85" title="A 2. mentés automatikus — külön beállítás nem kell">Nincs második meghajtó csatlakoztatva</span>
<div class="layer-actions">
<a href="/stacks/t2err/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/t2err/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -402,7 +402,7 @@
<span class="layer-unconfigured">2. mentés kikapcsolva</span>
<div class="layer-actions">
<a href="/stacks/disconnected/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/disconnected/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -476,7 +476,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/hdd/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/hdd/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -538,7 +538,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/volume/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/volume/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -598,7 +598,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/confonly/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/confonly/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -659,7 +659,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact1/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact1/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -720,7 +720,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -781,7 +781,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact3/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact3/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -864,7 +864,7 @@
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="Biztosan visszaállítod a teljes egységet?">Teljes visszaállítás a másolatból</button>
</form>
<a href="/stacks/okrun/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/okrun/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -942,7 +942,7 @@
<a href="/stacks/oksucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/oksucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1015,7 +1015,7 @@
<a href="/stacks/nosucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1082,7 +1082,7 @@
<a href="/stacks/nosucc2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1141,7 +1141,7 @@
<span class="layer-reason" style="opacity:.85" title="A 2. mentés automatikus — külön beállítás nem kell">Nincs második meghajtó csatlakoztatva</span>
<div class="layer-actions">
<a href="/stacks/t2err/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/t2err/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -316,7 +316,7 @@
<span class="layer-unconfigured">2. mentés kikapcsolva</span>
<div class="layer-actions">
<a href="/stacks/disconnected/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/disconnected/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -390,7 +390,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/hdd/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/hdd/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -452,7 +452,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/volume/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/volume/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -512,7 +512,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/confonly/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/confonly/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -573,7 +573,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact1/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact1/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -634,7 +634,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -695,7 +695,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact3/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact3/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -778,7 +778,7 @@
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="Biztosan visszaállítod a teljes egységet?">Teljes visszaállítás a másolatból</button>
</form>
<a href="/stacks/okrun/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/okrun/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -856,7 +856,7 @@
<a href="/stacks/oksucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/oksucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -929,7 +929,7 @@
<a href="/stacks/nosucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -996,7 +996,7 @@
<a href="/stacks/nosucc2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1055,7 +1055,7 @@
<span class="layer-reason" style="opacity:.85" title="A 2. mentés automatikus — külön beállítás nem kell">Nincs második meghajtó csatlakoztatva</span>
<div class="layer-actions">
<a href="/stacks/t2err/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/t2err/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -316,7 +316,7 @@
<span class="layer-unconfigured">2. mentés kikapcsolva</span>
<div class="layer-actions">
<a href="/stacks/disconnected/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/disconnected/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -390,7 +390,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/hdd/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/hdd/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -452,7 +452,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/volume/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/volume/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -512,7 +512,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/confonly/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/confonly/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -573,7 +573,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact1/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact1/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -634,7 +634,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -695,7 +695,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact3/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact3/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -778,7 +778,7 @@
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="Biztosan visszaállítod a teljes egységet?">Teljes visszaállítás a másolatból</button>
</form>
<a href="/stacks/okrun/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/okrun/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -856,7 +856,7 @@
<a href="/stacks/oksucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/oksucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -929,7 +929,7 @@
<a href="/stacks/nosucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -996,7 +996,7 @@
<a href="/stacks/nosucc2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1055,7 +1055,7 @@
<span class="layer-reason" style="opacity:.85" title="A 2. mentés automatikus — külön beállítás nem kell">Nincs második meghajtó csatlakoztatva</span>
<div class="layer-actions">
<a href="/stacks/t2err/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/t2err/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -402,7 +402,7 @@
<span class="layer-unconfigured">2. mentés kikapcsolva</span>
<div class="layer-actions">
<a href="/stacks/disconnected/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/disconnected/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -476,7 +476,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/hdd/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/hdd/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -538,7 +538,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/volume/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/volume/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -598,7 +598,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/confonly/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/confonly/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -659,7 +659,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact1/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact1/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -720,7 +720,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -781,7 +781,7 @@
<span class="tier-contents" style="opacity:.6">Teljes helyreállítási egység</span>
<div class="layer-actions">
<a href="/stacks/inact3/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/inact3/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -864,7 +864,7 @@
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="Biztosan visszaállítod a teljes egységet?">Teljes visszaállítás a másolatból</button>
</form>
<a href="/stacks/okrun/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/okrun/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -942,7 +942,7 @@
<a href="/stacks/oksucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/oksucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1015,7 +1015,7 @@
<a href="/stacks/nosucc/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1082,7 +1082,7 @@
<a href="/stacks/nosucc2/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/nosucc2/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -1141,7 +1141,7 @@
<span class="layer-reason" style="opacity:.85" title="A 2. mentés automatikus — külön beállítás nem kell">Nincs második meghajtó csatlakoztatva</span>
<div class="layer-actions">
<a href="/stacks/t2err/backup" class="btn btn-xs btn-outline">Beállítás</a>
<a href="/apps/t2err/data" class="btn btn-xs btn-outline">Beállítás</a>
</div>
</div>
@@ -463,7 +463,7 @@
<button class="btn btn-sm btn-warning" onclick="stackAction(event, 'privatebin', 'restart')" title="Újraindítás"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, 'privatebin', 'stop')" title="Leállítás"><svg class="ico ico-sm"><use href="#i-square"/></svg></button>
<a href="/stacks/privatebin/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/apps/privatebin/logs" class="btn btn-sm btn-outline">Napló</a>
@@ -498,7 +498,7 @@
<button class="btn btn-sm btn-success" onclick="stackAction(event, 'kimai', 'start')" title="Indítás"><svg class="ico ico-sm"><use href="#i-play"/></svg></button>
<button class="btn btn-sm btn-danger" onclick="removeStack('kimai')">Eltávolítás</button>
<a href="/stacks/kimai/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/apps/kimai/logs" class="btn btn-sm btn-outline">Napló</a>
@@ -533,7 +533,7 @@
<button class="btn btn-sm btn-success" onclick="stackAction(event, 'oldapp', 'start')" title="Indítás"><svg class="ico ico-sm"><use href="#i-play"/></svg></button>
<a href="/stacks/oldapp/logs" class="btn btn-sm btn-outline">Napló</a>
<a href="/apps/oldapp/logs" class="btn btn-sm btn-outline">Napló</a>
<button class="btn btn-sm btn-danger" onclick="deleteOrphanStack('oldapp')">Törlés</button>
@@ -564,7 +564,7 @@
<a href="/stacks/gokapi/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, 'gokapi')">Telepítés</a>
<a href="/apps/gokapi/install" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, 'gokapi')">Telepítés</a>
</div>
+11 -493
View File
@@ -172,17 +172,19 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<a href="/apps/privatebin" class="btn btn-sm btn-outline" data-install-back="1">← Vissza</a>
<h2>Private Bin — Telepítés</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/apps/privatebin" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-info"/></svg> Részletek</a>
</div>
</div>
<div class="deploy-container">
<div class="flash flash-success">A mentés sikeresen elkészült.</div>
<div class="flash flash-error">A művelet most nem sikerült.</div>
@@ -194,7 +196,6 @@
<p>Egy teszt alkalmazás leírása</p>
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
</div>
@@ -205,395 +206,6 @@
</div>
<div class="app-optional-config">
<h3>Integrációk</h3>
<p class="config-group-desc">
Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor.
</p>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció egy</strong>
<p class="config-field-help" style="margin:0">Első integráció leírása</p>
<span class="tag tag-off" style="margin-top:.25rem"><span class="dot"></span>Nincs telepítve</span>
</div>
<label class="toggle">
<input type="checkbox"
disabled title="A célalkalmazás nincs telepítve"
onchange="toggleIntegration('privatebin', 'a', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció kettő</strong>
<p class="config-field-help" style="margin:0">Második integráció leírása</p>
<span class="tag tag-neutral" style="margin-top:.25rem"><span class="dot"></span>Célalkalmazás leállítva</span>
</div>
<label class="toggle">
<input type="checkbox"
disabled title="A célalkalmazás nem fut"
onchange="toggleIntegration('privatebin', 'b', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció három</strong>
<p class="config-field-help" style="margin:0">Harmadik integráció leírása</p>
<span class="tag tag-crit" style="margin-top:.25rem"><span class="dot"></span>Hiba</span>
</div>
<label class="toggle">
<input type="checkbox"
onchange="toggleIntegration('privatebin', 'c', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció négy</strong>
<p class="config-field-help" style="margin:0">Negyedik integráció leírása</p>
<span class="tag tag-run" style="margin-top:.25rem"><span class="dot"></span>Aktív</span>
</div>
<label class="toggle">
<input type="checkbox"
checked
onchange="toggleIntegration('privatebin', 'd', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció öt</strong>
<p class="config-field-help" style="margin:0">Ötödik integráció leírása</p>
</div>
<label class="toggle">
<input type="checkbox"
onchange="toggleIntegration('privatebin', 'e', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
</div>
<script>
async function toggleIntegration(provider, target, enable, checkbox) {
checkbox.disabled = true;
try {
var resp = await fetch('/api/integrations/' + provider + '/' + target, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({enabled: enable})
});
var data = await resp.json();
if (!data.ok) {
checkbox.checked = !enable;
showAlert(data.error || 'Hiba történt');
} else {
setTimeout(function(){ location.reload(); }, 500);
return;
}
} catch(err) {
checkbox.checked = !enable;
showAlert('Hálózati hiba');
}
checkbox.disabled = false;
}
</script>
<div class="app-optional-config">
<h3>Földrajzi korlátozás</h3>
<p class="config-group-desc">
Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi.
</p>
<label class="toggle" style="margin-bottom:1rem">
<input type="checkbox" id="app-geo-override-toggle"
checked
onchange="toggleAppGeoOverride(this.checked)">
<span class="toggle-label">Egyéni országkorlátozás</span>
</label>
<div id="app-geo-override-details" >
<div class="form-group">
<label>Engedélyezett országok ehhez az alkalmazáshoz</label>
<div class="geo-country-selector">
<input type="text" id="app-geo-search" class="form-control config-input"
placeholder="Ország keresése..." autocomplete="off"
oninput="filterAppGeoCountries(this.value)"
onfocus="showAppGeoList()"
onblur="setTimeout(function(){hideAppGeoList()},200)">
<div class="geo-country-list" id="app-geo-country-list"></div>
</div>
<div class="geo-selected-tags" id="app-geo-tags"></div>
</div>
<div class="config-actions">
<button class="btn btn-primary" onclick="saveAppGeoOverride()">Mentés</button>
<span id="app-geo-status" class="config-save-status"></span>
</div>
</div>
</div>
<script>
(function(){
var allCountries = [];
var appGeoCountries = ["HU","AT"];
var stackName = 'privatebin';
function loadCountries(cb) {
if (allCountries.length > 0) { cb(); return; }
fetch('/api/geo/countries', {headers: csrfHeaders()})
.then(function(r){return r.json()})
.then(function(d){ if(d.ok) allCountries = d.data; cb(); })
.catch(function(){ cb(); });
}
window.toggleAppGeoOverride = function(enabled) {
document.getElementById('app-geo-override-details').style.display = enabled ? '' : 'none';
if (enabled) {
if (!appGeoCountries || appGeoCountries.length === 0) {
appGeoCountries = ["HU"];
}
loadCountries(renderAppGeoTags);
} else {
fetch('/api/stacks/' + stackName + '/geo/override', {method:'DELETE', headers:csrfHeaders()});
}
};
window.showAppGeoList = function() { loadCountries(function(){ filterAppGeoCountries(''); }); };
window.hideAppGeoList = function() { document.getElementById('app-geo-country-list').style.display='none'; };
window.filterAppGeoCountries = function(q) {
var list = document.getElementById('app-geo-country-list');
q = q.toLowerCase();
var html = ''; var count = 0;
for (var i = 0; i < allCountries.length && count < 15; i++) {
var c = allCountries[i];
if (appGeoCountries.indexOf(c.code) >= 0) continue;
if (q && c.name.toLowerCase().indexOf(q) < 0 && c.code.toLowerCase().indexOf(q) < 0) continue;
html += '<div class="geo-country-option" onmousedown="addAppGeoCountry(\'' + c.code + '\')">'
+ esc(c.name) + ' <small>(' + c.code + ')</small></div>';
count++;
}
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">Nincs találat</div>';
list.style.display = (count > 0 || q) ? '' : 'none';
};
window.addAppGeoCountry = function(code) {
if (appGeoCountries.indexOf(code) >= 0) return;
appGeoCountries.push(code);
renderAppGeoTags();
document.getElementById('app-geo-search').value = '';
hideAppGeoList();
};
window.removeAppGeoCountry = function(code) {
appGeoCountries = appGeoCountries.filter(function(c){return c !== code});
renderAppGeoTags();
};
function renderAppGeoTags() {
var el = document.getElementById('app-geo-tags');
var html = '';
for (var i = 0; i < appGeoCountries.length; i++) {
var code = appGeoCountries[i];
var name = code;
for (var j = 0; j < allCountries.length; j++) {
if (allCountries[j].code === code) { name = allCountries[j].name; break; }
}
html += '<span class="geo-tag">' + esc(name) + ' (' + code + ') '
+ '<span class="geo-tag-remove" onclick="removeAppGeoCountry(\'' + code + '\')">&times;</span></span>';
}
el.innerHTML = html;
}
window.saveAppGeoOverride = function() {
var status = document.getElementById('app-geo-status');
fetch('/api/stacks/' + stackName + '/geo/override', {
method: 'POST',
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
body: JSON.stringify({allowed_countries: appGeoCountries})
})
.then(function(r){return r.json()})
.then(function(d){
status.textContent = d.ok ? (d.message || 'Mentve') : (d.error || 'Hiba');
status.className = 'config-save-status ' + (d.ok ? 'config-save-ok' : 'config-save-err');
setTimeout(function(){ status.textContent=''; }, 5000);
})
.catch(function(){
status.textContent = 'Hálózati hiba';
status.className = 'config-save-status config-save-err';
});
};
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
if (document.getElementById('app-geo-override-toggle') && document.getElementById('app-geo-override-toggle').checked) {
loadCountries(renderAppGeoTags);
}
})();
</script>
<div class="app-optional-config">
<h3>Opcionális beállítások</h3>
<div class="config-group">
<h4>Külső szolgáltatások</h4>
<p class="config-group-desc">Opcionális kulcsok leírása</p>
<div class="config-fields">
<div class="config-field">
<label for="opt-API_KEY">API kulcs mező</label>
<p class="config-field-help">Ide jön a kulcs</p>
<p class="config-field-help"><a href="https://example.org/reg" target="_blank">Regisztrációs útmutató ↗</a></p>
<div class="input-with-button">
<input type="password" id="opt-API_KEY" name="API_KEY" class="config-input"
value="" placeholder="••••••••••••"
data-secret-set="1" autocomplete="off">
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('privatebin','API_KEY', this, 'opt-API_KEY')">Megjelenítés</button>
</div>
</div>
<div class="config-field">
<label for="opt-OTHER">Másik mező itt</label>
<input type="text"
id="opt-OTHER"
name="OTHER"
class="config-input"
value=""
placeholder="Másik mező itt"
autocomplete="off">
</div>
</div>
</div>
<div class="config-actions">
<button class="btn btn-primary" id="save-optional-config" onclick="saveOptionalConfig('privatebin')">
Mentés
</button>
<span id="config-save-status" class="config-save-status"></span>
</div>
</div>
<script>
async function saveOptionalConfig(stackName) {
var btn = document.getElementById('save-optional-config');
var status = document.getElementById('config-save-status');
var inputs = document.querySelectorAll('.config-input');
var values = {};
inputs.forEach(function(input) {
if (input.dataset.secretSet === '1' && input.dataset.revealed !== '1' && input.value === '') return;
values[input.name] = input.value;
});
btn.disabled = true;
btn.textContent = 'Mentés...';
status.textContent = '';
status.className = 'config-save-status';
try {
var resp = await fetch('/api/stacks/' + stackName + '/optional-config', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({values: values})
});
var data = await resp.json();
if (data.ok) {
status.textContent = (data.message || 'Mentve');
status.className = 'config-save-status config-save-ok';
} else {
status.textContent = (data.error || 'Hiba');
status.className = 'config-save-status config-save-err';
}
} catch(err) {
status.textContent = 'Hálózati hiba';
status.className = 'config-save-status config-save-err';
}
btn.disabled = false;
btn.textContent = 'Mentés';
setTimeout(function() { status.textContent = ''; }, 5000);
}
</script>
<div class="app-optional-config">
<h3>Email-küldés</h3>
<p class="config-group-desc">
Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,
külön szolgáltató beállítása nélkül. A feladó címe: <strong>noreply@example.hu</strong>.
</p>
<p class="alert alert-warning" style="margin-bottom:1rem">
Az alkalmazás-email jelenleg ki van kapcsolva globálisan. Kapcsold be a
<a href="/settings">Beállítások</a> oldalon, hogy itt is működjön.
</p>
<form method="POST" action="/stacks/privatebin/app-email">
<label style="display:flex;align-items:center;gap:.5rem;margin-bottom:1rem">
<input type="checkbox" name="app_email_enabled" value="on" checked>
Email-küldés engedélyezése ennél az alkalmazásnál
</label>
<div class="config-actions">
<button class="btn btn-primary" type="submit">Mentés</button>
<span class="config-group-desc" style="margin-left:.5rem">A mentés újraindítja az alkalmazást.</span>
</div>
</form>
</div>
<div class="memory-summary">
@@ -644,6 +256,8 @@
</div>
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
@@ -865,10 +479,11 @@
</div>
<div class="deploy-actions">
<button type="submit" class="btn btn-primary btn-lg">Telepítés indítása</button>
<a href="/stacks" class="btn btn-outline">Mégsem</a>
<a href="/apps/privatebin" class="btn btn-outline" data-install-cancel="1">Mégsem</a>
</div>
</form>
@@ -1009,7 +624,7 @@ function buildPostDeployCard(stackName) {
html += '<div style="display:flex;gap:.75rem;margin-top:1.5rem;flex-wrap:wrap">' +
'<a href="/stacks/' + stackName + '/deploy" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/apps/privatebin/settings" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>' +
'</div>';
@@ -1032,56 +647,6 @@ document.addEventListener('DOMContentLoaded', function() {
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function toggleAutoField(fieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
@@ -1148,53 +713,6 @@ function copyPostDeployCred(btn, val) {
}
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
document.getElementById('deploy-form').addEventListener('submit', async function(e) {
e.preventDefault();
@@ -1344,7 +862,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A telepítés nem sikerült: ' + deployError +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
'</div><a href="/apps/privatebin/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
} else if (state === 'deploying') {
@@ -1379,7 +897,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A konténer leállt. Ellenőrizd a naplókat.' +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
'</div><a href="/apps/privatebin/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
}
@@ -172,17 +172,19 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<a href="/apps/privatebin" class="btn btn-sm btn-outline" data-install-back="1">← Vissza</a>
<h2>Private Bin — Telepítés</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/apps/privatebin" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-info"/></svg> Részletek</a>
</div>
</div>
<div class="deploy-container">
<div class="flash flash-success">A mentés sikeresen elkészült.</div>
<div class="flash flash-error">A művelet most nem sikerült.</div>
@@ -194,7 +196,6 @@
<p>Egy teszt alkalmazás leírása</p>
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
</div>
@@ -205,390 +206,6 @@
</div>
<div class="app-optional-config">
<h3>Integrációk</h3>
<p class="config-group-desc">
Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor.
</p>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció egy</strong>
<p class="config-field-help" style="margin:0">Első integráció leírása</p>
<span class="tag tag-off" style="margin-top:.25rem"><span class="dot"></span>Nincs telepítve</span>
</div>
<label class="toggle">
<input type="checkbox"
disabled title="A célalkalmazás nincs telepítve"
onchange="toggleIntegration('privatebin', 'a', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció kettő</strong>
<p class="config-field-help" style="margin:0">Második integráció leírása</p>
<span class="tag tag-neutral" style="margin-top:.25rem"><span class="dot"></span>Célalkalmazás leállítva</span>
</div>
<label class="toggle">
<input type="checkbox"
disabled title="A célalkalmazás nem fut"
onchange="toggleIntegration('privatebin', 'b', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció három</strong>
<p class="config-field-help" style="margin:0">Harmadik integráció leírása</p>
<span class="tag tag-crit" style="margin-top:.25rem"><span class="dot"></span>Hiba</span>
</div>
<label class="toggle">
<input type="checkbox"
onchange="toggleIntegration('privatebin', 'c', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció négy</strong>
<p class="config-field-help" style="margin:0">Negyedik integráció leírása</p>
<span class="tag tag-run" style="margin-top:.25rem"><span class="dot"></span>Aktív</span>
</div>
<label class="toggle">
<input type="checkbox"
checked
onchange="toggleIntegration('privatebin', 'd', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció öt</strong>
<p class="config-field-help" style="margin:0">Ötödik integráció leírása</p>
</div>
<label class="toggle">
<input type="checkbox"
onchange="toggleIntegration('privatebin', 'e', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
</div>
<script>
async function toggleIntegration(provider, target, enable, checkbox) {
checkbox.disabled = true;
try {
var resp = await fetch('/api/integrations/' + provider + '/' + target, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({enabled: enable})
});
var data = await resp.json();
if (!data.ok) {
checkbox.checked = !enable;
showAlert(data.error || 'Hiba történt');
} else {
setTimeout(function(){ location.reload(); }, 500);
return;
}
} catch(err) {
checkbox.checked = !enable;
showAlert('Hálózati hiba');
}
checkbox.disabled = false;
}
</script>
<div class="app-optional-config">
<h3>Földrajzi korlátozás</h3>
<p class="config-group-desc">
Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi.
</p>
<label class="toggle" style="margin-bottom:1rem">
<input type="checkbox" id="app-geo-override-toggle"
onchange="toggleAppGeoOverride(this.checked)">
<span class="toggle-label">Egyéni országkorlátozás</span>
</label>
<div id="app-geo-override-details" style="display:none">
<div class="form-group">
<label>Engedélyezett országok ehhez az alkalmazáshoz</label>
<div class="geo-country-selector">
<input type="text" id="app-geo-search" class="form-control config-input"
placeholder="Ország keresése..." autocomplete="off"
oninput="filterAppGeoCountries(this.value)"
onfocus="showAppGeoList()"
onblur="setTimeout(function(){hideAppGeoList()},200)">
<div class="geo-country-list" id="app-geo-country-list"></div>
</div>
<div class="geo-selected-tags" id="app-geo-tags"></div>
</div>
<div class="config-actions">
<button class="btn btn-primary" onclick="saveAppGeoOverride()">Mentés</button>
<span id="app-geo-status" class="config-save-status"></span>
</div>
</div>
</div>
<script>
(function(){
var allCountries = [];
var appGeoCountries = ["HU","AT"];
var stackName = 'privatebin';
function loadCountries(cb) {
if (allCountries.length > 0) { cb(); return; }
fetch('/api/geo/countries', {headers: csrfHeaders()})
.then(function(r){return r.json()})
.then(function(d){ if(d.ok) allCountries = d.data; cb(); })
.catch(function(){ cb(); });
}
window.toggleAppGeoOverride = function(enabled) {
document.getElementById('app-geo-override-details').style.display = enabled ? '' : 'none';
if (enabled) {
if (!appGeoCountries || appGeoCountries.length === 0) {
appGeoCountries = ["HU"];
}
loadCountries(renderAppGeoTags);
} else {
fetch('/api/stacks/' + stackName + '/geo/override', {method:'DELETE', headers:csrfHeaders()});
}
};
window.showAppGeoList = function() { loadCountries(function(){ filterAppGeoCountries(''); }); };
window.hideAppGeoList = function() { document.getElementById('app-geo-country-list').style.display='none'; };
window.filterAppGeoCountries = function(q) {
var list = document.getElementById('app-geo-country-list');
q = q.toLowerCase();
var html = ''; var count = 0;
for (var i = 0; i < allCountries.length && count < 15; i++) {
var c = allCountries[i];
if (appGeoCountries.indexOf(c.code) >= 0) continue;
if (q && c.name.toLowerCase().indexOf(q) < 0 && c.code.toLowerCase().indexOf(q) < 0) continue;
html += '<div class="geo-country-option" onmousedown="addAppGeoCountry(\'' + c.code + '\')">'
+ esc(c.name) + ' <small>(' + c.code + ')</small></div>';
count++;
}
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">Nincs találat</div>';
list.style.display = (count > 0 || q) ? '' : 'none';
};
window.addAppGeoCountry = function(code) {
if (appGeoCountries.indexOf(code) >= 0) return;
appGeoCountries.push(code);
renderAppGeoTags();
document.getElementById('app-geo-search').value = '';
hideAppGeoList();
};
window.removeAppGeoCountry = function(code) {
appGeoCountries = appGeoCountries.filter(function(c){return c !== code});
renderAppGeoTags();
};
function renderAppGeoTags() {
var el = document.getElementById('app-geo-tags');
var html = '';
for (var i = 0; i < appGeoCountries.length; i++) {
var code = appGeoCountries[i];
var name = code;
for (var j = 0; j < allCountries.length; j++) {
if (allCountries[j].code === code) { name = allCountries[j].name; break; }
}
html += '<span class="geo-tag">' + esc(name) + ' (' + code + ') '
+ '<span class="geo-tag-remove" onclick="removeAppGeoCountry(\'' + code + '\')">&times;</span></span>';
}
el.innerHTML = html;
}
window.saveAppGeoOverride = function() {
var status = document.getElementById('app-geo-status');
fetch('/api/stacks/' + stackName + '/geo/override', {
method: 'POST',
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
body: JSON.stringify({allowed_countries: appGeoCountries})
})
.then(function(r){return r.json()})
.then(function(d){
status.textContent = d.ok ? (d.message || 'Mentve') : (d.error || 'Hiba');
status.className = 'config-save-status ' + (d.ok ? 'config-save-ok' : 'config-save-err');
setTimeout(function(){ status.textContent=''; }, 5000);
})
.catch(function(){
status.textContent = 'Hálózati hiba';
status.className = 'config-save-status config-save-err';
});
};
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
if (document.getElementById('app-geo-override-toggle') && document.getElementById('app-geo-override-toggle').checked) {
loadCountries(renderAppGeoTags);
}
})();
</script>
<div class="app-optional-config">
<h3>Opcionális beállítások</h3>
<div class="config-group">
<h4>Külső szolgáltatások</h4>
<p class="config-group-desc">Opcionális kulcsok leírása</p>
<div class="config-fields">
<div class="config-field">
<label for="opt-API_KEY">API kulcs mező</label>
<p class="config-field-help">Ide jön a kulcs</p>
<p class="config-field-help"><a href="https://example.org/reg" target="_blank">Regisztrációs útmutató ↗</a></p>
<div class="input-with-button">
<input type="password" id="opt-API_KEY" name="API_KEY" class="config-input"
value="" placeholder="••••••••••••"
data-secret-set="1" autocomplete="off">
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('privatebin','API_KEY', this, 'opt-API_KEY')">Megjelenítés</button>
</div>
</div>
<div class="config-field">
<label for="opt-OTHER">Másik mező itt</label>
<input type="text"
id="opt-OTHER"
name="OTHER"
class="config-input"
value=""
placeholder="Másik mező itt"
autocomplete="off">
</div>
</div>
</div>
<div class="config-actions">
<button class="btn btn-primary" id="save-optional-config" onclick="saveOptionalConfig('privatebin')">
Mentés
</button>
<span id="config-save-status" class="config-save-status"></span>
</div>
</div>
<script>
async function saveOptionalConfig(stackName) {
var btn = document.getElementById('save-optional-config');
var status = document.getElementById('config-save-status');
var inputs = document.querySelectorAll('.config-input');
var values = {};
inputs.forEach(function(input) {
if (input.dataset.secretSet === '1' && input.dataset.revealed !== '1' && input.value === '') return;
values[input.name] = input.value;
});
btn.disabled = true;
btn.textContent = 'Mentés...';
status.textContent = '';
status.className = 'config-save-status';
try {
var resp = await fetch('/api/stacks/' + stackName + '/optional-config', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({values: values})
});
var data = await resp.json();
if (data.ok) {
status.textContent = (data.message || 'Mentve');
status.className = 'config-save-status config-save-ok';
} else {
status.textContent = (data.error || 'Hiba');
status.className = 'config-save-status config-save-err';
}
} catch(err) {
status.textContent = 'Hálózati hiba';
status.className = 'config-save-status config-save-err';
}
btn.disabled = false;
btn.textContent = 'Mentés';
setTimeout(function() { status.textContent = ''; }, 5000);
}
</script>
<div class="app-optional-config">
<h3>Email-küldés</h3>
<p class="config-group-desc">
Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,
külön szolgáltató beállítása nélkül. A feladó címe: <strong>noreply@example.hu</strong>.
</p>
<form method="POST" action="/stacks/privatebin/app-email">
<label style="display:flex;align-items:center;gap:.5rem;margin-bottom:1rem">
<input type="checkbox" name="app_email_enabled" value="on" >
Email-küldés engedélyezése ennél az alkalmazásnál
</label>
<div class="config-actions">
<button class="btn btn-primary" type="submit">Mentés</button>
<span class="config-group-desc" style="margin-left:.5rem">A mentés újraindítja az alkalmazást.</span>
</div>
</form>
</div>
<div class="memory-summary memory-blocked">
@@ -621,6 +238,8 @@
A telepítés ezért átmenetileg le van tiltva — szabadíts fel helyet, vagy bővítsd a tárhelyet.
</div>
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
@@ -814,10 +433,11 @@
</div>
<div class="deploy-actions">
<button type="submit" class="btn btn-primary btn-lg" disabled title="Nincs elég szabad tárhely">Telepítés indítása</button>
<a href="/stacks" class="btn btn-outline">Mégsem</a>
<a href="/apps/privatebin" class="btn btn-outline" data-install-cancel="1">Mégsem</a>
</div>
</form>
@@ -958,7 +578,7 @@ function buildPostDeployCard(stackName) {
html += '<div style="display:flex;gap:.75rem;margin-top:1.5rem;flex-wrap:wrap">' +
'<a href="/stacks/' + stackName + '/deploy" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/apps/privatebin/settings" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>' +
'</div>';
@@ -981,56 +601,6 @@ document.addEventListener('DOMContentLoaded', function() {
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function toggleAutoField(fieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
@@ -1097,53 +667,6 @@ function copyPostDeployCred(btn, val) {
}
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
document.getElementById('deploy-form').addEventListener('submit', async function(e) {
e.preventDefault();
@@ -1293,7 +816,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A telepítés nem sikerült: ' + deployError +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
'</div><a href="/apps/privatebin/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
} else if (state === 'deploying') {
@@ -1328,7 +851,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A konténer leállt. Ellenőrizd a naplókat.' +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
'</div><a href="/apps/privatebin/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
}
@@ -172,17 +172,19 @@
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
<a href="/apps/privatebin" class="btn btn-sm btn-outline" data-install-back="1">← Vissza</a>
<h2>Private Bin — Telepítés</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/apps/privatebin" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-info"/></svg> Részletek</a>
</div>
</div>
<div class="deploy-container">
<div class="flash flash-success">A mentés sikeresen elkészült.</div>
<div class="flash flash-error">A művelet most nem sikerült.</div>
@@ -194,7 +196,6 @@
<p>Egy teszt alkalmazás leírása</p>
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
</div>
@@ -205,395 +206,6 @@
</div>
<div class="app-optional-config">
<h3>Integrációk</h3>
<p class="config-group-desc">
Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor.
</p>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció egy</strong>
<p class="config-field-help" style="margin:0">Első integráció leírása</p>
<span class="tag tag-off" style="margin-top:.25rem"><span class="dot"></span>Nincs telepítve</span>
</div>
<label class="toggle">
<input type="checkbox"
disabled title="A célalkalmazás nincs telepítve"
onchange="toggleIntegration('privatebin', 'a', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció kettő</strong>
<p class="config-field-help" style="margin:0">Második integráció leírása</p>
<span class="tag tag-neutral" style="margin-top:.25rem"><span class="dot"></span>Célalkalmazás leállítva</span>
</div>
<label class="toggle">
<input type="checkbox"
disabled title="A célalkalmazás nem fut"
onchange="toggleIntegration('privatebin', 'b', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció három</strong>
<p class="config-field-help" style="margin:0">Harmadik integráció leírása</p>
<span class="tag tag-crit" style="margin-top:.25rem"><span class="dot"></span>Hiba</span>
</div>
<label class="toggle">
<input type="checkbox"
onchange="toggleIntegration('privatebin', 'c', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció négy</strong>
<p class="config-field-help" style="margin:0">Negyedik integráció leírása</p>
<span class="tag tag-run" style="margin-top:.25rem"><span class="dot"></span>Aktív</span>
</div>
<label class="toggle">
<input type="checkbox"
checked
onchange="toggleIntegration('privatebin', 'd', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>Integráció öt</strong>
<p class="config-field-help" style="margin:0">Ötödik integráció leírása</p>
</div>
<label class="toggle">
<input type="checkbox"
onchange="toggleIntegration('privatebin', 'e', this.checked, this)">
<span class="toggle-label"></span>
</label>
</div>
</div>
<script>
async function toggleIntegration(provider, target, enable, checkbox) {
checkbox.disabled = true;
try {
var resp = await fetch('/api/integrations/' + provider + '/' + target, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({enabled: enable})
});
var data = await resp.json();
if (!data.ok) {
checkbox.checked = !enable;
showAlert(data.error || 'Hiba történt');
} else {
setTimeout(function(){ location.reload(); }, 500);
return;
}
} catch(err) {
checkbox.checked = !enable;
showAlert('Hálózati hiba');
}
checkbox.disabled = false;
}
</script>
<div class="app-optional-config">
<h3>Földrajzi korlátozás</h3>
<p class="config-group-desc">
Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi.
</p>
<label class="toggle" style="margin-bottom:1rem">
<input type="checkbox" id="app-geo-override-toggle"
onchange="toggleAppGeoOverride(this.checked)">
<span class="toggle-label">Egyéni országkorlátozás</span>
</label>
<div id="app-geo-override-details" style="display:none">
<div class="form-group">
<label>Engedélyezett országok ehhez az alkalmazáshoz</label>
<div class="geo-country-selector">
<input type="text" id="app-geo-search" class="form-control config-input"
placeholder="Ország keresése..." autocomplete="off"
oninput="filterAppGeoCountries(this.value)"
onfocus="showAppGeoList()"
onblur="setTimeout(function(){hideAppGeoList()},200)">
<div class="geo-country-list" id="app-geo-country-list"></div>
</div>
<div class="geo-selected-tags" id="app-geo-tags"></div>
</div>
<div class="config-actions">
<button class="btn btn-primary" onclick="saveAppGeoOverride()">Mentés</button>
<span id="app-geo-status" class="config-save-status"></span>
</div>
</div>
</div>
<script>
(function(){
var allCountries = [];
var appGeoCountries = ["HU","AT"];
var stackName = 'privatebin';
function loadCountries(cb) {
if (allCountries.length > 0) { cb(); return; }
fetch('/api/geo/countries', {headers: csrfHeaders()})
.then(function(r){return r.json()})
.then(function(d){ if(d.ok) allCountries = d.data; cb(); })
.catch(function(){ cb(); });
}
window.toggleAppGeoOverride = function(enabled) {
document.getElementById('app-geo-override-details').style.display = enabled ? '' : 'none';
if (enabled) {
if (!appGeoCountries || appGeoCountries.length === 0) {
appGeoCountries = ["HU"];
}
loadCountries(renderAppGeoTags);
} else {
fetch('/api/stacks/' + stackName + '/geo/override', {method:'DELETE', headers:csrfHeaders()});
}
};
window.showAppGeoList = function() { loadCountries(function(){ filterAppGeoCountries(''); }); };
window.hideAppGeoList = function() { document.getElementById('app-geo-country-list').style.display='none'; };
window.filterAppGeoCountries = function(q) {
var list = document.getElementById('app-geo-country-list');
q = q.toLowerCase();
var html = ''; var count = 0;
for (var i = 0; i < allCountries.length && count < 15; i++) {
var c = allCountries[i];
if (appGeoCountries.indexOf(c.code) >= 0) continue;
if (q && c.name.toLowerCase().indexOf(q) < 0 && c.code.toLowerCase().indexOf(q) < 0) continue;
html += '<div class="geo-country-option" onmousedown="addAppGeoCountry(\'' + c.code + '\')">'
+ esc(c.name) + ' <small>(' + c.code + ')</small></div>';
count++;
}
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">Nincs találat</div>';
list.style.display = (count > 0 || q) ? '' : 'none';
};
window.addAppGeoCountry = function(code) {
if (appGeoCountries.indexOf(code) >= 0) return;
appGeoCountries.push(code);
renderAppGeoTags();
document.getElementById('app-geo-search').value = '';
hideAppGeoList();
};
window.removeAppGeoCountry = function(code) {
appGeoCountries = appGeoCountries.filter(function(c){return c !== code});
renderAppGeoTags();
};
function renderAppGeoTags() {
var el = document.getElementById('app-geo-tags');
var html = '';
for (var i = 0; i < appGeoCountries.length; i++) {
var code = appGeoCountries[i];
var name = code;
for (var j = 0; j < allCountries.length; j++) {
if (allCountries[j].code === code) { name = allCountries[j].name; break; }
}
html += '<span class="geo-tag">' + esc(name) + ' (' + code + ') '
+ '<span class="geo-tag-remove" onclick="removeAppGeoCountry(\'' + code + '\')">&times;</span></span>';
}
el.innerHTML = html;
}
window.saveAppGeoOverride = function() {
var status = document.getElementById('app-geo-status');
fetch('/api/stacks/' + stackName + '/geo/override', {
method: 'POST',
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
body: JSON.stringify({allowed_countries: appGeoCountries})
})
.then(function(r){return r.json()})
.then(function(d){
status.textContent = d.ok ? (d.message || 'Mentve') : (d.error || 'Hiba');
status.className = 'config-save-status ' + (d.ok ? 'config-save-ok' : 'config-save-err');
setTimeout(function(){ status.textContent=''; }, 5000);
})
.catch(function(){
status.textContent = 'Hálózati hiba';
status.className = 'config-save-status config-save-err';
});
};
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
if (document.getElementById('app-geo-override-toggle') && document.getElementById('app-geo-override-toggle').checked) {
loadCountries(renderAppGeoTags);
}
})();
</script>
<div class="app-optional-config">
<h3>Opcionális beállítások</h3>
<div class="config-group">
<h4>Külső szolgáltatások</h4>
<p class="config-group-desc">Opcionális kulcsok leírása</p>
<div class="config-fields">
<div class="config-field">
<label for="opt-API_KEY">API kulcs mező</label>
<p class="config-field-help">Ide jön a kulcs</p>
<p class="config-field-help"><a href="https://example.org/reg" target="_blank">Regisztrációs útmutató ↗</a></p>
<div class="input-with-button">
<input type="password" id="opt-API_KEY" name="API_KEY" class="config-input"
value="" placeholder="••••••••••••"
data-secret-set="1" autocomplete="off">
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('privatebin','API_KEY', this, 'opt-API_KEY')">Megjelenítés</button>
</div>
</div>
<div class="config-field">
<label for="opt-OTHER">Másik mező itt</label>
<input type="text"
id="opt-OTHER"
name="OTHER"
class="config-input"
value=""
placeholder="Másik mező itt"
autocomplete="off">
</div>
</div>
</div>
<div class="config-actions">
<button class="btn btn-primary" id="save-optional-config" onclick="saveOptionalConfig('privatebin')">
Mentés
</button>
<span id="config-save-status" class="config-save-status"></span>
</div>
</div>
<script>
async function saveOptionalConfig(stackName) {
var btn = document.getElementById('save-optional-config');
var status = document.getElementById('config-save-status');
var inputs = document.querySelectorAll('.config-input');
var values = {};
inputs.forEach(function(input) {
if (input.dataset.secretSet === '1' && input.dataset.revealed !== '1' && input.value === '') return;
values[input.name] = input.value;
});
btn.disabled = true;
btn.textContent = 'Mentés...';
status.textContent = '';
status.className = 'config-save-status';
try {
var resp = await fetch('/api/stacks/' + stackName + '/optional-config', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({values: values})
});
var data = await resp.json();
if (data.ok) {
status.textContent = (data.message || 'Mentve');
status.className = 'config-save-status config-save-ok';
} else {
status.textContent = (data.error || 'Hiba');
status.className = 'config-save-status config-save-err';
}
} catch(err) {
status.textContent = 'Hálózati hiba';
status.className = 'config-save-status config-save-err';
}
btn.disabled = false;
btn.textContent = 'Mentés';
setTimeout(function() { status.textContent = ''; }, 5000);
}
</script>
<div class="app-optional-config">
<h3>Email-küldés</h3>
<p class="config-group-desc">
Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,
külön szolgáltató beállítása nélkül. A feladó címe: <strong>noreply@example.hu</strong>.
</p>
<p class="alert alert-warning" style="margin-bottom:1rem">
Az alkalmazás-email jelenleg ki van kapcsolva globálisan. Kapcsold be a
<a href="/settings">Beállítások</a> oldalon, hogy itt is működjön.
</p>
<form method="POST" action="/stacks/privatebin/app-email">
<label style="display:flex;align-items:center;gap:.5rem;margin-bottom:1rem">
<input type="checkbox" name="app_email_enabled" value="on" >
Email-küldés engedélyezése ennél az alkalmazásnál
</label>
<div class="config-actions">
<button class="btn btn-primary" type="submit">Mentés</button>
<span class="config-group-desc" style="margin-left:.5rem">A mentés újraindítja az alkalmazást.</span>
</div>
</form>
</div>
<div class="memory-summary memory-blocked">
@@ -621,6 +233,8 @@
</div>
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
@@ -844,10 +458,11 @@
</div>
<div class="deploy-actions">
<button type="submit" class="btn btn-primary btn-lg" disabled title="Nincs elég memória">Telepítés indítása</button>
<a href="/stacks" class="btn btn-outline">Mégsem</a>
<a href="/apps/privatebin" class="btn btn-outline" data-install-cancel="1">Mégsem</a>
</div>
</form>
@@ -988,7 +603,7 @@ function buildPostDeployCard(stackName) {
html += '<div style="display:flex;gap:.75rem;margin-top:1.5rem;flex-wrap:wrap">' +
'<a href="/stacks/' + stackName + '/deploy" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/apps/privatebin/settings" class="btn btn-outline">Beállítások és jelszavak megtekintése</a>' +
'<a href="/stacks" class="btn btn-sm btn-outline">← Alkalmazások</a>' +
'</div>';
@@ -1011,56 +626,6 @@ document.addEventListener('DOMContentLoaded', function() {
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = 'Megjelenítés';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
btn.dataset.shown = '1';
btn.textContent = 'Elrejtés';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function toggleAutoField(fieldId, btn) {
var el = document.getElementById(fieldId);
if (!el) return;
@@ -1127,53 +692,6 @@ function copyPostDeployCred(btn, val) {
}
}
function deleteStaleData(stackName, stalePath, btn) {
felhomConfirm(btn, 'Biztosan törölni szeretnéd a korábbi adatokat? Tárhely: ' + stalePath + ' — FIGYELEM: a művelet visszavonhatatlan! Előtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.', function () {
felhomConfirm(btn, 'UTOLSÓ FIGYELMEZTETÉS! A törlés visszavonhatatlan. Biztosan folytatod?', function () {
deleteStaleDataRun(stackName, stalePath, btn);
});
});
}
function deleteStaleDataRun(stackName, stalePath, btn) {
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
return;
}
var msg = 'Korábbi adatok törölve!\n\nFelszabadított hely: ' + (data.freed_human || '?');
if (data.errors && data.errors.length > 0) {
msg += '\n\nNéhány hiba történt:\n' + data.errors.join('\n');
}
showAlert(msg);
var item = btn.closest('.stale-data-item');
if (item) item.remove();
var container = document.querySelector('.deploy-stale-data');
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
container.remove();
}
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Korábbi adatok törlése';
});
}
document.getElementById('deploy-form').addEventListener('submit', async function(e) {
e.preventDefault();
@@ -1323,7 +841,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A telepítés nem sikerült: ' + deployError +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
'</div><a href="/apps/privatebin/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
} else if (state === 'deploying') {
@@ -1358,7 +876,7 @@ document.getElementById('deploy-form').addEventListener('submit', async function
progressEl.querySelector('h3').textContent = 'Telepítés sikertelen';
resultEl.innerHTML = '<div class="alert alert-error" style="margin-top:1rem">' +
'A konténer leállt. Ellenőrizd a naplókat.' +
'</div><a href="/stacks/' + stackName + '/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
'</div><a href="/apps/privatebin/logs" class="btn btn-outline" style="margin-top:.75rem">Naplók megtekintése</a>' +
' <a href="/stacks" class="btn btn-primary" style="margin-top:.75rem">Alkalmazások</a>';
resultEl.style.display = 'block';
}
@@ -223,7 +223,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
@@ -273,7 +272,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
@@ -288,7 +286,7 @@
<div class="alert alert-error" data-held="true">A frissítés megállt, a mentés hiányzik. <a href="/backups/apps" class="btn btn-sm btn-outline">Mentések</a></div>
<button class="btn btn-danger" onclick="removeStack('kimai')">Eltávolítás</button>
<a href="/stacks/kimai/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/kimai/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/kimai" class="btn btn-outline">Részletek</a>
@@ -324,7 +322,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
@@ -338,7 +335,7 @@
<span class="tag tag-progress" data-update-phase="pull"><span class="dot"></span>Letöltés folyamatban…</span>
<a href="/stacks/romm/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/romm/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/romm" class="btn btn-outline">Részletek</a>
@@ -374,7 +371,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
@@ -400,7 +396,7 @@
<button class="btn btn-warning" onclick="stackAction(event, 'gokapi', 'restart')">Újraindítás</button>
<button class="btn btn-danger" onclick="stackAction(event, 'gokapi', 'stop')">Leállítás</button>
<a href="/stacks/gokapi/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/gokapi/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/gokapi" class="btn btn-outline">Részletek</a>
@@ -436,7 +432,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
@@ -452,7 +447,7 @@
<button class="btn btn-success" onclick="stackAction(event, 'bookstack', 'start')">Indítás</button>
<button class="btn btn-danger" onclick="removeStack('bookstack')">Eltávolítás</button>
<a href="/stacks/bookstack/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/bookstack/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/bookstack" class="btn btn-outline">Részletek</a>
@@ -488,7 +483,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
@@ -504,7 +498,7 @@
<button class="btn btn-success" onclick="stackAction(event, 'oldapp', 'start')">Indítás</button>
<a href="/stacks/oldapp/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/oldapp/logs" class="btn btn-outline">Naplók</a>
<button class="btn btn-danger" onclick="deleteOrphanStack('oldapp')">Törlés</button>
@@ -547,7 +541,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
<span class="metarow" title="Adattároló: HDD egy"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD egy</span>
@@ -564,7 +557,7 @@
<button class="btn btn-warning" onclick="stackAction(event, 'privatebin', 'restart')">Újraindítás</button>
<button class="btn btn-danger" onclick="stackAction(event, 'privatebin', 'stop')">Leállítás</button>
<a href="/stacks/privatebin/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/privatebin/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/privatebin" class="btn btn-outline">Részletek</a>
@@ -600,7 +593,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
@@ -611,7 +603,7 @@
<div class="stack-detail-actions">
<a href="/stacks/immich/deploy" class="btn btn-primary" onclick="return checkBeforeDeploy(event, 'immich')">Telepítés</a>
<a href="/apps/immich/install" class="btn btn-primary" onclick="return checkBeforeDeploy(event, 'immich')">Telepítés</a>
<a href="/apps/immich" class="btn btn-outline">Részletek</a>
</div>
@@ -218,7 +218,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
@@ -229,7 +228,7 @@
<div class="stack-detail-actions">
<div class="alert alert-warning" data-install-interrupted="true">A telepítés egy újraindítás miatt félbemaradt, és a doboz eltávolította, amit elkezdett. Nyomd meg újra a Telepítés gombot.</div><a href="/stacks/n8n/deploy" class="btn btn-primary" onclick="return checkBeforeDeploy(event, 'n8n')">Telepítés</a>
<div class="alert alert-warning" data-install-interrupted="true">A telepítés egy újraindítás miatt félbemaradt, és a doboz eltávolította, amit elkezdett. Nyomd meg újra a Telepítés gombot.</div><a href="/apps/n8n/install" class="btn btn-primary" onclick="return checkBeforeDeploy(event, 'n8n')">Telepítés</a>
<a href="/apps/n8n" class="btn btn-outline">Részletek</a>
</div>
@@ -218,7 +218,6 @@
<div class="metarows">
<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~128M</span></span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>
<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>
<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>
@@ -233,7 +232,7 @@
<div class="alert alert-warning" data-unhealthy-stop="true">Az alkalmazást leállítottuk.</div>
<button class="btn btn-success" onclick="stackAction(event, 'gokapi', 'start')">Indítás</button>
<a href="/stacks/gokapi/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/gokapi/logs" class="btn btn-outline">Naplók</a>
<a href="/apps/gokapi" class="btn btn-outline">Részletek</a>
@@ -1,553 +0,0 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Második mentés beállítása — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group is-open">
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="active">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<h2>2. mentés beállítása — Private Bin</h2>
<a href="/backups/apps" class="btn btn-outline btn-sm">← Vissza a mentésekhez</a>
</div>
<div class="monitoring-banner monitoring-banner-green">A beállítás elmentve rendben.</div>
<div class="monitoring-banner monitoring-banner-red">A mentés nem sikerült most.</div>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
A 2. mentés egy <strong>másik fizikai meghajtóra</strong> készít másolatot az alkalmazás
helyreállítási csomagjáról és adatairól. Ez az egyetlen off-drive védelem a böngészhető
felhasználói fájlokhoz (a teljes rendszermentés/PBS nem éri el ezeket).
</p>
<div class="monitoring-banner monitoring-banner-yellow" style="margin-top:1rem" data-system-backup="unknown">
Ennek az alkalmazásnak az adatai a belső rendszerlemezen vannak, és a teljes rendszermentés része.
Hogy a rendszermentés most hová készül, azt nem tudtuk lekérdezni — a Mentések oldalon láthatod.
</div>
</div>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -1,553 +0,0 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Második mentés beállítása — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group is-open">
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="active">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<h2>2. mentés beállítása — Private Bin</h2>
<a href="/backups/apps" class="btn btn-outline btn-sm">← Vissza a mentésekhez</a>
</div>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
A 2. mentés egy <strong>másik fizikai meghajtóra</strong> készít másolatot az alkalmazás
helyreállítási csomagjáról és adatairól. Ez az egyetlen off-drive védelem a böngészhető
felhasználói fájlokhoz (a teljes rendszermentés/PBS nem éri el ezeket).
</p>
<div class="monitoring-banner monitoring-banner-red" style="margin-top:1rem" data-system-backup="absent">
Ennek az alkalmazásnak az adatai a belső rendszerlemezen vannak. A teljes rendszermentés meghajtója
<strong>most nem érhető el</strong> — amíg vissza nem csatlakoztatod, erről az alkalmazásról sem készül friss rendszermentés.
</div>
</div>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -1,555 +0,0 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Második mentés beállítása — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group is-open">
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="active">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<h2>2. mentés beállítása — Private Bin</h2>
<a href="/backups/apps" class="btn btn-outline btn-sm">← Vissza a mentésekhez</a>
</div>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
A 2. mentés egy <strong>másik fizikai meghajtóra</strong> készít másolatot az alkalmazás
helyreállítási csomagjáról és adatairól. Ez az egyetlen off-drive védelem a böngészhető
felhasználói fájlokhoz (a teljes rendszermentés/PBS nem éri el ezeket).
</p>
<div class="monitoring-banner monitoring-banner-yellow" style="margin-top:1rem" data-system-backup="protected">
Ennek az alkalmazásnak az adatai a belső rendszerlemezen vannak, amelyek
<strong>szerepelnek a teljes rendszermentésben</strong>, és az egy külön meghajtóra készül. A 2. (off-drive) másolat
kiegészítő, és elsősorban a külső adatmeghajtón tárolt alkalmazásokhoz készül — ehhez az
alkalmazáshoz nincs külön teendő.
</div>
</div>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
@@ -1,554 +0,0 @@
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Második mentés beállítása — Felhom.eu</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.247.0">
<link rel="stylesheet" href="/static/style.css?v=0.247.0">
<meta name="csrf-token" content="tok">
<script>
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
</svg>
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v=0.247.0" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="">Meghajtók</a></li>
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
</ul>
</li>
<li class="nav-group is-open">
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="active">Áttekintés</a></li>
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
</ul>
</li>
<li class="nav-group">
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Hibakeresés</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">0.247.0</span><details class="lang-globe">
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
</ul>
</details>
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
</div>
</div>
</nav>
<main class="content">
<div class="page-header">
<h2>2. mentés beállítása — Private Bin</h2>
<a href="/backups/apps" class="btn btn-outline btn-sm">← Vissza a mentésekhez</a>
</div>
<div class="monitor-card">
<p style="color:var(--text-3);font-size:.9rem;margin-top:0">
A 2. mentés egy <strong>másik fizikai meghajtóra</strong> készít másolatot az alkalmazás
helyreállítási csomagjáról és adatairól. Ez az egyetlen off-drive védelem a böngészhető
felhasználói fájlokhoz (a teljes rendszermentés/PBS nem éri el ezeket).
</p>
<div class="monitoring-banner monitoring-banner-yellow" style="margin-top:1rem" data-system-backup="same_disk">
Ennek az alkalmazásnak az adatai a belső rendszerlemezen vannak. A teljes rendszermentés
<strong>jelenleg ugyanerre a lemezre készül</strong> — így hibás fájlok ellen véd, lemezhiba ellen nem.
Ha csatlakoztatsz egy második meghajtót, és a Mentések oldalon kijelölöd a rendszermentés helyéül, ez az alkalmazás is védett lesz.
</div>
</div>
</main>
<script>
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
if (data.ok && data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
var udPaths = '';
data.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
hddInfo += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
var keepOnly = hddResp.ok && hddResp.data && hddResp.data.keep_data_only;
if (keepOnly) {
sections += '<div class="alert alert-info" style="margin-bottom:.75rem" data-remove-keep-only="true">Az ügyfélszolgálat foglalkozik ezzel az alkalmazással, ezért az adatai megmaradnak: most csak magát az alkalmazást távolíthatod el.</div>';
}
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
var hddCheckbox = '';
if (!keepOnly && hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíted az alkalmazást, az adatokat újra importálnod kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
var backupCheckbox = '';
if (!keepOnly && backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
if (hddResp.ok && hddResp.data && hddResp.data.userdata_kept && hddResp.data.userdata_kept.length > 0) {
var udPaths = '';
hddResp.data.userdata_kept.forEach(function(p) { udPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>'; });
sections += '<div class="modal-section" data-userdata-kept="true"><strong>A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket:</strong>' + udPaths + '</div>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.userdata_kept && data.data.userdata_kept.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem" data-userdata-kept="true">A fájljaid ezekben a mappákban megmaradnak — a fájlböngészőben látod őket: ' + data.data.userdata_kept.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>