v0.4.5: Add dedicated Backup page (Biztonsági mentés)
New /backups page with full backup system visibility: - Status overview cards (local/remote backup, DB count, repo size) - Schedule section with next-run times and retention policy - Database table with type, size, validation (table count), status - Snapshot history table with per-snapshot stats - Repository info card with paths, integrity status, remote placeholder - "Mentés most" button with auto-refresh polling - Empty state when backup not configured Backend: SnapshotRecord history (ring buffer), DumpValidation, ListDumpFiles, ListSnapshots, GetFullStatus, restic check tracking. Server accepts scheduler for next-run time calculation. Sidebar nav updated with 3rd item, dashboard backup card title clickable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,10 @@ package web
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
@@ -157,5 +160,145 @@ func (s *Server) templateFuncMap() template.FuncMap {
|
||||
return "available"
|
||||
}
|
||||
},
|
||||
"timeAgo": func(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "–"
|
||||
}
|
||||
loc, _ := time.LoadLocation("Europe/Budapest")
|
||||
if loc == nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
d := now.Sub(t.In(loc))
|
||||
switch {
|
||||
case d < time.Minute:
|
||||
return "most"
|
||||
case d < time.Hour:
|
||||
return fmt.Sprintf("%d perce", int(d.Minutes()))
|
||||
case d < 24*time.Hour:
|
||||
return fmt.Sprintf("%d órája", int(d.Hours()))
|
||||
case d < 48*time.Hour:
|
||||
return "tegnap"
|
||||
default:
|
||||
return fmt.Sprintf("%d napja", int(d.Hours()/24))
|
||||
}
|
||||
},
|
||||
"fmtTime": func(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "–"
|
||||
}
|
||||
loc, _ := time.LoadLocation("Europe/Budapest")
|
||||
if loc == nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
return t.In(loc).Format("2006-01-02 15:04")
|
||||
},
|
||||
"fmtTimeShort": func(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "–"
|
||||
}
|
||||
loc, _ := time.LoadLocation("Europe/Budapest")
|
||||
if loc == nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
lt := t.In(loc)
|
||||
now := time.Now().In(loc)
|
||||
if lt.Year() == now.Year() && lt.YearDay() == now.YearDay() {
|
||||
return lt.Format("15:04")
|
||||
}
|
||||
return lt.Format("01-02 15:04")
|
||||
},
|
||||
"dbTypeLabel": func(t backup.DBType) string {
|
||||
switch t {
|
||||
case backup.DBTypePostgres:
|
||||
return "PostgreSQL"
|
||||
case backup.DBTypeMariaDB:
|
||||
return "MariaDB"
|
||||
default:
|
||||
return string(t)
|
||||
}
|
||||
},
|
||||
"nextRunLabel": func(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "–"
|
||||
}
|
||||
loc, _ := time.LoadLocation("Europe/Budapest")
|
||||
if loc == nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
lt := t.In(loc)
|
||||
now := time.Now().In(loc)
|
||||
timeStr := lt.Format("15:04")
|
||||
if lt.Year() == now.Year() && lt.YearDay() == now.YearDay() {
|
||||
return "ma " + timeStr
|
||||
}
|
||||
if lt.Year() == now.Year() && lt.YearDay() == now.YearDay()+1 {
|
||||
return "holnap " + timeStr
|
||||
}
|
||||
return lt.Format("2006-01-02") + " " + timeStr
|
||||
},
|
||||
"pruneLabel": func(s string) string {
|
||||
switch strings.ToLower(s) {
|
||||
case "weekly":
|
||||
return "vasárnap"
|
||||
case "daily":
|
||||
return "naponta"
|
||||
case "sunday":
|
||||
return "vasárnap"
|
||||
default:
|
||||
return s
|
||||
}
|
||||
},
|
||||
"nextPruneLabel": func(schedule string) string {
|
||||
loc, _ := time.LoadLocation("Europe/Budapest")
|
||||
if loc == nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
var next time.Time
|
||||
switch strings.ToLower(schedule) {
|
||||
case "daily":
|
||||
next = now.Add(24 * time.Hour)
|
||||
default: // weekly/sunday
|
||||
daysUntilSunday := (7 - int(now.Weekday())) % 7
|
||||
if daysUntilSunday == 0 && now.Hour() >= 4 {
|
||||
daysUntilSunday = 7
|
||||
}
|
||||
next = now.AddDate(0, 0, daysUntilSunday)
|
||||
}
|
||||
return next.Format("2006-01-02")
|
||||
},
|
||||
"fmtDuration": func(d time.Duration) string {
|
||||
if d < time.Second {
|
||||
return "< 1s"
|
||||
}
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%ds", int(d.Seconds()))
|
||||
}
|
||||
return fmt.Sprintf("%dm %ds", int(d.Minutes()), int(d.Seconds())%60)
|
||||
},
|
||||
"fmtBytes": func(b int64) string {
|
||||
const (
|
||||
kb = 1024
|
||||
mb = 1024 * kb
|
||||
gb = 1024 * mb
|
||||
)
|
||||
switch {
|
||||
case b >= int64(gb):
|
||||
return fmt.Sprintf("%.1f GB", float64(b)/float64(gb))
|
||||
case b >= int64(mb):
|
||||
return fmt.Sprintf("%.1f MB", float64(b)/float64(mb))
|
||||
case b >= int64(kb):
|
||||
return fmt.Sprintf("%.1f KB", float64(b)/float64(kb))
|
||||
default:
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
},
|
||||
"shortID": func(id string) string {
|
||||
if len(id) > 8 {
|
||||
return id[:8]
|
||||
}
|
||||
return id
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
)
|
||||
@@ -181,3 +182,18 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, _ *http.Request, slug s
|
||||
|
||||
s.render(w, "app_info", data)
|
||||
}
|
||||
|
||||
func (s *Server) backupsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
data := s.baseData("backups", "Biztonsági mentés")
|
||||
|
||||
if s.backupMgr != nil {
|
||||
nextDBDump := scheduler.NextDailyRun(s.cfg.Backup.DBDumpSchedule)
|
||||
nextBackup := scheduler.NextDailyRun(s.cfg.Backup.ResticSchedule)
|
||||
fullStatus := s.backupMgr.GetFullStatus(nextDBDump, nextBackup)
|
||||
data["Backup"] = fullStatus
|
||||
} else {
|
||||
data["Backup"] = nil
|
||||
}
|
||||
|
||||
s.render(w, "backups", data)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
)
|
||||
@@ -21,6 +22,7 @@ type Server struct {
|
||||
stackMgr *stacks.Manager
|
||||
cpuCollector *system.CPUCollector
|
||||
backupMgr *backup.Manager
|
||||
scheduler *scheduler.Scheduler
|
||||
logger *log.Logger
|
||||
version string
|
||||
tmpl *template.Template
|
||||
@@ -29,12 +31,13 @@ type Server struct {
|
||||
sessionsMu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, logger *log.Logger, version string) *Server {
|
||||
func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, sched *scheduler.Scheduler, logger *log.Logger, version string) *Server {
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
stackMgr: stackMgr,
|
||||
cpuCollector: cpuCollector,
|
||||
backupMgr: backupMgr,
|
||||
scheduler: sched,
|
||||
logger: logger,
|
||||
version: version,
|
||||
sessions: make(map[string]*session),
|
||||
@@ -59,6 +62,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.dashboardHandler(w, r)
|
||||
case path == "/stacks":
|
||||
s.stacksHandler(w, r)
|
||||
case path == "/backups":
|
||||
s.backupsHandler(w, r)
|
||||
case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/logs"):
|
||||
name := strings.TrimPrefix(path, "/stacks/")
|
||||
name = strings.TrimSuffix(name, "/logs")
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
{{define "backups"}}
|
||||
{{template "layout_start" .}}
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Biztonsági mentés</h2>
|
||||
<span class="domain-badge">{{.Domain}}</span>
|
||||
</div>
|
||||
|
||||
{{if not .Backup}}
|
||||
<div class="backup-empty-state">
|
||||
<div class="backup-empty-icon">🛡</div>
|
||||
<h3>Biztonsági mentés nincs beállítva</h3>
|
||||
<p>A biztonsági mentés funkció nem aktív.<br>
|
||||
Kérjük, vegye fel a kapcsolatot a Felhom csapattal a beállításhoz.</p>
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
<!-- Section 1: Status overview cards -->
|
||||
<div class="stats-grid backup-page-cards">
|
||||
{{if .Backup.LastBackup}}
|
||||
{{if .Backup.LastBackup.Success}}
|
||||
<div class="stat-card stat-running">
|
||||
<div class="stat-value">✓</div>
|
||||
<div class="stat-label">Helyi mentés aktív</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="stat-card stat-stopped">
|
||||
<div class="stat-value">✗</div>
|
||||
<div class="stat-label">Helyi mentés sikertelen</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">–</div>
|
||||
<div class="stat-label">Helyi mentés</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="stat-card" style="border-left-color: var(--gray);">
|
||||
<div class="stat-value" style="background:var(--gray);-webkit-background-clip:text;background-clip:text;">–</div>
|
||||
<div class="stat-label">Távoli mentés<br><span class="relative-time">nincs beállítva</span></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-total">
|
||||
<div class="stat-value">
|
||||
{{if .Backup.LastDBDump}}{{len .Backup.LastDBDump.Results}}{{else}}{{len .Backup.DumpFiles}}{{end}}
|
||||
</div>
|
||||
<div class="stat-label">Adatbázis mentve</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-total">
|
||||
<div class="stat-value">
|
||||
{{if .Backup.RepoStats}}{{.Backup.RepoStats.TotalSize}}{{else}}–{{end}}
|
||||
</div>
|
||||
<div class="stat-label">Tároló méret
|
||||
{{if .Backup.RepoStats}}<br><span class="relative-time">{{.Backup.RepoStats.SnapshotCount}} pillanatkép</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Schedule -->
|
||||
<div class="schedule-card">
|
||||
<h3>Ütemezés</h3>
|
||||
<div class="schedule-rows">
|
||||
<div class="schedule-row">
|
||||
<span class="schedule-task">Adatbázis mentés</span>
|
||||
<span class="schedule-time">{{.Backup.DBDumpSchedule}}</span>
|
||||
<span class="schedule-next">Következő: {{nextRunLabel .Backup.NextDBDump}}</span>
|
||||
</div>
|
||||
<div class="schedule-row">
|
||||
<span class="schedule-task">Restic pillanatkép</span>
|
||||
<span class="schedule-time">{{.Backup.ResticSchedule}}</span>
|
||||
<span class="schedule-next">Következő: {{nextRunLabel .Backup.NextBackup}}</span>
|
||||
</div>
|
||||
<div class="schedule-row">
|
||||
<span class="schedule-task">Karbantartás</span>
|
||||
<span class="schedule-time">{{pruneLabel .Backup.PruneSchedule}}</span>
|
||||
<span class="schedule-next">Következő: {{nextPruneLabel .Backup.PruneSchedule}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-summary">
|
||||
{{if .Backup.LastBackup}}
|
||||
<div class="schedule-summary-row">
|
||||
<span>Utolsó sikeres mentés:</span>
|
||||
<span class="schedule-summary-value">{{fmtTime .Backup.LastBackup.LastRun}} ({{timeAgo .Backup.LastBackup.LastRun}})</span>
|
||||
</div>
|
||||
<div class="schedule-summary-row">
|
||||
<span>Mentés időtartam:</span>
|
||||
<span class="schedule-summary-value">{{fmtDuration .Backup.LastBackup.Duration}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="schedule-summary-row">
|
||||
<span>Utolsó sikeres mentés:</span>
|
||||
<span class="schedule-summary-value relative-time">Még nem futott</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="schedule-summary-row">
|
||||
<span>Megőrzés:</span>
|
||||
<span class="schedule-summary-value">{{.Backup.Retention.KeepDaily}} napi · {{.Backup.Retention.KeepWeekly}} heti · {{.Backup.Retention.KeepMonthly}} havi</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-actions">
|
||||
<button class="btn btn-sm btn-primary" onclick="triggerBackupFromPage()" id="backup-page-btn"
|
||||
{{if .Backup.Running}}disabled{{end}}>
|
||||
{{if .Backup.Running}}Mentés folyamatban...{{else}}Mentés most{{end}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3: Databases -->
|
||||
<div class="backup-section-card">
|
||||
<h3>Adatbázisok</h3>
|
||||
{{if or .Backup.DumpFiles .Backup.DiscoveredDBs}}
|
||||
<div class="backup-table-wrap">
|
||||
<table class="db-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alkalmazás</th>
|
||||
<th>Típus</th>
|
||||
<th>Méret</th>
|
||||
<th>Utolsó</th>
|
||||
<th>Érvényesítés</th>
|
||||
<th>Állapot</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if .Backup.LastDBDump}}
|
||||
{{range .Backup.LastDBDump.Results}}
|
||||
<tr>
|
||||
<td>{{.DB.StackName}}</td>
|
||||
<td><span class="db-type-badge db-type-{{.DB.DBType}}">{{dbTypeLabel .DB.DBType}}</span></td>
|
||||
<td class="mono">{{if .Error}}–{{else}}{{fmtBytes .Size}}{{end}}</td>
|
||||
<td class="mono">{{if .Error}}–{{else}}{{fmtTimeShort $.Backup.LastDBDump.LastRun}}{{end}}</td>
|
||||
<td>
|
||||
{{if .Error}}
|
||||
<span class="validation-badge validation-na">–</span>
|
||||
{{else if .Validation.Valid}}
|
||||
<span class="validation-badge validation-ok">{{.Validation.TableCount}} tábla</span>
|
||||
{{else}}
|
||||
<span class="validation-badge validation-fail" title="{{.Validation.Error}}">Hiba</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .Error}}
|
||||
<span class="validation-badge validation-fail" title="{{.Error}}">Hiba</span>
|
||||
{{else}}
|
||||
<span class="validation-badge validation-ok">OK</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{range .Backup.DumpFiles}}
|
||||
<tr>
|
||||
<td>{{.StackName}}</td>
|
||||
<td><span class="db-type-badge db-type-{{.DBType}}">{{dbTypeLabel .DBType}}</span></td>
|
||||
<td class="mono">{{fmtBytes .Size}}</td>
|
||||
<td class="mono">{{fmtTimeShort .ModTime}}</td>
|
||||
<td>
|
||||
{{if .Validation.Valid}}
|
||||
<span class="validation-badge validation-ok">{{.Validation.TableCount}} tábla</span>
|
||||
{{else if .Validation.Error}}
|
||||
<span class="validation-badge validation-fail" title="{{.Validation.Error}}">Hiba</span>
|
||||
{{else}}
|
||||
<span class="validation-badge validation-na">–</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td><span class="validation-badge validation-ok">OK</span></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="backup-table-empty">Nem található adatbázis mentés.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Section 4: Snapshots -->
|
||||
<div class="backup-section-card">
|
||||
<h3>Pillanatképek</h3>
|
||||
{{if .Backup.SnapshotHistory}}
|
||||
<div class="backup-table-wrap">
|
||||
<table class="snapshot-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Azonosító</th>
|
||||
<th>Időpont</th>
|
||||
<th>Méret</th>
|
||||
<th>Új fájl</th>
|
||||
<th>Változott</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Backup.SnapshotHistory}}
|
||||
<tr>
|
||||
<td class="mono">{{shortID .SnapshotID}}</td>
|
||||
<td class="mono">{{fmtTime .Time}}</td>
|
||||
<td class="mono">{{if .HasStats}}+{{.DataAdded}}{{else}}–{{end}}</td>
|
||||
<td class="mono">{{if .HasStats}}{{.FilesNew}}{{else}}–{{end}}</td>
|
||||
<td class="mono">{{if .HasStats}}{{.FilesChanged}}{{else}}–{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="snapshot-footer">
|
||||
Összesen: {{len .Backup.SnapshotHistory}} pillanatkép
|
||||
{{if .Backup.RepoStats}} · {{.Backup.RepoStats.TotalSize}}{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="backup-table-empty">Még nincs pillanatkép.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Section 5: Repository -->
|
||||
<div class="repo-card">
|
||||
<h3>Tároló</h3>
|
||||
<div class="repo-info-rows">
|
||||
<div class="repo-info-row">
|
||||
<span class="repo-label">Helyszín:</span>
|
||||
<span class="repo-value mono">{{.Backup.RepoPath}} (helyi)</span>
|
||||
</div>
|
||||
{{if .Backup.RepoStats}}
|
||||
<div class="repo-info-row">
|
||||
<span class="repo-label">Méret:</span>
|
||||
<span class="repo-value">{{.Backup.RepoStats.TotalSize}}</span>
|
||||
</div>
|
||||
<div class="repo-info-row">
|
||||
<span class="repo-label">Pillanatképek:</span>
|
||||
<span class="repo-value">{{.Backup.RepoStats.SnapshotCount}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="repo-info-row">
|
||||
<span class="repo-label">Integritás:</span>
|
||||
<span class="repo-value">
|
||||
{{if .Backup.LastCheckTime.IsZero}}
|
||||
<span class="relative-time">Még nem ellenőrzött</span>
|
||||
{{else if .Backup.LastCheckOK}}
|
||||
<span class="backup-status-ok">Rendben</span> <span class="relative-time">({{fmtTime .Backup.LastCheckTime}})</span>
|
||||
{{else}}
|
||||
<span class="backup-status-fail">Hiba</span> <span class="relative-time">({{fmtTime .Backup.LastCheckTime}})</span>
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-paths">
|
||||
<span class="repo-label">Mentett útvonalak:</span>
|
||||
<ul class="repo-path-list">
|
||||
{{range .Backup.BackupPaths}}
|
||||
<li class="mono">{{.}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="repo-remote">
|
||||
<span class="repo-label">Távoli másolat:</span>
|
||||
<div class="repo-remote-status">
|
||||
<span class="relative-time">Nincs beállítva</span>
|
||||
<span class="relative-time">(B2/S3/SFTP támogatás hamarosan)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{end}}
|
||||
|
||||
<script>
|
||||
function triggerBackupFromPage() {
|
||||
const btn = document.getElementById('backup-page-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Mentés indítása...';
|
||||
fetch('/api/backup/run', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
btn.textContent = 'Mentés folyamatban...';
|
||||
btn.classList.add('loading');
|
||||
startBackupPolling();
|
||||
} else {
|
||||
btn.textContent = data.error || 'Hiba';
|
||||
setTimeout(() => { btn.textContent = 'Mentés most'; btn.disabled = false; }, 3000);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btn.textContent = 'Hiba';
|
||||
setTimeout(() => { btn.textContent = 'Mentés most'; btn.disabled = false; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
function startBackupPolling() {
|
||||
const poll = setInterval(() => {
|
||||
fetch('/api/backup/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok && data.data && !data.data.running) {
|
||||
clearInterval(poll);
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Auto-poll if backup is already running on page load
|
||||
{{if .Backup}}{{if .Backup.Running}}
|
||||
startBackupPolling();
|
||||
{{end}}{{end}}
|
||||
</script>
|
||||
|
||||
{{template "layout_end" .}}
|
||||
{{end}}
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
{{if .BackupEnabled}}
|
||||
<div class="backup-status-card">
|
||||
<h3>Biztonsági mentés</h3>
|
||||
<h3><a href="/backups" class="backup-card-link">Biztonsági mentés</a></h3>
|
||||
{{if .BackupStatus}}
|
||||
<div class="backup-info-row">
|
||||
<span class="backup-label">Utolsó mentés:</span>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ul class="nav-links">
|
||||
<li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}">Vezérlőpult</a></li>
|
||||
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}">Alkalmazások</a></li>
|
||||
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}">Biztonsági mentés</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">v{{.Version}}</span>
|
||||
|
||||
@@ -1226,6 +1226,257 @@ a.stat-card:hover {
|
||||
.backup-status-fail { color: var(--red); }
|
||||
.backup-status-none { color: var(--text-muted); }
|
||||
|
||||
/* Dashboard backup card link */
|
||||
.backup-card-link {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.backup-card-link:hover {
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
/* --- Backup page --- */
|
||||
.backup-page-cards {
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
}
|
||||
|
||||
.backup-empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.backup-empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.backup-empty-state h3 {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.backup-empty-state p {
|
||||
color: var(--text-secondary);
|
||||
font-size: .9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.schedule-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.schedule-card h3 {
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.schedule-rows {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.schedule-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: .4rem 0;
|
||||
font-size: .85rem;
|
||||
border-bottom: 1px solid rgba(48, 54, 61, 0.5);
|
||||
}
|
||||
.schedule-row:last-child { border-bottom: none; }
|
||||
.schedule-task {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
min-width: 160px;
|
||||
}
|
||||
.schedule-time {
|
||||
color: var(--accent-light);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .8rem;
|
||||
min-width: 80px;
|
||||
}
|
||||
.schedule-next {
|
||||
color: var(--text-secondary);
|
||||
font-size: .8rem;
|
||||
}
|
||||
.schedule-summary {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: .75rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.schedule-summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: .25rem 0;
|
||||
font-size: .85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.schedule-summary-value {
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .8rem;
|
||||
}
|
||||
.schedule-actions {
|
||||
padding-top: .5rem;
|
||||
}
|
||||
|
||||
.backup-section-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.backup-section-card h3 {
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
.backup-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.db-table, .snapshot-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.db-table th, .snapshot-table th {
|
||||
text-align: left;
|
||||
padding: .5rem .75rem;
|
||||
color: var(--text-muted);
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.db-table td, .snapshot-table td {
|
||||
padding: .5rem .75rem;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid rgba(48, 54, 61, 0.3);
|
||||
}
|
||||
.db-table tr:nth-child(even), .snapshot-table tr:nth-child(even) {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
.db-table tr:nth-child(odd), .snapshot-table tr:nth-child(odd) {
|
||||
background: var(--bg-card);
|
||||
}
|
||||
.db-table td.mono, .snapshot-table td.mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.db-type-badge {
|
||||
display: inline-block;
|
||||
padding: .1rem .5rem;
|
||||
border-radius: 999px;
|
||||
font-size: .75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.db-type-postgres {
|
||||
background: rgba(0, 136, 204, 0.15);
|
||||
color: var(--accent-light);
|
||||
}
|
||||
.db-type-mariadb {
|
||||
background: rgba(210, 153, 34, 0.15);
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.validation-badge {
|
||||
display: inline-block;
|
||||
padding: .1rem .5rem;
|
||||
border-radius: 999px;
|
||||
font-size: .75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.validation-ok {
|
||||
background: var(--green-bg);
|
||||
color: var(--green);
|
||||
}
|
||||
.validation-fail {
|
||||
background: var(--red-bg);
|
||||
color: var(--red);
|
||||
cursor: help;
|
||||
}
|
||||
.validation-na {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.snapshot-footer {
|
||||
padding: .75rem .75rem 0;
|
||||
font-size: .8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.backup-table-empty {
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: .85rem;
|
||||
}
|
||||
|
||||
.repo-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.repo-card h3 {
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.repo-info-rows {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.repo-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: .3rem 0;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.repo-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.repo-value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.repo-value.mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .8rem;
|
||||
}
|
||||
.repo-paths {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: .75rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.repo-path-list {
|
||||
list-style: disc;
|
||||
padding-left: 1.5rem;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.repo-path-list li {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .8rem;
|
||||
color: var(--text-secondary);
|
||||
padding: .15rem 0;
|
||||
}
|
||||
.repo-remote {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: .75rem;
|
||||
}
|
||||
.repo-remote-status {
|
||||
margin-top: .25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .15rem;
|
||||
}
|
||||
|
||||
.relative-time {
|
||||
color: var(--text-muted);
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media(max-width: 768px) {
|
||||
.sidebar { width: 100%; height: auto; position: relative; border-right: none; border-bottom: 1px solid var(--border-color); }
|
||||
|
||||
Reference in New Issue
Block a user