7f3e58ff0a
gates / gates (push) Failing after 49s
v0.315.0 worked and looked, in the operator's words, messy: boxes of different widths stacked down
the page, with the same card shape around things you act on and things you only read. The mockup
approved on 2026-10-15 replaces it, and each of its three rules has a reason:
ONE COLUMN OF ONE WIDTH (820px). Differing widths read as a page assembled by accident.
SECTIONS ARE HAIRLINES, NOT CARDS. A card says "this is a thing you act on"; most of an app page
is a thing you READ, so a card there is a promise the content cannot keep.
A BOX MEANS SOMETHING. Exactly two remain: blue when the app is waiting for the household,
amber when something is risky. The exception-colour principle applied to shape as well.
What moved:
- the hero loses its box: a 64px logo tile, the name at 26px, the tagline, then hairline chips
(state, lifecycle, update) and meta text (the address, the last automatic update);
- the tabs are links with a 2px blue underline under the open one — no pills, no fills;
- sections are `padding: 28px 0` with a bottom hairline; rows are a 200px label column so every
value on the page starts at the same x, and stack under 720px;
- the after-start callout is now the FIRST thing on Beallitasok. A household opening a Plex that
wants its claim code should meet the code box, not a Restart button;
- "2. mentes beallitasa — <app>" and "<app> — Exportalas" are gone: they were the headings of
pages that no longer exist, on a tab that already says which app this is;
- the settings tab gains "Cim es kulcsok" with one address row, and the disabled subdomain input
that said the same thing is skipped for an installed app (the install wizard still asks);
- the "this app is already installed, the settings are read-only" BANNER becomes the hint under
the fields it is about;
- the install wizard gets the same back link, hero and column, so pressing Telepites does not
land the household on a page that looks like a different product.
THE AUTO-UPDATE SUCCESS BANNER IS GONE. It was a full-width blue box above the tabs, level with
"the update failed" and "your files are missing". A success that asked nothing of the household is
a date, and it is now meta text in the hero. Every banner that DOES need a decision — a failed
update, a hold, lost files, a missing drive, an interrupted install — is untouched, above the tabs,
exactly where R-571 puts it.
Tests: TestAppPage_OneColumnLayout (one column, one aria-current, no card-shaped section, no
success banner), TestAppPage_AutoUpdateSuccessIsMetaNotBanner, TestAppPage_CalloutsOnlyForAskAndWarn
(one blue callout on a waiting app and it comes first; one amber on a same-disk backup; none on the
good case). Four red-proofs recorded in REPORT.
app_row_dedup_gate convicted `.app-row`, correctly: that class already means "a row in the app
list". The app page's label/value row is `.app-field-row`. Restoring the list's own rule after an
over-broad rename was part of the fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
1058 lines
61 KiB
HTML
1058 lines
61 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>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=ASSET">
|
|
<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,'&').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>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="app-page">
|
|
|
|
<a href="/stacks" class="app-back">← Alkalmazások</a>
|
|
|
|
<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">
|
|
<h2 class="app-hero-name">PrivateBin</h2>
|
|
<p class="app-hero-tagline">Egy teszt alkalmazás leírása</p>
|
|
<div class="app-chips">
|
|
|
|
<span class="app-chip" data-state-chip="running">● Fut</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="app-section" 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-section" 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 kettő (/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="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-section">
|
|
<h3>2. mentés beállítása — PrivateBin</h3>
|
|
|
|
<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>
|
|
|
|
|
|
<h3 style="margin-top:1.5rem">Jelenlegi állapot</h3>
|
|
<div class="sysinfo-grid">
|
|
<div class="sysinfo-row">
|
|
<span class="sysinfo-label">2. mentés</span>
|
|
<span class="sysinfo-value">Bekapcsolva</span>
|
|
</div>
|
|
|
|
<div class="sysinfo-row">
|
|
<span class="sysinfo-label">Cél meghajtó</span>
|
|
<span class="sysinfo-value">HDD kettő</span>
|
|
</div>
|
|
<div class="sysinfo-row">
|
|
<span class="sysinfo-label">Kiválasztás módja</span>
|
|
<span class="sysinfo-value" style="color:var(--text-3)">automatikus — a legnagyobb szabad hely</span>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<form method="POST" action="/stacks/privatebin/backup" style="margin-top:1.5rem">
|
|
|
|
<div class="form-group">
|
|
<label style="display:flex;align-items:center;gap:.5rem">
|
|
<input type="checkbox" name="enabled" value="on" checked>
|
|
2. mentés bekapcsolva
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="target">Cél meghajtó</label>
|
|
<select id="target" name="target" class="form-control">
|
|
<option value="">Automatikus (jelenleg: HDD kettő)</option>
|
|
|
|
</select>
|
|
|
|
<div style="font-size:.8rem;color:var(--text-3);margin-top:.4rem">
|
|
Nincs másik adatmeghajtó — automatikus cél a belső SSD (csak DB/konfiguráció). Egy 2.
|
|
adatmeghajtó hozzáadásával a teljes adat is off-drive menthető.
|
|
</div>
|
|
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Mentés</button>
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="app-section">
|
|
<h3>PrivateBin — Exportálás</h3>
|
|
<h4 style="font-size:14px;font-weight:600;margin:12px 0 6px">Mentés helye</h4>
|
|
<select id="destDrive" onchange="loadEstimate()" style="width:100%;padding:.5rem;margin-bottom:1rem">
|
|
<option value="">Válassz tárolót...</option>
|
|
|
|
<option value="/mnt/hdd1">HDD egy (/mnt/hdd1)</option>
|
|
|
|
</select>
|
|
|
|
<div id="estimateBox" style="display:none;background:var(--bg-2);border-radius:8px;padding:1rem;margin-bottom:1.5rem">
|
|
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
|
|
<span>Konfiguráció:</span>
|
|
<span id="estConfig">-</span>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
|
|
<span>Felhasználói adatok:</span>
|
|
<span id="estData">-</span>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between;font-weight:600;margin-bottom:.25rem;border-top:1px solid var(--line);padding-top:.5rem">
|
|
<span>Összesen:</span>
|
|
<span><span id="estTotal">-</span> <span id="estFree" style="color:var(--text-3)">(szabad: -)</span></span>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between">
|
|
<span>Becsült idő:</span>
|
|
<span id="estTime">-</span>
|
|
</div>
|
|
<div id="estWarning" style="display:none;color:var(--crit);margin-top:.5rem;font-weight:600"></div>
|
|
</div>
|
|
|
|
<div id="classSelect" style="display:none;margin-bottom:1.5rem">
|
|
<div id="mandBox" style="display:none;margin-bottom:1rem">
|
|
<div style="font-weight:600;margin-bottom:.25rem">Mindig része a mentésnek:</div>
|
|
<div id="mandList" style="color:var(--text-3);font-size:.9rem"></div>
|
|
</div>
|
|
<div id="optBox" style="display:none;margin-bottom:1rem">
|
|
<div style="font-weight:600;margin-bottom:.25rem">Választható tartalom:</div>
|
|
<div id="optList"></div>
|
|
</div>
|
|
<div id="exclBox" style="display:none">
|
|
<div id="exclSummary" style="margin-bottom:.25rem"></div>
|
|
<div style="color:var(--text-3);font-size:.85rem;margin-bottom:.5rem">A kihagyott mappák tartalma a Fájlkezelőben bármikor elérhető, és külön is lementhető.</div>
|
|
<button type="button" class="btn btn-xs btn-outline" onclick="toggleExcl()" id="exclToggle">Kihagyott tartalom megjelenítése</button>
|
|
<div id="exclList" style="display:none;margin-top:.5rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Jelszó (opcionális)</h3>
|
|
<div style="display:flex;gap:.5rem;margin-bottom:1rem">
|
|
<input type="password" id="exportPassword" placeholder="Titkosítási jelszó" style="flex:1;padding:.5rem">
|
|
<button class="btn btn-sm btn-outline" onclick="togglePw()" type="button" title="Jelszó mutatása">👁</button>
|
|
</div>
|
|
|
|
<label style="display:flex;align-items:flex-start;gap:.5rem;margin-bottom:.5rem;cursor:pointer">
|
|
<input type="checkbox" id="stopApp" checked style="margin-top:3px">
|
|
<div>
|
|
<strong>Alkalmazás leállítása mentés előtt (ajánlott)</strong>
|
|
<div style="color:var(--text-3);font-size:.85rem">Az adatok konzisztenciája érdekében javasolt leállítani az alkalmazást mentés közben.</div>
|
|
</div>
|
|
</label>
|
|
|
|
<button id="startBtn" class="btn btn-primary" onclick="startExport()" style="margin-top:1rem;width:100%" disabled>
|
|
Exportálás indítása
|
|
</button>
|
|
</div>
|
|
|
|
<div id="progressCard" class="card" style="max-width:700px;display:none">
|
|
<h3>Folyamat</h3>
|
|
<div id="progressSteps"></div>
|
|
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
|
|
</div>
|
|
|
|
<div id="doneCard" class="card" style="max-width:700px;display:none">
|
|
<h3 style="color:var(--blue)">Kész!</h3>
|
|
<div style="margin-bottom:1rem">
|
|
<span id="doneFile" style="font-weight:600"></span>
|
|
<span id="doneSize" style="color:var(--text-3)"></span>
|
|
</div>
|
|
<a id="doneFBLink" href="#" target="_blank" class="btn btn-outline">Megnyitás FileBrowser-ben ↗</a>
|
|
</div>
|
|
|
|
<script>
|
|
var stackName = 'privatebin';
|
|
var domain = '' ? '.example.hu' : '';
|
|
var pollTimer = null;
|
|
|
|
function csrfH() {
|
|
var el = document.querySelector('meta[name="csrf-token"]');
|
|
return el ? {'X-CSRF-Token': el.content, 'Content-Type': 'application/json'} : {'Content-Type': 'application/json'};
|
|
}
|
|
|
|
function togglePw() {
|
|
var inp = document.getElementById('exportPassword');
|
|
inp.type = inp.type === 'password' ? 'text' : 'password';
|
|
}
|
|
|
|
async function loadEstimate() {
|
|
var drive = document.getElementById('destDrive').value;
|
|
var box = document.getElementById('estimateBox');
|
|
var btn = document.getElementById('startBtn');
|
|
|
|
if (!drive) {
|
|
box.style.display = 'none';
|
|
btn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var resp = await fetch('/api/export/estimate?stack=' + encodeURIComponent(stackName) + '&drive=' + encodeURIComponent(drive));
|
|
var data = await resp.json();
|
|
if (!data.ok) {
|
|
box.style.display = 'none';
|
|
btn.disabled = true;
|
|
return;
|
|
}
|
|
var est = data.data;
|
|
document.getElementById('estConfig').textContent = est.config_size_human;
|
|
document.getElementById('estData').textContent = est.data_size_human;
|
|
document.getElementById('estTotal').textContent = '~' + est.total_size_human;
|
|
document.getElementById('estFree').textContent = '(szabad: ' + est.dest_free_human + ')';
|
|
document.getElementById('estTime').textContent = '~' + est.estimated_minutes + ' perc';
|
|
|
|
var warn = document.getElementById('estWarning');
|
|
if (!est.fits_on_dest) {
|
|
warn.textContent = 'Nincs elég szabad hely a kiválasztott tárolón!';
|
|
warn.style.display = 'block';
|
|
btn.disabled = true;
|
|
} else {
|
|
warn.style.display = 'none';
|
|
btn.disabled = false;
|
|
}
|
|
box.style.display = 'block';
|
|
renderClassSelect(est);
|
|
} catch(e) {
|
|
console.error('Estimate error:', e);
|
|
}
|
|
}
|
|
|
|
|
|
var lastEst = null;
|
|
function humanBytes(b) {
|
|
if (b >= 1073741824) return fhNum(b/1073741824, 1) + ' GB';
|
|
if (b >= 1048576) return fhNum(b/1048576, 1) + ' MB';
|
|
if (b >= 1024) return fhNum(b/1024, 1) + ' KB';
|
|
return b + ' B';
|
|
}
|
|
function renderClassSelect(est) {
|
|
lastEst = est;
|
|
var wrap = document.getElementById('classSelect');
|
|
if (!est.has_classification) { wrap.style.display = 'none'; return; }
|
|
wrap.style.display = 'block';
|
|
|
|
var mand = est.mandatory_items || [], opt = est.optional_items || [], excl = est.excluded_items || [];
|
|
|
|
var mb = document.getElementById('mandBox');
|
|
if (mand.length) {
|
|
mb.style.display = 'block';
|
|
document.getElementById('mandList').innerHTML = mand.map(function(i){
|
|
return '<div>' + esc(i.rel_path) + ' <span style="opacity:.7">(' + i.human + ')</span></div>';
|
|
}).join('');
|
|
} else { mb.style.display = 'none'; }
|
|
|
|
var ob = document.getElementById('optBox');
|
|
if (opt.length) {
|
|
ob.style.display = 'block';
|
|
document.getElementById('optList').innerHTML = opt.map(function(i){
|
|
return '<label style="display:flex;gap:.5rem;align-items:center;cursor:pointer;padding:.15rem 0">' +
|
|
'<input type="checkbox" class="fab-opt" checked data-key="' + esc(i.key) + '" data-bytes="' + i.bytes + '" onchange="recomputeTotal()">' +
|
|
'<span>' + esc(i.rel_path) + ' <span style="opacity:.7">(' + i.human + ')</span></span></label>';
|
|
}).join('');
|
|
} else { ob.style.display = 'none'; }
|
|
|
|
var eb = document.getElementById('exclBox');
|
|
if (excl.length) {
|
|
eb.style.display = 'block';
|
|
document.getElementById('exclList').innerHTML = excl.map(function(i){
|
|
return '<label style="display:flex;gap:.5rem;align-items:center;cursor:pointer;padding:.15rem 0">' +
|
|
'<input type="checkbox" class="fab-excl" data-key="' + esc(i.key) + '" data-bytes="' + i.bytes + '" onchange="recomputeTotal()">' +
|
|
'<span>' + esc(i.rel_path) + ' <span style="opacity:.7">(' + i.human + ')</span></span></label>';
|
|
}).join('');
|
|
} else { eb.style.display = 'none'; }
|
|
recomputeTotal();
|
|
}
|
|
function recomputeTotal() {
|
|
if (!lastEst) return;
|
|
var base = lastEst.base_bytes || 0;
|
|
var optSum = 0, exclSum = 0, exclAll = 0;
|
|
document.querySelectorAll('.fab-opt').forEach(function(cb){ if (cb.checked) optSum += parseInt(cb.dataset.bytes,10)||0; });
|
|
document.querySelectorAll('.fab-excl').forEach(function(cb){ var b = parseInt(cb.dataset.bytes,10)||0; exclAll += b; if (cb.checked) exclSum += b; });
|
|
var alap = base + optSum;
|
|
var full = alap + exclAll;
|
|
var s = document.getElementById('exclSummary');
|
|
if ((lastEst.excluded_items||[]).length) {
|
|
s.innerHTML = 'Alap mentés: ~' + humanBytes(alap) + '. A kihagyott, nagy méretű tartalommal együtt: ~' + humanBytes(full) + '.';
|
|
}
|
|
}
|
|
function toggleExcl() {
|
|
var l = document.getElementById('exclList');
|
|
var shown = l.style.display !== 'none';
|
|
l.style.display = shown ? 'none' : 'block';
|
|
document.getElementById('exclToggle').textContent = shown ? 'Kihagyott tartalom megjelenítése' : 'Kihagyott tartalom elrejtése';
|
|
}
|
|
function gatherSelections() {
|
|
var deselect = [], optIn = [];
|
|
document.querySelectorAll('.fab-opt').forEach(function(cb){ if (!cb.checked) deselect.push(cb.dataset.key); });
|
|
document.querySelectorAll('.fab-excl').forEach(function(cb){ if (cb.checked) optIn.push(cb.dataset.key); });
|
|
return { deselect_optional: deselect, opt_in_excluded: optIn };
|
|
}
|
|
function esc(s) { return String(s).replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); }
|
|
|
|
async function startExport() {
|
|
var drive = document.getElementById('destDrive').value;
|
|
var password = document.getElementById('exportPassword').value;
|
|
var stopApp = document.getElementById('stopApp').checked;
|
|
|
|
document.getElementById('startBtn').disabled = true;
|
|
document.getElementById('progressCard').style.display = 'block';
|
|
document.getElementById('doneCard').style.display = 'none';
|
|
|
|
try {
|
|
var sel = gatherSelections();
|
|
var resp = await fetch('/api/export/start', {
|
|
method: 'POST',
|
|
headers: csrfH(),
|
|
body: JSON.stringify({
|
|
stack_name: stackName,
|
|
dest_drive: drive,
|
|
password: password,
|
|
stop_app: stopApp,
|
|
deselect_optional: sel.deselect_optional,
|
|
opt_in_excluded: sel.opt_in_excluded
|
|
})
|
|
});
|
|
var data = await resp.json();
|
|
if (!data.ok) {
|
|
showError(data.error || 'Hiba történt');
|
|
return;
|
|
}
|
|
pollStatus();
|
|
} catch(e) {
|
|
showError(e.message);
|
|
}
|
|
}
|
|
|
|
function pollStatus() {
|
|
if (pollTimer) clearInterval(pollTimer);
|
|
pollTimer = setInterval(async function() {
|
|
try {
|
|
var resp = await fetch('/api/export/status');
|
|
var data = await resp.json();
|
|
renderSteps(data.steps || []);
|
|
|
|
if (data.error) {
|
|
showError(data.error);
|
|
clearInterval(pollTimer);
|
|
return;
|
|
}
|
|
|
|
if (data.done && !data.error) {
|
|
clearInterval(pollTimer);
|
|
showDone(data);
|
|
}
|
|
} catch(e) {
|
|
console.error('Poll error:', e);
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
function renderSteps(steps) {
|
|
var html = '';
|
|
for (var i = 0; i < steps.length; i++) {
|
|
var s = steps[i];
|
|
var icon = '○';
|
|
if (s.status === 'running') icon = '⟳';
|
|
if (s.status === 'done') icon = '✓';
|
|
if (s.status === 'failed') icon = '✗';
|
|
var cls = s.status === 'failed' ? 'color:var(--crit)' : s.status === 'done' ? 'color:var(--blue)' : s.status === 'running' ? 'color:var(--blue)' : '';
|
|
html += '<div style="padding:.25rem 0;' + cls + '">' + icon + ' ' + s.label;
|
|
if (s.error) html += ' <span style="font-size:.85rem">(' + s.error + ')</span>';
|
|
html += '</div>';
|
|
}
|
|
document.getElementById('progressSteps').innerHTML = html;
|
|
}
|
|
|
|
function showError(msg) {
|
|
var el = document.getElementById('progressError');
|
|
el.textContent = msg;
|
|
el.style.display = 'block';
|
|
document.getElementById('startBtn').disabled = false;
|
|
}
|
|
|
|
function showDone(data) {
|
|
document.getElementById('progressCard').style.display = 'none';
|
|
document.getElementById('doneCard').style.display = 'block';
|
|
var fileName = (data.output_path || '').split('/').pop();
|
|
document.getElementById('doneFile').textContent = fileName;
|
|
document.getElementById('doneSize').textContent = data.output_size ? '(' + data.output_size + ')' : '';
|
|
|
|
|
|
var drive = document.getElementById('destDrive').value;
|
|
var fbPath = drive + '/felhom-data/exports/';
|
|
document.getElementById('doneFBLink').href = 'https://files.' + location.hostname.split('.').slice(-2).join('.') + '/files' + fbPath;
|
|
}
|
|
</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>
|
|
|
|
</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>
|
|
|