Compare commits

...

4 Commits

Author SHA1 Message Date
admin 7df061c00f D0 Part 4: settings visual pass + remaining pages; grep gate zero
- settings.html: Aktív/Inaktív rows -> run-blue check / neutral gray
  (Inaktív no longer red), update states -> run/crit/progress with
  sprite check/x/spinner, pencil + cancel buttons -> icons, storage
  badges -> tags, host-disk bar + JS drive capBar -> meters
  (usageColorClass -> nominal/warn/crit), state-text-* consumers on the
  new suffixes incl. JS-built class names.
- debug.html, app_info.html, storage_init/attach.html, logs.html:
  emoji -> sprite icons or plain text in templates AND JS strings.
- catchall.html (standalone, no sprite): v2 token sweep of its inline
  style, status emoji -> inline SVGs; a stopped app renders neutral,
  not red.
- login.html: two-tone H1 (last word blue-bright).
- setup_hub_versions.html: stale var(--border,#30363d) fallback -> v2.
- Test Group F grep gate: all 34 banned patterns at ZERO across
  internal/web + internal/setup; Scenario E test now asserts the
  '--bg-0: #0A1220' token literal.
2026-07-02 14:38:51 +02:00
admin f100cef842 D0 Part 3: deploy + backups + monitoring in v2
- deploy.html: stacked memory bar -> meter track with two segments
  (committed solid blue, new app 35%-opacity blue), neutral threshold
  ticks; integration status badges -> tags; meta pills -> metarows;
  3-step deploy progress icons -> sprite (check/x/triangle-alert/
  spinning rotate-cw); emoji -> icons or plain text incl. the JS
  confirm string.
- backups.html: storage bars -> meters, disconnected drives render an
  empty track with a neutral 'Leválasztva' (no red), drive-state badges
  -> warn tags, checkmark/warning glyphs -> sprite icons, JS flash/info
  strings de-emojified.
- monitoring.html: bars -> meters in both template and JS-generated
  host-storage markup (usageColorClass now emits nominal/warn/crit);
  Chart.js palette per spec (cpu #2EA8F5, memory #8E7CE8, temp #E0A93E,
  load #5EC4B6 — categorical data series keep distinct hues); tooltip/
  grid/tick colors moved to v2 literals; mem-dist palette rebalanced.
- Templates repo-wide: legacy var(--*) names in inline styles and JS
  string literals renamed to v2 tokens (old names no longer resolve).
- style.css: memory-bar family rewritten for the two-segment meter,
  .ico-spin animation added (reduced-motion respected).
2026-07-02 14:32:00 +02:00
admin 5dc277f2b1 D0 Part 2: design system v2 — tokens, meter/tag/metarow, funcmap remap, dashboard + stacks
- style.css: navy token palette (--bg-0/1/2, --line, --text-1/2/3, --blue,
  --warn, --crit), single 2px radius, all box-shadows and the bg grid
  overlay removed, fonts via --font-ui/--font-data.
- Components: .meter (3px hairline track, blue nominal fill, neutral
  70/85 ticks, warn/crit flag), .tag (square 2px state chip + dot, pulse
  on progress, reduced-motion respected), .metarow (icon + text, no
  container), .panel/.list/.section-h primitives, boxless .stats with
  hairline dividers, buttons 2px (danger = crit outline).
- funcmap: stateColor -> run/progress/warn/neutral/off (stopped is
  neutral, NOT red — operator-approved exception-color change),
  usageColor/tempColor -> nominal/warn/crit (thresholds unchanged);
  stateLabel Hungarian copy untouched (guarded by test).
- layout.html: sprite nav icons (layout-grid/cloud/shield/cpu/wrench/
  settings), alert banner emoji -> triangle-alert/info icons.
- dashboard.html: meters with disk warn/crit flags (Fogyóban a hely /
  Kritikusan kevés hely), boxless stats (Leállítva 0 muted, >0 amber),
  single-panel stack list with 2px state edges, tags instead of badges,
  icon action buttons.
- stacks.html: state tag + metarow rows; catalog keeps its grid.
- setup minimalCSS retokened to v2 (drops GitHub-dark hexes).
- Tests: §8 truth tables for stateColor/usageColor/tempColor +
  stateLabel byte-identity guard (red-proven vs pre-change funcmap:
  stopped->red and 0->green failed as required).
2026-07-02 14:22:27 +02:00
admin b073cc474d D0 Part 1: vendored fonts + Lucide sprite + setup CSS fix
- Vendor Plus Jakarta Sans + JetBrains Mono as variable woff2 (latin +
  latin-ext) under internal/web/static/fonts/, embedded via go:embed and
  served at /static/fonts/ (font/woff2, immutable cache). Google Fonts
  @import replaced with @font-face rules preserving unicode-range —
  removes the CDN dependency that silently broke on offline nodes.
- Add templates/icons.html: vendored Lucide sprite (30 icons, symbol
  ids i-<name>), included at the top of <body> in layout.html.
- Fix setup wizard handleCSS: serve the embedded web.StyleCSS() instead
  of a dataDir-derived filesystem path that never exists in the
  container (production setup silently served minimalCSS). Fallback to
  minimalCSS only if the embedded read errors, with a WARN log.
- Tests: font route + StyleCSS accessor (web), Scenario E embedded-CSS
  test (setup; red-proven against the pre-fix handler).
2026-07-02 14:05:41 +02:00
32 changed files with 1475 additions and 1114 deletions
+51
View File
@@ -0,0 +1,51 @@
package setup
import (
"bytes"
"io"
"log"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
"gitea.dooplex.hu/admin/felhom-controller/internal/web"
)
// TestSetupCSSServesEmbedded (Scenario E, TASK-D0): the setup wizard must serve
// the web package's EMBEDDED stylesheet. The pre-fix handleCSS read a filesystem
// path derived from dataDir, which does not exist inside the container image, so
// production setup mode silently served the minimalCSS fallback.
func TestSetupCSSServesEmbedded(t *testing.T) {
// dataDir whose derived legacy path (internal/web/templates/style.css
// relative to its grandparent) cannot exist.
dataDir := filepath.Join(t.TempDir(), "nonexistent-nested", "data")
logger := log.New(io.Discard, "", 0)
s := NewServer(&config.Config{}, dataDir, logger, "test")
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/static/style.css", nil)
s.Handler().ServeHTTP(rec, req)
if rec.Code != 200 {
t.Fatalf("status = %d, want 200", rec.Code)
}
body := rec.Body.Bytes()
if len(body) <= 10000 {
t.Fatalf("body length = %d, want > 10000 (minimalCSS fallback served?)", len(body))
}
if !strings.Contains(string(body), "--bg-0: #0A1220") {
t.Error("served CSS is missing the v2 token block (--bg-0)")
}
embedded, err := web.StyleCSS()
if err != nil {
t.Fatalf("web.StyleCSS() error: %v", err)
}
if !bytes.Equal(body, embedded) {
t.Error("served CSS differs from the embedded web stylesheet")
}
if strings.Contains(string(body), ".setup-container") {
t.Error("minimalCSS fallback was served instead of the embedded stylesheet")
}
}
+17 -16
View File
@@ -189,11 +189,12 @@ func (s *Server) handleFailed(w http.ResponseWriter, r *http.Request) {
// --- Static Assets (reuse from web package embed) ---
func (s *Server) handleCSS(w http.ResponseWriter, r *http.Request) {
// Read the main style.css from the web package templates
cssPath := filepath.Join(filepath.Dir(s.dataDir), "..", "internal", "web", "templates", "style.css")
data, err := os.ReadFile(cssPath)
// Serve the web package's embedded stylesheet. The previous implementation
// read a filesystem path derived from dataDir, which does not exist inside
// the container image — setup mode silently fell back to minimalCSS.
data, err := web.StyleCSS()
if err != nil {
// Fallback: serve minimal CSS
s.logger.Printf("[WARN] Setup: embedded style.css unavailable (%v) — serving minimal fallback CSS", err)
w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Write([]byte(minimalCSS))
return
@@ -560,40 +561,40 @@ func isError(err, target error) bool {
return err != nil && strings.Contains(err.Error(), target.Error())
}
// Minimal CSS for when the main stylesheet can't be loaded
// Minimal CSS for when the main stylesheet can't be loaded (v2 tokens)
const minimalCSS = `
:root { --bg-primary: #0d1117; --bg-card: #1c2128; --text-primary: #e6edf3; --text-secondary: #8b949e; --accent-blue: #0088cc; --border: #30363d; --green: #238636; --red: #da3633; }
:root { --bg-primary: #0A1220; --bg-card: #0F1B2E; --text-primary: #EDF2F9; --text-secondary: #94A6BF; --accent-blue: #0083D8; --border: #22344F; --green: #0083D8; --red: #E5534B; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg-primary); color: var(--text-primary); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
.setup-container { max-width: 700px; margin: 0 auto; padding: 2rem 1.5rem; }
.setup-header { text-align: center; margin-bottom: 2rem; }
.setup-header img { width: 120px; margin-bottom: 1rem; }
.setup-header h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.setup-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; cursor: pointer; transition: border-color 0.2s; }
.setup-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 2px; padding: 1.5rem; margin-bottom: 1rem; cursor: pointer; transition: border-color 0.2s; }
.setup-card:hover { border-color: var(--accent-blue); }
.setup-card h3 { margin-bottom: 0.5rem; }
.setup-card p { color: var(--text-secondary); font-size: 0.9rem; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.25rem; font-size: 0.9rem; color: var(--text-secondary); }
.form-control { width: 100%; padding: 0.5rem 0.75rem; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 0.9rem; }
.btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.6rem 1.5rem; border-radius: 6px; border: none; font-size: 0.9rem; font-weight: 500; cursor: pointer; text-decoration: none; }
.form-control { width: 100%; padding: 0.5rem 0.75rem; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 2px; color: var(--text-primary); font-size: 0.9rem; }
.btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.6rem 1.5rem; border-radius: 2px; border: none; font-size: 0.9rem; font-weight: 500; cursor: pointer; text-decoration: none; }
.btn-primary { background: var(--green); color: #fff; }
.btn-primary:hover { background: #2ea043; }
.btn-primary:hover { background: #2EA8F5; }
.btn-outline { background: transparent; color: var(--text-secondary); border: 1px solid var(--border); }
.alert { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
.alert-error { background: rgba(218,54,51,0.15); color: #f85149; border: 1px solid rgba(218,54,51,0.3); }
.alert-info { background: rgba(0,136,204,0.15); color: #58a6ff; border: 1px solid rgba(0,136,204,0.3); }
.info-box { background: rgba(0,136,204,0.1); border: 1px solid rgba(0,136,204,0.2); border-radius: 6px; padding: 0.75rem 1rem; margin-bottom: 1.5rem; font-size: 0.85rem; color: var(--text-secondary); }
.alert { padding: 0.75rem 1rem; border-radius: 2px; margin-bottom: 1rem; font-size: 0.9rem; }
.alert-error { background: rgba(229,83,75,.12); color: #E5534B; border: 1px solid rgba(229,83,75,.3); }
.alert-info { background: rgba(0,131,216,.13); color: #2EA8F5; border: 1px solid rgba(0,131,216,.3); }
.info-box { background: rgba(0,131,216,.13); border: 1px solid rgba(0,131,216,.2); border-radius: 2px; padding: 0.75rem 1rem; margin-bottom: 1.5rem; font-size: 0.85rem; color: var(--text-secondary); }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; padding: 0.5rem 0.75rem; color: var(--text-secondary); font-size: 0.85rem; border-bottom: 1px solid var(--border); }
td { padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; }
.badge-ok { background: rgba(63,185,80,0.15); color: var(--green); }
.badge-error { background: rgba(218,54,51,0.15); color: var(--red); }
.badge-error { background: rgba(229,83,75,.12); color: var(--red); }
.spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid var(--border); border-top-color: var(--accent-blue); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.section { margin-bottom: 1.5rem; }
.section-header { cursor: pointer; padding: 0.75rem 1rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px; display: flex; justify-content: space-between; align-items: center; }
.section-header { cursor: pointer; padding: 0.75rem 1rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: 2px; display: flex; justify-content: space-between; align-items: center; }
.section-body { padding: 1rem; border: 1px solid var(--border); border-top: none; border-radius: 0 0 6px 6px; }
.step-list { list-style: none; }
.step-list li { padding: 0.5rem 0; display: flex; align-items: center; gap: 0.75rem; }
@@ -31,7 +31,7 @@
</thead>
<tbody>
{{range $i, $v := .Versions}}
<tr style="border-top: 1px solid var(--border, #30363d);">
<tr style="border-top: 1px solid var(--line);">
<td style="padding: 0.5rem;">
<input type="radio" name="version_id" value="{{$v.ID}}" {{if eq $i 0}}checked{{end}}>
</td>
@@ -58,7 +58,7 @@
if (step.status === 'done') icon = '<span class="step-done">&#10003;</span>';
else if (step.status === 'running') icon = '<span class="spinner"></span>';
else if (step.status === 'failed') icon = '<span class="step-failed">&#10007;</span>';
else icon = '<span style="color: var(--text-secondary);">&#9675;</span>';
else icon = '<span style="color: var(--text-2);">&#9675;</span>';
li.innerHTML = icon + ' ' + step.label;
if (step.error) li.innerHTML += '<br><small style="color: var(--red, #f85149);">' + step.error + '</small>';
list.appendChild(li);
+10
View File
@@ -7,3 +7,13 @@ var templateFS embed.FS
//go:embed static/chart.min.js
var chartJS []byte
//go:embed static/fonts/*.woff2
var fontFS embed.FS
// StyleCSS returns the embedded dashboard stylesheet. Exported so the setup
// wizard can serve the same CSS instead of reading a filesystem path that
// does not exist inside the container image.
func StyleCSS() ([]byte, error) {
return templateFS.ReadFile("templates/style.css")
}
+58
View File
@@ -0,0 +1,58 @@
package web
import (
"net/http/httptest"
"strings"
"testing"
)
// TestServeFontHandler asserts the vendored woff2 files are embedded and served
// with the right content type + immutable caching (design system v2, D0 Part 1.1).
func TestServeFontHandler(t *testing.T) {
s := &Server{}
for _, name := range []string{"pjs-latin.woff2", "pjs-latin-ext.woff2", "jbm-latin.woff2", "jbm-latin-ext.woff2"} {
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/static/fonts/"+name, nil)
s.serveFontHandler(rec, req, name)
if rec.Code != 200 {
t.Errorf("%s: status = %d, want 200", name, rec.Code)
}
if ct := rec.Header().Get("Content-Type"); ct != "font/woff2" {
t.Errorf("%s: Content-Type = %q, want font/woff2", name, ct)
}
if cc := rec.Header().Get("Cache-Control"); !strings.Contains(cc, "immutable") {
t.Errorf("%s: Cache-Control = %q, want immutable", name, cc)
}
// woff2 magic number: "wOF2"
if body := rec.Body.Bytes(); len(body) < 4 || string(body[:4]) != "wOF2" {
t.Errorf("%s: body is not a woff2 file (len=%d)", name, len(body))
}
}
// Unknown font → 404, and path traversal is neutralized by filepath.Base.
rec := httptest.NewRecorder()
s.serveFontHandler(rec, httptest.NewRequest("GET", "/static/fonts/nope.woff2", nil), "nope.woff2")
if rec.Code != 404 {
t.Errorf("unknown font: status = %d, want 404", rec.Code)
}
}
// TestStyleCSSAccessor asserts the exported accessor returns the embedded
// stylesheet (consumed by the setup wizard) with the vendored font-face rules
// and no CDN import.
func TestStyleCSSAccessor(t *testing.T) {
data, err := StyleCSS()
if err != nil {
t.Fatalf("StyleCSS() error: %v", err)
}
css := string(data)
if len(css) < 10000 {
t.Fatalf("StyleCSS() length = %d, want > 10000", len(css))
}
if !strings.Contains(css, "@font-face") || !strings.Contains(css, "/static/fonts/pjs-latin.woff2") {
t.Error("StyleCSS() missing vendored @font-face rules")
}
if strings.Contains(css, "fonts.googleapis"+".com") {
t.Error("StyleCSS() still references the Google Fonts CDN")
}
}
+24 -13
View File
@@ -49,20 +49,29 @@ func (s *Server) templateFuncMap() template.FuncMap {
loc := getTimezone()
return template.FuncMap{
// stateColor maps container states to design-system-v2 semantic tokens,
// consumed as class suffixes (tag-run, stack-state-run, state-text-run...).
// Exception-color principle: a customer-stopped app is neutral, NOT red —
// genuine failures surface via unhealthy state + the alert system.
"stateColor": func(state stacks.ContainerState) string {
switch state {
case stacks.StateRunning:
return "green"
return "run"
case stacks.StateStarting, stacks.StateDeploying:
return "orange"
return "progress"
case stacks.StateUnhealthy:
return "yellow"
case stacks.StateStopped, stacks.StateExited:
return "red"
return "warn"
case stacks.StateRestarting:
return "yellow"
// a restart loop is a problem, not progress
return "warn"
case stacks.StateStopped, stacks.StateExited:
return "neutral"
case stacks.StatePaused:
return "neutral"
case stacks.StateNotDeployed:
return "off"
default:
return "gray"
return "off"
}
},
"stateLabel": func(state stacks.ContainerState) string {
@@ -126,14 +135,15 @@ func (s *Server) templateFuncMap() template.FuncMap {
"appPageURL": func(slug string) string {
return s.cfg.AppPageURL(slug)
},
// usageColor: capacity meters are blue below 70%, amber 7085, red ≥85 (v2).
"usageColor": func(percent float64) string {
if percent >= 85 {
return "red"
return "crit"
}
if percent >= 70 {
return "yellow"
return "warn"
}
return "green"
return "nominal"
},
"fmtMB": func(mb uint64) string {
if mb >= 1024 {
@@ -171,14 +181,15 @@ func (s *Server) templateFuncMap() template.FuncMap {
}
return result
},
// tempColor: thresholds unchanged; outputs remapped to v2 tokens.
"tempColor": func(celsius float64) string {
if celsius > 75 {
return "red"
return "crit"
}
if celsius >= 60 {
return "yellow"
return "warn"
}
return "green"
return "nominal"
},
"fmtTemp": func(celsius float64) string {
return fmt.Sprintf("%.0f°C", celsius)
+113
View File
@@ -0,0 +1,113 @@
package web
import (
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// TASK-D0 §8 truth tables. These are the semantic contract of the v2 re-skin:
// every class suffix the templates key on comes from these three functions.
func TestStateColorTruthTable(t *testing.T) {
s := &Server{}
fm := s.templateFuncMap()
stateColor := fm["stateColor"].(func(stacks.ContainerState) string)
cases := []struct {
state stacks.ContainerState
want string
}{
{stacks.StateRunning, "run"},
{stacks.StateStarting, "progress"},
{stacks.StateDeploying, "progress"},
{stacks.StateUnhealthy, "warn"},
{stacks.StateRestarting, "warn"}, // a restart loop is a problem, not progress
{stacks.StateStopped, "neutral"}, // deliberately NOT red — customer-stopped is not an emergency
{stacks.StateExited, "neutral"},
{stacks.StatePaused, "neutral"},
{stacks.StateNotDeployed, "off"},
{stacks.ContainerState("bogus"), "off"},
}
for _, c := range cases {
if got := stateColor(c.state); got != c.want {
t.Errorf("stateColor(%q) = %q, want %q", c.state, got, c.want)
}
}
}
func TestUsageColorTruthTable(t *testing.T) {
s := &Server{}
fm := s.templateFuncMap()
usageColor := fm["usageColor"].(func(float64) string)
cases := []struct {
percent float64
want string
}{
{0, "nominal"},
{69.9, "nominal"},
{70, "warn"},
{84.9, "warn"},
{85, "crit"},
{100, "crit"},
}
for _, c := range cases {
if got := usageColor(c.percent); got != c.want {
t.Errorf("usageColor(%v) = %q, want %q", c.percent, got, c.want)
}
}
}
func TestTempColorTruthTable(t *testing.T) {
s := &Server{}
fm := s.templateFuncMap()
tempColor := fm["tempColor"].(func(float64) string)
// thresholds unchanged from v1: >75 crit, >=60 warn, else nominal
cases := []struct {
celsius float64
want string
}{
{40, "nominal"},
{59.9, "nominal"},
{60, "warn"},
{75, "warn"},
{75.1, "crit"},
{90, "crit"},
}
for _, c := range cases {
if got := tempColor(c.celsius); got != c.want {
t.Errorf("tempColor(%v) = %q, want %q", c.celsius, got, c.want)
}
}
}
// TestStateLabelUnchanged guards the Hungarian copy against accidental edits
// during the re-skin — labels must stay byte-identical to pre-D0.
func TestStateLabelUnchanged(t *testing.T) {
s := &Server{}
fm := s.templateFuncMap()
stateLabel := fm["stateLabel"].(func(stacks.ContainerState) string)
cases := []struct {
state stacks.ContainerState
want string
}{
{stacks.StateRunning, "Fut"},
{stacks.StateStarting, "Indulás..."},
{stacks.StateDeploying, "Telepítés..."},
{stacks.StateUnhealthy, "Nem egészséges"},
{stacks.StateStopped, "Leállítva"},
{stacks.StateExited, "Leállítva"},
{stacks.StateRestarting, "Újraindítás..."},
{stacks.StateNotDeployed, "Nincs telepítve"},
{stacks.StatePaused, "Szüneteltetve"},
{stacks.ContainerState("bogus"), "Ismeretlen"},
}
for _, c := range cases {
if got := stateLabel(c.state); got != c.want {
t.Errorf("stateLabel(%q) = %q, want %q", c.state, got, c.want)
}
}
}
+16
View File
@@ -322,6 +322,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.serveCSSHandler(w, r)
case path == "/static/chart.min.js":
s.serveChartJSHandler(w, r)
case strings.HasPrefix(path, "/static/fonts/"):
s.serveFontHandler(w, r, strings.TrimPrefix(path, "/static/fonts/"))
case path == "/static/felhom-logo.svg":
s.serveLogoHandler(w, r)
case path == "/static/favicon.svg":
@@ -489,6 +491,20 @@ func (s *Server) serveChartJSHandler(w http.ResponseWriter, r *http.Request) {
w.Write(chartJS)
}
// serveFontHandler serves the vendored woff2 files embedded in the binary
// (self-hosted fonts — no Google Fonts CDN dependency on offline nodes).
func (s *Server) serveFontHandler(w http.ResponseWriter, r *http.Request, filename string) {
filename = filepath.Base(filename)
data, err := fontFS.ReadFile("static/fonts/" + filename)
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "font/woff2")
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
w.Write(data)
}
func (s *Server) serveLogoHandler(w http.ResponseWriter, r *http.Request) {
// Try synced asset first (allows logo updates via Hub without rebuild)
if s.assetsSyncer != nil {
Binary file not shown.
Binary file not shown.
@@ -17,7 +17,7 @@
{{end}}
</select>
<div id="estimateBox" style="display:none;background:var(--bg-secondary);border-radius:8px;padding:1rem;margin-bottom:1.5rem">
<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>
@@ -26,15 +26,15 @@
<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(--border);padding-top:.5rem">
<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-muted)">(szabad: -)</span></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(--danger);margin-top:.5rem;font-weight:600"></div>
<div id="estWarning" style="display:none;color:var(--crit);margin-top:.5rem;font-weight:600"></div>
</div>
<h3>Jelszó (opcionális)</h3>
@@ -47,7 +47,7 @@
<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-muted);font-size:.85rem">Az adatok konzisztenciája érdekében javasolt leállítani az alkalmazást mentés közben.</div>
<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>
@@ -59,14 +59,14 @@
<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(--danger);margin-top:1rem;font-weight:600"></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(--success)">Kész!</h3>
<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-muted)"></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 &nearr;</a>
</div>
@@ -190,7 +190,7 @@ function renderSteps(steps) {
if (s.status === 'running') icon = '&#10227;';
if (s.status === 'done') icon = '&#10003;';
if (s.status === 'failed') icon = '&#10007;';
var cls = s.status === 'failed' ? 'color:var(--danger)' : s.status === 'done' ? 'color:var(--success)' : s.status === 'running' ? 'color:var(--primary)' : '';
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>';
@@ -10,8 +10,8 @@
{{if not .Bundles}}
<div class="card" style="max-width:700px">
<p style="color:var(--text-muted)">Nem található .fab csomag a regisztrált tárolókon.</p>
<p style="color:var(--text-muted);font-size:.85rem">Exportálj egy alkalmazást az alkalmazás oldaláról, vagy másolj egy .fab fájlt a <code>{tároló}/felhom-data/exports/</code> könyvtárba.</p>
<p style="color:var(--text-3)">Nem található .fab csomag a regisztrált tárolókon.</p>
<p style="color:var(--text-3);font-size:.85rem">Exportálj egy alkalmazást az alkalmazás oldaláról, vagy másolj egy .fab fájlt a <code>{tároló}/felhom-data/exports/</code> könyvtárba.</p>
</div>
{{else}}
<div class="card" style="max-width:900px">
@@ -52,11 +52,11 @@
<input type="password" id="importPassword" placeholder="Jelszó" style="flex:1;padding:.5rem">
<button class="btn btn-primary" onclick="loadManifest()">Megnyitás</button>
</div>
<div id="passwordError" style="display:none;color:var(--danger);margin-top:.5rem"></div>
<div id="passwordError" style="display:none;color:var(--crit);margin-top:.5rem"></div>
</div>
<div id="manifestInfo" style="display:none">
<div style="background:var(--bg-secondary);border-radius:8px;padding:1rem;margin-bottom:1rem">
<div style="background:var(--bg-2);border-radius:8px;padding:1rem;margin-bottom:1rem">
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem">
<span>Alkalmazás:</span>
<strong id="mfName">-</strong>
@@ -97,11 +97,11 @@
<div id="progressCard" class="card" style="max-width:700px;display:none">
<h3>Importálás folyamata</h3>
<div id="progressSteps"></div>
<div id="progressError" style="display:none;color:var(--danger);margin-top:1rem;font-weight:600"></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(--success)">Importálás kész!</h3>
<h3 style="color:var(--blue)">Importálás kész!</h3>
<p>Az alkalmazás sikeresen visszaállítva.</p>
<a id="doneLink" href="/stacks" class="btn btn-primary">Alkalmazások megtekintése</a>
</div>
@@ -267,7 +267,7 @@ function renderSteps(steps) {
if (s.status === 'running') icon = '&#10227;';
if (s.status === 'done') icon = '&#10003;';
if (s.status === 'failed') icon = '&#10007;';
var cls = s.status === 'failed' ? 'color:var(--danger)' : s.status === 'done' ? 'color:var(--success)' : s.status === 'running' ? 'color:var(--primary)' : '';
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>';
@@ -26,7 +26,7 @@
{{if .MissingStorageLabel}}
<div class="alert alert-warning" style="margin-top:1rem">
<strong> Hiányzó tárhely: {{.MissingStorageLabel}}</strong><br>
<strong><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Hiányzó tárhely: {{.MissingStorageLabel}}</strong><br>
Ennek az alkalmazásnak az adattárolója jelenleg nem elérhető, ezért le van állítva. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre.
</div>
{{end}}
@@ -73,7 +73,7 @@
</select>
<button class="btn btn-sm btn-outline" onclick="appMigrate('{{.Stack.Name}}','{{.Meta.DisplayName}}')">Áthelyezés</button>
</div>
<div id="app-migrate-progress" style="display:none;margin-top:.75rem;padding:.75rem;border:1px solid var(--accent);border-radius:6px;background:rgba(0,136,204,0.06)"></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 (Number(b||0)/1e9).toFixed(1)+' GB'; }
@@ -91,7 +91,7 @@ function appMigWatch(){
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==='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); });
@@ -146,12 +146,12 @@ function appMigrate(app,label){
<table class="initcred-table">
{{if .InitialCreds.Username}}
<tr>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-muted);white-space:nowrap">Felhasználónév</td>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">Felhasználónév</td>
<td><code class="initcred-user" style="user-select:all">{{.InitialCreds.Username}}</code></td>
</tr>
{{end}}
<tr>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-muted);white-space:nowrap;vertical-align:middle">Jelszó</td>
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap;vertical-align:middle">Jelszó</td>
<td style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
<code id="initcred-pw">••••••••••••</code>
<span id="initcred-pw-val" hidden>{{.InitialCreds.Password}}</span>
@@ -208,7 +208,7 @@ function icCopyPw(btn) {
if (!val) return;
navigator.clipboard.writeText(val).then(function () {
var orig = btn.textContent;
btn.textContent = 'Másolva';
btn.textContent = 'Másolva';
setTimeout(function () { btn.textContent = orig; }, 1500);
});
}
+27 -27
View File
@@ -28,32 +28,32 @@
<div class="storage-overview-grid">
<div class="storage-bars">
{{with $.SystemInfo}}
<div class="storage-item">
<div class="storage-item meter {{usageColor .DiskPercent}}">
<div class="storage-header">
<span class="storage-label">Rendszer (/)</span>
<span class="storage-value">{{fmtGB .DiskUsedGB}} / {{fmtGB .DiskTotalGB}} ({{printf "%.0f" .DiskPercent}}%)</span>
</div>
<div class="system-bar">
<div class="system-bar-fill {{usageColor .DiskPercent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .DiskPercent}}%"></div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.1f" .DiskPercent}}%"></div>
</div>
</div>
{{range $.StorageBars}}
{{if .Disconnected}}
<div class="storage-item storage-disconnected">
<div class="storage-item meter meter-empty storage-disconnected">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value badge-error" style="font-size:.75rem">Leválasztva</span>
<span class="storage-value"><span class="state-text-neutral">Leválasztva</span></span>
</div>
<div class="system-bar"><div class="system-bar-disconnected"></div></div>
<div class="meter-track"></div>
</div>
{{else}}
<div class="storage-item">
<div class="storage-item meter {{usageColor .Percent}}">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value">{{fmtGB .UsedGB}} / {{fmtGB .TotalGB}} ({{printf "%.0f" .Percent}}%)</span>
</div>
<div class="system-bar">
<div class="system-bar-fill {{usageColor .Percent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .Percent}}%"></div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.1f" .Percent}}%"></div>
</div>
</div>
{{end}}
@@ -107,7 +107,7 @@
{{if .CanTrigger}}
<div class="schedule-actions" style="margin-top:1rem">
<button class="btn btn-sm btn-primary" id="wg-backup-btn" onclick="triggerGuestBackup()" {{if .Running}}disabled{{end}}>Mentés most</button>
{{if .StopMode}}<span class="form-hint" style="margin-left:.5rem"> A mentés idejére az alkalmazások rövid időre leállnak.</span>
{{if .StopMode}}<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A mentés idejére az alkalmazások rövid időre leállnak.</span>
{{else}}<span class="form-hint" style="margin-left:.5rem">Pillanatkép-mód: az alkalmazások csak néhány másodpercre állnak le.</span>{{end}}
<div id="wg-backup-result" style="margin-top:.6rem"></div>
</div>
@@ -223,8 +223,8 @@
<div class="stat-label">Távoli mentés<br><span class="relative-time">külön hardveren (PBS)</span></div>
</div>
{{else}}
<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-card" style="border-left-color: var(--text-3);">
<div class="stat-value" style="background:var(--text-3);-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>
{{end}}
@@ -361,7 +361,7 @@
<span class="app-backup-row-name">{{.DisplayName}}</span>
<div class="app-backup-row-meta">
{{if .DriveDisconnected}}
<span class="badge badge-error" style="font-size:.7rem">Meghajtó leválasztva</span>
<span class="tag tag-warn">Meghajtó leválasztva</span>
{{else if .HasHDDData}}
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
<span class="mono app-backup-size" style="font-size:.8rem">{{.HDDSizeHuman}}</span>
@@ -382,13 +382,13 @@
<span class="tier-location">helyi</span>
{{if .Tier1LastRun}}
<span class="layer-last">Utolsó: {{.Tier1LastRun}}
{{if eq .Tier1LastStatus "ok"}}<span class="text-ok"></span>
{{else if eq .Tier1LastStatus "error"}}<span class="text-error"></span>{{end}}
{{if eq .Tier1LastStatus "ok"}}<span class="text-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg></span>
{{else if eq .Tier1LastStatus "error"}}<span class="text-error"><svg class="ico ico-sm"><use href="#i-x"/></svg></span>{{end}}
</span>
{{end}}
<span class="tier-contents">{{.BackupContents}}</span>
{{if and .HasDB (eq .Tier1DBStatus "error")}}
<span class="text-error" style="font-size:.8rem"> DB dump hiba</span>
<span class="text-error" style="font-size:.8rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> DB dump hiba</span>
{{end}}
</div>
<!-- Tier 2: Cross-drive backup (opt-in, different device) -->
@@ -402,7 +402,7 @@
{{else if and .Tier2Configured .Tier2DestDisconnected}}
<span class="layer-method" style="opacity:.6">rsync</span>
<span class="layer-dest" style="opacity:.6">→ {{.Tier2Dest}}</span>
<span class="badge badge-warn" style="font-size:.7rem">Cél meghajtó leválasztva</span>
<span class="tag tag-warn">Cél meghajtó leválasztva</span>
{{if .Tier2LastRun}}
<span class="layer-last" style="opacity:.6">Utolsó: {{.Tier2LastRun}}</span>
{{end}}
@@ -413,7 +413,7 @@
{{else if and .Tier2Configured .Tier2DestInactive}}
<span class="layer-method" style="opacity:.6">rsync</span>
<span class="layer-dest" style="opacity:.6">→ {{.Tier2Dest}}</span>
<span class="badge badge-warn" style="font-size:.7rem">Cél meghajtó inaktív</span>
<span class="tag tag-warn">Cél meghajtó inaktív</span>
{{if .Tier2LastRun}}
<span class="layer-last" style="opacity:.6">Utolsó: {{.Tier2LastRun}}</span>
{{end}}
@@ -439,8 +439,8 @@
<a href="/stacks/{{.StackName}}/backup" class="btn btn-xs btn-outline">Beállítás</a>
</div>
{{else}}
<span class="layer-auto-ok"> 1. mentés auto</span>
<span class="layer-unconfigured"> Nincs 2. (off-drive) másolat</span>
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> 1. mentés auto</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Nincs 2. (off-drive) másolat</span>
{{if .Tier2LastError}}
<span class="layer-reason" style="opacity:.85" title="A 2. mentés automatikus — külön beállítás nem kell">{{.Tier2LastError}}</span>
{{end}}
@@ -452,9 +452,9 @@
<!-- Tier 3: Remote backup (future) -->
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">3. mentés</span>
<span class="layer-badge" style="background:var(--bg-tertiary);color:var(--text-muted)">Hamarosan</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">Hamarosan</span>
<span class="tier-location">távoli (offsite)</span>
<span class="tier-contents" style="font-style:normal;color:var(--text-muted)">B2 / S3 / SFTP — hamarosan elérhető</span>
<span class="tier-contents" style="font-style:normal;color:var(--text-3)">B2 / S3 / SFTP — hamarosan elérhető</span>
</div>
</div>
{{if .Warnings}}
@@ -569,7 +569,7 @@
Még nincs mentés felhasználói adattal.
</div>
<div class="restore-warning">
A visszaállítás felülírja az alkalmazás jelenlegi adatait a kiválasztott mentés állapotával.
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A visszaállítás felülírja az alkalmazás jelenlegi adatait a kiválasztott mentés állapotával.
Az alkalmazás a folyamat során automatikusan leáll és újraindul.
</div>
<div class="restore-confirm">
@@ -581,7 +581,7 @@
<div class="restore-actions">
<button type="button" class="btn btn-sm btn-danger" id="restore-btn" disabled onclick="submitRestore()">Visszaállítás indítása</button>
</div>
<div style="margin-top: 1rem; text-align: center; border-top: 1px solid var(--border); padding-top: 1rem;">
<div style="margin-top: 1rem; text-align: center; border-top: 1px solid var(--line); padding-top: 1rem;">
<a href="/import" class="btn btn-sm btn-outline">Importálás mentett csomagból (.fab)</a>
</div>
</div>
@@ -679,7 +679,7 @@ function pollGuestBackup(out, btn) {
if (data.ok && data.data) {
var ph = data.data.phase;
out.innerHTML = '<span class="form-hint">Állapot: ' + ph + '</span>';
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">A rendszermentés elkészült.</div>'; setTimeout(() => window.location.reload(), 1500); }
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">A rendszermentés elkészült.</div>'; setTimeout(() => window.location.reload(), 1500); }
else if (ph === 'failed') { clearInterval(iv); out.innerHTML = '<div class="alert alert-error">A mentés sikertelen.</div>'; btn.disabled = false; }
}
})
@@ -746,10 +746,10 @@ function onRestoreAppChange() {
typeInfo.innerHTML = '🔄 Teljes visszaállítás: adatbázis + konfiguráció + felhasználói adatok a kiválasztott pillanatképből.';
typeInfo.className = 'restore-info';
} else if (hasDB) {
typeInfo.innerHTML = ' Adatbázis és konfiguráció visszaállítása — az alkalmazásnak nincs külön felhasználói adata.';
typeInfo.innerHTML = 'Adatbázis és konfiguráció visszaállítása — az alkalmazásnak nincs külön felhasználói adata.';
typeInfo.className = 'restore-info restore-info-partial';
} else {
typeInfo.innerHTML = ' Csak konfiguráció visszaállítása (compose fájlok, beállítások).';
typeInfo.innerHTML = 'Csak konfiguráció visszaállítása (compose fájlok, beállítások).';
typeInfo.className = 'restore-info restore-info-partial';
}
typeInfo.style.display = 'block';
+12 -12
View File
@@ -8,28 +8,28 @@
<link rel="icon" type="image/svg+xml" href="{{.ControllerURL}}/static/favicon.svg">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0d1117;color:#e6edf3;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center}
body{background:#0A1220;color:#EDF2F9;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center}
.container{text-align:center;max-width:480px;padding:2rem}
.logo{width:80px;height:auto;margin-bottom:1.5rem;opacity:.85}
.app-name{font-size:1.5rem;font-weight:600;margin-bottom:.5rem}
.status-text{color:#8b949e;font-size:1.1rem;margin-bottom:2rem;line-height:1.5}
.status-text{color:#94A6BF;font-size:1.1rem;margin-bottom:2rem;line-height:1.5}
.status-icon{font-size:2.5rem;margin-bottom:1rem}
.status-stopped .status-icon{color:#da3633}
.status-not_deployed .status-icon{color:#8b949e}
.status-unknown .status-icon{color:#8b949e}
.btn{display:inline-block;padding:.6rem 1.2rem;border-radius:6px;text-decoration:none;font-size:.9rem;font-weight:500;transition:all .2s}
.btn-primary{background:#0088cc;color:#fff}
.btn-primary:hover{background:#00aaff}
.btn-outline{border:1px solid #30363d;color:#8b949e;margin-left:.5rem}
.btn-outline:hover{border-color:#8b949e;color:#e6edf3}
.host-label{color:#6e7681;font-size:.85rem;margin-top:2rem}
.status-stopped .status-icon{color:#94A6BF}
.status-not_deployed .status-icon{color:#94A6BF}
.status-unknown .status-icon{color:#94A6BF}
.btn{display:inline-block;padding:.6rem 1.2rem;border-radius:2px;text-decoration:none;font-size:.9rem;font-weight:500;transition:all .2s}
.btn-primary{background:#0083D8;color:#fff}
.btn-primary:hover{background:#2EA8F5}
.btn-outline{border:1px solid #22344F;color:#94A6BF;margin-left:.5rem}
.btn-outline:hover{border-color:#94A6BF;color:#EDF2F9}
.host-label{color:#5E7392;font-size:.85rem;margin-top:2rem}
</style>
</head>
<body>
<div class="container status-{{.Status}}">
<img class="logo" src="{{.ControllerURL}}/static/felhom-logo.svg" alt="felhom.eu">
<div class="status-icon">
{{if eq .Status "stopped"}}⏸{{else if eq .Status "not_deployed"}}📦{{else}}🔍{{end}}
{{if eq .Status "stopped"}}<svg width="40" height="40" 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"/></svg>{{else if eq .Status "not_deployed"}}<svg width="40" height="40" 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"/></svg>{{else}}<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>{{end}}
</div>
{{if .AppName}}<div class="app-name">{{.AppName}}</div>{{end}}
<div class="status-text">{{.StatusText}}</div>
@@ -7,15 +7,15 @@
</div>
<div class="stats-grid">
<a href="/stacks?filter=running" class="stat-card stat-running">
<a href="/stacks?filter=running" class="stat-card">
<div class="stat-value">{{.RunningCount}}</div>
<div class="stat-label">Futó alkalmazás</div>
</a>
<a href="/stacks?filter=stopped" class="stat-card stat-stopped">
<a href="/stacks?filter=stopped" class="stat-card {{if gt .StoppedCount 0}}stat-warn{{else}}stat-zero{{end}}">
<div class="stat-value">{{.StoppedCount}}</div>
<div class="stat-label">Leállítva</div>
</a>
<a href="/stacks" class="stat-card stat-total">
<a href="/stacks" class="stat-card">
<div class="stat-value">{{.TotalCount}}</div>
<div class="stat-label">Összes alkalmazás</div>
</a>
@@ -24,65 +24,66 @@
{{if .SystemInfo.TotalMemMB}}
<div class="system-info-card">
<div class="system-info-items">
<div class="system-info-item">
<div class="system-info-item meter {{usageColor .SystemInfo.MemPercent}}">
<div class="system-info-header">
<span class="system-info-label">Memória</span>
<span class="system-info-value">{{fmtMB .SystemInfo.UsedMemMB}} / {{fmtMB .SystemInfo.TotalMemMB}} ({{printf "%.0f" .SystemInfo.MemPercent}}%)</span>
</div>
<div class="system-bar">
<div class="system-bar-fill system-bar-{{usageColor .SystemInfo.MemPercent}}" style="width:{{printf "%.0f" .SystemInfo.MemPercent}}%"></div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .SystemInfo.MemPercent}}%"></div>
</div>
</div>
<div class="system-info-item">
<div class="system-info-item meter {{usageColor .SystemInfo.CPUPercent}}">
<div class="system-info-header">
<span class="system-info-label">CPU</span>
<span class="system-info-value">{{printf "%.0f" .SystemInfo.CPUPercent}}%</span>
</div>
<div class="system-bar">
<div class="system-bar-fill system-bar-{{usageColor .SystemInfo.CPUPercent}}" style="width:{{printf "%.0f" .SystemInfo.CPUPercent}}%"></div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .SystemInfo.CPUPercent}}%"></div>
</div>
<div class="system-load-avg">Load: {{fmtLoad .SystemInfo.LoadAvg1}} / {{fmtLoad .SystemInfo.LoadAvg5}} / {{fmtLoad .SystemInfo.LoadAvg15}}</div>
</div>
{{if .SystemInfo.TemperatureCelsius}}
<div class="system-info-item system-info-item-compact">
<div class="system-info-header">
<span class="system-info-label">Hőmérséklet</span>
<span class="system-info-value">
<span class="temp-dot temp-dot-{{tempColor .SystemInfo.TemperatureCelsius}}"></span>
<span class="temp-value-pill temp-pill-{{tempColor .SystemInfo.TemperatureCelsius}}">{{fmtTemp .SystemInfo.TemperatureCelsius}}</span>
</span>
<span class="system-info-label"><svg class="ico ico-sm"><use href="#i-thermometer"/></svg> Hőmérséklet</span>
<span class="system-info-value temp-value state-text-{{tempColor .SystemInfo.TemperatureCelsius}}">{{fmtTemp .SystemInfo.TemperatureCelsius}}</span>
</div>
</div>
{{end}}
</div>
<div class="system-info-items" style="margin-top: 1rem;">
<div class="system-info-item">
{{$duc := usageColor .SystemInfo.DiskPercent}}
<div class="system-info-item meter {{$duc}}">
<div class="system-info-header">
<span class="system-info-label">Rendszer (/)</span>
<span class="system-info-value">{{fmtGB .SystemInfo.DiskUsedGB}} / {{fmtGB .SystemInfo.DiskTotalGB}} ({{printf "%.0f" .SystemInfo.DiskPercent}}%)</span>
</div>
<div class="system-bar">
<div class="system-bar-fill system-bar-{{usageColor .SystemInfo.DiskPercent}}" style="width:{{printf "%.0f" .SystemInfo.DiskPercent}}%"></div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .SystemInfo.DiskPercent}}%"></div>
</div>
{{if eq $duc "warn"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>Fogyóban a hely</div>{{else if eq $duc "crit"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>Kritikusan kevés hely</div>{{end}}
</div>
{{range .StorageBars}}
{{if .Disconnected}}
<div class="system-info-item storage-disconnected">
<div class="system-info-item meter meter-empty storage-disconnected">
<div class="system-info-header">
<span class="system-info-label">{{.Label}}</span>
<span class="system-info-value badge-error" style="font-size:.75rem">Leválasztva</span>
<span class="system-info-value"><span class="state-text-neutral">Leválasztva</span></span>
</div>
<div class="system-bar"><div class="system-bar-disconnected"></div></div>
<div class="meter-track"></div>
</div>
{{else}}
<div class="system-info-item">
{{$uc := usageColor .Percent}}
<div class="system-info-item meter {{$uc}}">
<div class="system-info-header">
<span class="system-info-label">{{.Label}}</span>
<span class="system-info-value">{{fmtGB .UsedGB}} / {{fmtGB .TotalGB}} ({{printf "%.0f" .Percent}}%)</span>
</div>
<div class="system-bar">
<div class="system-bar-fill system-bar-{{usageColor .Percent}}" style="width:{{printf "%.0f" .Percent}}%"></div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .Percent}}%"></div>
</div>
{{if eq $uc "warn"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>Fogyóban a hely</div>{{else if eq $uc "crit"}}<div class="meter-flag"><svg class="ico"><use href="#i-triangle-alert"/></svg>Kritikusan kevés hely</div>{{end}}
</div>
{{end}}
{{end}}
@@ -117,7 +118,7 @@
<span class="backup-label">Utolsó mentés:</span>
<span class="backup-value">
{{if .BackupStatus.Success}}
<span class="backup-status-ok">{{.BackupStatus.LastRun.Format "2006-01-02 15:04"}}</span>
<span>{{.BackupStatus.LastRun.Format "2006-01-02 15:04"}}</span>
{{else}}
<span class="backup-status-fail">Sikertelen</span>
{{end}}
@@ -155,27 +156,27 @@
</div>
</div>
<div class="stack-actions">
<span class="stack-state-label">{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="badge badge-orphaned">Elavult</span>{{end}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="badge badge-missing-storage" title="Az alkalmazás adattárolója nem elérhető. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre.">Hiányzó tárhely: {{$ms}}</span>{{end}}
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="badge badge-warn" title="A hálózati tárhely (NAS) jelenleg nem érhető el. Az alkalmazás fut; az adatok elérése a NAS visszatértével helyreáll.">Hálózati tárhely nem elérhető: {{$nw}}</span>{{end}}
{{if and .Deployed (routeUnpublished .State)}}<span class="badge badge-route-unpublished" title="A proxy (Traefik) csak egészséges konténerhez publikál nyilvános útvonalat. Amíg az alkalmazás nem egészséges, az URL 404-et ad, pedig a konténer fut.">URL nem elérhető</span>{{end}}
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="tag tag-warn">Elavult</span>{{end}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="Az alkalmazás adattárolója nem elérhető. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hiányzó tárhely: {{$ms}}</span>{{end}}
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="A hálózati tárhely (NAS) jelenleg nem érhető el. Az alkalmazás fut; az adatok elérése a NAS visszatértével helyreáll."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely nem elérhető: {{$nw}}</span>{{end}}
{{if and .Deployed (routeUnpublished .State)}}<span class="tag tag-warn" title="A proxy (Traefik) csak egészséges konténerhez publikál nyilvános útvonalat. Amíg az alkalmazás nem egészséges, az URL 404-et ad, pedig a konténer fut."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>URL nem elérhető</span>{{end}}
{{if .Protected}}
<span class="badge badge-protected">Védett</span>
<span class="tag"><svg class="ico ico-sm"><use href="#i-lock"/></svg>Védett</span>
{{if isOperational .State}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')"></button>
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')" title="Újraindítás"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
{{end}}
{{else if not .Deployed}}
<a href="/stacks/{{.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">Telepítés</a>
{{else}}
{{if isOperational .State}}
{{$subdomain := index $.Subdomains .Name}}
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">Megnyitás </a>{{end}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')"></button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')"></button>
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">Megnyitás <svg class="ico ico-sm"><use href="#i-external-link"/></svg></a>{{end}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')" title="Újraindítás"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')" title="Leállítás"><svg class="ico ico-sm"><use href="#i-square"/></svg></button>
{{else}}
<button class="btn btn-sm btn-success" onclick="stackAction(event, '{{.Name}}', 'start')"></button>
<button class="btn btn-sm btn-success" onclick="stackAction(event, '{{.Name}}', 'start')" title="Indítás"><svg class="ico ico-sm"><use href="#i-play"/></svg></button>
{{if not .Orphaned}}<button class="btn btn-sm btn-danger" onclick="removeStack('{{.Name}}')">Eltávolítás</button>{{end}}
{{end}}
<a href="/stacks/{{.Name}}/logs" class="btn btn-sm btn-outline">Napló</a>
+17 -17
View File
@@ -6,7 +6,7 @@
</div>
<div class="debug-banner">
Debug mód aktív — ez az oldal csak fejlesztéshez és hibaelhárításhoz
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Debug mód aktív — ez az oldal csak fejlesztéshez és hibaelhárításhoz
</div>
<!-- Section 1: System Diagnostic -->
@@ -171,7 +171,7 @@
<div id="dr-status"><span class="text-muted">Betöltés...</span></div>
<div class="debug-actions" style="margin-top:1rem">
<div class="debug-dr-danger">
<p style="color:var(--red);font-weight:600">Vészhelyzet szimuláció</p>
<p style="color:var(--crit);font-weight:600">Vészhelyzet szimuláció</p>
<p class="text-muted" style="font-size:.85rem;margin-bottom:.5rem">Ez törli az ügyfél azonosítót és újraindítja a controllert setup módba. A konfigurációs fájlok megmaradnak a meghajtókon.</p>
<div class="debug-form-row">
<label>Írja be "RESET" a megerősítéshez:</label>
@@ -417,7 +417,7 @@ function loadBackupStatus() {
if (!data.ok) { document.getElementById('backup-status').innerHTML = '<span class="text-muted">Nem elérhető</span>'; return; }
var d = data.data || {};
var html = '<div class="debug-kv-grid">';
html += '<span>Állapot:</span><span>' + (d.running ? '🔄 Fut' : 'Kész') + '</span>';
html += '<span>Állapot:</span><span>' + (d.running ? 'Fut...' : 'Kész') + '</span>';
if (d.last_db_dump) html += '<span>Utolsó DB dump:</span><span>' + fmtTime(d.last_db_dump) + '</span>';
if (d.last_backup) html += '<span>Utolsó restic:</span><span>' + fmtTime(d.last_backup) + '</span>';
if (d.snapshot_count !== undefined) html += '<span>Snapshotok:</span><span>' + d.snapshot_count + '</span>';
@@ -512,7 +512,7 @@ function loadSelfUpdateStatus() {
if (u.last_check) {
html += '<span>Jelenlegi:</span><span class="mono">' + u.last_check.current_version + '</span>';
html += '<span>Legújabb:</span><span class="mono">' + u.last_check.latest_version + '</span>';
html += '<span>Frissítés:</span><span>' + (u.last_check.update_available ? '<span class="state-text-green">Elérhető</span>' : 'Naprakész') + '</span>';
html += '<span>Frissítés:</span><span>' + (u.last_check.update_available ? '<span class="state-text-run">Elérhető</span>' : 'Naprakész') + '</span>';
}
html += '</div>';
document.getElementById('selfupdate-status').innerHTML = html;
@@ -531,7 +531,7 @@ function loadDRStatus() {
html += '<table class="info-table debug-table"><tr><th>Meghajtó</th><th>Cimke</th><th>Állapot</th><th>Utolsó módosítás</th><th>Fájlok</th></tr>';
d.drives.forEach(function(dr) {
var files = (dr.files || []).join(', ') || '-';
html += '<tr><td class="mono">' + dr.path + '</td><td>' + (dr.label||'-') + '</td><td>' + (dr.has_backup ? 'Van' : '❌ Nincs') + '</td><td>' + (dr.last_modified ? fmtTime(dr.last_modified) : '-') + '</td><td class="text-muted" style="font-size:.75rem">' + files + '</td></tr>';
html += '<tr><td class="mono">' + dr.path + '</td><td>' + (dr.label||'-') + '</td><td>' + (dr.has_backup ? 'Van' : '<span class="state-text-warn">Nincs</span>') + '</td><td>' + (dr.last_modified ? fmtTime(dr.last_modified) : '-') + '</td><td class="text-muted" style="font-size:.75rem">' + files + '</td></tr>';
});
html += '</table>';
} else {
@@ -681,8 +681,8 @@ function renderTelemetryDetail(data) {
for (var i = 0; i < apps.length; i++) {
var a = apps[i];
var errorClass = a.log_errors > 0 ? ' style="color:var(--red);font-weight:600"' : '';
var warnClass = a.log_warnings > 0 ? ' style="color:var(--yellow);font-weight:600"' : '';
var errorClass = a.log_errors > 0 ? ' style="color:var(--crit);font-weight:600"' : '';
var warnClass = a.log_warnings > 0 ? ' style="color:var(--warn);font-weight:600"' : '';
html += '<tr>';
html += '<td><strong>' + escapeHtml(a.display_name || a.app_name) + '</strong></td>';
html += '<td class="mono" style="font-size:.8rem">' + (a.containers||[]).map(escapeHtml).join(', ') + '</td>';
@@ -698,7 +698,7 @@ function renderTelemetryDetail(data) {
if (a.issues && a.issues.length > 0) {
for (var j = 0; j < a.issues.length; j++) {
var issue = a.issues[j];
var sevColor = issue.severity === 'error' ? 'var(--red)' : 'var(--yellow)';
var sevColor = issue.severity === 'error' ? 'var(--crit)' : 'var(--warn)';
html += '<tr style="font-size:.8rem;opacity:.85"><td></td>';
html += '<td colspan="6" style="padding-left:1.5rem"><span style="color:' + sevColor + '">' + escapeHtml(issue.severity.toUpperCase()) + '</span> ' + escapeHtml(issue.message) + '</td>';
html += '<td colspan="2">×' + (issue.count||0) + '</td></tr>';
@@ -731,11 +731,11 @@ function renderAppExportStatus(d) {
return;
}
var html = '<div class="debug-kv-grid">';
html += '<dt>Debug mód</dt><dd>' + (d.debug_enabled ? '<span class="state-text-green">Aktív</span>' : 'Inaktív') + '</dd>';
html += '<dt>Debug mód</dt><dd>' + (d.debug_enabled ? '<span class="state-text-run">Aktív</span>' : 'Inaktív') + '</dd>';
html += '<dt>Verzió</dt><dd class="mono">' + (d.version||'-') + '</dd>';
html += '<dt>Csomagok</dt><dd>' + (d.bundle_count||0) + ' db</dd>';
html += '<dt>Temp fájlok</dt><dd>' + (d.stale_temp_count||0) + ' db';
if (d.stale_temp_count > 0) html += ' <span style="color:var(--warning)"></span>';
if (d.stale_temp_count > 0) html += ' <span style="color:var(--warn)"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg></span>';
html += '</dd>';
html += '</div>';
@@ -746,7 +746,7 @@ function renderAppExportStatus(d) {
html += '<div class="debug-kv-grid">';
html += '<dt>Típus</dt><dd>' + (j.job_type||'-') + '</dd>';
html += '<dt>Stack</dt><dd>' + (j.display_name || j.stack_name || '-') + '</dd>';
html += '<dt>Állapot</dt><dd>' + (j.running ? '🔄 Fut' : j.done ? 'Kész' : 'Várakozik') + '</dd>';
html += '<dt>Állapot</dt><dd>' + (j.running ? 'Fut...' : j.done ? 'Kész' : 'Várakozik') + '</dd>';
if (j.error) html += '<dt>Hiba</dt><dd class="debug-result-error">' + escapeHtml(j.error) + '</dd>';
if (j.output_path) html += '<dt>Kimenet</dt><dd class="mono" style="font-size:.75rem">' + escapeHtml(j.output_path) + '</dd>';
if (j.output_size) html += '<dt>Méret</dt><dd>' + j.output_size + '</dd>';
@@ -754,8 +754,8 @@ function renderAppExportStatus(d) {
if (j.steps && j.steps.length > 0) {
html += '<div style="margin-top:.5rem">';
j.steps.forEach(function(s) {
var icon = s.status === 'done' ? '' : s.status === 'running' ? '' : s.status === 'failed' ? '✗' : '○';
var cls = s.status === 'failed' ? 'color:var(--danger)' : s.status === 'done' ? 'color:var(--success)' : s.status === 'running' ? 'color:var(--primary)' : 'color:var(--text-muted)';
var icon = s.status === 'done' ? '&#10003;' : s.status === 'running' ? '&#8635;' : s.status === 'failed' ? '&#215;' : '&#9675;';
var cls = s.status === 'failed' ? 'color:var(--crit)' : s.status === 'done' ? 'color:var(--blue)' : s.status === 'running' ? 'color:var(--blue)' : 'color:var(--text-3)';
html += '<div style="font-size:.85rem;padding:.1rem 0;' + cls + '">' + icon + ' ' + escapeHtml(s.label);
if (s.error) html += ' <span style="font-size:.8rem">(' + escapeHtml(s.error) + ')</span>';
html += '</div>';
@@ -769,14 +769,14 @@ function renderAppExportStatus(d) {
html += '<h4 style="margin-top:.75rem">Export könyvtárak</h4>';
html += '<table class="info-table debug-table"><tr><th>Útvonal</th><th>Cimke</th><th>Létezik</th></tr>';
d.export_dirs.forEach(function(dir) {
html += '<tr><td class="mono" style="font-size:.75rem">' + escapeHtml(dir.path) + '</td><td>' + (dir.label||'-') + '</td><td>' + (dir.exists ? '' : '') + '</td></tr>';
html += '<tr><td class="mono" style="font-size:.75rem">' + escapeHtml(dir.path) + '</td><td>' + (dir.label||'-') + '</td><td>' + (dir.exists ? 'igen' : '<span class="state-text-warn">nincs</span>') + '</td></tr>';
});
html += '</table>';
}
// Stale temp files
if (d.stale_temp_files && d.stale_temp_files.length > 0) {
html += '<h4 style="margin-top:.75rem;color:var(--warning)">Elavult temp fájlok</h4>';
html += '<h4 style="margin-top:.75rem;color:var(--warn)">Elavult temp fájlok</h4>';
html += '<ul style="font-size:.85rem;margin:0;padding-left:1.5rem">';
d.stale_temp_files.forEach(function(f) {
html += '<li class="mono" style="font-size:.75rem">' + escapeHtml(f) + '</li>';
@@ -836,8 +836,8 @@ function renderAppBundles(bundles) {
html += '<td>' + (b.size_human || '-') + '</td>';
html += '<td>' + escapeHtml(b.drive_label || b.drive_path) + '</td>';
html += '<td>' + (b.encrypted ? '🔒' : '-') + '</td>';
html += '<td>' + (b.has_db ? '' : '-') + '</td>';
html += '<td>' + (b.needs_hdd ? '' : '-') + '</td>';
html += '<td>' + (b.has_db ? 'igen' : '-') + '</td>';
html += '<td>' + (b.needs_hdd ? 'igen' : '-') + '</td>';
html += '<td class="mono" style="font-size:.7rem;max-width:200px;overflow:hidden;text-overflow:ellipsis">' + escapeHtml(b.path) + '</td>';
html += '</tr>';
});
+33 -33
View File
@@ -18,7 +18,7 @@
<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" 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>
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-info"/></svg> Részletek</a>
</div>
</div>
@@ -30,11 +30,11 @@
<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 class="metarows">
{{if .Meta.Resources.MemRequest}}<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~{{.Meta.Resources.MemRequest}}</span></span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>{{end}}
{{if .Meta.Resources.NeedsHDD}}<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>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
@@ -52,7 +52,7 @@
<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>
<span class="settings-value">{{.StorageInfo.Label}} <span class="mono" style="color:var(--text-2)">({{.StorageInfo.Path}})</span></span>
</div>
{{if .StorageInfo.DataSizeHuman}}
<div class="settings-row">
@@ -86,7 +86,7 @@
<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>
<span class="settings-value">{{.Label}} <span class="mono" style="color:var(--text-2)">({{.Path}})</span></span>
</div>
<div class="settings-row">
<span class="settings-label">Méret</span>
@@ -113,7 +113,7 @@
<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>
<a href="/backups" style="color:var(--blue)">Mentési állapot →</a>
</span>
</div>
</div>
@@ -127,18 +127,18 @@
</p>
{{range .Integrations}}
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--border)">
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
<div style="flex:1">
<strong>{{.Label}}</strong>
<p class="config-field-help" style="margin:0">{{.Description}}</p>
{{if not .TargetDeployed}}
<span class="badge badge-muted" style="margin-top:.25rem;display:inline-block">Nincs telepítve</span>
<span class="tag tag-off" style="margin-top:.25rem"><span class="dot"></span>Nincs telepítve</span>
{{else if not .TargetRunning}}
<span class="badge badge-orphaned" style="margin-top:.25rem;display:inline-block">Célalkalmazás leállítva</span>
<span class="tag tag-neutral" style="margin-top:.25rem"><span class="dot"></span>Célalkalmazás leállítva</span>
{{else if eq .Status "error"}}
<span class="badge badge-orphaned" style="margin-top:.25rem;display:inline-block">Hiba</span>
<span class="tag tag-crit" style="margin-top:.25rem"><span class="dot"></span>Hiba</span>
{{else if .Enabled}}
<span class="badge badge-ok" style="margin-top:.25rem;display:inline-block">Aktív</span>
<span class="tag tag-run" style="margin-top:.25rem"><span class="dot"></span>Aktív</span>
{{end}}
</div>
<label class="toggle">
@@ -433,7 +433,7 @@
<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="meter-track meter-track-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>
@@ -456,7 +456,7 @@
<form id="deploy-form" class="deploy-form">
{{if .DockerBelowReserve}}
<div class="alert alert-warning" style="margin-bottom:1rem">
Nincs elég szabad tárhely a telepítéshez. Jelenleg {{.DockerFreeHuman}} szabad, és a rendszer
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Nincs elég szabad tárhely a telepítéshez. Jelenleg {{.DockerFreeHuman}} szabad, és a rendszer
{{.DockerReserveHuman}} tartalékot tart fenn az alapszolgáltatások (vezérlő, proxy) védelmében.
A telepítés ezért átmenetileg le van tiltva — szabadítson fel helyet, vagy bővítse a tárhelyet.
</div>
@@ -474,7 +474,7 @@
{{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>
<label>{{.Label}} {{if eq .Type "secret"}}<span class="auto-generated-badge"><svg class="ico ico-sm"><use href="#i-check"/></svg> Automatikusan generálva</span>{{end}}</label>
{{if $val}}
{{if eq .Type "secret"}}
<div class="input-with-button">
@@ -569,11 +569,11 @@
</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 id="storage-space-warn" class="form-hint" style="color:var(--warn);display:none">
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A kiválasztott tárhely majdnem megtelt.
</div>
<div class="form-hint" style="margin-top:.4rem;opacity:.8">
A kiválasztott meghajtón az alkalmazás <strong>fájljai</strong> (média, dokumentumok) tárolódnak.
<svg class="ico ico-sm"><use href="#i-info"/></svg> A kiválasztott meghajtón az alkalmazás <strong>fájljai</strong> (média, dokumentumok) tárolódnak.
Az <strong>adatbázis a gyors belső SSD-n</strong> fut — és az alkalmazással együtt készül róla biztonsági mentés.
</div>
{{else}}
@@ -582,7 +582,7 @@
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>
<span class="form-hint" style="color:var(--warn)">Nincs regisztrált adattároló — adja meg kézzel az útvonalat</span>
{{end}}
{{else}}
<input type="text" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
@@ -612,15 +612,15 @@
<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-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konfiguráció mentése...</span>
</div>
<div class="deploy-step" id="step-containers">
<span class="step-icon">&#9203;</span>
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Konténer(ek) indítása...</span>
</div>
<div class="deploy-step" id="step-health">
<span class="step-icon">&#9203;</span>
<span class="step-icon"><svg class="ico"><use href="#i-rotate-cw"/></svg></span>
<span class="step-text">Alkalmazás inicializálása...</span>
</div>
</div>
@@ -690,15 +690,15 @@ function buildPostDeployCard(stackName) {
if (!val) continue;
if (isPassword) {
var credId = 'postdeploy-cred-' + i;
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-muted);white-space:nowrap">' + f.label + '</td>' +
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;display:flex;align-items:center;gap:.5rem">' +
'<code id="' + credId + '" style="font-family:var(--font-mono);letter-spacing:1px">••••••••••••</code>' +
'<code id="' + credId + '" style="font-family:var(--font-data);letter-spacing:1px">••••••••••••</code>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="togglePostDeployCred(this,\'' + credId + '\',\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Megjelenítés</button>' +
'<button type="button" class="btn btn-sm btn-outline" onclick="copyPostDeployCred(this,\'' + val.replace(/'/g, "\\'") + '\')" style="padding:.15rem .4rem;font-size:.75rem">Másolás</button>' +
'</td></tr>';
} else {
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-muted);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;font-family:var(--font-mono);user-select:all">' + val + '</td></tr>';
credRows += '<tr><td style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">' + f.label + '</td>' +
'<td style="padding:.25rem 0;font-family:var(--font-data);user-select:all">' + val + '</td></tr>';
}
}
}
@@ -790,7 +790,7 @@ function copyPostDeployCred(btn, val) {
}
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.')) {
if (!confirm('Biztosan törölni szeretnéd a korábbi adatokat?\n\nTárhely: ' + stalePath + '\n\nFIGYELEM: 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
@@ -913,10 +913,10 @@ document.getElementById('deploy-form').addEventListener('submit', async function
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';
if (status === 'done') icon.innerHTML = '<svg class="ico"><use href="#i-check"/></svg>';
else if (status === 'error') icon.innerHTML = '<svg class="ico"><use href="#i-x"/></svg>';
else if (status === 'warn') icon.innerHTML = '<svg class="ico"><use href="#i-triangle-alert"/></svg>';
else if (status === 'active') icon.innerHTML = '<svg class="ico ico-spin"><use href="#i-rotate-cw"/></svg>';
}
// Phase 1: Deploy request
@@ -0,0 +1,32 @@
{{define "icon_sprite"}}<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-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-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>
</svg>{{end}}
+21 -20
View File
@@ -10,24 +10,25 @@
<meta name="csrf-token" content="{{.CSRFToken}}">
<script>
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
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-secondary);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</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);}
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</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>
{{template "icon_sprite"}}
<nav class="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="sidebar-logo">
<span class="customer-name">{{.CustomerName}}</span>
</div>
<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>
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}">Rendszermonitor</a></li>
{{if .DebugMode}}<li><a href="/debug" class="{{if eq .Page "debug"}}active{{end}}">🔧 Debug</a></li>{{end}}
<li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés</a></li>
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}"><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
{{if .DebugMode}}<li><a href="/debug" class="{{if eq .Page "debug"}}active{{end}}"><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>{{end}}
</ul>
<div class="sidebar-bottom">
<a href="/settings" class="sidebar-settings-link {{if eq .Page "settings"}}active{{end}}">Beállítások</a>
<a href="/settings" class="sidebar-settings-link {{if eq .Page "settings"}}active{{end}}"><svg class="ico"><use href="#i-settings"/></svg>Beállítások</a>
<div class="sidebar-footer">
<span class="version">{{.Version}}</span>
{{if .AuthEnabled}}<a href="/logout" class="logout-link">Kijelentkezés ↗</a>{{end}}
@@ -40,7 +41,7 @@
{{range .Alerts}}
{{if and (not .Inline) (or (not .PageOnly) (pageMatch .PageOnly $.Page))}}
<div class="alert-banner alert-banner-{{.Level}}">
<span class="alert-icon">{{if eq .Level "error"}}🔴{{else if eq .Level "warning"}}🟡{{else}}️{{end}}</span>
<span class="alert-icon"><svg class="ico">{{if eq .Level "error"}}<use href="#i-triangle-alert"/>{{else if eq .Level "warning"}}<use href="#i-triangle-alert"/>{{else}}<use href="#i-info"/>{{end}}</svg></span>
<span class="alert-message">{{.Message}}</span>
{{if .Link}}<a href="{{.Link}}" class="alert-link">{{.LinkText}} →</a>{{end}}
</div>
@@ -155,7 +156,7 @@
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-secondary);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>' +
'<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">' +
@@ -164,7 +165,7 @@
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-secondary)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<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>';
}
}
@@ -189,15 +190,15 @@
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-secondary);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
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-secondary);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-secondary)">Az alkalmazás (' + name + ') törölve lett.</p>' +
'<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 {
@@ -227,7 +228,7 @@
// Section 1: Always removed
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-secondary);font-size:.85rem">' +
'<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>' +
@@ -265,7 +266,7 @@
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-secondary);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<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">' +
@@ -274,7 +275,7 @@
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-secondary)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<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>';
}
}
@@ -301,18 +302,18 @@
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-secondary);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
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-secondary);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
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-secondary);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-secondary)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
'<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 {
@@ -10,6 +10,7 @@
<body class="login-body">
<div class="login-card">
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="login-logo">
<h1 class="login-title">Otthoni <span class="title-accent">vezérlőpult</span></h1>
<p class="login-subtitle">{{.CustomerName}}</p>
{{if .Flash}}<div class="alert alert-info">{{.Flash}}</div>{{end}}
{{if .Error}}<div class="alert alert-error">{{.Error}}</div>{{end}}
+1 -1
View File
@@ -58,7 +58,7 @@
} else {
stopPolling();
indicator.className = 'logs-live-indicator logs-live-paused';
indicator.innerHTML = 'Szünetelve';
indicator.innerHTML = 'Szünetelve';
toggleBtn.textContent = 'Folytatás';
}
};
@@ -10,7 +10,7 @@
<div class="monitor-card" id="host-health-card">
<div class="monitor-card-header">
<h3>Szerver állapota (gazdagép)</h3>
<span class="sysinfo-value" id="host-health-updated" style="font-size:.75rem;color:var(--text-muted)"></span>
<span class="sysinfo-value" id="host-health-updated" style="font-size:.75rem;color:var(--text-3)"></span>
</div>
<div id="host-health-unavailable" class="monitoring-banner monitoring-banner-yellow" style="display:none">
A gazdagép metrikái jelenleg nem elérhetők.
@@ -79,32 +79,32 @@
<h3>Tárhely</h3>
<div class="storage-bars">
{{with .SystemInfo}}
<div class="storage-item">
<div class="storage-item meter {{usageColor .DiskPercent}}">
<div class="storage-header">
<span class="storage-label">Rendszer (/)</span>
<span class="storage-value">{{fmtGB .DiskUsedGB}} / {{fmtGB .DiskTotalGB}} ({{printf "%.0f" .DiskPercent}}%)</span>
</div>
<div class="system-bar">
<div class="system-bar-fill {{usageColor .DiskPercent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .DiskPercent}}%"></div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.1f" .DiskPercent}}%"></div>
</div>
</div>
{{range $.StorageBars}}
{{if .Disconnected}}
<div class="storage-item storage-disconnected">
<div class="storage-item meter meter-empty storage-disconnected">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value badge-error" style="font-size:.75rem">Leválasztva</span>
<span class="storage-value"><span class="state-text-neutral">Leválasztva</span></span>
</div>
<div class="system-bar"><div class="system-bar-disconnected"></div></div>
<div class="meter-track"></div>
</div>
{{else}}
<div class="storage-item">
<div class="storage-item meter {{usageColor .Percent}}">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value">{{fmtGB .UsedGB}} / {{fmtGB .TotalGB}} ({{printf "%.0f" .Percent}}%)</span>
</div>
<div class="system-bar">
<div class="system-bar-fill {{usageColor .Percent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .Percent}}%"></div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.1f" .Percent}}%"></div>
</div>
{{if .Purpose}}<div class="storage-purpose" style="font-size:.72rem;opacity:.65;margin-top:.2rem">{{.Purpose}}</div>{{end}}
</div>
@@ -259,10 +259,10 @@
// --- Chart.js dark theme defaults ---
var colors = {
cpu: {border: '#0088cc', bg: 'rgba(0,136,204,0.1)'},
memory: {border: '#238636', bg: 'rgba(35,134,54,0.1)'},
temp: {border: '#d29922', bg: 'rgba(210,153,34,0.1)'},
load: {border: '#db6d28', bg: 'rgba(219,109,40,0.1)'}
cpu: {border: '#2EA8F5', bg: 'rgba(46,168,245,0.1)'},
memory: {border: '#8E7CE8', bg: 'rgba(142,124,232,0.1)'},
temp: {border: '#E0A93E', bg: 'rgba(224,169,62,0.1)'},
load: {border: '#5EC4B6', bg: 'rgba(94,196,182,0.1)'}
};
// --- Range helper: returns milliseconds for a range string ---
@@ -294,10 +294,10 @@
plugins: {
legend: {display: false},
tooltip: {
backgroundColor: '#1c2128',
titleColor: '#e6edf3',
bodyColor: '#8b949e',
borderColor: '#30363d',
backgroundColor: '#0F1B2E',
titleColor: '#EDF2F9',
bodyColor: '#94A6BF',
borderColor: '#22344F',
borderWidth: 1,
callbacks: {
title: function(items) {
@@ -319,9 +319,9 @@
type: 'linear',
min: now - defaultRangeMs,
max: now,
grid: {color: 'rgba(48,54,61,0.5)'},
grid: {color: 'rgba(34,52,79,0.5)'},
ticks: {
color: '#8b949e',
color: '#94A6BF',
maxTicksLimit: 8,
callback: function(v) {
return formatTimeLabel(v);
@@ -329,10 +329,10 @@
}
},
y: {
grid: {color: 'rgba(48,54,61,0.5)'},
ticks: {color: '#8b949e'},
grid: {color: 'rgba(34,52,79,0.5)'},
ticks: {color: '#94A6BF'},
beginAtZero: beginAtZero !== false,
title: {display: !!yLabel, text: yLabel || '', color: '#6e7681', font: {size: 11}}
title: {display: !!yLabel, text: yLabel || '', color: '#5E7392', font: {size: 11}}
}
}
};
@@ -481,23 +481,23 @@
plugins: {
legend: {display: false},
tooltip: {
backgroundColor: '#1c2128',
titleColor: '#e6edf3',
bodyColor: '#8b949e',
borderColor: '#30363d',
backgroundColor: '#0F1B2E',
titleColor: '#EDF2F9',
bodyColor: '#94A6BF',
borderColor: '#22344F',
borderWidth: 1
}
},
scales: {
x: {
grid: {color: 'rgba(48,54,61,0.5)'},
ticks: {color: '#8b949e'},
grid: {color: 'rgba(34,52,79,0.5)'},
ticks: {color: '#94A6BF'},
beginAtZero: true,
title: {display: true, text: xLabel, color: '#6e7681', font: {size: 11}}
title: {display: true, text: xLabel, color: '#5E7392', font: {size: 11}}
},
y: {
grid: {display: false},
ticks: {color: '#8b949e', font: {size: 12}}
ticks: {color: '#94A6BF', font: {size: 12}}
}
},
onClick: function(evt, elements) {
@@ -513,12 +513,12 @@
chartContainerCPU = new Chart(document.getElementById('chart-container-cpu'), {
type: 'bar',
data: {labels: [], datasets: [{data: [], backgroundColor: '#0088cc', borderRadius: 4}]},
data: {labels: [], datasets: [{data: [], backgroundColor: '#2EA8F5', borderRadius: 2}]},
options: barOpts('%')
});
chartContainerMem = new Chart(document.getElementById('chart-container-mem'), {
type: 'bar',
data: {labels: [], datasets: [{data: [], backgroundColor: '#238636', borderRadius: 4}]},
data: {labels: [], datasets: [{data: [], backgroundColor: '#8E7CE8', borderRadius: 2}]},
options: barOpts('MB')
});
}
@@ -560,7 +560,7 @@
}
}
var memDistPalette = ['#238636','#0088cc','#d29922','#da3633','#8b5cf6','#ec6547','#2ea043','#1f6feb','#e3b341','#f47067'];
var memDistPalette = ['#2EA8F5','#8E7CE8','#5EC4B6','#E0A93E','#D67BB5','#7FA6E8','#59B88C','#E8935E','#6FCADF','#B3A1F0'];
async function buildMemoryDistributionBar(containers) {
var totalMB = {{.SystemInfo.TotalMemMB}};
@@ -608,12 +608,12 @@
// OS / system overhead
if (osMB > 10) {
var osPct = (osMB / totalMB * 100).toFixed(2);
html += '<div class="memory-bar-segment" style="width:' + osPct + '%;background:var(--text-muted);opacity:0.5" title="Rendszer: ' + fmtMB(osMB) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--text-muted);opacity:0.5"></span>Rendszer (' + fmtMB(osMB) + ')</div>';
html += '<div class="memory-bar-segment" style="width:' + osPct + '%;background:var(--text-3);opacity:0.5" title="Rendszer: ' + fmtMB(osMB) + '"></div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--text-3);opacity:0.5"></span>Rendszer (' + fmtMB(osMB) + ')</div>';
}
// Free space legend only
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--bg-secondary);border:1px solid var(--border-color)"></span>Szabad (' + fmtMB(freeMB) + ')</div>';
legendHtml += '<div class="memory-legend-item"><span class="memory-legend-dot" style="background:var(--bg-2);border:1px solid var(--line)"></span>Szabad (' + fmtMB(freeMB) + ')</div>';
bar.innerHTML = html;
legend.innerHTML = legendHtml;
@@ -688,9 +688,9 @@
// HOST (PROXMOX BOX) HEALTH — slice 9, proxied from the host agent
// =============================================
function usageColorClass(pct) {
if (pct >= 85) return 'system-bar-red';
if (pct >= 70) return 'system-bar-yellow';
return 'system-bar-green';
if (pct >= 85) return 'crit';
if (pct >= 70) return 'warn';
return 'nominal';
}
function fmtBytesGB(bytes) {
@@ -745,21 +745,21 @@
var bars = document.getElementById('host-storage-bars');
var targets = Array.isArray(d.storage_targets) ? d.storage_targets : [];
if (!targets.length) {
bars.innerHTML = '<div class="sysinfo-value" style="color:var(--text-muted)">Nincs tároló adat.</div>';
bars.innerHTML = '<div class="sysinfo-value" style="color:var(--text-3)">Nincs tároló adat.</div>';
} else {
var html = '';
targets.forEach(function(t) {
// Friendly label (server-supplied) with the raw PVE storage id shown muted for clarity.
var friendly = escapeHtml(t.label || t.name || '');
var rawId = escapeHtml(t.name || '');
var idHtml = rawId ? ' <span style="color:var(--text-muted);font-size:.72rem">(' + rawId + ')</span>' : '';
var idHtml = rawId ? ' <span style="color:var(--text-3);font-size:.72rem">(' + rawId + ')</span>' : '';
var label = friendly + idHtml;
var purposeHtml = t.purpose ? '<div class="storage-purpose" style="font-size:.72rem;opacity:.65;margin-top:.2rem">' + escapeHtml(t.purpose) + '</div>' : '';
if (t.state && t.state !== 'attached') {
html += '<div class="storage-item storage-disconnected">' +
html += '<div class="storage-item meter meter-empty storage-disconnected">' +
'<div class="storage-header"><span class="storage-label">' + label + '</span>' +
'<span class="storage-value badge-error" style="font-size:.75rem">Nem elérhető</span></div>' +
'<div class="system-bar"><div class="system-bar-disconnected"></div></div>' + purposeHtml + '</div>';
'<span class="storage-value"><span class="state-text-neutral">Nem elérhető</span></span></div>' +
'<div class="meter-track"></div>' + purposeHtml + '</div>';
return;
}
var pct = (t.used_fraction != null ? t.used_fraction * 100 : 0);
@@ -772,13 +772,13 @@
if (t.smart.temperature_c != null) extra.push(t.smart.temperature_c + ' °C');
if (t.smart.percentage_used != null) extra.push('kopás: ' + t.smart.percentage_used + '%');
}
var extraHtml = extra.length ? ' <span style="color:var(--text-muted);font-size:.75rem">· ' + escapeHtml(extra.join(' · ')) + '</span>' : '';
html += '<div class="storage-item">' +
var extraHtml = extra.length ? ' <span style="color:var(--text-3);font-size:.75rem">· ' + escapeHtml(extra.join(' · ')) + '</span>' : '';
html += '<div class="storage-item meter ' + usageColorClass(pct) + '">' +
'<div class="storage-header">' +
'<span class="storage-label">' + label + extraHtml + '</span>' +
'<span class="storage-value">' + fmtBytesGB(t.used_bytes) + ' / ' + fmtBytesGB(t.total_bytes) +
' (' + Math.round(pct) + '%)</span></div>' +
'<div class="system-bar"><div class="system-bar-fill ' + usageColorClass(pct) +
'<div class="meter-track"><div class="meter-fill' +
'" style="width:' + Math.min(100, pct).toFixed(1) + '%"></div></div>' + purposeHtml + '</div>';
});
bars.innerHTML = html;
+30 -29
View File
@@ -34,7 +34,7 @@
</div>
<div class="settings-row">
<span class="settings-label">Biztonsági mentés</span>
<span class="settings-value">{{if .BackupEnabled}}<span class="state-text-green"> Aktív</span>{{else}}<span class="state-text-red">Inaktív</span>{{end}}</span>
<span class="settings-value">{{if .BackupEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}<span class="state-text-neutral">Inaktív</span>{{end}}</span>
</div>
{{if .BackupEnabled}}
<div class="settings-row">
@@ -44,7 +44,7 @@
{{end}}
<div class="settings-row">
<span class="settings-label">Monitoring</span>
<span class="settings-value">{{if .MonitoringEnabled}}<span class="state-text-green"> Aktív</span>{{else}}<span class="state-text-red">Inaktív</span>{{end}}</span>
<span class="settings-value">{{if .MonitoringEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}<span class="state-text-neutral">Inaktív</span>{{end}}</span>
</div>
{{if .MonitoringEnabled}}
<div class="settings-row">
@@ -54,7 +54,7 @@
{{end}}
<div class="settings-row">
<span class="settings-label">Hub jelentés</span>
<span class="settings-value">{{if .HubEnabled}}<span class="state-text-green"> Aktív</span>{{else}}{{end}}</span>
<span class="settings-value">{{if .HubEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}{{end}}</span>
</div>
</div>
</div>
@@ -74,9 +74,9 @@
<span class="settings-value mono">
{{.LatestVersion}}
{{if .UpdateAvailable}}
<span class="state-text-green" style="margin-left:0.5em;">● Frissítés elérhető</span>
<span class="state-text-run" style="margin-left:0.5em;">● Frissítés elérhető</span>
{{else}}
<span style="margin-left:0.5em; color:#888;">— naprakész</span>
<span style="margin-left:0.5em; color:var(--text-3);">— naprakész</span>
{{end}}
</span>
</div>
@@ -90,13 +90,13 @@
{{if .LastCheckError}}
<div class="settings-row">
<span class="settings-label">Hiba</span>
<span class="settings-value state-text-red">{{.LastCheckError}}</span>
<span class="settings-value state-text-crit">{{.LastCheckError}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Automatikus frissítés</span>
<span class="settings-value">
{{if .AutoUpdateEnabled}}<span class="state-text-green"> Aktív</span> <span class="mono">({{.AutoUpdateTime}})</span>{{else}}{{end}}
{{if .AutoUpdateEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span> <span class="mono">({{.AutoUpdateTime}})</span>{{else}}{{end}}
</span>
</div>
{{if .ControllerFloor}}
@@ -111,16 +111,16 @@
{{if .UpdateRunning}}
<div class="settings-row">
<span class="settings-label">Állapot</span>
<span class="settings-value state-text-yellow" id="auto-update-status"> Frissítés folyamatban — a vezérlő hamarosan újraindul…</span>
<span class="settings-value state-text-progress" id="auto-update-status"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> Frissítés folyamatban — a vezérlő hamarosan újraindul…</span>
</div>
{{end}}
{{with .LastUpdateState}}
<div class="settings-row">
<span class="settings-label">Utolsó frissítés</span>
<span class="settings-value">
{{if eq .Status "success"}}<span class="state-text-green"> Sikeres</span> ({{.PreviousVersion}} → {{.TargetVersion}})
{{else if eq .Status "failed"}}<span class="state-text-red"> Sikertelen</span> — {{.Error}}
{{else if eq .Status "pending"}}<span class="state-text-yellow"> Folyamatban</span>
{{if eq .Status "success"}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Sikeres</span> ({{.PreviousVersion}} → {{.TargetVersion}})
{{else if eq .Status "failed"}}<span class="state-text-crit"><svg class="ico ico-sm"><use href="#i-x"/></svg> Sikertelen</span> — {{.Error}}
{{else if eq .Status "pending"}}<span class="state-text-progress"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> Folyamatban</span>
{{end}}
</span>
</div>
@@ -231,7 +231,7 @@ pollUntilBack();
<div class="storage-path-info">
<div class="storage-path-label-wrap" id="label-wrap-{{.Path}}">
<span class="storage-path-label" id="label-display-{{.Path}}">{{.Label}}</span>
{{if not (or .Disconnected .Decommissioned)}}<button class="btn btn-xs btn-ghost" onclick="editStorageLabel('{{.Path}}', '{{.Label}}')" title="Átnevezés">✏️</button>{{end}}
{{if not (or .Disconnected .Decommissioned)}}<button class="btn btn-xs btn-ghost" onclick="editStorageLabel('{{.Path}}', '{{.Label}}')" title="Átnevezés"><svg class="ico ico-sm"><use href="#i-pencil"/></svg></button>{{end}}
</div>
<span class="storage-path-path mono">{{.Path}}</span>
</div>
@@ -239,11 +239,11 @@ pollUntilBack();
{{if .Disconnected}}
<span class="badge badge-error">Leválasztva</span>
{{else if .Decommissioned}}
<span class="badge state-gray">Kiváltva</span>
<span class="tag tag-off"><span class="dot"></span>Kiváltva</span>
{{else}}
{{if .IsDefault}}<span class="badge state-green">Alapértelmezett</span>{{end}}
{{if .Schedulable}}<span class="badge" style="background:rgba(0,136,204,0.15);color:var(--accent-light)">Aktív</span>{{else}}<span class="badge state-gray">Inaktív</span>{{end}}
{{if .IsUSB}}<span class="badge" style="background:rgba(255,165,0,0.15);color:var(--yellow)">USB</span>{{end}}
{{if .IsDefault}}<span class="tag">Alapértelmezett</span>{{end}}
{{if .Schedulable}}<span class="tag tag-run"><span class="dot"></span>Aktív</span>{{else}}<span class="tag tag-off"><span class="dot"></span>Inaktív</span>{{end}}
{{if .IsUSB}}<span class="badge" style="background:rgba(255,165,0,0.15);color:var(--warn)">USB</span>{{end}}
{{if not .IsMounted}}<span class="badge badge-warn">Rendszermeghajtón</span>{{end}}
{{end}}
</div>
@@ -283,9 +283,10 @@ pollUntilBack();
<div class="system-info-header">
<span class="system-info-value">{{.DiskInfo.UsedHuman}} / {{.DiskInfo.TotalHuman}}</span>
</div>
<div class="system-bar">
<div class="system-bar-fill {{if ge .DiskInfo.UsedPercent 90.0}}system-bar-red{{else if ge .DiskInfo.UsedPercent 70.0}}system-bar-yellow{{else}}system-bar-green{{end}}"
style="width:{{printf "%.0f" .DiskInfo.UsedPercent}}%"></div>
<div class="meter {{if ge .DiskInfo.UsedPercent 90.0}}crit{{else if ge .DiskInfo.UsedPercent 70.0}}warn{{else}}nominal{{end}}">
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.0f" .DiskInfo.UsedPercent}}%"></div>
</div>
</div>
</div>
{{end}}
@@ -296,7 +297,7 @@ pollUntilBack();
{{end}}
{{if .StoppedApps}}
<div class="storage-stopped-apps-info" id="storage-stopped-{{.Path}}">
<span class="form-hint" style="color:var(--accent-light)">Újraindításra váró alkalmazások: {{range $i, $name := .StoppedApps}}{{if $i}}, {{end}}{{$name}}{{end}}</span>
<span class="form-hint" style="color:var(--blue-bright)">Újraindításra váró alkalmazások: {{range $i, $name := .StoppedApps}}{{if $i}}, {{end}}{{$name}}{{end}}</span>
<button class="btn btn-xs btn-primary" onclick="storageRestartApps('{{.Path}}')" style="margin-left:.5rem">Alkalmazások indítása</button>
</div>
{{end}}
@@ -458,7 +459,7 @@ pollUntilBack();
msg.textContent = 'Csatlakoztatás folyamatban…';
fetch('/api/storage/netstorage/add',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify(body)})
.then(function(r){return r.json();}).then(function(d){
if(d.ok){ msg.textContent='Sikeres'; setTimeout(function(){location.reload();},900); }
if(d.ok){ msg.textContent='Sikeres'; setTimeout(function(){location.reload();},900); }
else { msg.textContent='Hiba: '+(d.error||'ismeretlen'); }
}).catch(function(e){ msg.textContent='Hiba: '+e; });
}
@@ -471,7 +472,7 @@ pollUntilBack();
}
</script>
<div id="migrate-progress" style="display:none;margin-top:1rem;padding:1rem;border:1px solid var(--accent);border-radius:6px;background:rgba(0,136,204,0.06)">
<div id="migrate-progress" style="display:none;margin-top:1rem;padding:1rem;border:1px solid var(--blue);border-radius:6px;background:rgba(0,136,204,0.06)">
<strong>Adatok áthelyezése</strong>
<div id="migrate-progress-body" style="margin-top:.5rem"></div>
</div>
@@ -494,7 +495,7 @@ pollUntilBack();
var job=d.data&&d.data.job;
if(!job){ if(body) body.textContent='Nincs folyamatban migráció.'; return; }
if(body) body.innerHTML=migRender(job);
if(job.phase==='done'){ if(body) body.innerHTML+='<br><strong>Kész</strong>'; setTimeout(function(){location.reload();},1500); return; }
if(job.phase==='done'){ if(body) body.innerHTML+='<br><strong>Kész</strong>'; setTimeout(function(){location.reload();},1500); return; }
if(job.phase==='aborted'){ if(body) body.innerHTML+='<br><strong style="color:var(--danger,#c0392b)">Megszakadt: '+(job.error||'')+'</strong><br>A forrás adatai érintetlenek.'; return; }
setTimeout(tick,1500);
}).catch(function(){ setTimeout(tick,2000); });
@@ -539,7 +540,7 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
(function(){
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
function hum(b){ if(!b||b<=0) return ''; var u=['B','KB','MB','GB','TB'],i=0,v=b; while(v>=1024&&i<u.length-1){v/=1024;i++;} return (v>=10||i===0?Math.round(v):v.toFixed(1))+' '+u[i]; }
function usageColorClass(p){ if(p>=85) return 'system-bar-red'; if(p>=70) return 'system-bar-yellow'; return 'system-bar-green'; }
function usageColorClass(p){ if(p>=85) return 'crit'; if(p>=70) return 'warn'; return 'nominal'; }
function classBadge(d){
if(d.class==='fast') return '<span class="badge badge-ok">gyors</span>';
if(d.class==='slow') return '<span class="badge badge-muted">lassú</span>';
@@ -582,7 +583,7 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
if(!d.total_bytes || d.total_bytes<=0) return '';
var pct = d.used_fraction ? d.used_fraction*100 : (d.used_bytes/d.total_bytes*100);
pct = Math.max(0, Math.min(100, pct));
return '<div class="drive-cap"><div class="system-bar"><div class="system-bar-fill '+usageColorClass(pct)+'" style="width:'+pct.toFixed(1)+'%"></div></div>'
return '<div class="drive-cap meter '+usageColorClass(pct)+'"><div class="meter-track"><div class="meter-fill" style="width:'+pct.toFixed(1)+'%"></div></div>'
+'<div class="drive-cap-label">'+hum(d.used_bytes)+' / '+hum(d.total_bytes)+' ('+pct.toFixed(0)+'%)</div></div>';
}
function actions(d, registered){
@@ -797,7 +798,7 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
<button class="btn btn-sm btn-outline" onclick="triggerGeoSync()">Kézi szinkronizálás</button>
<span id="geo-sync-status" class="form-hint">
{{if .GeoLastSync}}Utolsó szinkronizálás: {{.GeoLastSync}}{{end}}
{{if .GeoLastError}} <span class="state-text-red">{{.GeoLastError}}</span>{{end}}
{{if .GeoLastError}} <span class="state-text-crit">{{.GeoLastError}}</span>{{end}}
</span>
</div>
</div>
@@ -1054,7 +1055,7 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
var sync = d.data.last_sync || '';
var err = d.data.last_sync_error || '';
status.innerHTML = sync ? ('Utolsó: ' + sync.substring(0,19).replace('T',' ')) : '';
if (err) status.innerHTML += ' <span class="state-text-red">' + escHtml(err) + '</span>';
if (err) status.innerHTML += ' <span class="state-text-crit">' + escHtml(err) + '</span>';
}
});
}, 5000);
@@ -1332,7 +1333,7 @@ function editStorageLabel(path, currentLabel) {
'<input type="hidden" name="storage_path" value="' + path + '">' +
'<input type="text" name="storage_label" class="form-control" value="' + currentLabel.replace(/"/g, '&quot;') + '" style="width:200px;padding:.3rem .5rem;font-size:.9rem" maxlength="50">' +
'<button type="submit" class="btn btn-xs btn-primary">OK</button>' +
'<button type="button" class="btn btn-xs btn-outline" onclick="cancelEditLabel(\'' + path + '\', \'' + currentLabel.replace(/'/g, "\\'") + '\')"></button>' +
'<button type="button" class="btn btn-xs btn-outline" onclick="cancelEditLabel(\'' + path + '\', \'' + currentLabel.replace(/'/g, "\\'") + '\')">&#215;</button>' +
'</form>';
wrap.querySelector('input[name=storage_label]').focus();
}
@@ -1439,7 +1440,7 @@ function cancelEditLabel(path, label) {
var btn = document.createElement('button');
btn.className = 'btn btn-xs btn-ghost';
btn.setAttribute('title', 'Átnevezés');
btn.textContent = '✏️';
btn.innerHTML = '<svg class="ico ico-sm"><use href="#i-pencil"/></svg>';
btn.addEventListener('click', function() { editStorageLabel(path, label); });
wrap.appendChild(span);
wrap.appendChild(document.createTextNode(' '));
+12 -12
View File
@@ -31,27 +31,27 @@
{{$subdomain}}.{{$.Domain}} ↗
</a>
{{if and .Deployed (routeUnpublished .State)}}
<span class="route-unpublished" title="A proxy (Traefik) csak egészséges konténerhez publikál nyilvános útvonalat. Amíg az alkalmazás nem egészséges, az URL nem érhető el (404), pedig a konténer fut."> URL nem elérhető útvonal nincs publikálva</span>
<span class="route-unpublished" title="A proxy (Traefik) csak egészséges konténerhez publikál nyilvános útvonalat. Amíg az alkalmazás nem egészséges, az URL nem érhető el (404), pedig a konténer fut."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> URL nem elérhető útvonal nincs publikálva</span>
{{end}}
{{end}}
</div>
</div>
<span class="stack-state-badge state-{{stateColor .State}}">{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="badge badge-orphaned">Elavult</span>{{end}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="badge badge-missing-storage" title="Az alkalmazás adattárolója nem elérhető. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat.">Hiányzó tárhely: {{$ms}}</span>{{end}}
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="badge badge-warn" title="A hálózati tárhely (NAS) jelenleg nem érhető el. Az alkalmazás fut; az adatok elérése a NAS visszatértével helyreáll.">Hálózati tárhely nem elérhető: {{$nw}}</span>{{end}}
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="tag tag-warn">Elavult</span>{{end}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="Az alkalmazás adattárolója nem elérhető. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hiányzó tárhely: {{$ms}}</span>{{end}}
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="A hálózati tárhely (NAS) jelenleg nem érhető el. Az alkalmazás fut; az adatok elérése a NAS visszatértével helyreáll."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely nem elérhető: {{$nw}}</span>{{end}}
</div>
{{if .Meta.Description}}
<p class="stack-detail-desc">{{.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}}
{{if and .Deployed (index $.StorageLabels .Name)}}<span class="meta-badge meta-badge-storage" title="Adattároló: {{index $.StorageLabels .Name}}">💾 {{index $.StorageLabels .Name}}</span>{{end}}
<div class="metarows">
{{if .Meta.Resources.MemRequest}}<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~{{.Meta.Resources.MemRequest}}</span></span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>{{end}}
{{if .Meta.Resources.NeedsHDD}}<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>{{end}}
{{if and .Deployed (index $.StorageLabels .Name)}}<span class="metarow" title="Adattároló: {{index $.StorageLabels .Name}}"><svg class="ico"><use href="#i-hard-drive"/></svg>{{index $.StorageLabels .Name}}</span>{{end}}
</div>
{{if .Containers}}
@@ -67,7 +67,7 @@
<div class="stack-detail-actions">
{{if .Protected}}
<span class="badge badge-protected">Védett rendszerkomponens</span>
<span class="tag"><svg class="ico ico-sm"><use href="#i-lock"/></svg>Védett rendszerkomponens</span>
{{if isOperational .State}}
<button class="btn btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">Újraindítás</button>
{{end}}
@@ -104,7 +104,7 @@ async function submitAttach(ev){
var r=await fetch('/api/storage/attach',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders()),body:JSON.stringify(body)});
var j=await r.json();
if(!j.ok){ throw new Error(j.error||'Hiba'); }
out.innerHTML='<div class="alert alert-success">A meghajtó sikeresen csatolva és regisztrálva: <strong class="mono">'+(j.data.where||'')+'</strong>. <a href="/settings">Vissza a Beállításokhoz →</a></div>';
out.innerHTML='<div class="alert alert-success">A meghajtó sikeresen csatolva és regisztrálva: <strong class="mono">'+(j.data.where||'')+'</strong>. <a href="/settings">Vissza a Beállításokhoz →</a></div>';
}catch(e){ out.innerHTML='<div class="alert alert-error">Hiba: '+e.message+'</div>'; btn.disabled=false; }
return false;
}
@@ -49,7 +49,7 @@
<span class="toggle-label">Beállítás alapértelmezett adattárolóként új telepítéseknél</span>
</label>
<div class="alert alert-warning" id="warn-databearing" style="display:none;margin-bottom:1rem">
⚠️ A kiválasztott meghajtó <strong>adatot tartalmaz</strong>. Az inicializálás (formázás) törli a rajta lévő
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A kiválasztott meghajtó <strong>adatot tartalmaz</strong>. Az inicializálás (formázás) törli a rajta lévő
összes adatot — a folytatáshoz meg kell erősítenie.
</div>
<div class="form-actions" style="gap:.75rem">
@@ -134,7 +134,7 @@ async function submitInit(ev){
// SYSTEM/BACKUP (shouldn't reach here — filtered out — but surface the opsign if it does).
if(res.status===409 && res.j.data && res.j.data.refused){
out.innerHTML='<div class="alert alert-warning"><strong>Operátori aláírás szükséges.</strong> Ez a meghajtó védett (rendszer/biztonsági mentés).'
+(res.j.data.opsign?('<pre class="mono" style="white-space:pre-wrap;background:var(--bg-primary);padding:.75rem;border-radius:6px;margin-top:.5rem">'+esc(res.j.data.opsign)+'</pre>'):'')+'</div>';
+(res.j.data.opsign?('<pre class="mono" style="white-space:pre-wrap;background:var(--bg-0);padding:.75rem;border-radius:6px;margin-top:.5rem">'+esc(res.j.data.opsign)+'</pre>'):'')+'</div>';
btn.disabled=false; return false;
}
finishInit(res.j, out);
@@ -144,7 +144,7 @@ async function submitInit(ev){
function renderConfirm(durableId, out, btn){
var name=document.getElementById('mount-name').value;
out.innerHTML='<div class="alert alert-warning">⚠️ A meghajtó adatot tartalmaz. A formázás <strong>véglegesen törli</strong> a rajta lévő összes adatot.</div>'
out.innerHTML='<div class="alert alert-warning">A meghajtó adatot tartalmaz. A formázás <strong>véglegesen törli</strong> a rajta lévő összes adatot.</div>'
+'<div class="confirm-input"><label>Megerősítéshez írja be a csatlakoztatási nevet: <strong class="mono">'+esc(name)+'</strong></label>'
+'<input type="text" id="init-confirm-type" class="form-control" autocomplete="off" oninput="document.getElementById(\'init-confirm-go\').disabled=(this.value!==\''+esc(name)+'\')"></div>'
+'<div class="form-actions" style="gap:.6rem;margin-top:.75rem"><button id="init-confirm-go" class="btn btn-danger-outline" disabled>Törlés és inicializálás megerősítése</button></div>'
@@ -161,7 +161,7 @@ function renderConfirm(durableId, out, btn){
function finishInit(j, out){
if(!j.ok){ out.innerHTML='<div class="alert alert-error">Hiba: '+esc(j.error||'')+'</div>'; document.getElementById('init-btn').disabled=false; return; }
out.innerHTML='<div class="alert alert-success">A meghajtó sikeresen inicializálva és regisztrálva: <strong class="mono">'+esc(j.data&&j.data.where)+'</strong>. <a href="/settings">Vissza a Beállításokhoz →</a></div>';
out.innerHTML='<div class="alert alert-success">A meghajtó sikeresen inicializálva és regisztrálva: <strong class="mono">'+esc(j.data&&j.data.where)+'</strong>. <a href="/settings">Vissza a Beállításokhoz →</a></div>';
}
loadDisks();
</script>
File diff suppressed because it is too large Load Diff
@@ -11,7 +11,7 @@
<div class="monitor-card">
{{with .Tier2}}
<p style="color:var(--text-muted);font-size:.9rem;margin-top:0">
<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).
@@ -38,7 +38,7 @@
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Megjegyzés</span>
<span class="sysinfo-value" style="color:var(--text-muted)">{{.NoTargetReason}}</span>
<span class="sysinfo-value" style="color:var(--text-3)">{{.NoTargetReason}}</span>
</div>
{{else}}
<div class="sysinfo-row">
@@ -47,7 +47,7 @@
</div>
<div class="sysinfo-row">
<span class="sysinfo-label">Kiválasztás módja</span>
<span class="sysinfo-value" style="color:var(--text-muted)">{{if .Preferred}}kézi választás{{else}}automatikus{{end}} — {{.EffectiveDesc}}</span>
<span class="sysinfo-value" style="color:var(--text-3)">{{if .Preferred}}kézi választás{{else}}automatikus{{end}} — {{.EffectiveDesc}}</span>
</div>
{{end}}
</div>
@@ -77,7 +77,7 @@
{{end}}
</select>
{{if not .Alternatives}}
<div style="font-size:.8rem;color:var(--text-muted);margin-top:.4rem">
<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>