0b1486df8f
gates / gates (push) Successful in 24s
/recovery is in the AUTHENTICATED route table — its reader is the household, not a visitor. The first draft of v0.254.0 gave it the anonymous form, which sets the felhom_lang cookie that langFor deliberately ignores once there is a session: the button would have appeared to work and done nothing. Found by the live probe on demo-hp reporting no globe on /recovery (it 302s to /login without a session) and then reading the route table. executeTemplateLang now branches on hasSession: household form with its session CSRF, or the visitor form without. The parity harness and TestI18nDirectRenderPagesFollowLanguage carry the same branch, so the fixture is the form the real page serves — the trap this release already walked into once with the shells. A per-session CSRF token cannot be a fixture value, so it is blanked on both sides of every parity comparison, exactly as relative ages already were. What stays pinned is that the field is THERE and WHICH form it sits in — the half that says whether the globe writes the household's setting or the visitor's cookie. Evidence regenerated: 3 change shapes across 106 fixtures, 5 byte-identical (both guest share pages and the catch-all — the three that must not change). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
1591 lines
81 KiB
HTML
1591 lines
81 KiB
HTML
|
|
|
|
<!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 telepítése — 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}:{};}
|
|
|
|
|
|
|
|
|
|
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>')+'</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>Debug</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:.5rem">
|
|
<a href="/stacks" class="btn btn-sm btn-outline">← 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>
|
|
<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>
|
|
</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 + '\')">×</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>
|
|
<input type="password"
|
|
id="opt-API_KEY"
|
|
name="API_KEY"
|
|
class="config-input"
|
|
value="abc123"
|
|
placeholder="API kulcs mező"
|
|
autocomplete="off">
|
|
</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) {
|
|
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">
|
|
|
|
<div class="alert alert-error" style="margin-bottom:0">
|
|
Nincs elég memória! Foglalás telepítés után: 5000 MB / 4000 MB
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form id="deploy-form" class="deploy-form">
|
|
|
|
|
|
|
|
|
|
<div class="form-hint" style="margin-bottom:1rem">
|
|
|
|
Az alkalmazás adatai az alább kiválasztott adatmeghajtóra kerülnek.
|
|
|
|
</div>
|
|
|
|
|
|
<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ítson fel helyet, vagy bővítse a tárhelyet.
|
|
</div>
|
|
|
|
|
|
<div class="form-section">
|
|
<h4>Automatikusan generált értékek</h4>
|
|
|
|
<p class="form-section-desc">Ezek az értékek a telepítéssel együtt mentésre kerülnek. Jegyezze fel a szükséges jelszavakat!</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group form-group-auto">
|
|
<label>Titkos kulcs érték <span class="auto-generated-badge"><svg class="ico ico-sm"><use href="#i-check"/></svg> Automatikusan generálva</span></label>
|
|
|
|
|
|
|
|
<div class="input-with-button">
|
|
<input type="password" id="auto-field-SECRET_KEY" class="form-control" value="generated-secret-value" readonly>
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="toggleAutoField('auto-field-SECRET_KEY', this)">Megjelenítés</button>
|
|
</div>
|
|
|
|
|
|
|
|
<input type="hidden" name="SECRET_KEY" value="generated-secret-value">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group form-group-auto">
|
|
<label>Adatbázis neve itt </label>
|
|
|
|
|
|
<input type="text" id="auto-field-DB_NAME" class="form-control" value="privatebin_db" readonly>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group form-group-auto">
|
|
<label>Üres mező értéke </label>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
<h4>Beállítások</h4>
|
|
|
|
<div class="form-group">
|
|
<label for="field-SUBDOMAIN">
|
|
Webcím eleje
|
|
<span class="required">*</span>
|
|
|
|
</label>
|
|
|
|
|
|
<div class="subdomain-input-group">
|
|
<input type="text" id="field-SUBDOMAIN" name="SUBDOMAIN"
|
|
class="form-control subdomain-input"
|
|
|
|
value="paste"
|
|
placeholder="aldomain"
|
|
pattern="[a-z0-9]([a-z0-9-]*[a-z0-9])?"
|
|
required
|
|
|
|
oninput="this.value=this.value.toLowerCase().replace(/[^a-z0-9-]/g,'')">
|
|
<span class="subdomain-suffix">.example.hu</span>
|
|
</div>
|
|
|
|
<span class="form-hint">Az aldomain telepítés után nem módosítható. Megváltoztatáshoz az alkalmazás eltávolítása és újratelepítése szükséges (minden adat törlődik).</span>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="field-LANG_SEL">
|
|
Felület nyelve
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select id="field-LANG_SEL" name="LANG_SEL" class="form-control"
|
|
>
|
|
|
|
<option value="hu">Magyar nyelv</option>
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="field-ADMIN_PASSWORD">
|
|
Admin jelszó mező
|
|
<span class="required">*</span>
|
|
<span class="locked-hint">telepítés után nem módosítható</span>
|
|
</label>
|
|
|
|
|
|
<div class="input-with-button">
|
|
<input type="password" id="field-ADMIN_PASSWORD" name="ADMIN_PASSWORD"
|
|
class="form-control" value=""
|
|
placeholder="Legalább 12 karakter"
|
|
data-field-type="password"
|
|
required
|
|
>
|
|
<button type="button" class="btn btn-sm btn-outline pw-toggle-btn"
|
|
onclick="togglePasswordField('field-ADMIN_PASSWORD', 'field-confirm-ADMIN_PASSWORD', this)"
|
|
title="Megjelenítés">👁</button>
|
|
|
|
<button type="button" class="btn btn-sm btn-outline"
|
|
onclick="generatePassword('field-ADMIN_PASSWORD', 'field-confirm-ADMIN_PASSWORD')">Generálás</button>
|
|
|
|
</div>
|
|
|
|
<div class="input-with-button" style="margin-top:.25rem">
|
|
<input type="password" id="field-confirm-ADMIN_PASSWORD"
|
|
class="form-control" placeholder="Jelszó megerősítése"
|
|
data-confirm-for="field-ADMIN_PASSWORD">
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="field-FLAG">
|
|
Kapcsoló mező itt
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="toggle">
|
|
<input type="checkbox" id="field-FLAG" name="FLAG" value="true"
|
|
>
|
|
<span class="toggle-label">Igen</span>
|
|
</label>
|
|
|
|
|
|
|
|
<span class="form-hint">Egy kapcsoló leírása</span>
|
|
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="field-HDD_PATH">
|
|
Adatok helye itt
|
|
<span class="required">*</span>
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<input type="text" id="field-HDD_PATH" name="HDD_PATH"
|
|
class="form-control" value=""
|
|
placeholder="/mnt/valahol"
|
|
required
|
|
>
|
|
<span class="form-hint" style="color:var(--warn)">Nincs regisztrált adattároló — adja meg kézzel az útvonalat</span>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="field-NOTE">
|
|
Megjegyzés mező
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input type="text" id="field-NOTE" name="NOTE"
|
|
class="form-control" value=""
|
|
placeholder="Írj ide valamit"
|
|
|
|
>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</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>
|
|
</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 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 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 revealAutoField(stackName, envVar, btn) {
|
|
var el = document.getElementById('auto-field-' + envVar);
|
|
if (!el) return;
|
|
if (btn.dataset.shown === '1') {
|
|
el.value = '';
|
|
el.type = 'password';
|
|
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;
|
|
el.type = el.type === 'password' ? 'text' : 'password';
|
|
btn.textContent = el.type === 'password' ? 'Megjelenítés' : 'Elrejtés';
|
|
}
|
|
function generatePassword(fieldId, confirmFieldId) {
|
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
let pass = '';
|
|
const arr = new Uint8Array(16);
|
|
crypto.getRandomValues(arr);
|
|
for (let i = 0; i < 16; i++) {
|
|
pass += chars[arr[i] % chars.length];
|
|
}
|
|
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 () {
|
|
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();
|
|
|
|
|
|
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álja a Generálás gombot vagy írjon 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})
|
|
});
|
|
var data = await resp.json();
|
|
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 = 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őrizze 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>
|
|
|
|
|
|
</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>';
|
|
}
|
|
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>';
|
|
}
|
|
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 = '';
|
|
|
|
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 (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íti az alkalmazást, az adatokat újra importálnia 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 (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>';
|
|
}
|
|
}
|
|
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.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>
|
|
|