Files
deploy-felhom-compose/controller/internal/web/templates/deploy.html
T
admin eb2207fb62 feat: password fields with masked input, reveal toggle, confirmation
- Password deploy fields now use type=password (masked by default)
- Added eye toggle button to reveal/hide password and confirm fields
- Added confirmation field below each password input
- Generate button fills both password and confirmation fields
- Form validation checks password confirmation matches before deploy
- Confirmation field only shown for new deployments (not already deployed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 16:33:33 +01:00

826 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "deploy"}}
{{template "layout_start" .}}
<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>{{.Meta.DisplayName}} — {{if .AlreadyDeployed}}Beállítások{{else}}Telepítés{{end}}</h2>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
{{if .AlreadyDeployed}}
{{if isOperational .Stack.State}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Stack.Name}}', 'restart')">Újraindítás</button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Stack.Name}}', 'stop')">Leállítás</button>
{{else}}
<button class="btn btn-sm btn-success" onclick="stackAction(event, '{{.Stack.Name}}', 'start')">Indítás</button>
{{end}}
{{if and (isOperational .Stack.State) .EffectiveSubdomain}}
<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}" target="_blank" class="btn btn-sm btn-outline">Megnyitás ↗</a>
{{end}}
{{end}}
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline">️ Részletek</a>
</div>
</div>
<div class="deploy-container">
{{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</div>{{end}}
{{if .FlashError}}<div class="flash flash-error">{{.FlashError}}</div>{{end}}
<div class="deploy-info">
<img class="deploy-logo" src="{{.LogoURL}}" alt="" onerror="this.onerror=function(){this.style.display='none'};this.src='{{.LogoPNGURL}}'">
<div>
<h3>{{.Meta.DisplayName}}</h3>
{{if .Meta.Description}}<p>{{.Meta.Description}}</p>{{end}}
<div class="stack-meta-badges">
{{if .Meta.Resources.MemRequest}}<span class="meta-badge">~{{.Meta.Resources.MemRequest}}</span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="meta-badge meta-badge-ok">Pi kompatibilis</span>{{end}}
{{if .Meta.Resources.NeedsHDD}}<span class="meta-badge">HDD szükséges</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="meta-badge meta-badge-ok">Magyar felület</span>{{end}}
</div>
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline" style="margin-top:0.5rem">
Részletes leírás, képernyőképek
</a>
</div>
</div>
{{if .AlreadyDeployed}}
<div class="alert alert-info">
Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók.
</div>
{{if .StorageInfo}}
<div class="deploy-storage-info">
<h4>Adattárolás</h4>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Tárhely</span>
<span class="settings-value">{{.StorageInfo.Label}} <span class="mono" style="color:var(--text-secondary)">({{.StorageInfo.Path}})</span></span>
</div>
{{if .StorageInfo.DataSizeHuman}}
<div class="settings-row">
<span class="settings-label">Adatméret</span>
<span class="settings-value mono">{{.StorageInfo.DataSizeHuman}}</span>
</div>
{{end}}
{{if .StorageInfo.FreeHuman}}
<div class="settings-row">
<span class="settings-label">Szabad hely</span>
<span class="settings-value mono">{{.StorageInfo.FreeHuman}} ({{printf "%.0f" .StorageInfo.FreePercent}}% szabad)</span>
</div>
{{end}}
</div>
{{if .OtherStoragePaths}}
<a href="/stacks/{{.Meta.Slug}}/migrate" class="btn btn-sm btn-outline" style="margin-top:.75rem">
📦 Mozgatás másik tárolóra
</a>
{{end}}
</div>
{{end}}
{{if .StaleData}}
<div class="deploy-stale-data">
<h4>Korábbi adatok</h4>
<p class="form-hint" style="margin-bottom:1rem">
Az alkalmazás adatainak másolata megtalálható egy másik tárolón is.
Ez általában áthelyezés után marad hátra.
</p>
{{range .StaleData}}
<div class="stale-data-item">
<div class="settings-grid" style="margin-bottom:.75rem">
<div class="settings-row">
<span class="settings-label">Tárhely</span>
<span class="settings-value">{{.Label}} <span class="mono" style="color:var(--text-secondary)">({{.Path}})</span></span>
</div>
<div class="settings-row">
<span class="settings-label">Méret</span>
<span class="settings-value mono">{{.SizeHuman}}</span>
</div>
<div class="settings-row">
<span class="settings-label">Mappák</span>
<span class="settings-value mono" style="font-size:.85rem">{{range .Mounts}}{{.}}<br>{{end}}</span>
</div>
</div>
<button class="btn btn-sm btn-danger" onclick="deleteStaleData('{{$.Meta.Slug}}', '{{.Path}}', this)">
Korábbi adatok törlése
</button>
</div>
{{end}}
</div>
{{end}}
{{end}}
{{if .AlreadyDeployed}}
<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(--accent-blue)">Mentési állapot →</a>
</span>
</div>
<hr style="border-color:var(--border);margin:1rem 0">
<p style="font-weight:500;margin-bottom:1rem">2. mentés — másolat másik meghajtóra:</p>
{{if .BackupDestWarning}}
<div class="alert {{if eq .BackupDestWarningSeverity "critical"}}alert-error{{else}}alert-warning{{end}}" style="margin-bottom:1rem">{{.BackupDestWarning}}</div>
{{end}}
{{if not .BackupDestPaths}}
<div class="alert alert-info">
Másik adattároló szükséges a másolat készítéséhez.
<a href="/settings" style="color:var(--accent-blue)">Csatlakoztass egy külső meghajtót a Beállítások oldalon.</a>
</div>
{{else}}
<form method="post" action="/settings/cross-backup/{{.Meta.Slug}}">
{{.CSRFField}}
<div class="settings-grid" style="margin-bottom:1rem">
<div class="settings-row">
<span class="settings-label">Engedélyezve</span>
<label class="toggle" style="margin:0">
<input type="checkbox" name="cross_drive_enabled" id="cross-drive-enabled"
{{if and .CrossDriveConfig .CrossDriveConfig.Enabled}}checked{{end}}
onchange="toggleCrossDriveFields()">
<span class="toggle-label">Igen</span>
</label>
</div>
<div class="settings-row">
<span class="settings-label">Cél tárhely</span>
<select name="cross_drive_dest" id="cd-dest" class="form-control cross-drive-field" style="max-width:20rem"
{{if not (and .CrossDriveConfig .CrossDriveConfig.Enabled)}}disabled{{end}}>
{{range .BackupDestPaths}}
<option value="{{.Path}}"
{{if and $.CrossDriveConfig (eq $.CrossDriveConfig.DestinationPath .Path)}}selected{{end}}>
{{.Label}} ({{.Path}}){{if .IsDefault}} ★{{end}}
{{if .FreeHuman}} — {{.FreeHuman}} szabad{{end}}
</option>
{{end}}
</select>
</div>
<div class="settings-row">
<span class="settings-label">Ütemezés</span>
<div>
<select name="cross_drive_schedule" id="cd-schedule" class="form-control cross-drive-field" style="max-width:20rem"
{{if not (and .CrossDriveConfig .CrossDriveConfig.Enabled)}}disabled{{end}}
onchange="onScheduleChange()">
<option value="daily" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "daily")}}selected{{end}}>
Naponta (az éjszakai mentés után)
</option>
<option value="weekly" {{if or (and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "weekly")) (and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "manual"))}}selected{{end}}>
Hetente, vasárnap (az éjszakai mentés után)
</option>
</select>
<div id="weekly-note" class="form-hint" style="margin-top:.5rem;display:{{if and .CrossDriveConfig (or (eq .CrossDriveConfig.Schedule "weekly") (eq .CrossDriveConfig.Schedule "manual"))}}block{{else}}none{{end}}">
Heti mentés esetén visszaállításkor az adatbázis is a mentés napjára áll vissza
a konzisztencia érdekében. A mentés napja és a visszaállítás között keletkezett
adatbázis-változások elvesznek (max. 7 nap).
</div>
</div>
</div>
</div>
{{if .CrossDriveConfig}}
{{if .CrossDriveConfig.LastRun}}
<div class="form-hint" style="margin-bottom:.75rem">
Utolsó futás: {{.CrossDriveConfig.LastRun}}
{{if eq .CrossDriveConfig.LastStatus "ok"}}Sikeres{{else if eq .CrossDriveConfig.LastStatus "error"}}Hiba: {{.CrossDriveConfig.LastError}}{{else if eq .CrossDriveConfig.LastStatus "running"}}Fut...{{end}}
{{if .CrossDriveConfig.LastDuration}} ({{.CrossDriveConfig.LastDuration}}){{end}}
{{if .CrossDriveConfig.LastSizeHuman}} — {{.CrossDriveConfig.LastSizeHuman}}{{end}}
</div>
{{end}}
{{end}}
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<button type="submit" class="btn btn-sm btn-primary">Beállítások mentése</button>
{{if and .CrossDriveConfig .CrossDriveConfig.Enabled}}
<button type="button" class="btn btn-sm btn-outline"
onclick="triggerCrossDriveBackup('{{.Meta.Slug}}', this)">
Mentés most
</button>
{{end}}
</div>
</form>
<div class="form-hint" style="margin-top:.75rem;color:var(--text-muted)">
A cél meghajtó legyen más fizikai eszköz a meghibásodás elleni védelem érdekében.
</div>
{{end}}
</div>
{{end}}
{{if and (not .AlreadyDeployed) .MemoryInfo}}
{{with .MemoryInfo}}
{{if .Available}}
<div class="memory-summary{{if .Blocked}} memory-blocked{{end}}">
{{if .Blocked}}
<div class="alert alert-error" style="margin-bottom:0">
Nincs elég memória! Foglalás telepítés után: {{.AfterMB}} MB / {{.UsableMB}} MB
</div>
{{else}}
<div class="memory-summary-header">
<span class="memory-summary-label">Memória</span>
<span class="memory-summary-value">{{.AfterMB}} MB / {{.UsableMB}} MB ({{.Percent}}%)</span>
</div>
<div class="memory-bar-stacked">
<div class="memory-bar-segment memory-bar-committed" style="width:{{.UsedPercent}}%" title="Jelenlegi használat: {{.UsedMB}} MB"></div>
<div class="memory-bar-segment memory-bar-new" style="width:{{subtract .Percent .UsedPercent}}%" title="{{$.Meta.DisplayName}}: +{{.NewRequestMB}} MB"></div>
</div>
<div class="memory-bar-legend">
<span class="memory-legend-item"><span class="memory-legend-dot memory-legend-committed"></span>Jelenlegi használat ({{.UsedMB}} MB)</span>
<span class="memory-legend-item"><span class="memory-legend-dot memory-legend-new"></span>{{$.Meta.DisplayName}} (+{{.NewRequestMB}} MB)</span>
</div>
{{if .OvercommitWarn}}
<div class="alert alert-warning" style="margin-top:0.5rem;margin-bottom:0">
Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát.
Normál használat mellett ez nem okoz problémát.
</div>
{{end}}
{{end}}
</div>
{{end}}
{{end}}
{{end}}
<form id="deploy-form" class="deploy-form">
{{if .AutoFields}}
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
{{if .AlreadyDeployed}}
<p class="form-section-desc">Ezek az értékek automatikusan jöttek létre a telepítéskor.</p>
{{else}}
<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>
{{end}}
{{$autoValues := .AutoFieldValues}}
{{$isDeployed := .AlreadyDeployed}}
{{range .AutoFields}}
{{$val := index $autoValues .EnvVar}}
<div class="form-group form-group-auto">
<label>{{.Label}} {{if eq .Type "secret"}}<span class="auto-generated-badge">✓ Automatikusan generálva</span>{{end}}</label>
{{if $val}}
{{if eq .Type "secret"}}
<div class="input-with-button">
<input type="password" id="auto-field-{{.EnvVar}}" class="form-control" value="{{$val}}" readonly>
<button type="button" class="btn btn-sm btn-outline" onclick="toggleAutoField('auto-field-{{.EnvVar}}', this)">Megjelenítés</button>
</div>
{{else}}
<input type="text" id="auto-field-{{.EnvVar}}" class="form-control" value="{{$val}}" readonly>
{{end}}
{{if and (not $isDeployed) (eq .Type "secret")}}
<input type="hidden" name="{{.EnvVar}}" value="{{$val}}">
{{end}}
{{end}}
</div>
{{end}}
</div>
{{end}}
{{if .UserFields}}
<div class="form-section">
<h4>Beállítások</h4>
{{range .UserFields}}
<div class="form-group">
<label for="field-{{.EnvVar}}">
{{.Label}}
{{if or .Required (eq .Type "password")}}<span class="required">*</span>{{end}}
{{if .LockedAfterDeploy}}<span class="locked-hint">telepítés után nem módosítható</span>{{end}}
</label>
{{if eq .Type "subdomain"}}
<div class="subdomain-input-group">
<input type="text" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
class="form-control subdomain-input"
value="{{if and $.AlreadyDeployed $.DeployedFieldValues}}{{index $.DeployedFieldValues .EnvVar}}{{else}}{{.Default}}{{end}}"
placeholder="aldomain"
pattern="[a-z0-9]([a-z0-9-]*[a-z0-9])?"
required
{{if $.AlreadyDeployed}}disabled{{end}}
oninput="this.value=this.value.toLowerCase().replace(/[^a-z0-9-]/g,'')">
<span class="subdomain-suffix">.{{$.Domain}}</span>
</div>
{{if not $.AlreadyDeployed}}
<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>
{{end}}
{{else if eq .Type "select"}}
<select id="field-{{.EnvVar}}" name="{{.EnvVar}}" class="form-control"
{{if $.AlreadyDeployed}}disabled{{end}}>
{{range .Options}}
<option value="{{.Value}}">{{.Label}}</option>
{{end}}
</select>
{{else if eq .Type "password"}}
<div class="input-with-button">
<input type="password" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
class="form-control" value="{{.Default}}"
placeholder="{{.Placeholder}}"
data-field-type="password"
required
{{if $.AlreadyDeployed}}disabled{{end}}>
<button type="button" class="btn btn-sm btn-outline pw-toggle-btn"
onclick="togglePasswordField('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}', this)"
title="Megjelenítés">&#128065;</button>
<button type="button" class="btn btn-sm btn-outline"
onclick="generatePassword('field-{{.EnvVar}}', 'field-confirm-{{.EnvVar}}')">Generálás</button>
</div>
{{if not $.AlreadyDeployed}}
<div class="input-with-button" style="margin-top:.25rem">
<input type="password" id="field-confirm-{{.EnvVar}}"
class="form-control" placeholder="Jelszó megerősítése"
data-confirm-for="field-{{.EnvVar}}">
</div>
{{end}}
{{else if eq .Type "boolean"}}
<label class="toggle">
<input type="checkbox" id="field-{{.EnvVar}}" name="{{.EnvVar}}" value="true"
{{if $.AlreadyDeployed}}disabled{{end}}>
<span class="toggle-label">Igen</span>
</label>
{{else if eq .Type "path"}}
{{if $.StoragePaths}}
<select id="field-{{.EnvVar}}" name="{{.EnvVar}}" class="form-control"
{{if $.AlreadyDeployed}}disabled{{end}}
onchange="checkStorageSpace(this)">
{{range $.StoragePaths}}
<option value="{{.Path}}" data-free-percent="{{printf "%.0f" .FreePercent}}"
{{if .IsDefault}}selected{{end}}>
{{.Label}} — {{.FreeHuman}} szabad{{if .IsDefault}} ★{{end}}
</option>
{{end}}
</select>
<div id="storage-space-warn" class="form-hint" style="color:var(--yellow);display:none">
⚠️ A kiválasztott tárhely majdnem megtelt.
</div>
{{else}}
<input type="text" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
class="form-control" value="{{.Default}}"
placeholder="{{.Placeholder}}"
{{if .Required}}required{{end}}
{{if $.AlreadyDeployed}}disabled{{end}}>
<span class="form-hint" style="color:var(--yellow)">Nincs regisztrált adattároló — adja meg kézzel az útvonalat</span>
{{end}}
{{else}}
<input type="text" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
class="form-control" value="{{.Default}}"
placeholder="{{.Placeholder}}"
{{if .Required}}required{{end}}
{{if $.AlreadyDeployed}}disabled{{end}}>
{{end}}
{{if and .Description (ne .Type "subdomain")}}
<span class="form-hint">{{.Description}}</span>
{{end}}
</div>
{{end}}
</div>
{{end}}
{{if not .AlreadyDeployed}}
<div class="deploy-actions">
<button type="submit" class="btn btn-primary btn-lg"{{if and .MemoryInfo (index .MemoryInfo "Blocked")}} disabled title="Nincs elég memória"{{end}}>Telepítés indítása</button>
<a href="/stacks" class="btn btn-outline">Mégsem</a>
</div>
{{end}}
</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">&#9203;</span>
<span class="step-text">Konfiguráció mentése...</span>
</div>
<div class="deploy-step" id="step-containers">
<span class="step-icon">&#9203;</span>
<span class="step-text">Konténer(ek) indítása...</span>
</div>
<div class="deploy-step" id="step-health">
<span class="step-icon">&#9203;</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: {{json .Meta.AppInfo.FirstSteps}},
defaultCreds: {{json .Meta.AppInfo.DefaultCreds}},
docsURL: {{json .Meta.AppInfo.DocsURL}},
domain: {{json .Domain}},
displayName: {{json .Meta.DisplayName}},
deployFields: {{json .Meta.DeployFields}}
};
function buildPostDeployCard(stackName) {
var subdomain = '';
var sdField = document.getElementById('field-SUBDOMAIN');
if (sdField) subdomain = sdField.value.trim();
if (!subdomain && '{{.Meta.Subdomain}}') subdomain = '{{.Meta.Subdomain}}';
var html = '';
// App link
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>';
}
// First steps
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>';
}
// Credentials from deploy fields (show actual values from form)
var credRows = '';
if (postDeployInfo.deployFields) {
for (var i = 0; i < postDeployInfo.deployFields.length; i++) {
var f = postDeployInfo.deployFields[i];
// Show secrets, passwords, and username-like text fields
var isCredential = f.type === 'secret' || f.type === 'password';
if (!isCredential && f.type === 'text') {
var ev = f.env_var.toUpperCase();
isCredential = ev.indexOf('USER') >= 0 || ev.indexOf('ADMIN') >= 0 || ev.indexOf('LOGIN') >= 0;
}
if (!isCredential) continue;
// Skip internal DB credentials
var evUp = f.env_var.toUpperCase();
if (evUp.indexOf('DB_PASSWORD') >= 0 || evUp.indexOf('MYSQL_ROOT') >= 0 || evUp.indexOf('SECRET_KEY') >= 0) continue;
// Read value from DOM
var el = document.getElementById('auto-field-' + f.env_var) || document.getElementById('field-' + f.env_var);
var val = el ? el.value : '';
if (!val) continue;
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-muted);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;font-family:var(--font-mono);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>';
}
// Docs link
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>';
}
// Action buttons
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 toggleCrossDriveFields() {
var enabled = document.getElementById('cross-drive-enabled').checked;
var fields = document.querySelectorAll('.cross-drive-field');
for (var i = 0; i < fields.length; i++) {
fields[i].disabled = !enabled;
}
}
function onScheduleChange() {
var sel = document.getElementById('cd-schedule');
var note = document.getElementById('weekly-note');
if (sel && note) {
note.style.display = sel.value === 'weekly' ? 'block' : 'none';
}
}
function triggerCrossDriveBackup(stackName, btn) {
btn.disabled = true;
btn.textContent = 'Mentés folyamatban...';
fetch('/api/stacks/' + stackName + '/cross-backup/run', {method: 'POST', headers: csrfHeaders()})
.then(function(r) { return r.json(); })
.then(function(d) {
if (!d.ok) {
showAlert('Hiba: ' + (d.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Mentés most';
return;
}
btn.textContent = 'Mentés folyamatban...';
// Poll status
var poll = setInterval(function() {
fetch('/api/stacks/' + stackName + '/cross-backup/status')
.then(function(r) { return r.json(); })
.then(function(s) {
if (!s.ok || !s.data) return;
if (!s.data.running) {
clearInterval(poll);
var status = s.data.last_status;
if (status === 'ok') {
btn.textContent = 'Mentés kész';
} else {
btn.textContent = 'Hiba';
showAlert('Hiba: ' + (s.data.last_error || 'Ismeretlen hiba'));
}
setTimeout(function() { location.reload(); }, 2000);
}
}).catch(function(){});
}, 3000);
})
.catch(function(e) {
showAlert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Mentés most';
});
}
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';
}
// Check on page load
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
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 deleteStaleData(stackName, stalePath, btn) {
if (!confirm('Biztosan törölni szeretnéd a korábbi adatokat?\n\nTárhely: ' + stalePath + '\n\n⚠️ Ez a művelet visszavonhatatlan!\nElőtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.')) {
return;
}
// Second confirmation
if (!confirm('UTOLSÓ FIGYELMEZTETÉS!\n\nA törlés visszavonhatatlan. Biztosan folytatod?')) {
return;
}
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);
// Remove the stale data card from DOM
var item = btn.closest('.stale-data-item');
if (item) item.remove();
// If no more stale items, remove the whole section
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();
// Client-side validation: check all password fields are filled
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;
}
}
// Client-side validation: check password confirmation matches
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;
}
}
// Client-side validation: check subdomain format
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;
}
}
// Client-side validation: check all required fields
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 = '{{.Stack.Name}}';
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.textContent = '\u2705';
else if (status === 'error') icon.textContent = '\u274C';
else if (status === 'warn') icon.textContent = '\u26A0\uFE0F';
else if (status === 'active') icon.textContent = '\u23F3';
}
// Phase 1: Deploy request
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;
}
// Deploy API returned success — switch to progress view
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';
}
// Phase 2: Poll stack status
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) {
// Async compose-up failed
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') {
// Compose up in progress (pulling images, creating containers)
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>
{{template "layout_end" .}}
{{end}}