v0.39.1: 8C orphan-template cleanup (delete 5 dead templates)
Remove five orphaned HTML templates left behind when slice 8C retired the disk/storage/restore web handlers (storage_handlers.go, handler_restore.go and the /api/storage/* + /api/restore/* routes): storage_init, storage_attach, migrate, migrate_drive, restore. Zero .go references, zero cross-template references, no route, no nav entry; embed is a glob so deletion is safe (14 templates remain, build + tests green). No behaviour change; the deleted pages were already unreachable. Also ships the live demo validation (v0.39.0) writeup in REPORT.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,27 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.39.1 — 8C orphan-template cleanup (source hygiene) (2026-06-11)
|
||||||
|
|
||||||
|
Dead-template removal — no behaviour change. Slice 8C de-privileged the controller and retired the
|
||||||
|
disk/storage/restore web handlers (`storage_handlers.go`, `handler_restore.go` and the `/api/storage/*`
|
||||||
|
+ `/api/restore/*` routes), but five HTML templates that those handlers rendered were left behind.
|
||||||
|
They have **zero** `.go` references, **zero** cross-template `{{template …}}` references, no route, and
|
||||||
|
no nav entry; the embed is a glob (`//go:embed templates/*.html templates/*.css`), so deleting them is
|
||||||
|
safe and the remaining 14 templates still embed cleanly.
|
||||||
|
|
||||||
|
#### Removed (`internal/web/templates`)
|
||||||
|
- `storage_init.html`, `storage_attach.html`, `migrate.html`, `migrate_drive.html`, `restore.html` —
|
||||||
|
orphaned pages for removed endpoints. Re-confirmed unreferenced before deletion
|
||||||
|
(`grep -rn` over `internal/`: only the templates' own `{{define}}` lines matched).
|
||||||
|
|
||||||
|
#### Noted, not changed (dead-but-harmless restic/cross-drive remnants)
|
||||||
|
- Two never-called notifier methods `NotifyCrossDriveCompleted`/`NotifyCrossDriveFailed`
|
||||||
|
(`internal/notify/notifier.go:353,359`) and a vestigial `crossdrive_failed` entry in the
|
||||||
|
notification-events list (`internal/web/handlers.go:937`) that still renders a settings toggle for an
|
||||||
|
event that can no longer fire. Plus restic config fields/comments in `config/config.go`,
|
||||||
|
`settings/settings.go`, `report/types.go`. None are live emitters — left in place, flagged for a
|
||||||
|
future dedicated cleanup.
|
||||||
|
|
||||||
### v0.39.0 — slice 9: host metrics in the controller (customer host-health view) (2026-06-10)
|
### v0.39.0 — slice 9: host metrics in the controller (customer host-health view) (2026-06-10)
|
||||||
|
|
||||||
The customer-facing half of slice 9. Pairs with `felhom-agent` v0.14.0. The de-privileged controller (slice 8C) sees only its own cgroup, so it can't read the host. The monitoring page now shows the **real Proxmox box** — CPU% + load, memory used/total, **CPU/chassis temperature** (or "n/a" when the hardware exposes none), uptime, and **per-storage capacity** (used/total bar, thin-pool fill, disk temp/wear) — proxied from the agent's new `GET /host/metrics`.
|
The customer-facing half of slice 9. Pairs with `felhom-agent` v0.14.0. The de-privileged controller (slice 8C) sees only its own cgroup, so it can't read the host. The monitoring page now shows the **real Proxmox box** — CPU% + load, memory used/total, **CPU/chassis temperature** (or "n/a" when the hardware exposes none), uptime, and **per-storage capacity** (used/total bar, thin-pool fill, disk temp/wear) — proxied from the agent's new `GET /host/metrics`.
|
||||||
|
|||||||
@@ -1,40 +1,203 @@
|
|||||||
# REPORT — slice 9 (controller half): host-health view (v0.39.0) (2026-06-10)
|
# REPORT — Live demo validation of felhom-controller v0.39.0 + 8C orphan-template cleanup (2026-06-11)
|
||||||
|
|
||||||
> Overwrite-latest report. Cumulative history: [CHANGELOG.md](CHANGELOG.md).
|
Two things this session: (1) **provisioned a fresh customer guest from a v0.39.0 golden on the demo
|
||||||
|
Proxmox host and walked the full controller flow**, reporting what works vs breaks against the live
|
||||||
|
guest; (2) a small source-hygiene code change — deleting five dead 8C orphan templates (v0.39.1).
|
||||||
|
|
||||||
## What was implemented
|
The code change is **source-only** (ships in the next golden); the running demo guest stays the
|
||||||
|
v0.39.0 golden provisioned below.
|
||||||
|
|
||||||
The customer-facing half of **slice 9**. Pairs with `felhom-agent` v0.14.0. The de-privileged
|
---
|
||||||
controller (slice 8C) sees only its own cgroup, so it can't read the host. The monitoring page now
|
|
||||||
shows the **real Proxmox box**, proxied from the agent's new `GET /host/metrics`.
|
|
||||||
|
|
||||||
### `internal/agentapi` — client method
|
## Premise correction (surfaced to Viktor, decision taken)
|
||||||
- **`Client.HostMetrics(ctx)`** — calls the agent's `GET /host/metrics` over the leaf-pinned,
|
|
||||||
per-guest-token channel (same client as the 8C disk proxy). New mirror structs `HostMetrics` (with
|
|
||||||
nullable `CPUTempC`), `StorageTarget`, `ThinPoolFill`, `SmartSummary` (a **subset** — only the
|
|
||||||
fields the UI renders; unknown wire keys ignored).
|
|
||||||
|
|
||||||
### `internal/web` — proxy + UI
|
The session brief said "controller v0.39.0 is built and baked into the current golden." That was
|
||||||
- **`ServeHostMetricsAPI`** (`agent_host_metrics_handler.go`) — a thin read-only proxy:
|
**false on the baked half**:
|
||||||
`GET /api/host-metrics` → agent `GET /host/metrics`. Returns the `{ok,data,error}` envelope; 503
|
- v0.39.0 **source** committed (`d8d1e17`, slice 9). ✓
|
||||||
when the local API is not configured (unprovisioned guest), 502 on an agent error. Wired in
|
- v0.39.0 **image** built & pushed to the registry — but under tag **`0.39.0`** (no `v` prefix,
|
||||||
`main.go` behind `RequireAuth` (GET-only → no CSRF wrapper).
|
unlike every prior build; same digest as `latest`). The `v0.39.0` alias was **never pushed**.
|
||||||
- **Monitoring view** (`templates/monitoring.html`): a new **"Szerver állapota (gazdagép)"** card at
|
- v0.39.0 **golden** did **not exist** — the newest golden baked **v0.38.0** (others v0.37.0/.36/.35).
|
||||||
the top renders the host block (CPU% + load, memory used/total, **CPU temp** or **"n/a"** when
|
|
||||||
null, uptime) + per-storage capacity bars (used/total, thin-pool fill, disk temp/wear), reusing
|
|
||||||
the existing `system-bar`/`storage-item` styling. Polls `/api/host-metrics` every **8 s** while the
|
|
||||||
page is open (a live snapshot, distinct from the controller's own 60 s charts); yellow "nem
|
|
||||||
elérhető" banner when the agent is unreachable.
|
|
||||||
|
|
||||||
## Tests (green)
|
Per the plan's Step-1.6 gate, this was surfaced. **Viktor chose: re-bake the v0.39.0 golden, then run
|
||||||
- `agentapi/host_metrics_test.go`: decodes host + storage (thin-pool, SMART temp + NVMe wear), USB
|
the full session.** Done (Step 1b below).
|
||||||
drive's null SMART, and a null `cpu_temp_c` → nil pointer.
|
|
||||||
- `go build ./...` + `go test ./internal/agentapi ./internal/web` green.
|
|
||||||
|
|
||||||
## Versioning / docs
|
---
|
||||||
- Version `0.38.0 → 0.39.0` (set at build via ldflags); `CHANGELOG.md` + `controller/README.md`
|
|
||||||
(Monitoring → "Host (Proxmox box) Health" section) updated.
|
|
||||||
|
|
||||||
## Pending
|
## Step 1 — Discovery (observed)
|
||||||
- **Build + deploy** controller v0.39.0 to the demo nodes and live-validate the monitoring page
|
|
||||||
against the real N100 (cross-check vs `pvesh`/`free`/`df`).
|
| Item | Value |
|
||||||
|
|---|---|
|
||||||
|
| Live agent config path | `/root/.config/felhom-agent/agent.json` (NOT the `/etc/...` default — confirmed from `systemctl cat felhom-agent` `ExecStart`) |
|
||||||
|
| Agent version (deployed) | **v0.18.0** (serves `GET /host/metrics`, `/disks`, `/disks/format`) |
|
||||||
|
| `local_api` | `enable: true`, `listen_addr: 192.168.0.162:8443` ✓ |
|
||||||
|
| `backup.restore_storage` | `local-lvm` ✓ |
|
||||||
|
| `hub` | `url: https://hub.felhom.eu`, `host_id: demo-felhom-01`, `api_key` set (stored out-of-band) ✓ |
|
||||||
|
| Free VMID chosen | **9200** (in use: 9001 spike, 9999 selftest-scratch) |
|
||||||
|
| Golden volid (re-baked) | `local:backup/vzdump-lxc-9100-2026_06_11-11_55_03.tar.zst` (baked image `gitea.dooplex.hu/admin/felhom-controller:0.39.0`, verified by extracting `/etc/felhom-controller-image` from the archive) |
|
||||||
|
| Hub row key | Controller report keys on `customer.id`; the existing **`demo-felhom` customer row exists** (stale, last reports Feb 2026 up to v0.28.8). Agent host-report keys on `host_id=demo-felhom-01` (separate row). |
|
||||||
|
| Daemon contention | **None at mint time** — provision ran with the daemon up, no token-store/leaf lock. (But see the post-provision 401 finding under Step 2.) |
|
||||||
|
|
||||||
|
### Step 1b — re-bake the v0.39.0 golden
|
||||||
|
- The supplied Gitea token (read-only) **lacks container-registry scope** — the build LXC's
|
||||||
|
`docker pull` failed `401 unauthorized` (verified: the token gets HTTP 401 from the registry token
|
||||||
|
endpoint). Re-used the **build server's existing registry credential** (package-scoped, the same one
|
||||||
|
prior bakes used; **stored out-of-band**) purely inside the build LXC. `build-golden.sh` logs out +
|
||||||
|
removes `/root/.docker/config.json` before archiving, so **no credential is baked** into the golden;
|
||||||
|
the build logfile contains no secret.
|
||||||
|
- New golden built (VMID 9100 build LXC → archived → build guest destroyed). Baked image confirmed
|
||||||
|
`:0.39.0`.
|
||||||
|
- **Follow-up for Viktor:** push the `v0.39.0` tag alias for naming convention, and bump the
|
||||||
|
`build-golden.sh:33` default off the stale `:v0.35.0`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Provision guest 9200 as `demo-felhom`
|
||||||
|
|
||||||
|
`felhom-agent --selftest=provision -config /root/.config/felhom-agent/agent.json -archive <golden>
|
||||||
|
-vmid 9200 -customer-id demo-felhom -customer-domain demo-felhom.eu -customer-name "Felhom Demo"
|
||||||
|
-hostname felhom-demo` → `selftest=provision OK — guest 9200 provisioned + bootstrap-mounted (KEPT)`.
|
||||||
|
|
||||||
|
**`pct reboot 9200` was mandatory** (the bootstrap oneshot is gated on the mount existing at boot, and
|
||||||
|
provision attaches the mount *after* the front-half boot). After one reboot the controller deployed:
|
||||||
|
|
||||||
|
| Assertion | Result |
|
||||||
|
|---|---|
|
||||||
|
| Controller running | `gitea.dooplex.hu/admin/felhom-controller:0.39.0 Up (healthy)` — startup line `felhom-controller 0.39.0 starting (customer: demo-felhom, domain: demo-felhom.eu)` |
|
||||||
|
| Not setup mode | ✓ — no `setup mode` line; came up **configured** |
|
||||||
|
| Bootstrap-seeded config | ✓ — `controller.yaml` written **0600 root:root** at boot, with customer id + hub + `local_api.{endpoint,fingerprint,token}` |
|
||||||
|
| De-privileged container | ✓ — `Privileged=false`; mounts = **exactly 3**: `/etc/felhom-bootstrap` (ro), `felhom-controller-data` volume (rw), `/var/run/docker.sock` (rw). No `/dev`, `/etc/fstab`, `/mnt` rshared, `/sys`, `/run/udev`. LXC `unprivileged=1`, features `nesting,keyctl` only, single `mp9` bootstrap mount, no device passthrough/hookscript. |
|
||||||
|
| No registry pull | ✓ — bootstrap unit did `docker run` (no pull); image shows `created 20 hours ago` (golden bake time) |
|
||||||
|
| bootstrap.json identity | ✓ — `/etc/felhom-bootstrap/bootstrap.json` `600 root:root`, `customer.id=demo-felhom`, hub creds, `local_api.{endpoint=192.168.0.162:8443, fingerprint=60b5974d…, token}` (token/api_key out-of-band) |
|
||||||
|
|
||||||
|
### FINDING 2a — local-API channel 401 until the agent daemon is restarted
|
||||||
|
At controller startup: `local-api: GET /storage failed (agentapi: GET /storage: HTTP 401) — channel
|
||||||
|
not verified`. Root cause: provision minted the per-guest token and durably recorded its hash
|
||||||
|
(`/var/lib/felhom-agent/local-tokens.log` carries `{"v":9200,...}`), but the **already-running daemon
|
||||||
|
loaded its in-memory token map at its own startup, before the mint** — so it rejected the controller's
|
||||||
|
token. **`systemctl restart felhom-agent` cleared it** (the daemon reloads the durable store on
|
||||||
|
restart; minted hash persists). After restart, all agent-channel calls succeed.
|
||||||
|
**Recommendation:** provision should signal the running daemon to reload the token store (or the
|
||||||
|
local-API should consult the durable store per-request), OR the provisioning runbook must include a
|
||||||
|
post-provision `systemctl restart felhom-agent`. As-is, a guest provisioned while the daemon runs has a
|
||||||
|
dead local-API channel until the daemon restarts.
|
||||||
|
|
||||||
|
### FINDING 2b — bootstrap seed-log line absent (cosmetic)
|
||||||
|
The seed functionally worked (controller.yaml 0600 written at boot, configured, not setup mode), but
|
||||||
|
the explicit `[INFO] bootstrap: seeded … coming up configured` line (`bootstrap/bootstrap.go:111`) did
|
||||||
|
**not** appear in `docker logs`. Functionally correct; logging-only discrepancy worth a glance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Full flow (validated from inside the container via `docker exec … curl localhost:8080`)
|
||||||
|
|
||||||
|
The bootstrap `docker run` publishes no port (bridge-only), so host-`localhost` is refused — all checks
|
||||||
|
ran inside the container (host=localhost passes the catch-all gate).
|
||||||
|
|
||||||
|
1. **UI renders** — all HTTP 200 with Hungarian markers: `/` **Vezérlőpult** (open dashboard — no
|
||||||
|
web password set, logged `no password configured — dashboard is open`), `/stacks` (Alkalmazások),
|
||||||
|
`/monitoring` **Rendszermonitor** + the slice-9 **"Szerver állapota (gazdagép)"** host card,
|
||||||
|
`/backups` (Biztonsági mentés), `/settings` (Beállítások).
|
||||||
|
- **Host-gate routing proven live:** `Host: felhom.demo-felhom.eu` → **200**; `Host: 1.2.3.4` →
|
||||||
|
**404** (catch-all). External browser access (`felhom.demo-felhom.eu`) needs the **Cloudflare
|
||||||
|
tunnel**, which is **unconfigured on a bare bootstrap** (`cf_tunnel_token` empty) — same slice-10
|
||||||
|
onboarding gap as below; not separately testable without the tunnel/DNS.
|
||||||
|
2. **`/api/host-metrics` (slice 9)** — `{ok:true}` HTTP 200. **Cross-checked against the host:**
|
||||||
|
memory_total **exact** (16537989120 = pvesh), memory_used ~match (2.49 vs 2.43 GB), loadavg same
|
||||||
|
ballpark, uptime match (324255 vs 324283), disk match (93.9 GB / 10.5% vs `df` 94G/12%),
|
||||||
|
felhom-usb match (915.8 GiB / 0.87%, SMART 31°C). **`cpu_temp_c` is a real value** — read 46 live,
|
||||||
|
matching sysfs `x86_pkg_temp=44`/coretemp max 46 (`sensors` is **not installed**; the agent sources
|
||||||
|
temp from coretemp/thermal, not the `sensors` CLI). An earlier reading of 59 was a genuine transient
|
||||||
|
load peak during the golden-build+provision.
|
||||||
|
3. **`/api/disks` (8C proxy)** — `{ok:true}` HTTP 200, 4 devices with data-bearing flags;
|
||||||
|
**felhom-usb flagged `data_bearing:true` (reason "device is mounted", `/dev/sdb1`)**.
|
||||||
|
|
||||||
|
### BREAK 3.2 — storage auto-discovery (by-design consequence of 8C de-privileging)
|
||||||
|
The 1TB HDD does **NOT** appear under Settings → Storage Paths. The de-privileged container sees **no
|
||||||
|
host storage mounts** (`df` inside shows only the bootstrap bind, its own disk, udev; `/mnt` empty;
|
||||||
|
logs: "no storage paths registered", "stat /mnt/sys_drive: no such file or directory"). This is correct
|
||||||
|
for the 8C model (mounts limited to bootstrap+data+docker.sock). The HDD is instead surfaced via the
|
||||||
|
**agent host-metrics storage view** (felhom-usb appears there with capacity + SMART). The legacy local
|
||||||
|
`discoverHDDPaths` path is effectively **vestigial** in v0.39.0 — worth retiring or repurposing onto the
|
||||||
|
agent-sourced storage list.
|
||||||
|
|
||||||
|
### BREAK 3.3 — deploy→run→remove an app: not exercisable on a bare bootstrap
|
||||||
|
`/api/stacks` → `{ok:true,data:[]}` (empty catalog). Catalog templates come from a synced
|
||||||
|
`catalog-cache/templates`, populated by git/assets sync — **disabled on a bootstrap-seeded controller**
|
||||||
|
(manual mode, no repo URL; the seed sets only identity/hub/local-api). No apps → nothing to deploy
|
||||||
|
(`POST /api/stacks/filebrowser/deploy` → 400 "invalid request body"; even with a body the slug isn't in
|
||||||
|
the catalog). Catalog configuration is **slice-10 (hub desired-state)** territory.
|
||||||
|
|
||||||
|
### BREAK 3.5 — hub reporting stays DOWN (HTTP 401): host-key vs customer-key gap
|
||||||
|
The controller's startup push and all 3 retries got **HTTP 401** from the hub
|
||||||
|
(`[report] Push failed: HTTP 401`), reproduced directly (`POST https://hub.felhom.eu/api/v1/report`
|
||||||
|
with the baked key → `Unauthorized` / 401). **Root cause (code-traced + DB-confirmed):**
|
||||||
|
- The hub's `POST /api/v1/report` authenticates a **customer-scoped** key — `checkAuthCustomer` →
|
||||||
|
`GetCustomerConfigByAPIKey` against the `customer_configs` table, then enforces
|
||||||
|
`authCustomerID == payload.CustomerID` (`hub/internal/api/handler.go:74-92, 208-234`).
|
||||||
|
- Provision baked the **agent's HOST key** (`hub.api_key`, keyed on `host_id=demo-felhom-01` in the
|
||||||
|
separate `hosts` table) into the guest's bootstrap. Host keys and customer keys are **distinct tables
|
||||||
|
/ code paths**.
|
||||||
|
- Hub DB confirms: a `demo-felhom` customer row exists (with its **own** dashboard-generated api_key),
|
||||||
|
a `demo-felhom-01` host row exists, and the baked key appears **once** (as the host key). So the
|
||||||
|
controller presents the host key → `GetCustomerConfigByAPIKey` returns nil → **401**.
|
||||||
|
|
||||||
|
The `demo-felhom` hub row therefore **stays DOWN/stale** — the freshly-provisioned controller can never
|
||||||
|
report ONLINE until the bootstrap carries the **customer-scoped** api_key (or provision creates/fetches
|
||||||
|
the customer config key, or the hub accepts host keys for customer reports). This is a **cross-component
|
||||||
|
provisioning gap (slice-10 onboarding)**, not a controller bug. *(Reported value differs from the
|
||||||
|
brief's "v0.34.0" — the DB shows last reports up to v0.28.8; immaterial, the row is stale either way.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Disk proxy (API-level only; NO destructive op) — INVARIANT PROVEN
|
||||||
|
|
||||||
|
1. **List** — `/api/disks` `{ok:true}` with per-device data-bearing flags (above).
|
||||||
|
2. **Data-bearing format → refusal** — hit the agent **directly** (dodges the controller's CSRF):
|
||||||
|
`POST https://192.168.0.162:8443/disks/format` with the guest's `local_api.token`, body
|
||||||
|
`{"vmid":9200,"device":"/dev/sdb1","fstype":"ext4"}` (felhom-usb — mounted, data-bearing). Result:
|
||||||
|
|
||||||
|
**HTTP 403** — `{ "formatted": false, "data_bearing": true, "reason": "device is mounted",
|
||||||
|
"pending_op": { "op":"storage_wipe", "host_scope":"demo-felhom-01",
|
||||||
|
"durable_id":"byid:wwn-0x5000039ddb108568-part1", "fstype":"ext4" },
|
||||||
|
"error": "device is data-bearing — format requires an operator signature (pending_signature)" }`
|
||||||
|
|
||||||
|
The agent **inspected the device itself** (`data_bearing:true`, reason "device is mounted"),
|
||||||
|
ignored any caller claim, refused with `pending_signature`, and surfaced the durable-id-bound op to
|
||||||
|
sign. **The disk was untouched** (post-test: `/dev/sdb1 ext4 915.8G, 8G used`, still mounted). No
|
||||||
|
operator signature was ever passed. The controller maps this 403→409 (`agentapi.ErrFormatRefused`,
|
||||||
|
already unit-tested). ✓
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 / 6 — Orphan-template cleanup + gate + push
|
||||||
|
|
||||||
|
- **Deleted** (re-confirmed unreferenced first — `grep -rn` over `internal/` matched only the
|
||||||
|
templates' own `{{define}}` lines): `internal/web/templates/{storage_init, storage_attach, migrate,
|
||||||
|
migrate_drive, restore}.html`. Embed is a glob; 14 templates remain.
|
||||||
|
- **Noted, not deleted** (dead-but-harmless): `NotifyCrossDriveCompleted`/`NotifyCrossDriveFailed`
|
||||||
|
(`notify/notifier.go:353,359`, no callers) + a vestigial `crossdrive_failed` notification toggle
|
||||||
|
(`web/handlers.go:937`) + restic config fields/comments. Flagged for a future dedicated cleanup.
|
||||||
|
- **Version:** v0.39.0 → **v0.39.1** (CHANGELOG entry added; version is ldflags-injected, applied at
|
||||||
|
the next build). Source-only — no re-bake this session.
|
||||||
|
- **Gate:** `go build ./...` **OK**; `go test ./...` **green** (agentapi, bootstrap, quiesce).
|
||||||
|
- **Commit:** see CHANGELOG / git log — pushed to `main`. No working UI feature lost (the deleted pages
|
||||||
|
were already unreachable — removed routes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What broke / what's missing (the headline)
|
||||||
|
|
||||||
|
| # | Item | Severity | Nature |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2a | Local-API channel 401 until `felhom-agent` restart after provisioning a live-daemon host | **Medium** | Provision doesn't make the running daemon reload its token store. Workaround: restart the daemon (done). Needs a provision→daemon reload signal or per-request store lookup. |
|
||||||
|
| 3.5 | Hub report 401 — bootstrap bakes the **host** api_key, but `/api/v1/report` needs the **customer** api_key | **Medium/High** | Cross-component provisioning gap (slice-10 onboarding). Controller stays DOWN on the hub until fixed. |
|
||||||
|
| 3.3 | No app catalog on a bare bootstrap (git/assets sync disabled) — deploy not exercisable | Expected (slice-10) | Catalog/desired-state comes from the hub later. |
|
||||||
|
| 3.2 | Legacy Storage-Paths auto-discovery finds nothing (de-privileged container has no host mounts) | Expected (8C) | HDD is correctly surfaced via agent host-metrics instead; retire/repurpose the legacy path. |
|
||||||
|
| 3.1 | External browser access (felhom.demo-felhom.eu) needs the Cloudflare tunnel (`cf_tunnel_token` empty) | Expected (slice-10) | Host-gate routing itself verified live (200 vs 404). |
|
||||||
|
| 2b | Bootstrap seed-log line absent | Cosmetic | Functionally correct; logging-only. |
|
||||||
|
| infra | Supplied Gitea token lacks registry/package scope; build-golden default tag stale (`:v0.35.0`) | Low | Used the build-server credential for the re-bake; flagged both for Viktor. |
|
||||||
|
|
||||||
|
**Worked cleanly:** golden re-bake, provision + reboot deploy, configured-not-setup bootstrap, 0600
|
||||||
|
bootstrap.json/controller.yaml, container de-privileging (Privileged=false, 3 mounts), no-registry-pull,
|
||||||
|
all 5 UI pages + slice-9 host card, host-metrics (cross-checked, real cpu_temp), `/api/disks`, and the
|
||||||
|
**8C data-bearing format-refusal invariant (403, disk untouched)**.
|
||||||
|
|||||||
@@ -1,264 +0,0 @@
|
|||||||
{{define "migrate"}}
|
|
||||||
{{template "layout_start" .}}
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<div style="display:flex;align-items:center;gap:.5rem">
|
|
||||||
<a href="/stacks/{{.Meta.Slug}}/deploy" class="btn btn-sm btn-outline">← Vissza</a>
|
|
||||||
<h2>{{.Meta.DisplayName}} — Adatáthelyezés</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="migrate-form-card">
|
|
||||||
<h3>Adatok áthelyezése másik tárolóra</h3>
|
|
||||||
|
|
||||||
<div class="settings-grid" style="margin-bottom:1.5rem">
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Jelenlegi tárhely</span>
|
|
||||||
<span class="settings-value mono">{{.CurrentLabel}} ({{.CurrentHDDPath}})</span>
|
|
||||||
</div>
|
|
||||||
{{if .DataSizeHuman}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Adatméret</span>
|
|
||||||
<span class="settings-value mono">{{.DataSizeHuman}}</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="target-path">Cél tárhely <span class="required">*</span></label>
|
|
||||||
<select id="target-path" class="form-control">
|
|
||||||
{{range .OtherPaths}}
|
|
||||||
<option value="{{.Path}}">{{.Label}} ({{.Path}}) — {{.FreeHuman}} szabad</option>
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-warning" style="margin-bottom:1.5rem">
|
|
||||||
<strong>Figyelmeztetések:</strong>
|
|
||||||
<ul style="margin:.5rem 0 0 1rem;padding:0">
|
|
||||||
<li>Az alkalmazás a mozgatás idejére leáll</li>
|
|
||||||
<li>Nagy adatmennyiségnél ez percekig tarthat</li>
|
|
||||||
<li>DB mentés fájlok is átkerülnek</li>
|
|
||||||
<li>A migráció után azonnal lefut egy biztonsági mentés az új meghajtón</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-bottom:1.5rem">
|
|
||||||
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
|
|
||||||
<input type="checkbox" id="auto-delete" checked>
|
|
||||||
<span>Régi adatok törlése a forrás meghajtóról</span>
|
|
||||||
</label>
|
|
||||||
<span class="form-hint" style="margin-left:1.5rem">Ha bekapcsolva, a forrás meghajtóról az alkalmazás adatai és DB mentései automatikusan törlődnek a sikeres áthelyezés után.</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="migrate-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
|
|
||||||
|
|
||||||
<div class="form-actions" style="gap:.75rem">
|
|
||||||
<button class="btn btn-primary" onclick="startMigrate()">📦 Mozgatás indítása</button>
|
|
||||||
<a href="/stacks/{{.Meta.Slug}}/deploy" class="btn btn-outline">Mégsem</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="migrate-progress-card" style="display:none">
|
|
||||||
<h3>Adatok áthelyezése...</h3>
|
|
||||||
|
|
||||||
<div class="disk-progress-steps" id="mig-steps">
|
|
||||||
<div class="disk-step" id="mstep-stopping"><span class="disk-step-icon">○</span> Alkalmazás leállítása</div>
|
|
||||||
<div class="disk-step" id="mstep-copying"><span class="disk-step-icon">○</span> Adatok másolása</div>
|
|
||||||
<div class="disk-step" id="mstep-updating"><span class="disk-step-icon">○</span> Konfiguráció frissítése</div>
|
|
||||||
<div class="disk-step" id="mstep-starting"><span class="disk-step-icon">○</span> Alkalmazás indítása</div>
|
|
||||||
<div class="disk-step" id="mstep-cleaning"><span class="disk-step-icon">○</span> Régi adatok törlése</div>
|
|
||||||
<div class="disk-step" id="mstep-backing_up"><span class="disk-step-icon">○</span> Biztonsági mentés</div>
|
|
||||||
<div class="disk-step" id="mstep-done"><span class="disk-step-icon">○</span> Kész</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="disk-progress-bar-wrap" style="margin-top:1.5rem">
|
|
||||||
<div class="system-bar" style="height:12px;border-radius:6px">
|
|
||||||
<div class="system-bar-fill system-bar-green" id="mig-progress-bar" style="width:0%;transition:width .4s ease;height:12px;border-radius:6px"></div>
|
|
||||||
</div>
|
|
||||||
<span class="mono form-hint" id="mig-progress-pct">0%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="mig-progress-msg" class="form-hint" style="margin-top:.75rem"></div>
|
|
||||||
<div id="mig-elapsed" class="form-hint mono" style="margin-top:.25rem"></div>
|
|
||||||
<div id="mig-progress-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="migrate-done-card" style="display:none">
|
|
||||||
<h3>✅ Adatáthelyezés kész!</h3>
|
|
||||||
<p id="done-msg" style="margin-top:.75rem;color:var(--text-secondary)">
|
|
||||||
Az alkalmazás az új tárolóról fut.
|
|
||||||
</p>
|
|
||||||
<div id="done-tier2-warning" class="alert alert-warning" style="display:none;margin-top:1rem">
|
|
||||||
A 2. szintű mentés törlésre került, mert a cél meghajtó megegyezett a mentési céllal.
|
|
||||||
<a href="/stacks/{{.Meta.Slug}}/deploy">Újrakonfigurálás →</a>
|
|
||||||
</div>
|
|
||||||
<div id="done-manual-steps" class="alert alert-warning" style="margin-top:1rem">
|
|
||||||
<strong>Javasolt lépések:</strong>
|
|
||||||
<ol style="margin:.5rem 0 0 1rem;padding:0">
|
|
||||||
<li>Ellenőrizd, hogy az alkalmazás megfelelően működik</li>
|
|
||||||
<li>Győződj meg róla, hogy minden adat megtalálható</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:1.5rem;display:flex;gap:.75rem;flex-wrap:wrap">
|
|
||||||
<a href="/stacks/{{.Meta.Slug}}/deploy" class="btn btn-primary">Alkalmazások megtekintése</a>
|
|
||||||
<button id="migrate-delete-old-btn" class="btn btn-outline btn-danger" onclick="deleteOldMigrationData()" style="display:none">
|
|
||||||
🗑️ Korábbi adatok törlése
|
|
||||||
</button>
|
|
||||||
<a href="/settings" class="btn btn-outline">Beállítások</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var stackName = '{{.Stack.Name}}';
|
|
||||||
var migPollTimer = null;
|
|
||||||
|
|
||||||
function startMigrate() {
|
|
||||||
var targetPath = document.getElementById('target-path').value;
|
|
||||||
if (!targetPath) {
|
|
||||||
document.getElementById('migrate-error').textContent = 'Válasszon cél tárhelyet.';
|
|
||||||
document.getElementById('migrate-error').style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('migrate-form-card').style.display = 'none';
|
|
||||||
document.getElementById('migrate-progress-card').style.display = 'block';
|
|
||||||
|
|
||||||
var autoDelete = document.getElementById('auto-delete').checked;
|
|
||||||
|
|
||||||
fetch('/api/storage/migrate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
||||||
body: JSON.stringify({stack_name: stackName, target_path: targetPath, auto_delete_stale: autoDelete})
|
|
||||||
})
|
|
||||||
.then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) {
|
|
||||||
showMigError(data.error || 'Ismeretlen hiba');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
migPollTimer = setInterval(pollMigProgress, 2000);
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
showMigError('Hálózati hiba: ' + e.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var migStepOrder = ['stopping','copying','updating','starting','cleaning','backing_up','done'];
|
|
||||||
|
|
||||||
function pollMigProgress() {
|
|
||||||
fetch('/api/storage/migrate/status')
|
|
||||||
.then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) return;
|
|
||||||
updateMigUI(data);
|
|
||||||
if (data.done) {
|
|
||||||
clearInterval(migPollTimer);
|
|
||||||
if (data.step === 'done') {
|
|
||||||
showMigDone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(){});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMigUI(data) {
|
|
||||||
var currentIdx = migStepOrder.indexOf(data.step);
|
|
||||||
if (currentIdx < 0 && data.step === 'rolling_back') {
|
|
||||||
currentIdx = 1; // show during copy step
|
|
||||||
}
|
|
||||||
|
|
||||||
migStepOrder.forEach(function(s, i) {
|
|
||||||
var el = document.getElementById('mstep-' + s);
|
|
||||||
if (!el) return;
|
|
||||||
var icon = el.querySelector('.disk-step-icon');
|
|
||||||
if (i < currentIdx) {
|
|
||||||
el.className = 'disk-step disk-step-done';
|
|
||||||
icon.textContent = '✅';
|
|
||||||
} else if (i === currentIdx) {
|
|
||||||
el.className = 'disk-step disk-step-active';
|
|
||||||
icon.textContent = (data.step === 'error' || data.step === 'rolling_back') ? '❌' : '⏳';
|
|
||||||
} else {
|
|
||||||
el.className = 'disk-step';
|
|
||||||
icon.textContent = '○';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var pct = data.pct || 0;
|
|
||||||
document.getElementById('mig-progress-bar').style.width = pct + '%';
|
|
||||||
document.getElementById('mig-progress-pct').textContent = pct + '%';
|
|
||||||
document.getElementById('mig-progress-msg').textContent = data.msg || '';
|
|
||||||
|
|
||||||
if (data.elapsed_sec) {
|
|
||||||
document.getElementById('mig-elapsed').textContent = data.elapsed_sec + ' másodperce fut';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.step === 'error' || (data.error && data.error !== '')) {
|
|
||||||
showMigError(data.error || data.msg || 'Ismeretlen hiba');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMigError(msg) {
|
|
||||||
clearInterval(migPollTimer);
|
|
||||||
document.getElementById('mig-progress-error').textContent = 'Hiba: ' + msg;
|
|
||||||
document.getElementById('mig-progress-error').style.display = 'block';
|
|
||||||
document.getElementById('migrate-progress-card').querySelector('h3').textContent = 'Áthelyezés sikertelen';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMigDone() {
|
|
||||||
document.getElementById('migrate-progress-card').style.display = 'none';
|
|
||||||
document.getElementById('migrate-done-card').style.display = 'block';
|
|
||||||
document.getElementById('migrate-done-card').scrollIntoView({behavior:'smooth'});
|
|
||||||
|
|
||||||
var autoDeleteChecked = document.getElementById('auto-delete').checked;
|
|
||||||
if (autoDeleteChecked) {
|
|
||||||
document.getElementById('done-msg').textContent =
|
|
||||||
'Az alkalmazás az új tárolóról fut. A régi adatok automatikusan törölve lettek.';
|
|
||||||
} else {
|
|
||||||
document.getElementById('done-msg').innerHTML =
|
|
||||||
'Az alkalmazás az új tárolóról fut.<br>A régi adatok a korábbi helyen megmaradtak.';
|
|
||||||
document.getElementById('migrate-delete-old-btn').style.display = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteOldMigrationData() {
|
|
||||||
var oldPath = '{{.CurrentHDDPath}}';
|
|
||||||
if (!confirm('Biztosan törölni szeretnéd a korábbi adatokat?\n\nTárhely: ' + oldPath + '\n\n⚠️ Ez a művelet visszavonhatatlan!\nElőtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!confirm('UTOLSÓ FIGYELMEZTETÉS!\n\nA törlés visszavonhatatlan. Biztosan folytatod?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var btn = document.getElementById('migrate-delete-old-btn');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Törlés folyamatban...';
|
|
||||||
|
|
||||||
fetch('/api/storage/stale-cleanup', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
||||||
body: JSON.stringify({stack_name: stackName, stale_path: oldPath})
|
|
||||||
})
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) {
|
|
||||||
alert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = '🗑️ Korábbi adatok törlése';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
btn.textContent = '✅ Korábbi adatok törölve (' + (data.freed_human || '') + ')';
|
|
||||||
btn.classList.remove('btn-danger');
|
|
||||||
btn.classList.add('btn-outline');
|
|
||||||
btn.onclick = null;
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
alert('Hálózati hiba: ' + e.message);
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = '🗑️ Korábbi adatok törlése';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{template "layout_end" .}}
|
|
||||||
{{end}}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
{{define "migrate_drive"}}
|
|
||||||
{{template "layout_start" .}}
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<div style="display:flex;align-items:center;gap:.5rem">
|
|
||||||
<a href="/settings" class="btn btn-sm btn-outline">← Vissza</a>
|
|
||||||
<h2>Meghajtó kiváltása</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="drive-mig-form-card">
|
|
||||||
<h3>Összes adat átköltöztetése másik meghajtóra</h3>
|
|
||||||
|
|
||||||
<div class="settings-grid" style="margin-bottom:1.5rem">
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Forrás meghajtó</span>
|
|
||||||
<span class="settings-value mono">{{.SourceLabel}} ({{.SourcePath}})</span>
|
|
||||||
</div>
|
|
||||||
{{if .SourceDiskInfo}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Használat</span>
|
|
||||||
<span class="settings-value mono">{{.SourceDiskInfo.UsedHuman}} / {{.SourceDiskInfo.TotalHuman}}</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Alkalmazások</span>
|
|
||||||
<span class="settings-value">{{range $i, $app := .AppsOnSource}}{{if $i}}, {{end}}{{$app.DisplayName}}{{end}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="dest-path">Cél meghajtó <span class="required">*</span></label>
|
|
||||||
<select id="dest-path" class="form-control">
|
|
||||||
{{range .DestPaths}}
|
|
||||||
<option value="{{.Path}}">{{.Label}} ({{.Path}}) — {{.FreeHuman}} szabad</option>
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-warning" style="margin-bottom:1.5rem">
|
|
||||||
<strong>Figyelmeztetések:</strong>
|
|
||||||
<ul style="margin:.5rem 0 0 1rem;padding:0">
|
|
||||||
<li>Minden alkalmazás leáll a mozgatás idejére</li>
|
|
||||||
<li>Nagy adatmennyiségnél ez hosszabb ideig tarthat</li>
|
|
||||||
<li>A restic mentés repók NEM kerülnek átmásolásra (helyet spórolunk)</li>
|
|
||||||
<li>A forrás meghajtó "Kiváltva" állapotba kerül</li>
|
|
||||||
<li>A 2. szintű mentések automatikusan átirányításra kerülnek</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if .Tier2Impact}}
|
|
||||||
<div class="alert alert-info" style="margin-bottom:1.5rem">
|
|
||||||
<strong>Mentési hatás:</strong>
|
|
||||||
<ul style="margin:.5rem 0 0 1rem;padding:0">
|
|
||||||
{{range .Tier2Impact}}
|
|
||||||
<li>{{.}}</li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<div id="drive-mig-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
|
|
||||||
|
|
||||||
<div class="form-actions" style="gap:.75rem">
|
|
||||||
<button class="btn btn-primary" onclick="startDriveMigrate()">📦 Meghajtó kiváltás indítása</button>
|
|
||||||
<a href="/settings" class="btn btn-outline">Mégsem</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="drive-mig-progress-card" style="display:none">
|
|
||||||
<h3>Meghajtó kiváltás folyamatban...</h3>
|
|
||||||
|
|
||||||
<div class="disk-progress-steps" id="dm-steps">
|
|
||||||
<div class="disk-step" id="dmstep-validating"><span class="disk-step-icon">○</span> Ellenőrzés</div>
|
|
||||||
<div class="disk-step" id="dmstep-stopping"><span class="disk-step-icon">○</span> Alkalmazások leállítása</div>
|
|
||||||
<div class="disk-step" id="dmstep-copying"><span class="disk-step-icon">○</span> Adatok másolása</div>
|
|
||||||
<div class="disk-step" id="dmstep-verifying"><span class="disk-step-icon">○</span> Ellenőrzés</div>
|
|
||||||
<div class="disk-step" id="dmstep-configuring"><span class="disk-step-icon">○</span> Konfiguráció</div>
|
|
||||||
<div class="disk-step" id="dmstep-starting"><span class="disk-step-icon">○</span> Alkalmazások indítása</div>
|
|
||||||
<div class="disk-step" id="dmstep-backup"><span class="disk-step-icon">○</span> Biztonsági mentés</div>
|
|
||||||
<div class="disk-step" id="dmstep-done"><span class="disk-step-icon">○</span> Kész</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="disk-progress-bar-wrap" style="margin-top:1.5rem">
|
|
||||||
<div class="system-bar" style="height:12px;border-radius:6px">
|
|
||||||
<div class="system-bar-fill system-bar-green" id="dm-progress-bar" style="width:0%;transition:width .4s ease;height:12px;border-radius:6px"></div>
|
|
||||||
</div>
|
|
||||||
<span class="mono form-hint" id="dm-progress-pct">0%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dm-progress-msg" class="form-hint" style="margin-top:.75rem"></div>
|
|
||||||
<div id="dm-progress-detail" class="form-hint mono" style="margin-top:.25rem;font-size:.85rem"></div>
|
|
||||||
<div id="dm-elapsed" class="form-hint mono" style="margin-top:.25rem"></div>
|
|
||||||
<div id="dm-progress-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="drive-mig-done-card" style="display:none">
|
|
||||||
<h3>Meghajtó kiváltás kész!</h3>
|
|
||||||
<p id="dm-done-msg" style="margin-top:.75rem;color:var(--text-secondary)"></p>
|
|
||||||
<div class="alert alert-info" style="margin-top:1rem">
|
|
||||||
<strong>A forrás meghajtó biztonságosan eltávolítható.</strong>
|
|
||||||
Ha nem szándékozod újrafelhasználni, a Beállítások oldalon eltávolíthatod a rendszerből.
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:1.5rem;display:flex;gap:.75rem;flex-wrap:wrap">
|
|
||||||
<a href="/settings" class="btn btn-primary">Beállítások</a>
|
|
||||||
<a href="/backups" class="btn btn-outline">Mentések</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var sourcePath = '{{.SourcePath}}';
|
|
||||||
var dmPollTimer = null;
|
|
||||||
|
|
||||||
function startDriveMigrate() {
|
|
||||||
var destPath = document.getElementById('dest-path').value;
|
|
||||||
if (!destPath) {
|
|
||||||
document.getElementById('drive-mig-error').textContent = 'Válasszon cél meghajtót.';
|
|
||||||
document.getElementById('drive-mig-error').style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirm('Biztosan ki szeretné váltani a forrás meghajtót?\n\nMinden alkalmazás leáll a migráció idejére.\nEz a művelet nem vonható vissza egyszerűen.')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('drive-mig-form-card').style.display = 'none';
|
|
||||||
document.getElementById('drive-mig-progress-card').style.display = 'block';
|
|
||||||
|
|
||||||
fetch('/api/storage/migrate-drive', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
||||||
body: JSON.stringify({source_path: sourcePath, dest_path: destPath})
|
|
||||||
})
|
|
||||||
.then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) {
|
|
||||||
showDMError(data.error || 'Ismeretlen hiba');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dmPollTimer = setInterval(pollDMProgress, 2000);
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
showDMError('Hálózati hiba: ' + e.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var dmStepOrder = ['validating','stopping','copying','verifying','configuring','starting','backup','done'];
|
|
||||||
|
|
||||||
function pollDMProgress() {
|
|
||||||
fetch('/api/storage/migrate-drive/status')
|
|
||||||
.then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) return;
|
|
||||||
updateDMUI(data);
|
|
||||||
if (data.done) {
|
|
||||||
clearInterval(dmPollTimer);
|
|
||||||
if (data.step === 'done') {
|
|
||||||
showDMDone(data.msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(){});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDMUI(data) {
|
|
||||||
var currentIdx = dmStepOrder.indexOf(data.step);
|
|
||||||
if (currentIdx < 0 && data.step === 'rolling_back') {
|
|
||||||
currentIdx = dmStepOrder.indexOf('copying');
|
|
||||||
}
|
|
||||||
|
|
||||||
dmStepOrder.forEach(function(s, i) {
|
|
||||||
var el = document.getElementById('dmstep-' + s);
|
|
||||||
if (!el) return;
|
|
||||||
var icon = el.querySelector('.disk-step-icon');
|
|
||||||
if (i < currentIdx) {
|
|
||||||
el.className = 'disk-step disk-step-done';
|
|
||||||
icon.textContent = '\u2705';
|
|
||||||
} else if (i === currentIdx) {
|
|
||||||
el.className = 'disk-step disk-step-active';
|
|
||||||
icon.textContent = (data.step === 'error' || data.step === 'rolling_back') ? '\u274C' : '\u23F3';
|
|
||||||
} else {
|
|
||||||
el.className = 'disk-step';
|
|
||||||
icon.textContent = '\u25CB';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var pct = data.pct || 0;
|
|
||||||
document.getElementById('dm-progress-bar').style.width = pct + '%';
|
|
||||||
document.getElementById('dm-progress-pct').textContent = pct + '%';
|
|
||||||
document.getElementById('dm-progress-msg').textContent = data.msg || '';
|
|
||||||
document.getElementById('dm-progress-detail').textContent = data.detail || '';
|
|
||||||
|
|
||||||
if (data.elapsed_sec) {
|
|
||||||
document.getElementById('dm-elapsed').textContent = data.elapsed_sec + ' másodperce fut';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.step === 'error' || (data.error && data.error !== '')) {
|
|
||||||
showDMError(data.error || data.msg || 'Ismeretlen hiba');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDMError(msg) {
|
|
||||||
clearInterval(dmPollTimer);
|
|
||||||
document.getElementById('dm-progress-error').textContent = 'Hiba: ' + msg;
|
|
||||||
document.getElementById('dm-progress-error').style.display = 'block';
|
|
||||||
document.getElementById('drive-mig-progress-card').querySelector('h3').textContent = 'Meghajtó kiváltás sikertelen';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDMDone(msg) {
|
|
||||||
document.getElementById('drive-mig-progress-card').style.display = 'none';
|
|
||||||
document.getElementById('drive-mig-done-card').style.display = 'block';
|
|
||||||
document.getElementById('dm-done-msg').textContent = msg || 'A meghajtó sikeresen kiváltva.';
|
|
||||||
document.getElementById('drive-mig-done-card').scrollIntoView({behavior:'smooth'});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{template "layout_end" .}}
|
|
||||||
{{end}}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
{{define "restore"}}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="hu">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Katasztrófa utáni visszaállítás — Felhom</title>
|
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
|
||||||
<meta name="csrf-token" content="{{.CSRFToken}}">
|
|
||||||
<script>function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{}}</script>
|
|
||||||
<style>
|
|
||||||
body { background: var(--bg-darker, #0d1117); margin: 0; padding: 0; }
|
|
||||||
.dr-container { max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem; }
|
|
||||||
.dr-header { text-align: center; margin-bottom: 2rem; }
|
|
||||||
.dr-header img { width: 48px; height: 48px; margin-bottom: 0.5rem; }
|
|
||||||
.dr-header h1 { color: var(--warning, #f0ad4e); font-size: 1.5rem; margin: 0.5rem 0; }
|
|
||||||
.dr-header p { color: var(--text-secondary, #8b949e); margin: 0.25rem 0; }
|
|
||||||
.dr-card { background: var(--card-bg, #161b22); border: 1px solid var(--border, #30363d); border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; }
|
|
||||||
.dr-card h3 { margin: 0 0 0.75rem 0; color: var(--text-primary, #e6edf3); font-size: 1rem; }
|
|
||||||
.dr-drives { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
|
||||||
.dr-drive { background: var(--bg-darker, #0d1117); border: 1px solid var(--border, #30363d); border-radius: 6px; padding: 0.75rem 1rem; flex: 1; min-width: 200px; }
|
|
||||||
.dr-drive-label { font-weight: 600; color: var(--text-primary, #e6edf3); }
|
|
||||||
.dr-drive-path { font-size: 0.85rem; color: var(--text-secondary, #8b949e); font-family: monospace; }
|
|
||||||
.dr-drive-status { font-size: 0.85rem; margin-top: 0.25rem; }
|
|
||||||
.dr-drive-ok { color: var(--success, #3fb950); }
|
|
||||||
.dr-drive-warn { color: var(--warning, #f0ad4e); }
|
|
||||||
table { width: 100%; border-collapse: collapse; }
|
|
||||||
th { text-align: left; padding: 0.5rem 0.75rem; color: var(--text-secondary, #8b949e); font-size: 0.85rem; font-weight: 500; border-bottom: 1px solid var(--border, #30363d); }
|
|
||||||
td { padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border, #30363d); color: var(--text-primary, #e6edf3); font-size: 0.9rem; }
|
|
||||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: 500; }
|
|
||||||
.badge-ok { background: rgba(63,185,80,0.15); color: var(--success, #3fb950); }
|
|
||||||
.badge-warn { background: rgba(240,173,78,0.15); color: var(--warning, #f0ad4e); }
|
|
||||||
.badge-none { background: rgba(139,148,158,0.15); color: var(--text-secondary, #8b949e); }
|
|
||||||
.status-pending { color: var(--text-secondary, #8b949e); }
|
|
||||||
.status-restoring { color: var(--info, #58a6ff); }
|
|
||||||
.status-done { color: var(--success, #3fb950); }
|
|
||||||
.status-failed { color: var(--danger, #f85149); }
|
|
||||||
.status-skipped { color: var(--text-secondary, #8b949e); }
|
|
||||||
.dr-actions { display: flex; gap: 0.75rem; justify-content: center; margin-top: 1.5rem; }
|
|
||||||
.btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.6rem 1.5rem; border-radius: 6px; border: 1px solid transparent; font-size: 0.9rem; font-weight: 500; cursor: pointer; text-decoration: none; transition: background 0.2s; }
|
|
||||||
.btn-primary { background: var(--accent, #238636); color: #fff; border-color: var(--accent, #238636); }
|
|
||||||
.btn-primary:hover { background: #2ea043; }
|
|
||||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
||||||
.btn-outline { background: transparent; color: var(--text-secondary, #8b949e); border-color: var(--border, #30363d); }
|
|
||||||
.btn-outline:hover { color: var(--text-primary, #e6edf3); border-color: var(--text-secondary, #8b949e); }
|
|
||||||
.btn-success { background: var(--accent, #238636); color: #fff; }
|
|
||||||
.progress-bar { height: 4px; background: var(--border, #30363d); border-radius: 2px; margin-top: 1rem; overflow: hidden; display: none; }
|
|
||||||
.progress-bar-inner { height: 100%; background: var(--accent, #238636); transition: width 0.5s; width: 0%; }
|
|
||||||
.dr-info { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
|
|
||||||
.dr-info-item { font-size: 0.9rem; }
|
|
||||||
.dr-info-label { color: var(--text-secondary, #8b949e); }
|
|
||||||
.dr-info-value { color: var(--text-primary, #e6edf3); font-weight: 500; }
|
|
||||||
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--border, #30363d); border-top-color: var(--info, #58a6ff); border-radius: 50%; animation: spin 0.8s linear infinite; vertical-align: middle; margin-right: 4px; }
|
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="dr-container">
|
|
||||||
<div class="dr-header">
|
|
||||||
<img src="/static/felhom-logo.svg" alt="Felhom">
|
|
||||||
<h1>Korábbi telepítés észlelve</h1>
|
|
||||||
<p>A rendszer biztonsági mentést talált a központi szerveren</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Info card -->
|
|
||||||
<div class="dr-card">
|
|
||||||
<h3>Rendszer információ</h3>
|
|
||||||
<div class="dr-info">
|
|
||||||
<div class="dr-info-item">
|
|
||||||
<span class="dr-info-label">Ügyfél: </span>
|
|
||||||
<span class="dr-info-value">{{.CustomerName}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="dr-info-item">
|
|
||||||
<span class="dr-info-label">Domain: </span>
|
|
||||||
<span class="dr-info-value">{{.Domain}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="dr-info-item">
|
|
||||||
<span class="dr-info-label">Mentés időpontja: </span>
|
|
||||||
<span class="dr-info-value">{{.Timestamp}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Drives card -->
|
|
||||||
<div class="dr-card">
|
|
||||||
<h3>Meghajtók</h3>
|
|
||||||
<div class="dr-drives">
|
|
||||||
{{range .Drives}}
|
|
||||||
<div class="dr-drive">
|
|
||||||
<div class="dr-drive-label">{{.Label}}</div>
|
|
||||||
<div class="dr-drive-path">{{.Path}}</div>
|
|
||||||
<div class="dr-drive-status">
|
|
||||||
{{if .Available}}
|
|
||||||
{{if .HasBackup}}
|
|
||||||
<span class="dr-drive-ok">Elérhető, mentés megtalálva</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="dr-drive-ok">Elérhető</span>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
<span class="dr-drive-warn">Nem elérhető</span>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if not .Drives}}
|
|
||||||
<p style="color:var(--text-secondary)">Nem találhatók csatlakoztatott meghajtók.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Apps table card -->
|
|
||||||
<div class="dr-card">
|
|
||||||
<h3>Visszaállítható alkalmazások</h3>
|
|
||||||
{{if .Apps}}
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Alkalmazás</th>
|
|
||||||
<th>Konfiguráció</th>
|
|
||||||
<th>Adatok</th>
|
|
||||||
<th>DB mentés</th>
|
|
||||||
<th>Állapot</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="app-table-body">
|
|
||||||
{{range .Apps}}
|
|
||||||
<tr data-app="{{.Name}}">
|
|
||||||
<td>
|
|
||||||
<strong>{{.DisplayName}}</strong>
|
|
||||||
<div style="font-size:.8rem;color:var(--text-secondary)">{{.Name}}</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{if .HasConfig}}
|
|
||||||
<span class="badge badge-ok">Megtalálva</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="badge badge-none">Hiányzik</span>
|
|
||||||
{{end}}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{if .HasData}}
|
|
||||||
<span class="badge badge-ok">Elérhető</span>
|
|
||||||
{{else if .HasRsyncData}}
|
|
||||||
<span class="badge badge-warn">Mentésből</span>
|
|
||||||
{{else if not .NeedsHDD}}
|
|
||||||
<span class="badge badge-none">Nem szükséges</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="badge badge-warn">Hiányzik</span>
|
|
||||||
{{end}}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{if .HasDBDump}}
|
|
||||||
<span class="badge badge-ok">Van</span>
|
|
||||||
{{else}}
|
|
||||||
<span class="badge badge-none">Nincs</span>
|
|
||||||
{{end}}
|
|
||||||
</td>
|
|
||||||
<td class="app-status" data-app="{{.Name}}">
|
|
||||||
<span class="status-{{.Status}}">{{statusText .Status}}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="progress-bar" id="progress-bar">
|
|
||||||
<div class="progress-bar-inner" id="progress-inner"></div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<p style="color:var(--text-secondary)">Nem találhatók visszaállítható alkalmazások.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action buttons -->
|
|
||||||
<div class="dr-actions" id="dr-actions">
|
|
||||||
{{if eq .PlanStatus "pending"}}
|
|
||||||
{{if .Apps}}
|
|
||||||
<button class="btn btn-primary" id="btn-restore-all" onclick="startRestoreAll()">
|
|
||||||
Összes visszaállítása ({{len .Apps}} alkalmazás)
|
|
||||||
</button>
|
|
||||||
{{end}}
|
|
||||||
<button class="btn btn-outline" id="btn-skip" onclick="skipRestore()">
|
|
||||||
Kihagyás — tovább a vezérlőpulthoz
|
|
||||||
</button>
|
|
||||||
{{else if eq .PlanStatus "restoring"}}
|
|
||||||
<button class="btn btn-primary" disabled>
|
|
||||||
<span class="spinner"></span> Visszaállítás folyamatban...
|
|
||||||
</button>
|
|
||||||
{{else if eq .PlanStatus "done"}}
|
|
||||||
<a href="/" class="btn btn-success" id="btn-continue" onclick="finishRestore(event)">
|
|
||||||
Tovább a vezérlőpulthoz
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var polling = null;
|
|
||||||
var planStatus = "{{.PlanStatus}}";
|
|
||||||
|
|
||||||
if (planStatus === "restoring") {
|
|
||||||
startPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
function startRestoreAll() {
|
|
||||||
var btn = document.getElementById('btn-restore-all');
|
|
||||||
var skipBtn = document.getElementById('btn-skip');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<span class="spinner"></span> Visszaállítás indítása...';
|
|
||||||
if (skipBtn) skipBtn.style.display = 'none';
|
|
||||||
|
|
||||||
fetch('/api/restore/all', { method: 'POST', headers: csrfHeaders() })
|
|
||||||
.then(function(resp) { return resp.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.ok) {
|
|
||||||
planStatus = 'restoring';
|
|
||||||
document.getElementById('progress-bar').style.display = 'block';
|
|
||||||
startPolling();
|
|
||||||
} else {
|
|
||||||
alert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Összes visszaállítása';
|
|
||||||
if (skipBtn) skipBtn.style.display = '';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
alert('Hálózati hiba: ' + err.message);
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Összes visszaállítása';
|
|
||||||
if (skipBtn) skipBtn.style.display = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function skipRestore() {
|
|
||||||
if (!confirm('Biztosan ki szeretné hagyni a visszaállítást? A vezérlőpult üres alkalmazáslistával fog elindulni.')) return;
|
|
||||||
fetch('/api/restore/skip', { method: 'POST', headers: csrfHeaders() })
|
|
||||||
.then(function(resp) { return resp.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.ok) {
|
|
||||||
window.location.href = '/';
|
|
||||||
} else {
|
|
||||||
alert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(err) { alert('Hálózati hiba: ' + err.message); });
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishRestore(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
fetch('/api/restore/skip', { method: 'POST', headers: csrfHeaders() })
|
|
||||||
.then(function() { window.location.href = '/'; })
|
|
||||||
.catch(function() { window.location.href = '/'; });
|
|
||||||
}
|
|
||||||
|
|
||||||
function startPolling() {
|
|
||||||
if (polling) return;
|
|
||||||
document.getElementById('progress-bar').style.display = 'block';
|
|
||||||
polling = setInterval(pollStatus, 2000);
|
|
||||||
pollStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
var pollErrors = 0;
|
|
||||||
function pollStatus() {
|
|
||||||
fetch('/api/restore/status')
|
|
||||||
.then(function(resp) {
|
|
||||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
||||||
return resp.json();
|
|
||||||
})
|
|
||||||
.then(function(data) {
|
|
||||||
pollErrors = 0;
|
|
||||||
if (!data.ok) return;
|
|
||||||
updateTable(data.apps || []);
|
|
||||||
updateProgress(data.apps || []);
|
|
||||||
|
|
||||||
if (data.status === 'done') {
|
|
||||||
clearInterval(polling);
|
|
||||||
polling = null;
|
|
||||||
planStatus = 'done';
|
|
||||||
updateActions();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
pollErrors++;
|
|
||||||
console.error('Poll error:', err);
|
|
||||||
if (pollErrors >= 10) {
|
|
||||||
clearInterval(polling);
|
|
||||||
polling = null;
|
|
||||||
var actions = document.getElementById('dr-actions');
|
|
||||||
if (actions) {
|
|
||||||
actions.innerHTML = '<p style="color:var(--danger)">Kapcsolat megszakadt. <a href="/restore">Oldal frissítése</a></p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTable(apps) {
|
|
||||||
apps.forEach(function(app) {
|
|
||||||
var cells = document.querySelectorAll('.app-status[data-app="' + app.name + '"]');
|
|
||||||
cells.forEach(function(cell) {
|
|
||||||
var span = document.createElement('span');
|
|
||||||
span.className = 'status-' + app.status;
|
|
||||||
if (app.status === 'restoring') {
|
|
||||||
var spinner = document.createElement('span');
|
|
||||||
spinner.className = 'spinner';
|
|
||||||
span.appendChild(spinner);
|
|
||||||
span.appendChild(document.createTextNode(' '));
|
|
||||||
}
|
|
||||||
span.appendChild(document.createTextNode(statusText(app.status)));
|
|
||||||
if (app.error) {
|
|
||||||
var errSpan = document.createElement('span');
|
|
||||||
errSpan.style.cssText = 'font-size:.8rem;color:var(--danger)';
|
|
||||||
errSpan.textContent = ' (' + app.error.substring(0, 60) + ')';
|
|
||||||
span.appendChild(errSpan);
|
|
||||||
}
|
|
||||||
cell.innerHTML = '';
|
|
||||||
cell.appendChild(span);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgress(apps) {
|
|
||||||
var total = apps.length;
|
|
||||||
if (total === 0) return;
|
|
||||||
var done = 0;
|
|
||||||
apps.forEach(function(a) {
|
|
||||||
if (a.status === 'done' || a.status === 'failed' || a.status === 'skipped') done++;
|
|
||||||
});
|
|
||||||
var pct = Math.round((done / total) * 100);
|
|
||||||
document.getElementById('progress-inner').style.width = pct + '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateActions() {
|
|
||||||
var actions = document.getElementById('dr-actions');
|
|
||||||
actions.innerHTML = '<a href="/" class="btn btn-success" id="btn-continue" onclick="finishRestore(event)">Tovább a vezérlőpulthoz</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusText(s) {
|
|
||||||
switch (s) {
|
|
||||||
case 'pending': return 'Várakozik';
|
|
||||||
case 'restoring': return 'Visszaállítás...';
|
|
||||||
case 'done': return 'Kész';
|
|
||||||
case 'failed': return 'Sikertelen';
|
|
||||||
case 'skipped': return 'Kihagyva';
|
|
||||||
default: return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,582 +0,0 @@
|
|||||||
{{define "storage_attach"}}
|
|
||||||
{{template "layout_start" .}}
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<div style="display:flex;align-items:center;gap:.5rem">
|
|
||||||
<a href="/settings" class="btn btn-sm btn-outline">← Vissza</a>
|
|
||||||
<h2>Meglévő meghajtó csatolása</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 1: Scan -->
|
|
||||||
<div class="settings-card" id="wizard-scan">
|
|
||||||
<h3>1. Meghajtók keresése</h3>
|
|
||||||
<p class="settings-card-desc">Keresse meg a rendszerhez csatlakoztatott, meglévő fájlrendszerrel rendelkező meghajtókat.</p>
|
|
||||||
|
|
||||||
<button class="btn btn-primary" onclick="scanDisks()" id="scan-btn">🔍 Meghajtók keresése</button>
|
|
||||||
<div id="scan-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
|
|
||||||
|
|
||||||
<div id="scan-result" style="display:none;margin-top:1.5rem">
|
|
||||||
<div id="available-disks"></div>
|
|
||||||
<div id="system-disks-note" style="display:none;margin-top:1rem"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 2: Browse -->
|
|
||||||
<div class="settings-card" id="wizard-browse" style="display:none">
|
|
||||||
<h3>2. Mappa kiválasztása</h3>
|
|
||||||
<p class="settings-card-desc">Válasszon ki egy mappát a meghajtón, amelyet a controller használni fog. Új mappát is létrehozhat.</p>
|
|
||||||
|
|
||||||
<div id="browse-info" class="settings-grid" style="margin-bottom:1rem">
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Partíció</span>
|
|
||||||
<span class="settings-value mono" id="browse-device"></span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Fájlrendszer</span>
|
|
||||||
<span class="settings-value mono" id="browse-fstype"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="browse-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
|
|
||||||
|
|
||||||
<div id="dir-browser" style="border:1px solid var(--border);border-radius:6px;padding:1rem;background:var(--card-bg);margin-bottom:1rem">
|
|
||||||
<div id="dir-breadcrumb" class="form-hint mono" style="margin-bottom:.75rem"></div>
|
|
||||||
<div id="dir-list" style="min-height:100px"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;gap:.75rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:1rem">
|
|
||||||
<div class="form-group" style="margin-bottom:0">
|
|
||||||
<label for="new-dir-name">Új mappa neve</label>
|
|
||||||
<input type="text" id="new-dir-name" class="form-control" placeholder="felhom_data"
|
|
||||||
pattern="[a-zA-Z0-9_]+" style="max-width:200px">
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-outline" onclick="createDir()" id="mkdir-btn">📁 Mappa létrehozása</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="selected-dir-info" class="alert alert-info" style="display:none;margin-bottom:1rem">
|
|
||||||
Kiválasztott mappa: <strong id="selected-dir-display" class="mono"></strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions" style="gap:.75rem">
|
|
||||||
<button class="btn btn-primary" onclick="goToConfigure()" id="browse-next-btn" disabled>Tovább →</button>
|
|
||||||
<button class="btn btn-outline" onclick="cancelAttach()">Mégsem</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 3: Configure -->
|
|
||||||
<div class="settings-card" id="wizard-configure" style="display:none">
|
|
||||||
<h3>3. Konfiguráció</h3>
|
|
||||||
<p class="settings-card-desc">Adja meg a csatolás paramétereit.</p>
|
|
||||||
|
|
||||||
<form id="attach-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Kiválasztott partíció</label>
|
|
||||||
<span class="settings-value mono" id="config-device-display"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Kiválasztott mappa</label>
|
|
||||||
<span class="settings-value mono" id="config-subpath-display"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="mount-name">Csatlakoztatási név <span class="required">*</span></label>
|
|
||||||
<div class="form-inline">
|
|
||||||
<span class="mono" style="opacity:.6">/mnt/</span>
|
|
||||||
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1"
|
|
||||||
pattern="[a-zA-Z0-9_]+" required style="max-width:160px">
|
|
||||||
</div>
|
|
||||||
<span class="form-hint">Pl. hdd_1 → a mappa a /mnt/hdd_1 útvonalra kerül</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="storage-label">Megnevezés</label>
|
|
||||||
<input type="text" id="storage-label" class="form-control" placeholder="Külső HDD 1TB" maxlength="50">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="toggle" style="margin-bottom:1.5rem">
|
|
||||||
<input type="checkbox" id="set-default" checked>
|
|
||||||
<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-info" style="margin-bottom:1.5rem">
|
|
||||||
<strong>ℹ️ Megjegyzés:</strong> A meghajtón lévő adatok <strong>NEM</strong> törlődnek.
|
|
||||||
A controller csak a kiválasztott mappában dolgozik.<br>
|
|
||||||
<strong>⚠️ A csatlakozási pont (/mnt/<név>) a meghajtó lecsatolásáig nem módosítható.</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions" style="gap:.75rem">
|
|
||||||
<button type="submit" class="btn btn-primary" id="attach-btn">Csatolás</button>
|
|
||||||
<button type="button" class="btn btn-outline" onclick="backToBrowse()">← Vissza</button>
|
|
||||||
</div>
|
|
||||||
<div id="attach-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 4: Progress -->
|
|
||||||
<div class="settings-card" id="wizard-progress" style="display:none">
|
|
||||||
<h3>4. Csatolás folyamatban...</h3>
|
|
||||||
|
|
||||||
<div class="disk-progress-steps" id="progress-steps">
|
|
||||||
<div class="disk-step" id="pstep-validating"><span class="disk-step-icon">○</span> Ellenőrzés</div>
|
|
||||||
<div class="disk-step" id="pstep-mounting"><span class="disk-step-icon">○</span> Csatlakoztatás</div>
|
|
||||||
<div class="disk-step" id="pstep-permissions"><span class="disk-step-icon">○</span> Mappák és jogosultságok</div>
|
|
||||||
<div class="disk-step" id="pstep-done"><span class="disk-step-icon">○</span> Regisztráció</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top:1.5rem;display:flex;align-items:center;gap:1rem">
|
|
||||||
<div class="progress-bar-task" style="flex:1">
|
|
||||||
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
|
|
||||||
</div>
|
|
||||||
<span id="progress-percent" style="font-size:0.9rem;color:var(--text-muted);font-family:'JetBrains Mono',monospace;white-space:nowrap">0%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="progress-msg" class="form-hint" style="margin-top:.75rem"></div>
|
|
||||||
<div id="progress-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Step 5: Done -->
|
|
||||||
<div class="settings-card" id="wizard-done" style="display:none">
|
|
||||||
<h3>✅ Meghajtó sikeresen csatolva!</h3>
|
|
||||||
<div id="done-info" class="settings-grid" style="margin-top:1rem">
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Útvonal</span>
|
|
||||||
<span class="settings-value mono" id="done-path"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="/settings" class="btn btn-primary" style="margin-top:1.5rem">← Vissza a Beállításokhoz</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var selectedDevice = null;
|
|
||||||
var selectedPartition = null;
|
|
||||||
var currentBrowsePath = '';
|
|
||||||
var rawMountPath = '';
|
|
||||||
var selectedSubPath = '';
|
|
||||||
var pollTimer = null;
|
|
||||||
|
|
||||||
// --- Step 1: Scan ---
|
|
||||||
|
|
||||||
function scanDisks() {
|
|
||||||
var btn = document.getElementById('scan-btn');
|
|
||||||
var errEl = document.getElementById('scan-error');
|
|
||||||
var resultEl = document.getElementById('scan-result');
|
|
||||||
btn.textContent = 'Keresés...';
|
|
||||||
btn.disabled = true;
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
resultEl.style.display = 'none';
|
|
||||||
|
|
||||||
// Clean up any stale raw mounts from interrupted previous sessions first,
|
|
||||||
// so the device appears as available in the scan results.
|
|
||||||
fetch('/api/storage/attach/cancel', {method:'POST', headers: csrfHeaders()})
|
|
||||||
.catch(function(){}) // ignore cancel errors
|
|
||||||
.then(function() { return fetch('/api/storage/scan', {method:'POST', headers: csrfHeaders()}); })
|
|
||||||
.then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
btn.textContent = '🔍 Meghajtók keresése';
|
|
||||||
btn.disabled = false;
|
|
||||||
if (!data.ok) {
|
|
||||||
errEl.textContent = data.error || 'Ismeretlen hiba';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
renderScanResult(data);
|
|
||||||
resultEl.style.display = 'block';
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
btn.textContent = '🔍 Meghajtók keresése';
|
|
||||||
btn.disabled = false;
|
|
||||||
errEl.textContent = 'Hálózati hiba: ' + e.message;
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderScanResult(data) {
|
|
||||||
var availEl = document.getElementById('available-disks');
|
|
||||||
var sysEl = document.getElementById('system-disks-note');
|
|
||||||
|
|
||||||
// Filter: only show disks that have at least one partition with a filesystem
|
|
||||||
var disksWithFS = [];
|
|
||||||
if (data.available) {
|
|
||||||
data.available.forEach(function(disk) {
|
|
||||||
if (disk.Partitions) {
|
|
||||||
var fsPartitions = disk.Partitions.filter(function(p) { return p.FSType && p.FSType !== ''; });
|
|
||||||
if (fsPartitions.length > 0) {
|
|
||||||
disksWithFS.push({disk: disk, partitions: fsPartitions});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disksWithFS.length === 0) {
|
|
||||||
availEl.innerHTML = '<div class="empty-state" style="padding:1rem">Nem található meglévő fájlrendszerrel rendelkező meghajtó.<br>' +
|
|
||||||
'<span class="form-hint">Ha üres meghajtót szeretne inicializálni, használja az <a href="/settings/storage/init">inicializálás varázslót</a>.</span></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var html = '<h4 style="margin-bottom:.75rem">Talált meghajtók csatolható partíciókkal:</h4>';
|
|
||||||
disksWithFS.forEach(function(item) {
|
|
||||||
var disk = item.disk;
|
|
||||||
html += '<div style="margin-bottom:1rem">';
|
|
||||||
html += '<div class="form-hint" style="margin-bottom:.5rem">' + disk.Path + ' — ' + (disk.Size || '?') +
|
|
||||||
(disk.Model ? ' — ' + disk.Model : '') + '</div>';
|
|
||||||
|
|
||||||
item.partitions.forEach(function(part) {
|
|
||||||
var info = part.FSType;
|
|
||||||
if (part.Label) info += ', címke: ' + part.Label;
|
|
||||||
if (part.UUID) info += ', UUID: ' + part.UUID.substring(0, 8) + '...';
|
|
||||||
|
|
||||||
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent;margin-bottom:.5rem" ' +
|
|
||||||
'onclick="selectPartition(this, \'' + part.Path + '\', \'' + (part.FSType || '') + '\', \'' + (part.Label || '') + '\')" ' +
|
|
||||||
'data-path="' + part.Path + '">' +
|
|
||||||
'<div class="storage-path-header"><div class="storage-path-info">' +
|
|
||||||
'<span class="storage-path-label">○ ' + part.Path + ' — ' + (part.Size || '?') + '</span>' +
|
|
||||||
'<span class="form-hint">' + info + '</span>' +
|
|
||||||
'</div></div></div>';
|
|
||||||
});
|
|
||||||
html += '</div>';
|
|
||||||
});
|
|
||||||
|
|
||||||
availEl.innerHTML = html;
|
|
||||||
|
|
||||||
if (data.system && data.system.length > 0) {
|
|
||||||
var sysNames = data.system.map(function(d){ return d.Path + ' (' + (d.Size||'?') + ')'; }).join(', ');
|
|
||||||
sysEl.innerHTML = '<span class="form-hint">A rendszermeghajtó(k) nem választhatók: ' + sysNames + '</span>';
|
|
||||||
sysEl.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectPartition(el, path, fsType, label) {
|
|
||||||
// Deselect all
|
|
||||||
document.querySelectorAll('[data-path]').forEach(function(d) {
|
|
||||||
d.style.border = '2px solid transparent';
|
|
||||||
var lbl = d.querySelector('.storage-path-label');
|
|
||||||
if (lbl) lbl.textContent = lbl.textContent.replace('● ', '○ ');
|
|
||||||
});
|
|
||||||
// Select this
|
|
||||||
el.style.border = '2px solid var(--accent-blue)';
|
|
||||||
var lbl = el.querySelector('.storage-path-label');
|
|
||||||
if (lbl) lbl.textContent = lbl.textContent.replace('○ ', '● ');
|
|
||||||
|
|
||||||
selectedDevice = path;
|
|
||||||
selectedPartition = {path: path, fsType: fsType, label: label};
|
|
||||||
|
|
||||||
// Mount raw and go to browse
|
|
||||||
mountRawAndBrowse(path, fsType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Step 2: Browse ---
|
|
||||||
|
|
||||||
function mountRawAndBrowse(devicePath, fsType) {
|
|
||||||
var errEl = document.getElementById('scan-error');
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
|
|
||||||
fetch('/api/storage/attach/mount-raw', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
||||||
body: JSON.stringify({device_path: devicePath})
|
|
||||||
}).then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) {
|
|
||||||
errEl.textContent = data.error || 'Raw mount sikertelen';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rawMountPath = data.raw_path;
|
|
||||||
|
|
||||||
// Show browse step
|
|
||||||
document.getElementById('browse-device').textContent = devicePath;
|
|
||||||
document.getElementById('browse-fstype').textContent = fsType;
|
|
||||||
document.getElementById('wizard-scan').style.display = 'none';
|
|
||||||
document.getElementById('wizard-browse').style.display = 'block';
|
|
||||||
document.getElementById('wizard-browse').scrollIntoView({behavior:'smooth'});
|
|
||||||
|
|
||||||
// Browse root
|
|
||||||
browseDirectory(rawMountPath);
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
errEl.textContent = 'Hálózati hiba: ' + e.message;
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function browseDirectory(path) {
|
|
||||||
currentBrowsePath = path;
|
|
||||||
var errEl = document.getElementById('browse-error');
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
|
|
||||||
// Update breadcrumb
|
|
||||||
var rel = path.replace(rawMountPath, '') || '/';
|
|
||||||
document.getElementById('dir-breadcrumb').textContent = 'Aktuális mappa: ' + rel;
|
|
||||||
|
|
||||||
fetch('/api/storage/attach/browse?path=' + encodeURIComponent(path))
|
|
||||||
.then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) {
|
|
||||||
errEl.textContent = data.error || 'Hiba a mappák listázásakor';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
renderDirList(data.dirs || [], path);
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
errEl.textContent = 'Hálózati hiba: ' + e.message;
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDirList(dirs, basePath) {
|
|
||||||
var listEl = document.getElementById('dir-list');
|
|
||||||
var html = '';
|
|
||||||
|
|
||||||
// "Use this directory" option (select the current directory itself)
|
|
||||||
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent;margin-bottom:.5rem;padding:.5rem .75rem" ' +
|
|
||||||
'onclick="selectDir(this, \'' + escapeJS(basePath) + '\')" data-dirpath="' + escapeAttr(basePath) + '">' +
|
|
||||||
'<span class="storage-path-label">📂 . (ez a mappa)</span></div>';
|
|
||||||
|
|
||||||
// Parent directory (if not at root)
|
|
||||||
if (basePath !== rawMountPath) {
|
|
||||||
var parentPath = basePath.substring(0, basePath.lastIndexOf('/'));
|
|
||||||
if (parentPath.length < rawMountPath.length) parentPath = rawMountPath;
|
|
||||||
html += '<div style="padding:.3rem .75rem;cursor:pointer;opacity:.7" onclick="browseDirectory(\'' + escapeJS(parentPath) + '\')">' +
|
|
||||||
'📁 .. (szülő mappa)</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dirs.length === 0) {
|
|
||||||
html += '<div class="form-hint" style="padding:.5rem .75rem">Üres mappa</div>';
|
|
||||||
} else {
|
|
||||||
dirs.forEach(function(dir) {
|
|
||||||
html += '<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.25rem">';
|
|
||||||
// Clickable to navigate into
|
|
||||||
if (dir.has_children) {
|
|
||||||
html += '<div style="padding:.3rem .75rem;cursor:pointer;flex:1" onclick="browseDirectory(\'' + escapeJS(dir.path) + '\')">' +
|
|
||||||
'📁 ' + dir.name + ' →</div>';
|
|
||||||
} else {
|
|
||||||
html += '<div style="padding:.3rem .75rem;flex:1">📁 ' + dir.name + '</div>';
|
|
||||||
}
|
|
||||||
// Select button
|
|
||||||
html += '<button class="btn btn-xs btn-outline" onclick="selectDir(null, \'' + escapeJS(dir.path) + '\')">Kiválasztás</button>';
|
|
||||||
html += '</div>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
listEl.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectDir(el, path) {
|
|
||||||
selectedSubPath = path;
|
|
||||||
document.getElementById('selected-dir-display').textContent = path.replace(rawMountPath, '') || '/';
|
|
||||||
document.getElementById('selected-dir-info').style.display = 'block';
|
|
||||||
document.getElementById('browse-next-btn').disabled = false;
|
|
||||||
|
|
||||||
// Highlight selected
|
|
||||||
document.querySelectorAll('[data-dirpath]').forEach(function(d) {
|
|
||||||
d.style.border = '2px solid transparent';
|
|
||||||
});
|
|
||||||
if (el) {
|
|
||||||
el.style.border = '2px solid var(--accent-blue)';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-fill mount name from partition label if available
|
|
||||||
var mountInput = document.getElementById('mount-name');
|
|
||||||
if (!mountInput.value && selectedPartition && selectedPartition.label) {
|
|
||||||
mountInput.value = selectedPartition.label.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDir() {
|
|
||||||
var nameInput = document.getElementById('new-dir-name');
|
|
||||||
var name = nameInput.value.trim();
|
|
||||||
if (!name) return;
|
|
||||||
|
|
||||||
var errEl = document.getElementById('browse-error');
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
|
|
||||||
// Client-side validation
|
|
||||||
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
|
|
||||||
errEl.textContent = 'A mappanéven csak betűk, számok és alávonás megengedett.';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (name.length > 32) {
|
|
||||||
errEl.textContent = 'A mappanév legfeljebb 32 karakter lehet.';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('/api/storage/attach/mkdir', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
||||||
body: JSON.stringify({path: currentBrowsePath, name: name})
|
|
||||||
}).then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) {
|
|
||||||
errEl.textContent = data.error || 'Mappa létrehozása sikertelen';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nameInput.value = '';
|
|
||||||
// Auto-select the created directory
|
|
||||||
selectedSubPath = data.created_path;
|
|
||||||
document.getElementById('selected-dir-display').textContent = data.created_path.replace(rawMountPath, '');
|
|
||||||
document.getElementById('selected-dir-info').style.display = 'block';
|
|
||||||
document.getElementById('browse-next-btn').disabled = false;
|
|
||||||
// Refresh directory listing
|
|
||||||
browseDirectory(currentBrowsePath);
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
errEl.textContent = 'Hálózati hiba: ' + e.message;
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToConfigure() {
|
|
||||||
if (!selectedSubPath) return;
|
|
||||||
document.getElementById('config-device-display').textContent = selectedDevice;
|
|
||||||
document.getElementById('config-subpath-display').textContent = selectedSubPath.replace(rawMountPath, '') || '/ (gyökérmappa)';
|
|
||||||
document.getElementById('wizard-browse').style.display = 'none';
|
|
||||||
document.getElementById('wizard-configure').style.display = 'block';
|
|
||||||
document.getElementById('wizard-configure').scrollIntoView({behavior:'smooth'});
|
|
||||||
}
|
|
||||||
|
|
||||||
function backToBrowse() {
|
|
||||||
document.getElementById('wizard-configure').style.display = 'none';
|
|
||||||
document.getElementById('wizard-browse').style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelAttach() {
|
|
||||||
// Cleanup raw mount
|
|
||||||
fetch('/api/storage/attach/cancel', {method:'POST', headers: csrfHeaders()}).catch(function(){});
|
|
||||||
window.location.href = '/settings';
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Step 3: Submit ---
|
|
||||||
|
|
||||||
document.getElementById('attach-form').addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
var mountName = document.getElementById('mount-name').value.trim();
|
|
||||||
var label = document.getElementById('storage-label').value.trim();
|
|
||||||
var setDefault = document.getElementById('set-default').checked;
|
|
||||||
var errEl = document.getElementById('attach-error');
|
|
||||||
|
|
||||||
if (!mountName) {
|
|
||||||
errEl.textContent = 'A csatlakoztatási nevet meg kell adni.';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
document.getElementById('wizard-configure').style.display = 'none';
|
|
||||||
document.getElementById('wizard-progress').style.display = 'block';
|
|
||||||
document.getElementById('wizard-progress').scrollIntoView({behavior:'smooth'});
|
|
||||||
|
|
||||||
var body = {
|
|
||||||
device_path: selectedDevice,
|
|
||||||
mount_name: mountName,
|
|
||||||
sub_path: selectedSubPath,
|
|
||||||
label: label,
|
|
||||||
set_default: setDefault
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('/api/storage/attach', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
}).then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) {
|
|
||||||
showProgressError(data.error || 'Ismeretlen hiba');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pollTimer = setInterval(pollProgress, 1500);
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
showProgressError('Hálózati hiba: ' + e.message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Step 4: Progress ---
|
|
||||||
|
|
||||||
var stepOrder = ['validating','mounting','permissions','done'];
|
|
||||||
|
|
||||||
function pollProgress() {
|
|
||||||
fetch('/api/storage/attach/status')
|
|
||||||
.then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) return;
|
|
||||||
updateProgressUI(data);
|
|
||||||
if (data.done) {
|
|
||||||
clearInterval(pollTimer);
|
|
||||||
if (data.step === 'done') {
|
|
||||||
showDone('/mnt/' + document.getElementById('mount-name').value.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(){});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgressUI(data) {
|
|
||||||
var currentIdx = stepOrder.indexOf(data.step);
|
|
||||||
stepOrder.forEach(function(s, i) {
|
|
||||||
var el = document.getElementById('pstep-' + s);
|
|
||||||
if (!el) return;
|
|
||||||
var icon = el.querySelector('.disk-step-icon');
|
|
||||||
if (i < currentIdx) {
|
|
||||||
el.className = 'disk-step disk-step-done';
|
|
||||||
icon.textContent = '✅';
|
|
||||||
} else if (i === currentIdx) {
|
|
||||||
el.className = 'disk-step disk-step-active';
|
|
||||||
icon.textContent = data.step === 'error' ? '❌' : '⏳';
|
|
||||||
} else {
|
|
||||||
el.className = 'disk-step';
|
|
||||||
icon.textContent = '○';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var pct = data.pct || 0;
|
|
||||||
document.getElementById('progress-fill').style.width = pct + '%';
|
|
||||||
document.getElementById('progress-percent').textContent = pct + '%';
|
|
||||||
document.getElementById('progress-msg').textContent = data.msg || '';
|
|
||||||
|
|
||||||
if (data.step === 'error' || data.error) {
|
|
||||||
showProgressError(data.error || data.msg || 'Ismeretlen hiba');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showProgressError(msg) {
|
|
||||||
clearInterval(pollTimer);
|
|
||||||
document.getElementById('progress-error').textContent = 'Hiba: ' + msg;
|
|
||||||
document.getElementById('progress-error').style.display = 'block';
|
|
||||||
document.getElementById('wizard-progress').querySelector('h3').textContent = 'Csatolás sikertelen';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDone(mountPath) {
|
|
||||||
document.getElementById('wizard-progress').style.display = 'none';
|
|
||||||
document.getElementById('wizard-done').style.display = 'block';
|
|
||||||
document.getElementById('done-path').textContent = mountPath;
|
|
||||||
document.getElementById('wizard-done').scrollIntoView({behavior:'smooth'});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Helpers ---
|
|
||||||
|
|
||||||
function escapeJS(s) {
|
|
||||||
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeAttr(s) {
|
|
||||||
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup on page unload (best-effort)
|
|
||||||
window.addEventListener('beforeunload', function() {
|
|
||||||
if (rawMountPath && !document.getElementById('wizard-done').style.display !== 'none') {
|
|
||||||
// Best-effort cleanup via fetch (sendBeacon can't send CSRF headers)
|
|
||||||
fetch('/api/storage/attach/cancel', {method:'POST', headers: csrfHeaders(), keepalive: true}).catch(function(){});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{template "layout_end" .}}
|
|
||||||
{{end}}
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
{{define "storage_init"}}
|
|
||||||
{{template "layout_start" .}}
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<div style="display:flex;align-items:center;gap:.5rem">
|
|
||||||
<a href="/settings" class="btn btn-sm btn-outline">← Vissza</a>
|
|
||||||
<h2>Új meghajtó inicializálása</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="wizard-scan">
|
|
||||||
<h3>1. Meghajtók keresése</h3>
|
|
||||||
<p class="settings-card-desc">Keresse meg a rendszerhez csatlakoztatott, még nem inicializált meghajtókat.</p>
|
|
||||||
|
|
||||||
<button class="btn btn-primary" onclick="scanDisks()" id="scan-btn">🔍 Meghajtók keresése</button>
|
|
||||||
<div id="scan-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
|
|
||||||
|
|
||||||
<div id="scan-result" style="display:none;margin-top:1.5rem">
|
|
||||||
<div id="available-disks"></div>
|
|
||||||
<div id="system-disks-note" style="display:none;margin-top:1rem"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="wizard-configure" style="display:none">
|
|
||||||
<h3>2. Konfiguráció</h3>
|
|
||||||
<p class="settings-card-desc">Adja meg az inicializálás paramétereit.</p>
|
|
||||||
|
|
||||||
<form id="init-form">
|
|
||||||
<input type="hidden" id="selected-device" name="device_path">
|
|
||||||
<input type="hidden" id="create-partition" name="create_partition" value="true">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Kiválasztott eszköz</label>
|
|
||||||
<span class="settings-value mono" id="selected-device-display"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="mount-name">Csatlakoztatási név <span class="required">*</span></label>
|
|
||||||
<div class="form-inline">
|
|
||||||
<span class="mono" style="opacity:.6">/mnt/</span>
|
|
||||||
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1"
|
|
||||||
pattern="[a-zA-Z0-9_]+" required style="max-width:160px">
|
|
||||||
</div>
|
|
||||||
<span class="form-hint">Pl. hdd_1 → a meghajtó a /mnt/hdd_1 útvonalra kerül</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="storage-label">Megnevezés</label>
|
|
||||||
<input type="text" id="storage-label" class="form-control" placeholder="Külső HDD 1TB" maxlength="50">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="toggle" style="margin-bottom:1.5rem">
|
|
||||||
<input type="checkbox" id="set-default" checked>
|
|
||||||
<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-error" style="margin-bottom:1.5rem">
|
|
||||||
<strong>⚠️ FIGYELEM:</strong> A meghajtó <strong>ÖSSZES</strong> adata törlődik!<br>
|
|
||||||
Ez a művelet <strong>NEM vonható vissza.</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="confirm-input">A folytatáshoz írja be: <strong>FORMÁZÁS</strong></label>
|
|
||||||
<input type="text" id="confirm-input" class="form-control" placeholder="FORMÁZÁS"
|
|
||||||
autocomplete="off" style="max-width:200px">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions" style="gap:.75rem">
|
|
||||||
<button type="submit" class="btn btn-danger-outline" id="init-btn" disabled>
|
|
||||||
Inicializálás indítása
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline" onclick="resetWizard()">Mégsem</button>
|
|
||||||
</div>
|
|
||||||
<div id="init-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="wizard-progress" style="display:none">
|
|
||||||
<h3>3. Inicializálás folyamatban...</h3>
|
|
||||||
|
|
||||||
<div class="disk-progress-steps" id="progress-steps">
|
|
||||||
<div class="disk-step" id="pstep-validating"><span class="disk-step-icon">○</span> Eszköz ellenőrzése</div>
|
|
||||||
<div class="disk-step" id="pstep-partitioning"><span class="disk-step-icon">○</span> Partíció létrehozása</div>
|
|
||||||
<div class="disk-step" id="pstep-formatting"><span class="disk-step-icon">○</span> Fájlrendszer formázása (ext4)</div>
|
|
||||||
<div class="disk-step" id="pstep-mounting"><span class="disk-step-icon">○</span> Csatlakoztatás</div>
|
|
||||||
<div class="disk-step" id="pstep-permissions"><span class="disk-step-icon">○</span> Mappák és jogosultságok</div>
|
|
||||||
<div class="disk-step" id="pstep-done"><span class="disk-step-icon">○</span> Regisztráció</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top:1.5rem;display:flex;align-items:center;gap:1rem">
|
|
||||||
<div class="progress-bar-task" style="flex:1">
|
|
||||||
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
|
|
||||||
</div>
|
|
||||||
<span id="progress-percent" style="font-size:0.9rem;color:var(--text-muted);font-family:'JetBrains Mono',monospace;white-space:nowrap">0%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="progress-msg" class="form-hint" style="margin-top:.75rem"></div>
|
|
||||||
<div id="progress-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card" id="wizard-done" style="display:none">
|
|
||||||
<h3>✅ Meghajtó sikeresen inicializálva!</h3>
|
|
||||||
<div id="done-info" class="settings-grid" style="margin-top:1rem">
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Útvonal</span>
|
|
||||||
<span class="settings-value mono" id="done-path"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="/settings" class="btn btn-primary" style="margin-top:1.5rem">← Vissza a Beállításokhoz</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var selectedDevice = null;
|
|
||||||
var pollTimer = null;
|
|
||||||
|
|
||||||
function scanDisks() {
|
|
||||||
var btn = document.getElementById('scan-btn');
|
|
||||||
var errEl = document.getElementById('scan-error');
|
|
||||||
var resultEl = document.getElementById('scan-result');
|
|
||||||
btn.textContent = 'Keresés...';
|
|
||||||
btn.disabled = true;
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
resultEl.style.display = 'none';
|
|
||||||
|
|
||||||
fetch('/api/storage/scan', {method:'POST', headers: csrfHeaders()})
|
|
||||||
.then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
btn.textContent = '🔍 Meghajtók keresése';
|
|
||||||
btn.disabled = false;
|
|
||||||
if (!data.ok) {
|
|
||||||
errEl.textContent = data.error || 'Ismeretlen hiba';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
renderScanResult(data);
|
|
||||||
resultEl.style.display = 'block';
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
btn.textContent = '🔍 Meghajtók keresése';
|
|
||||||
btn.disabled = false;
|
|
||||||
errEl.textContent = 'Hálózati hiba: ' + e.message;
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderScanResult(data) {
|
|
||||||
var availEl = document.getElementById('available-disks');
|
|
||||||
var sysEl = document.getElementById('system-disks-note');
|
|
||||||
var hasAvail = data.available && data.available.length > 0;
|
|
||||||
var hasFP = data.formatable_partitions && data.formatable_partitions.length > 0;
|
|
||||||
|
|
||||||
if (!hasAvail && !hasFP) {
|
|
||||||
availEl.innerHTML = '<div class="empty-state" style="padding:1rem">Nem található inicializálható meghajtó vagy partíció.</div>';
|
|
||||||
} else {
|
|
||||||
var html = '';
|
|
||||||
if (hasAvail) {
|
|
||||||
html += '<h4 style="margin-bottom:.75rem">Talált meghajtók (' + data.available.length + '):</h4>';
|
|
||||||
data.available.forEach(function(disk) {
|
|
||||||
var partInfo = '';
|
|
||||||
if (disk.Partitions && disk.Partitions.length > 0) {
|
|
||||||
partInfo = disk.Partitions.map(function(p) {
|
|
||||||
return p.Name + (p.FSType ? ' (' + p.FSType + ')' : ' (nincs fájlrendszer)') + (p.MountPoint ? ' → ' + p.MountPoint : '');
|
|
||||||
}).join(', ');
|
|
||||||
}
|
|
||||||
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent" ' +
|
|
||||||
'onclick="selectDisk(this, \'' + disk.Path + '\', true)" ' +
|
|
||||||
'data-path="' + disk.Path + '" id="disk-' + disk.Name + '">' +
|
|
||||||
'<div class="storage-path-header"><div class="storage-path-info">' +
|
|
||||||
'<span class="storage-path-label">○ ' + disk.Path + ' — ' + (disk.Size || '?') + '</span>' +
|
|
||||||
(disk.Model ? '<span class="storage-path-path">' + disk.Model + '</span>' : '') +
|
|
||||||
(partInfo ? '<span class="form-hint">' + partInfo + '</span>' : '<span class="form-hint">Nincs partíció</span>') +
|
|
||||||
'</div></div></div>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasFP) {
|
|
||||||
html += '<h4 style="margin-bottom:.75rem;margin-top:1.5rem">Formázható partíciók a rendszermeghajtón (' +
|
|
||||||
data.formatable_partitions.length + '):</h4>';
|
|
||||||
html += '<div class="alert alert-info" style="margin-bottom:.75rem;font-size:.85rem">' +
|
|
||||||
'Az alábbi partíciók a rendszermeghajtón találhatók, de nincsenek használatban. ' +
|
|
||||||
'Formázás után adattárolóként használhatók.</div>';
|
|
||||||
data.formatable_partitions.forEach(function(fp) {
|
|
||||||
var parentInfo = fp.ParentDiskPath + ' (' + fp.ParentDiskSize + ')';
|
|
||||||
if (fp.ParentDiskModel) parentInfo += ' — ' + fp.ParentDiskModel;
|
|
||||||
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent" ' +
|
|
||||||
'onclick="selectDisk(this, \'' + fp.Path + '\', false)" ' +
|
|
||||||
'data-path="' + fp.Path + '">' +
|
|
||||||
'<div class="storage-path-header"><div class="storage-path-info">' +
|
|
||||||
'<span class="storage-path-label">○ ' + fp.Path + ' — ' + (fp.Size || '?') + '</span>' +
|
|
||||||
'<span class="form-hint">Rendszermeghajtó partíciója: ' + parentInfo + '</span>' +
|
|
||||||
'<span class="form-hint">Nincs fájlrendszer — formázásra kész</span>' +
|
|
||||||
'</div></div></div>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
availEl.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.system && data.system.length > 0) {
|
|
||||||
var sysNames = data.system.map(function(d){ return d.Path + ' (' + (d.Size||'?') + ')'; }).join(', ');
|
|
||||||
sysEl.innerHTML = '<span class="form-hint">A rendszermeghajtó(k) nem választhatók: ' + sysNames + '</span>';
|
|
||||||
sysEl.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectDisk(el, path, needsPartition) {
|
|
||||||
// Deselect all
|
|
||||||
document.querySelectorAll('[data-path]').forEach(function(d) {
|
|
||||||
d.style.border = '2px solid transparent';
|
|
||||||
d.querySelector('.storage-path-label').textContent = d.querySelector('.storage-path-label').textContent.replace('● ', '○ ');
|
|
||||||
});
|
|
||||||
// Select this
|
|
||||||
el.style.border = '2px solid var(--accent-blue)';
|
|
||||||
el.querySelector('.storage-path-label').textContent = el.querySelector('.storage-path-label').textContent.replace('○ ', '● ');
|
|
||||||
|
|
||||||
selectedDevice = path;
|
|
||||||
document.getElementById('selected-device').value = path;
|
|
||||||
document.getElementById('create-partition').value = needsPartition ? 'true' : 'false';
|
|
||||||
document.getElementById('selected-device-display').textContent = path;
|
|
||||||
|
|
||||||
// Update warning text based on whole-disk vs partition-only operation
|
|
||||||
var warningEl = document.querySelector('#wizard-configure .alert-error');
|
|
||||||
if (needsPartition) {
|
|
||||||
warningEl.innerHTML = '<strong>⚠️ FIGYELEM:</strong> A meghajtó <strong>ÖSSZES</strong> adata törlődik!<br>' +
|
|
||||||
'Ez a művelet <strong>NEM vonható vissza.</strong>';
|
|
||||||
} else {
|
|
||||||
warningEl.innerHTML = '<strong>⚠️ FIGYELEM:</strong> A partíció formázva lesz, a rajta lévő adatok törlődnek!<br>' +
|
|
||||||
'A rendszermeghajtó többi partíciója <strong>NEM</strong> érintett.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show/hide the partitioning progress step
|
|
||||||
var partStep = document.getElementById('pstep-partitioning');
|
|
||||||
if (partStep) partStep.style.display = needsPartition ? '' : 'none';
|
|
||||||
|
|
||||||
// Show configure step
|
|
||||||
document.getElementById('wizard-configure').style.display = 'block';
|
|
||||||
document.getElementById('wizard-configure').scrollIntoView({behavior:'smooth'});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetWizard() {
|
|
||||||
selectedDevice = null;
|
|
||||||
document.getElementById('wizard-configure').style.display = 'none';
|
|
||||||
document.getElementById('init-error').style.display = 'none';
|
|
||||||
document.getElementById('confirm-input').value = '';
|
|
||||||
document.getElementById('init-btn').disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable init button only when confirmation is correct
|
|
||||||
document.getElementById('confirm-input').addEventListener('input', function() {
|
|
||||||
document.getElementById('init-btn').disabled = (this.value !== 'FORMÁZÁS');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('init-form').addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (document.getElementById('confirm-input').value !== 'FORMÁZÁS') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mountName = document.getElementById('mount-name').value.trim();
|
|
||||||
var label = document.getElementById('storage-label').value.trim();
|
|
||||||
var setDefault = document.getElementById('set-default').checked;
|
|
||||||
|
|
||||||
if (!mountName) {
|
|
||||||
document.getElementById('init-error').textContent = 'A csatlakoztatási nevet meg kell adni.';
|
|
||||||
document.getElementById('init-error').style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('wizard-scan').style.display = 'none';
|
|
||||||
document.getElementById('wizard-configure').style.display = 'none';
|
|
||||||
document.getElementById('wizard-progress').style.display = 'block';
|
|
||||||
document.getElementById('wizard-progress').scrollIntoView({behavior:'smooth'});
|
|
||||||
|
|
||||||
var body = {
|
|
||||||
device_path: selectedDevice,
|
|
||||||
mount_name: mountName,
|
|
||||||
label: label,
|
|
||||||
create_partition: document.getElementById('create-partition').value === 'true',
|
|
||||||
set_default: setDefault,
|
|
||||||
confirm: 'FORMÁZÁS'
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('/api/storage/init', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
}).then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) {
|
|
||||||
showProgressError(data.error || 'Ismeretlen hiba');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Start polling
|
|
||||||
pollTimer = setInterval(pollProgress, 1500);
|
|
||||||
})
|
|
||||||
.catch(function(e) {
|
|
||||||
showProgressError('Hálózati hiba: ' + e.message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var stepOrder = ['validating','partitioning','formatting','mounting','permissions','done'];
|
|
||||||
|
|
||||||
function pollProgress() {
|
|
||||||
fetch('/api/storage/init/status')
|
|
||||||
.then(function(r){ return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (!data.ok) return;
|
|
||||||
updateProgressUI(data);
|
|
||||||
if (data.done) {
|
|
||||||
clearInterval(pollTimer);
|
|
||||||
if (data.step === 'done') {
|
|
||||||
showDone('/mnt/' + document.getElementById('mount-name').value.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(){});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgressUI(data) {
|
|
||||||
// Update step icons
|
|
||||||
var currentIdx = stepOrder.indexOf(data.step);
|
|
||||||
stepOrder.forEach(function(s, i) {
|
|
||||||
var el = document.getElementById('pstep-' + s);
|
|
||||||
if (!el) return;
|
|
||||||
var icon = el.querySelector('.disk-step-icon');
|
|
||||||
if (i < currentIdx) {
|
|
||||||
el.className = 'disk-step disk-step-done';
|
|
||||||
icon.textContent = '✅';
|
|
||||||
} else if (i === currentIdx) {
|
|
||||||
el.className = 'disk-step disk-step-active';
|
|
||||||
icon.textContent = data.step === 'error' ? '❌' : '⏳';
|
|
||||||
} else {
|
|
||||||
el.className = 'disk-step';
|
|
||||||
icon.textContent = '○';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Progress bar
|
|
||||||
var pct = data.pct || 0;
|
|
||||||
document.getElementById('progress-fill').style.width = pct + '%';
|
|
||||||
document.getElementById('progress-percent').textContent = pct + '%';
|
|
||||||
document.getElementById('progress-msg').textContent = data.msg || '';
|
|
||||||
|
|
||||||
if (data.step === 'error' || data.error) {
|
|
||||||
showProgressError(data.error || data.msg || 'Ismeretlen hiba');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showProgressError(msg) {
|
|
||||||
clearInterval(pollTimer);
|
|
||||||
document.getElementById('progress-error').textContent = 'Hiba: ' + msg;
|
|
||||||
document.getElementById('progress-error').style.display = 'block';
|
|
||||||
document.getElementById('wizard-progress').querySelector('h3').textContent = 'Inicializálás sikertelen';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDone(mountPath) {
|
|
||||||
document.getElementById('wizard-progress').style.display = 'none';
|
|
||||||
document.getElementById('wizard-done').style.display = 'block';
|
|
||||||
document.getElementById('done-path').textContent = mountPath;
|
|
||||||
document.getElementById('wizard-done').scrollIntoView({behavior:'smooth'});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{template "layout_end" .}}
|
|
||||||
{{end}}
|
|
||||||
Reference in New Issue
Block a user