feat: 0.11.7 — Stale data cleanup + FileBrowser sync after migration + deploy title fix
- Detect stale data on non-active storage paths after migration; show on deploy/settings page with size info and two-step delete confirmation - Add POST /api/storage/stale-cleanup handler with safety checks (active path protection, registered-path validation, ProtectedHDDPaths guard) - Export ProtectedHDDPaths() from stacks package for reuse in web handlers - Sync FileBrowser mounts after successful app data migration - Deploy page title/h2 now shows "Beállítások" for already-deployed apps instead of always showing "Telepítés" - Also add delete-old-data button on migration-done card in migrate.html Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### What was just completed (2026-02-17 session 34)
|
||||||
|
- **v0.11.7 — Stale Data Cleanup + FileBrowser Sync + UI Title Fix:**
|
||||||
|
- **Feature: Stale data cleanup** — After app data migration, the deploy/settings page now shows leftover data on previous storage paths with size info and a delete button. Two-step confirmation required before deletion. Protected paths (storage root, media, Dokumentumok, appdata) cannot be deleted. Also available immediately after migration on the migration-done page.
|
||||||
|
- **Fix: FileBrowser sync after migration** — `syncFileBrowserMounts()` now called after successful data migration, ensuring FileBrowser mounts reflect the current storage layout.
|
||||||
|
- **Fix: Deploy page title** — Already-deployed apps now show "Beállítások" (Settings) instead of "Telepítés" (Deploy) in both the browser page title and the `<h2>` heading.
|
||||||
|
- **Internal: Exported `ProtectedHDDPaths()`** from stacks package for reuse in web handlers.
|
||||||
|
- **Files modified (7):** `internal/stacks/delete.go`, `internal/web/handlers.go`, `internal/web/storage_handlers.go`, `internal/web/templates/deploy.html`, `internal/web/templates/migrate.html`, `internal/web/templates/style.css`
|
||||||
|
|
||||||
### What was just completed (2026-02-17 session 33)
|
### What was just completed (2026-02-17 session 33)
|
||||||
- **v0.11.6 — FileBrowser Auto-Mount Sync + UI Polish (3 fixes):**
|
- **v0.11.6 — FileBrowser Auto-Mount Sync + UI Polish (3 fixes):**
|
||||||
- **Feature: FileBrowser auto-mount sync** — Added `syncFileBrowserMounts()` and `generateFileBrowserCompose()` to `handlers.go`. After a storage path is added (via storage init wizard) or removed, the controller regenerates `/opt/docker/stacks/filebrowser/docker-compose.yml` with volume mounts for all registered paths (`/mnt/hdd_1:/srv/hdd_1` etc.), then recreates the FileBrowser container. Domain is read from FileBrowser's `.env`. If FileBrowser isn't deployed, the function silently returns. The generated compose is self-contained (no env vars).
|
- **Feature: FileBrowser auto-mount sync** — Added `syncFileBrowserMounts()` and `generateFileBrowserCompose()` to `handlers.go`. After a storage path is added (via storage init wizard) or removed, the controller regenerates `/opt/docker/stacks/filebrowser/docker-compose.yml` with volume mounts for all registered paths (`/mnt/hdd_1:/srv/hdd_1` etc.), then recreates the FileBrowser container. Domain is read from FileBrowser's `.env`. If FileBrowser isn't deployed, the function silently returns. The generated compose is self-contained (no env vars).
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ type HDDPath struct {
|
|||||||
Exists bool `json:"exists"`
|
Exists bool `json:"exists"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// protectedHDDPaths returns the set of top-level HDD directories that must never be deleted.
|
// ProtectedHDDPaths returns the set of top-level HDD directories that must never be deleted.
|
||||||
func protectedHDDPaths(hddPath string) map[string]bool {
|
func ProtectedHDDPaths(hddPath string) map[string]bool {
|
||||||
if hddPath == "" {
|
if hddPath == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Handle HDD data
|
// Step 4: Handle HDD data
|
||||||
protected := protectedHDDPaths(hddPath)
|
protected := ProtectedHDDPaths(hddPath)
|
||||||
for _, mount := range hddMounts {
|
for _, mount := range hddMounts {
|
||||||
// Safety: never delete protected top-level dirs
|
// Safety: never delete protected top-level dirs
|
||||||
cleanPath := filepath.Clean(mount)
|
cleanPath := filepath.Clean(mount)
|
||||||
@@ -165,7 +165,7 @@ func (m *Manager) GetStackHDDData(name string) (*HDDDataResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mounts := ParseComposeHDDMounts(stack.ComposePath, hddPath)
|
mounts := ParseComposeHDDMounts(stack.ComposePath, hddPath)
|
||||||
protected := protectedHDDPaths(hddPath)
|
protected := ProtectedHDDPaths(hddPath)
|
||||||
|
|
||||||
for _, mount := range mounts {
|
for _, mount := range mounts {
|
||||||
cleanPath := filepath.Clean(mount)
|
cleanPath := filepath.Clean(mount)
|
||||||
|
|||||||
@@ -164,7 +164,11 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
|
|||||||
stack, _ := s.stackMgr.GetStack(name)
|
stack, _ := s.stackMgr.GetStack(name)
|
||||||
alreadyDeployed := appCfg != nil && appCfg.Deployed
|
alreadyDeployed := appCfg != nil && appCfg.Deployed
|
||||||
|
|
||||||
data := s.baseData("deploy", meta.DisplayName+" — Telepítés")
|
pageTitle := meta.DisplayName + " — Telepítés"
|
||||||
|
if alreadyDeployed {
|
||||||
|
pageTitle = meta.DisplayName + " — Beállítások"
|
||||||
|
}
|
||||||
|
data := s.baseData("deploy", pageTitle)
|
||||||
data["Stack"] = stack
|
data["Stack"] = stack
|
||||||
data["Meta"] = meta
|
data["Meta"] = meta
|
||||||
data["AppConfig"] = appCfg
|
data["AppConfig"] = appCfg
|
||||||
@@ -195,6 +199,11 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
|
|||||||
data["StorageInfo"] = storageInfo
|
data["StorageInfo"] = storageInfo
|
||||||
data["OtherStoragePaths"] = s.otherStoragePathsForStack(name)
|
data["OtherStoragePaths"] = s.otherStoragePathsForStack(name)
|
||||||
}
|
}
|
||||||
|
// Stale data from previous migrations (only for deployed apps with HDD data)
|
||||||
|
staleData := s.findStaleStorageData(name)
|
||||||
|
if len(staleData) > 0 {
|
||||||
|
data["StaleData"] = staleData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory info for deploy page (only for non-deployed apps)
|
// Memory info for deploy page (only for non-deployed apps)
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ func (s *Server) storageAPIHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.storageMigrateAPIHandler(w, r)
|
s.storageMigrateAPIHandler(w, r)
|
||||||
case path == "/api/storage/migrate/status" && r.Method == http.MethodGet:
|
case path == "/api/storage/migrate/status" && r.Method == http.MethodGet:
|
||||||
s.storageMigrateStatusAPIHandler(w, r)
|
s.storageMigrateStatusAPIHandler(w, r)
|
||||||
|
case path == "/api/storage/stale-cleanup" && r.Method == http.MethodPost:
|
||||||
|
s.staleDataCleanupHandler(w, r)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
@@ -452,6 +454,8 @@ func (s *Server) storageMigrateAPIHandler(w http.ResponseWriter, r *http.Request
|
|||||||
s.logger.Printf("[ERROR] Migration failed: stack=%s: %v", req.StackName, err)
|
s.logger.Printf("[ERROR] Migration failed: stack=%s: %v", req.StackName, err)
|
||||||
} else {
|
} else {
|
||||||
s.logger.Printf("[INFO] Migration complete: stack=%s → %s", req.StackName, req.TargetPath)
|
s.logger.Printf("[INFO] Migration complete: stack=%s → %s", req.StackName, req.TargetPath)
|
||||||
|
// Sync FileBrowser mounts (storage paths may now have new app data)
|
||||||
|
go s.syncFileBrowserMounts()
|
||||||
}
|
}
|
||||||
close(progressCh)
|
close(progressCh)
|
||||||
}()
|
}()
|
||||||
@@ -619,3 +623,189 @@ func (s *Server) storageLabelForPath(path string) string {
|
|||||||
}
|
}
|
||||||
return strings.TrimPrefix(path, "/mnt/")
|
return strings.TrimPrefix(path, "/mnt/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StaleStorageData describes leftover data on a non-active storage path.
|
||||||
|
type StaleStorageData struct {
|
||||||
|
Path string // e.g., "/mnt/hdd_placeholder"
|
||||||
|
Label string // e.g., "Külső tárhely (hdd_placeholder)"
|
||||||
|
Mounts []string // host-side paths with data
|
||||||
|
SizeHuman string // e.g., "48 MB"
|
||||||
|
SizeBytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// findStaleStorageData detects leftover app data on non-active storage paths.
|
||||||
|
// This happens after migration: the old data stays on the previous storage path.
|
||||||
|
func (s *Server) findStaleStorageData(stackName string) []StaleStorageData {
|
||||||
|
appCfg := s.stackMgr.LoadAppConfigByName(stackName)
|
||||||
|
if appCfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
currentHDDPath := appCfg.Env["HDD_PATH"]
|
||||||
|
if currentHDDPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stack, ok := s.stackMgr.GetStack(stackName)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []StaleStorageData
|
||||||
|
|
||||||
|
// Check all registered storage paths except the current one
|
||||||
|
for _, sp := range s.settings.GetStoragePaths() {
|
||||||
|
if sp.Path == currentHDDPath {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ParseComposeHDDMounts to find what dirs WOULD exist on this path
|
||||||
|
mounts := stacks.ParseComposeHDDMounts(stack.ComposePath, sp.Path)
|
||||||
|
if len(mounts) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check which mounts actually have data
|
||||||
|
var existingMounts []string
|
||||||
|
var totalSize int64
|
||||||
|
for _, m := range mounts {
|
||||||
|
info, err := os.Stat(m)
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
size := dirSizeInt64(m)
|
||||||
|
if size > 0 {
|
||||||
|
existingMounts = append(existingMounts, m)
|
||||||
|
totalSize += size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(existingMounts) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
label := sp.Label
|
||||||
|
if label == "" {
|
||||||
|
label = settings.InferStorageLabel(sp.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, StaleStorageData{
|
||||||
|
Path: sp.Path,
|
||||||
|
Label: label,
|
||||||
|
Mounts: existingMounts,
|
||||||
|
SizeHuman: dirSizeBytesHuman(totalSize),
|
||||||
|
SizeBytes: totalSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// staleDataCleanupHandler handles POST /api/storage/stale-cleanup.
|
||||||
|
// Deletes leftover app data from a previous storage path after migration.
|
||||||
|
func (s *Server) staleDataCleanupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
StackName string `json:"stack_name"`
|
||||||
|
StalePath string `json:"stale_path"` // the old storage root, e.g., "/mnt/hdd_placeholder"
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
jsonError(w, "Érvénytelen kérés", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.StackName == "" || req.StalePath == "" {
|
||||||
|
jsonError(w, "Hiányos paraméterek", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the app exists and is deployed
|
||||||
|
stack, ok := s.stackMgr.GetStack(req.StackName)
|
||||||
|
if !ok {
|
||||||
|
jsonError(w, "Alkalmazás nem található: "+req.StackName, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appCfg := s.stackMgr.LoadAppConfigByName(req.StackName)
|
||||||
|
if appCfg == nil || !appCfg.Deployed {
|
||||||
|
jsonError(w, "Az alkalmazás nincs telepítve", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHDDPath := appCfg.Env["HDD_PATH"]
|
||||||
|
if currentHDDPath == "" {
|
||||||
|
jsonError(w, "Az alkalmazásnak nincs HDD_PATH beállítva", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: StalePath must NOT be the current HDD_PATH
|
||||||
|
if req.StalePath == currentHDDPath {
|
||||||
|
jsonError(w, "Az aktív tárhely adatai nem törölhetők! Ez az alkalmazás aktuális adattárolója.", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: StalePath must be a registered storage path
|
||||||
|
found := false
|
||||||
|
for _, sp := range s.settings.GetStoragePaths() {
|
||||||
|
if sp.Path == req.StalePath {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
jsonError(w, "A megadott útvonal nem regisztrált adattároló", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find mounts to delete
|
||||||
|
mounts := stacks.ParseComposeHDDMounts(stack.ComposePath, req.StalePath)
|
||||||
|
if len(mounts) == 0 {
|
||||||
|
jsonError(w, "Nem találhatók törlendő adatok", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protected paths check
|
||||||
|
protected := stacks.ProtectedHDDPaths(req.StalePath)
|
||||||
|
|
||||||
|
var deleted []string
|
||||||
|
var errors []string
|
||||||
|
var totalFreed int64
|
||||||
|
|
||||||
|
for _, mountPath := range mounts {
|
||||||
|
cleanPath := filepath.Clean(mountPath)
|
||||||
|
|
||||||
|
// Safety: never delete protected top-level dirs
|
||||||
|
if protected != nil && protected[cleanPath] {
|
||||||
|
s.logger.Printf("[WARN] Refusing to delete protected HDD path: %s", cleanPath)
|
||||||
|
errors = append(errors, fmt.Sprintf("Védett útvonal, nem törölhető: %s", cleanPath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it actually exists and has data
|
||||||
|
info, err := os.Stat(cleanPath)
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
size := dirSizeInt64(cleanPath)
|
||||||
|
|
||||||
|
if err := os.RemoveAll(cleanPath); err != nil {
|
||||||
|
s.logger.Printf("[ERROR] Failed to remove stale data %s: %v", cleanPath, err)
|
||||||
|
errors = append(errors, fmt.Sprintf("Törlés sikertelen: %s — %v", cleanPath, err))
|
||||||
|
} else {
|
||||||
|
s.logger.Printf("[INFO] Removed stale data: %s (%s) for stack %s", cleanPath, dirSizeBytesHuman(size), req.StackName)
|
||||||
|
deleted = append(deleted, cleanPath)
|
||||||
|
totalFreed += size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deleted) == 0 && len(errors) > 0 {
|
||||||
|
jsonError(w, "Törlés sikertelen: "+strings.Join(errors, "; "), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(w, map[string]interface{}{
|
||||||
|
"ok": true,
|
||||||
|
"deleted": deleted,
|
||||||
|
"freed_human": dirSizeBytesHuman(totalFreed),
|
||||||
|
"errors": errors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div style="display:flex;align-items:center;gap:.5rem">
|
<div style="display:flex;align-items:center;gap:.5rem">
|
||||||
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
|
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
|
||||||
<h2>{{.Meta.DisplayName}} — Telepítés</h2>
|
<h2>{{.Meta.DisplayName}} — {{if .AlreadyDeployed}}Beállítások{{else}}Telepítés{{end}}</h2>
|
||||||
</div>
|
</div>
|
||||||
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline">ℹ️ Részletek</a>
|
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline">ℹ️ Részletek</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,6 +58,36 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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}}
|
{{end}}
|
||||||
|
|
||||||
{{if and (not .AlreadyDeployed) .MemoryInfo}}
|
{{if and (not .AlreadyDeployed) .MemoryInfo}}
|
||||||
@@ -236,6 +266,52 @@ function generatePassword(fieldId) {
|
|||||||
document.getElementById(fieldId).value = pass;
|
document.getElementById(fieldId).value = pass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data.ok) {
|
||||||
|
alert('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\n⚠️ Néhány hiba történt:\n' + data.errors.join('\n');
|
||||||
|
}
|
||||||
|
alert(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) {
|
||||||
|
alert('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) {
|
document.getElementById('deploy-form').addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,19 @@
|
|||||||
Az alkalmazás az új tárolóról fut.<br>
|
Az alkalmazás az új tárolóról fut.<br>
|
||||||
A régi adatok a korábbi helyen megmaradtak biztonsági másolatként.
|
A régi adatok a korábbi helyen megmaradtak biztonsági másolatként.
|
||||||
</p>
|
</p>
|
||||||
<div style="margin-top:1.5rem;display:flex;gap:.75rem">
|
<div class="alert alert-warning" style="margin-top:1rem">
|
||||||
<a href="/stacks" class="btn btn-primary">Alkalmazások megtekintése</a>
|
<strong>Javasolt lépések:</strong>
|
||||||
|
<ol style="margin:.5rem 0 0 1rem;padding:0">
|
||||||
|
<li>Ellenőrizd, hogy az alkalmazás megfelelően működik</li>
|
||||||
|
<li>Győződj meg róla, hogy minden adat megtalálható</li>
|
||||||
|
<li>Ha minden rendben, törölheted a korábbi adatokat</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:1.5rem;display:flex;gap:.75rem;flex-wrap:wrap">
|
||||||
|
<a href="/stacks/{{.Meta.Slug}}/deploy" class="btn btn-primary">Alkalmazások megtekintése</a>
|
||||||
|
<button id="migrate-delete-old-btn" class="btn btn-outline btn-danger" onclick="deleteOldMigrationData()" style="display:none">
|
||||||
|
🗑️ Korábbi adatok törlése
|
||||||
|
</button>
|
||||||
<a href="/settings" class="btn btn-outline">Beállítások</a>
|
<a href="/settings" class="btn btn-outline">Beállítások</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,6 +194,46 @@ function showMigDone() {
|
|||||||
document.getElementById('migrate-progress-card').style.display = 'none';
|
document.getElementById('migrate-progress-card').style.display = 'none';
|
||||||
document.getElementById('migrate-done-card').style.display = 'block';
|
document.getElementById('migrate-done-card').style.display = 'block';
|
||||||
document.getElementById('migrate-done-card').scrollIntoView({behavior:'smooth'});
|
document.getElementById('migrate-done-card').scrollIntoView({behavior:'smooth'});
|
||||||
|
// Show the delete button (old data is at the source path)
|
||||||
|
document.getElementById('migrate-delete-old-btn').style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteOldMigrationData() {
|
||||||
|
var oldPath = '{{.CurrentHDDPath}}';
|
||||||
|
if (!confirm('Biztosan törölni szeretnéd a korábbi adatokat?\n\nTárhely: ' + oldPath + '\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;
|
||||||
|
}
|
||||||
|
if (!confirm('UTOLSÓ FIGYELMEZTETÉS!\n\nA törlés visszavonhatatlan. Biztosan folytatod?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var btn = document.getElementById('migrate-delete-old-btn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Törlés folyamatban...';
|
||||||
|
|
||||||
|
fetch('/api/storage/stale-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({stack_name: stackName, stale_path: oldPath})
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data.ok) {
|
||||||
|
alert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '🗑️ Korábbi adatok törlése';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btn.textContent = '✅ Korábbi adatok törölve (' + (data.freed_human || '') + ')';
|
||||||
|
btn.classList.remove('btn-danger');
|
||||||
|
btn.classList.add('btn-outline');
|
||||||
|
btn.onclick = null;
|
||||||
|
})
|
||||||
|
.catch(function(e) {
|
||||||
|
alert('Hálózati hiba: ' + e.message);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '🗑️ Korábbi adatok törlése';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2231,3 +2231,37 @@ a.stat-card:hover {
|
|||||||
.storage-app-link:hover {
|
.storage-app-link:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
/* Stale data cleanup */
|
||||||
|
.deploy-stale-data {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--orange);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deploy-stale-data h4 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: var(--orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stale-data-item {
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 165, 0, 0.05);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stale-data-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--red);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user