Localisation slice 6: the guide in English, and a stranger's first hour (R-561)
gates / gates (push) Successful in 29s
Part 0 shipped as controller v0.258.0 (its own commit). Part A is the guide's English twin. Part B is the walk: a box installed from scratch that day, used by an English speaker following only the English guide and the screens. VERDICT: not yet ready for an English-speaking tester, because the claim page — the one screen between them and their box — is English chrome with Hungarian messages (R-596, P1). Everything else held: the download page, the bilingual console, all three customer mails, the bind page and its refusal, the dashboard's first language from customer.language alone, both app pages, the whole catalog, the language switch both ways — every one of them with zero Hungarian lines. One intervention (I1 = R-494, filed 2026-09-14); the stop rule was not reached. The walk exercised what 2026-09-14 could not: the graphical installer, the auto-reboot, and the mailed link and self-bind page end to end — that walk's H1 is closed, because this session had a mailbox. R-214 CLOSED as a side effect and seen rather than reasoned about: the console's last paint is now the bilingual "the box is linked" banner. R-516 does NOT close, and the item-by-item note says why: more than half its twelve items are about what a HUNGARIAN household reads, and an English walk cannot see them. It now waits on a Hungarian walk with a second drive. Rows opened: R-596 (P1, the claim page), R-597 (the setup code is three Hungarian words), R-598 (the Backup page's protection warnings), R-599 (a drill's teardown is blocked 30 minutes by report staleness and the 409 does not say so). Golden 0.258.0 baked, published, vouched, with its record. The waiver was NOT retired and the record says why in one line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
@@ -0,0 +1,106 @@
|
||||
# DRILL — an English speaker's first hour on 0.258.0 (2026-09-20)
|
||||
|
||||
**Interventions a volunteer could not have made: 1** (I1 — reaching the dashboard at all, R-494,
|
||||
filed 2026-09-14, long before this walk).
|
||||
**Felhom-owned Hungarian things the stranger met: 4 kinds** — the claim page's messages, the setup
|
||||
code itself, the Backup page's two protection warnings and its two target names, and the menu word
|
||||
"Debug" (which is correct English for this reader).
|
||||
|
||||
**Ready for an English-speaking tester: NOT YET — because the one screen that stands between them
|
||||
and their box answers them in Hungarian.** Everything else held: the mails, the console, the bind
|
||||
page, the dashboard, the apps, the catalog, the language switch. The blocker is R-596, and it is a
|
||||
day's work, not a redesign.
|
||||
|
||||
Evidence: `i18n-slice6-2026-09-20/drill/` — `journal.md` (every observable, in order),
|
||||
`screens/` (22 console and installer captures), `pages/` (the dashboard as the household saw it),
|
||||
`box-logs/`. Golden: `../tests/golden-0.258.0-2026-09-20/`. R-516: `../audits/i18n-slice6-2026-09-20/R-516-item-by-item.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. What held, in one table
|
||||
|
||||
| # | step | result | time |
|
||||
|---|---|---|---|
|
||||
| 0.1 | golden **0.258.0** baked, published, vouched — one run, no aborted attempts | **PASS** | bake ≈8 min |
|
||||
| 0.2 | customer created with `language=en`; bind mail auto-sent on creation | **PASS** | — |
|
||||
| 1 | download page `felhom.eu/en/download` | **PASS** — 200, **0 Hungarian**, names file + checksum; the ISO the VM booted hashes to the published checksum **exactly** | 0.12 s |
|
||||
| 2 | install — the **GRAPHICAL** installer, which 2026-09-14 did NOT exercise | **PASS**, with **F1** (the guide told an English reader to keep a Hungarian keyboard) and **F2** (it describes a reboot prompt the defaults never show) | install → console banner ≈11 min |
|
||||
| 3 | first screen, pairing | **PASS** — bilingual banner, code `P3G-JGW` readable in both halves; `/etc/issue` in English | power-on → "waiting to be paired" ≈11 min |
|
||||
| 4 | **self-bind through the mailed link** | **PASS — and this is what 2026-09-14 could not do at all** (its H1). Page, refusal and success all English, 0 Hungarian lines | bind at 16:54:42Z |
|
||||
| A2 | typo in the pairing code | **PASS** — *"Those details are not right…"*, in English, does not say which half was wrong; hub logged `attempt 1/5` | — |
|
||||
| 5 | setup code → claim | **PASS**, with **F3** (the page's messages are Hungarian) and **F4** (the code is three accented Hungarian words) | power-on → claimed ≈23 min |
|
||||
| 4b | the dashboard's **first** language | **PASS** — English from `customer.language` alone, nobody touched a switch | — |
|
||||
| 6 | the two apps the guide names | **PASS** — installed with the default subdomains; API answered in English; both app pages **0 Hungarian** | PrivateBin ≈40 s, BookStack ≈50 s |
|
||||
| 7 | the Backup page as a first-timer | **F5** — 4 Hungarian strings in 73 lines, and two of them are the protection warnings | — |
|
||||
| 9 | version labels | **PASS** — "Up to date" in English (R-589, fixed this morning, proven on a fresh box) | — |
|
||||
| 13 | the language switch, both ways | **PASS** — the globe moves the household; a visitor's switch on the sign-in page sets only their own cookie | — |
|
||||
| 14 | the mailbox | **PASS** — **all three customer mails in English**; the operator's copy Hungarian by design; `[+household(en)]` in the box's own log | — |
|
||||
| — | **R-214** (console never stops asking to be paired) | **CLOSED — it no longer reproduces.** The last paint is the bilingual "the box is linked" banner | — |
|
||||
|
||||
**Not walked, and saying so rather than implying coverage:** steps 8 (Back up now), 10 (remove with
|
||||
"delete my data too"), 11 (restore), 12 (the launcher a week later) and A1 (power cut). All five were
|
||||
proven on 2026-09-14 and none is language-bearing except through pages this walk already read in both
|
||||
languages. **Step 7's recovery-code section could not be walked at all** — the off-site tier needs
|
||||
ep0, which this task fenced, exactly as on 2026-09-14.
|
||||
|
||||
## 2. The intervention
|
||||
|
||||
**I1 — the dashboard has no reachable address (R-494).** The setup mail names
|
||||
`https://felhom.drill-en-0920.felhom.eu`; `getent hosts` finds nothing, because no Cloudflare tunnel
|
||||
was created and the token is an optional field on the customer page. The walk reached the dashboard
|
||||
by breaking glass into the box (the hub-vaulted `host_recovery` credential) and driving the
|
||||
controller from inside the guest. **R-494 was filed on 2026-09-14 and narrowed to P3 by an operator
|
||||
ruling, so it was filed long before it was performed.** It was performed once and covered every later
|
||||
dashboard and app request. **The stop rule (four) was not reached.**
|
||||
|
||||
## 3. Harness substitutions — not interventions, and why
|
||||
|
||||
| | what | consequence for the claim |
|
||||
|---|---|---|
|
||||
| **H1 (GONE)** | 2026-09-14 had **no mailbox** and could not exercise the mailed link or the self-bind page. **This walk had one** (the `@felhom.eu` catch-all), so the bind mail, the setup-code mail and the confirmation mail were all read as the customer, and the self-bind page was driven end to end. | the biggest gap in the previous walk is closed |
|
||||
| **H2 (NOT a substitution this time)** | The keyboard layout was set to U.S. English. On 2026-09-14 that was a harness need (`qm sendkey` speaks US scancodes). **Here it is what the persona would genuinely choose** — and the fact that the guide said otherwise is F1. | — |
|
||||
| H3 (GONE) | auto-reboot was left **ticked**, so the install rebooted itself — the path 2026-09-14 avoided. | the "stick still in" reboot is now exercised |
|
||||
| H4 (GONE) | the **graphical** installer was used (it is the default), not the Terminal UI. | the graphical path is now exercised |
|
||||
| H5 | no browser: every page was fetched through the endpoint it renders from, inside the guest. | script-rendered state was not observed |
|
||||
|
||||
**Harness slips, recorded:** the owner passphrase was printed to stdout once by a probe, against the
|
||||
project's own file→file rule — it belongs to a throwaway customer and the teardown invalidates it;
|
||||
every later use read a 0600 file. The first claim POST used the wrong field names (`password` instead
|
||||
of `new_password`) and was answered 200 — a refusal, not a success (the "HTTP 200 can be a refusal"
|
||||
trap, met and caught). Both are in `journal.md`.
|
||||
|
||||
## 4. Findings
|
||||
|
||||
| row | rank | one line |
|
||||
|---|---|---|
|
||||
| **R-596** | **P1** | The claim page — the first screen they touch — is English chrome with **16 Hungarian messages**, two of which the guide quotes in English |
|
||||
| **R-597** | P2 | The setup code is three Hungarian words with five accented characters, inside a fully English mail |
|
||||
| **R-598** | P2 | The Backup page's two **protection warnings** and its two target names are Hungarian on an English dashboard |
|
||||
| **R-599** | P3 | A drill's teardown is blocked for 30 minutes by design (report staleness), and the 409 does not say so |
|
||||
|
||||
**Classification of every Hungarian thing met:** (F) R-596, R-597, R-598 · (A) the apps' own English
|
||||
UIs, which for this reader are an advantage · (K) "Debug" in the menu (R-516 item 1, correct English
|
||||
here), the operator-tier event copies (Hungarian by design), the 18 counted formal „ön" forms
|
||||
(R-516, unchanged by rule).
|
||||
|
||||
## 5. Scope and what this walk does NOT claim
|
||||
|
||||
- **Not the recovery-code journey.** Off-site needs ep0, which was fenced. Guide §7 — its longest and
|
||||
most emphatic section — is unproven by this walk, exactly as on 2026-09-14.
|
||||
- **Not a backup/restore/remove/power-cut walk.** See the "not walked" note above.
|
||||
- **Not a Hungarian walk**, which is why **R-516 does not close** — see `R-516-item-by-item.md`.
|
||||
- **Not a browser.** Endpoint-level throughout, stated at every step.
|
||||
|
||||
## 6. Teardown — three layers
|
||||
|
||||
- **Machine:** VM 9301 stopped and `qm destroy --purge`d; `/mnt/hdd_1/images/` holds only 9202's disk.
|
||||
- **Hub:** customer `drill-en-0920` deleted through the guarded path (`confirm_id` + three
|
||||
acknowledgements + `expect_hosts`), which cascades the host, the claim, the DR recipe and 17
|
||||
residue rows. **The hub refused until the host went stale — 30 minutes after its last report
|
||||
(R-599).**
|
||||
- **ep0:** the WireGuard peer `10.77.0.5` the enrolment registered goes with the host delete, as on
|
||||
2026-09-14.
|
||||
|
||||
**Untouched, and checked:** demo-hp guests **9201** and **9202** (running throughout, 24 containers
|
||||
before and after), demo-felhom, DooPlex beyond the golden bake's own VM (reverted to `virgin`),
|
||||
Peti's box, and ep0 beyond the product's own peer.
|
||||
@@ -0,0 +1,38 @@
|
||||
# R-516, item by item — and why it does NOT close
|
||||
|
||||
R-516 enumerated *"English a customer meets on a fresh box and its apps' first screens"*, measured by
|
||||
the 2026-09-14 Hungarian walk. Localisation slice 6 was supposed to close it against
|
||||
`audits/I18N-INVENTORY-2026-09-17.md`.
|
||||
|
||||
**It does not close, and the reason is structural rather than a shortfall of effort: more than half
|
||||
of R-516's items are about what a HUNGARIAN household reads.** This walk was an English speaker's
|
||||
first hour. It can retire the items that are about English leaking into a Hungarian product only by
|
||||
checking the source and the Hungarian page, which it did where it could — but four items are
|
||||
observations about Hungarian copy quality (formal „ön" forms, a raw timestamp, a duplicated banner)
|
||||
that an English walk cannot see at all, and one is a wrong NUMBER rather than a wrong language.
|
||||
|
||||
| # | the item | verdict | evidence |
|
||||
|---|---|---|---|
|
||||
| 1 | the dashboard menu item **„Debug"** | **STILL THERE for a Hungarian household** — and correct for an English one. `>Debug<` is in the English page verbatim; the bundle has `debug.debug_mod` („Debug mód" / "Debug mode") but the MENU entry is the bare word | drill capture `pages/d1/settings.html`; bundle |
|
||||
| 2 | the dashboard CPU tile **„Load: …"** | **FIXED** by slice 1 — `monitoring.terheles_load_average`: hu „Terhelés (Load Average)", en "Load average" | bundle |
|
||||
| 3 | the launcher tile **„Filebrowser"** opens an English login with no Felhom text | **APP-OWNED (A)** for the login itself; the tile and the app page are Felhom's and are English now. R-513 owns the sign-in | drill capture `pages/d2/launcher.html` (0 Hungarian) |
|
||||
| 4 | the storage page mixes formal „Adjon hozzá / Csatlakoztasson" with the product's „te" | **NOT DECIDABLE FROM THIS WALK** — a Hungarian-copy-quality item. Counted by `HU_FORMAL_CEILING` (18 today) and owned by R-516 itself | `i18n_missing_gate.py` |
|
||||
| 5 | **Uptime Kuma opens on „Which database would you like to use?"** and the card's first steps do not mention it | **STILL THERE, APP-OWNED + CATALOG (A/F)** — the app's own screen; the catalog's English first steps for uptime-kuma still do not mention the database step. Not walked here (uptime-kuma was not one of the guide's two apps) | slice 5 catalog text |
|
||||
| 6 | PrivateBin, Gokapi, AdventureLog, FileBrowser UIs are English | **APP-OWNED (A) — and for an English household it is now an ADVANTAGE, not a defect.** The item was written from the Hungarian side | — |
|
||||
| 7 | the storage page prints a raw ISO UTC string „Leválasztva: 2026-09-14T19:58:02Z" | **NOT DECIDABLE FROM THIS WALK** — needs a disconnected drive; the drill VM had one disk. The English storage page carries **0 Hungarian lines**, so if it still happens it happens in both languages | `r516/storage.en.html` |
|
||||
| 8 | the „Meghajtó leválasztva" banner appears twice on every page | **NOT DECIDABLE FROM THIS WALK** — same reason | — |
|
||||
| 9 | „4 telepített alkalmazás nem fut…" uses the formal form | **NOT DECIDABLE FROM THIS WALK** — Hungarian copy quality; the English twin (`alert.deadapp.group`) exists and is English | bundle |
|
||||
| 10 | a banner in English on a Hungarian page, **"SSD disk usage high: 90%"** | **NOT REPRODUCED** — the drill box's system volume sat at 1 %. Still open on the Hungarian side | drill `pages/d1/backups.html` shows `System (/) 0.97 GB / 68.7 GB (1%)` |
|
||||
| 11 | the dashboard tile reads 90 % while `df` reports 95 % | **NOT A LOCALISATION ITEM** — a wrong NUMBER (reserved blocks). The LABEL is fixed (`common.rendszer_gyoker` → "System (/)"). It needs its own row | bundle |
|
||||
| 12 | six formal („ön") forms in the converted copy | **STILL THERE, BY DESIGN AND COUNTED** — now **18**, ratcheted, because a localisation release may not change Hungarian bytes (10-localisation.md §1). The ceiling is the record | `i18n_missing_gate.py` |
|
||||
|
||||
## What this means for R-516
|
||||
|
||||
- **Closed by this walk:** items 2, 3, 6 (and 1 for an English household only).
|
||||
- **Still there and now measured:** 1 (Hungarian side), 5, 12.
|
||||
- **Not decidable from an English walk:** 4, 7, 8, 9, 10.
|
||||
- **Belongs elsewhere:** 11 (a number, not a language).
|
||||
|
||||
**R-516 therefore stays OPEN**, narrowed to the Hungarian-side items, and the honest closure it is
|
||||
waiting for is a Hungarian walk on a box with a second drive — not this one. Saying otherwise would
|
||||
be the kind of closure that makes a register stop meaning anything.
|
||||
@@ -0,0 +1,120 @@
|
||||
Sep 20 19:05:37 felhom sudo[17653]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:37 felhom sudo[17663]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:05:37 felhom sudo[17663]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:37 felhom sudo[17663]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:37 felhom sudo[17666]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:05:37 felhom sudo[17666]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:37 felhom sudo[17666]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:38 felhom sudo[17673]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:05:38 felhom sudo[17673]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:38 felhom sudo[17673]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:38 felhom sudo[17676]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:05:38 felhom sudo[17676]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:38 felhom sudo[17676]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:38 felhom sudo[17697]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:05:38 felhom sudo[17697]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:38 felhom sudo[17697]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:38 felhom sudo[17706]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:05:38 felhom sudo[17706]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:38 felhom sudo[17706]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:38 felhom sudo[17713]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:05:38 felhom sudo[17713]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:38 felhom sudo[17713]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:38 felhom sudo[17716]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:05:38 felhom sudo[17716]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:38 felhom sudo[17716]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:38 felhom sudo[17723]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:05:38 felhom sudo[17723]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:38 felhom sudo[17723]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:38 felhom sudo[17726]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:05:38 felhom sudo[17726]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:38 felhom sudo[17726]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:38 felhom sudo[17729]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/bin/lxc-info -n 9201 -p -H
|
||||
Sep 20 19:05:38 felhom sudo[17729]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:38 felhom sudo[17729]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:39 felhom sudo[17750]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/bin/mkdir -p /mnt/felhom-drives
|
||||
Sep 20 19:05:39 felhom sudo[17750]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:39 felhom sudo[17750]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:39 felhom sudo[17757]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:05:39 felhom sudo[17757]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:39 felhom sudo[17757]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:39 felhom sudo[17760]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:05:39 felhom sudo[17760]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:39 felhom sudo[17760]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:39 felhom sudo[17767]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:05:39 felhom sudo[17767]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:39 felhom sudo[17767]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:39 felhom sudo[17770]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:05:39 felhom sudo[17770]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:39 felhom sudo[17770]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:39 felhom sudo[17773]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/bin/lxc-info -n 9201 -p -H
|
||||
Sep 20 19:05:39 felhom sudo[17773]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:39 felhom sudo[17773]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:39 felhom sudo[17780]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:05:39 felhom sudo[17780]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:39 felhom sudo[17780]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:39 felhom sudo[17783]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:05:39 felhom sudo[17783]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:39 felhom sudo[17783]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:39 felhom sudo[17786]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/bin/lxc-info -n 9201 -p -H
|
||||
Sep 20 19:05:39 felhom sudo[17786]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:39 felhom sudo[17786]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:59 felhom sudo[18104]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/bin/mkdir -p /mnt/felhom-drives
|
||||
Sep 20 19:05:59 felhom sudo[18104]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:59 felhom sudo[18104]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:59 felhom felhom-agent[4096]: time=2026-09-20T19:05:59.094+02:00 level=INFO msg="stale-lock: scanning pool guests" pool=felhom listed=1 scanned=1
|
||||
Sep 20 19:05:59 felhom sudo[18114]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- ip -4 -o addr show dev eth0
|
||||
Sep 20 19:05:59 felhom sudo[18114]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:59 felhom felhom-agent[4096]: time=2026-09-20T19:05:59.118+02:00 level=INFO msg="stale-lock: scanning pool guests" pool=felhom listed=1 scanned=1
|
||||
Sep 20 19:05:59 felhom felhom-agent[4096]: time=2026-09-20T19:05:59.123+02:00 level=INFO msg="stale-lock: scanning pool guests" pool=felhom listed=1 scanned=1
|
||||
Sep 20 19:05:59 felhom felhom-agent[4096]: time=2026-09-20T19:05:59.123+02:00 level=INFO msg="guest-power: watchdog alive" sweeps_since_boot=10 guests_evaluated=1 currently_stopped=0
|
||||
Sep 20 19:05:59 felhom sudo[18121]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- docker inspect -f {{.State.Status}} felhom-controller
|
||||
Sep 20 19:05:59 felhom sudo[18121]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:59 felhom sudo[18125]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:05:59 felhom sudo[18125]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:59 felhom sudo[18125]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:59 felhom sudo[18128]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:05:59 felhom sudo[18128]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:59 felhom sudo[18128]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:59 felhom sudo[18132]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/nft list set inet felhom_oob ssh_port
|
||||
Sep 20 19:05:59 felhom sudo[18132]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:59 felhom sudo[18132]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:59 felhom sudo[18135]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/nft list set inet felhom_oob operator_ips
|
||||
Sep 20 19:05:59 felhom sudo[18135]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:59 felhom sudo[18135]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:05:59 felhom sudo[18142]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/bin/wg show wg-felhom latest-handshakes
|
||||
Sep 20 19:05:59 felhom sudo[18142]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:05:59 felhom sudo[18142]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:00 felhom sudo[18114]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:00 felhom sudo[18151]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- ip route show default
|
||||
Sep 20 19:06:00 felhom sudo[18151]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:00 felhom sudo[18121]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:00 felhom felhom-agent[4096]: time=2026-09-20T19:06:00.072+02:00 level=INFO msg="controller-supervisor: alive" sweeps_since_boot=20 guests_evaluated=1 controllers_not_running=0
|
||||
Sep 20 19:06:01 felhom sudo[18151]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:01 felhom sudo[18166]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- cat /etc/network/interfaces
|
||||
Sep 20 19:06:01 felhom sudo[18166]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:01 felhom sudo[18166]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:01 felhom sudo[18172]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- pgrep -x dhclient
|
||||
Sep 20 19:06:01 felhom sudo[18172]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:02 felhom sudo[18172]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:03 felhom sudo[18197]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- ip -4 -o addr show dev eth0
|
||||
Sep 20 19:06:03 felhom sudo[18197]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:04 felhom sudo[18197]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:04 felhom sudo[18211]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/pct exec 9201 -- docker exec felhom-controller cat /opt/docker/felhom-controller/controller.yaml
|
||||
Sep 20 19:06:04 felhom sudo[18211]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:05 felhom sudo[18211]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:09 felhom sudo[18377]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:06:09 felhom sudo[18377]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:09 felhom sudo[18377]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:09 felhom sudo[18380]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:06:09 felhom sudo[18380]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:09 felhom sudo[18380]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:09 felhom sudo[18400]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/smartctl -a -j /dev/sda
|
||||
Sep 20 19:06:09 felhom sudo[18400]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:09 felhom sudo[18400]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:09 felhom sudo[18403]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/sbin/lvs --reportformat json --units b -o lv_name,data_percent,metadata_percent -- pve/data
|
||||
Sep 20 19:06:09 felhom sudo[18403]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:09 felhom sudo[18403]: pam_unix(sudo:session): session closed for user root
|
||||
Sep 20 19:06:09 felhom sudo[18407]: felhom-agent : PWD=/ ; USER=root ; COMMAND=/usr/bin/lxc-info -n 9201 -p -H
|
||||
Sep 20 19:06:09 felhom sudo[18407]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=999)
|
||||
Sep 20 19:06:09 felhom sudo[18407]: pam_unix(sudo:session): session closed for user root
|
||||
@@ -0,0 +1,6 @@
|
||||
bookstack lscr.io/linuxserver/bookstack:26.05.2 Up About a minute (healthy)
|
||||
bookstack-db mariadb:12.3 Up About a minute (healthy)
|
||||
privatebin privatebin/pdo:2.0.5 Up About a minute (healthy)
|
||||
filebrowser gtstef/filebrowser:1.3.3-stable Up 9 minutes (healthy)
|
||||
felhom-controller gitea.dooplex.hu/admin/felhom-controller:0.258.0 Up 9 minutes (healthy)
|
||||
traefik traefik:v3.6.7 Up 9 minutes
|
||||
@@ -0,0 +1,289 @@
|
||||
perl: warning: Setting locale failed.
|
||||
perl: warning: Please check that your locale settings:
|
||||
LANGUAGE = (unset),
|
||||
LC_ALL = (unset),
|
||||
LC_CTYPE = "UTF-8",
|
||||
LC_NUMERIC = (unset),
|
||||
LC_COLLATE = (unset),
|
||||
LC_TIME = (unset),
|
||||
LC_MESSAGES = (unset),
|
||||
LC_MONETARY = (unset),
|
||||
LC_ADDRESS = (unset),
|
||||
LC_IDENTIFICATION = (unset),
|
||||
LC_MEASUREMENT = (unset),
|
||||
LC_PAPER = (unset),
|
||||
LC_TELEPHONE = (unset),
|
||||
LC_NAME = (unset),
|
||||
LANG = "en_US.UTF-8"
|
||||
are supported and installed on your system.
|
||||
perl: warning: Falling back to a fallback locale ("en_US.UTF-8").
|
||||
2026/09/20 16:57:08 [INFO] local-api: channel up (agent 169.254.253.1:8443) — guest 9201, 3 mount(s) visible
|
||||
2026/09/20 16:57:08 [INFO] local-api: mount mp0 → /var/lib/felhom (storage=local-lvm, class=, backup=true)
|
||||
2026/09/20 16:57:08 [INFO] local-api: mount mp9 → /etc/felhom-bootstrap (storage=/var/lib/felhom-agent/guests/9201/bootstrap, class=, backup=false)
|
||||
2026/09/20 16:57:08 [INFO] local-api: mount mp8 → /mnt/felhom-drives (storage=/mnt/felhom-drives, class=, backup=false)
|
||||
2026/09/20 16:57:08 [INFO] felhom-controller 0.258.0 starting (customer: drill-en-0920, domain: drill-en-0920.felhom.eu)
|
||||
2026/09/20 16:57:08 [INFO] [settings] Loaded settings from /opt/docker/felhom-controller/data/settings.json
|
||||
2026/09/20 16:57:08 [INFO] Encryption key loaded from /opt/docker/felhom-controller/data/encryption.key
|
||||
2026/09/20 16:57:08 [INFO] [stacks] Using compose command: docker compose
|
||||
2026/09/20 16:57:08 [INFO] [stacks] ScanStacks complete: 55 stacks found (0 deployed, 55 available)
|
||||
2026/09/20 16:57:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:57:08 [INFO] [stacks] Encryption migration: no stacks needed migration
|
||||
2026/09/20 16:57:08 [INFO] [quiesce] loop started (poll 5m0s, max-quiesce 30m0s)
|
||||
2026/09/20 16:57:08 [INFO] [stacks] desired-state backfill: 0 app(s) recorded as running, 0 left unrecorded (state ambiguous — legacy boot behaviour retained)
|
||||
2026/09/20 16:57:08 [INFO] [stacks] installed-images backfill: 0 app(s) recorded, 0 already had a record, 0 left unrecorded (could not be observed completely — unknown, which renders nothing)
|
||||
2026/09/20 16:57:08 [INFO] [stacks] pin adoption: 0 pinned, 0 already pinned, 0 left unpinned (0 not completely observed, 0 running something the template no longer offers)
|
||||
2026/09/20 16:57:08 [INFO] [sync] Starting catalog sync (repo: https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git, interval: 15m0s)
|
||||
2026/09/20 16:57:08 [INFO] [sync] Starting catalog sync
|
||||
2026/09/20 16:57:08 [INFO] [sync] Pulling latest from https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git (branch: main)
|
||||
2026/09/20 16:57:08 [INFO] Metrics store opened at /opt/docker/felhom-controller/data/metrics.db
|
||||
2026/09/20 16:57:08 [INFO] Metrics collector started (60s interval)
|
||||
2026/09/20 16:57:08 [INFO] [backup] restore record wired: /opt/docker/felhom-controller/data/restore-status.json (interrupted at startup: false)
|
||||
2026/09/20 16:57:08 [INFO] Notifier enabled (hub: https://hub.felhom.eu)
|
||||
2026/09/20 16:57:08 [INFO] Self-update enabled (check every 6h, auto-update: false, auto-update time: 04:30)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: status-refresh (every 10s)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: stack-scan (every 2m0s)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: health-probes (every 10s)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: system-health (every 5m0s)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: deadapp-check (every 30s)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: ring-spill (every 30s)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Daily job db-dump scheduled for 2026-09-21 02:30 CEST
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: offsite-credential-retry (every 5m0s)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: backup-cache (every 5m0s)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Daily job tier2-backup scheduled for 2026-09-21 03:30 CEST
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Daily job offbox-backup scheduled for 2026-09-21 04:15 CEST
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Daily job offsite-abandon-sweep scheduled for 2026-09-21 05:10 CEST
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Daily job offsite-integrity scheduled for 2026-09-21 06:00 CEST
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Daily job offsite-proof scheduled for 2026-09-21 05:30 CEST
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Daily job metrics-prune scheduled for 2026-09-21 04:00 CEST
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Daily job fill-watch scheduled for 2026-09-21 03:30 CEST
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: hub-report (every 15m0s)
|
||||
2026/09/20 16:57:08 [INFO] Hub reporting enabled (every 15m0s to https://hub.felhom.eu)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: selfupdate-check (every 6h0m0s)
|
||||
2026/09/20 16:57:08 [INFO] ========== Startup Self-Test ==========
|
||||
2026/09/20 16:57:08 [INFO] [PASS] Docker socket: reachable (v29.8.1)
|
||||
2026/09/20 16:57:08 [INFO] [PASS] Stacks directory: /opt/docker/stacks
|
||||
2026/09/20 16:57:08 [INFO] [PASS] Data directory: /opt/docker/felhom-controller/data (writable)
|
||||
2026/09/20 16:57:08 [INFO] [PASS] System data path: /mnt/sys_drive
|
||||
2026/09/20 16:57:08 [INFO] [WARN] Storage paths: no storage paths registered
|
||||
2026/09/20 16:57:08 [INFO] [PASS] Git catalog: 53 app definitions found
|
||||
2026/09/20 16:57:08 [INFO] [infra] connected felhom-controller to traefik-public
|
||||
2026/09/20 16:57:08 [INFO] [infra] cloudflared skipped — no cf_tunnel_token configured (LAN-only node)
|
||||
2026/09/20 16:57:08 [WARN] [infra] filebrowser admin probe: Post "http://filebrowser:80/api/auth/login?username=admin": dial tcp: lookup filebrowser on 192.168.0.250:53: no such host (will retry)
|
||||
2026/09/20 16:57:08 [INFO] [PASS] Hub connectivity: https://hub.felhom.eu reachable (HTTP 200)
|
||||
2026/09/20 16:57:08 [INFO] [PASS] Metrics DB: 0.0 MB
|
||||
2026/09/20 16:57:08 [INFO] ========================================
|
||||
2026/09/20 16:57:08 [INFO] Self-test complete: 7 passed, 1 warnings, 0 failed
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Starting scheduler with 18 jobs
|
||||
2026/09/20 16:57:08 [INFO] [backup] Found 0 DB dump files across drives
|
||||
2026/09/20 16:57:08 [INFO] [report] hub wait channel active (hold ≤240s)
|
||||
2026/09/20 16:57:08 [INFO] [backup] Discovered 0 databases
|
||||
2026/09/20 16:57:08 [INFO] [backup] Discovered app data: 0 apps
|
||||
2026/09/20 16:57:08 [INFO] [backup] Backup status cache refreshed
|
||||
2026/09/20 16:57:08 [INFO] [web] Auth: no password configured — dashboard is open
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: disk-health-check (every 1h0m0s)
|
||||
2026/09/20 16:57:08 [INFO] [scheduler] Registered periodic job: agent-channel-health (every 1m0s)
|
||||
2026/09/20 16:57:08 [INFO] Web UI listening on :8080
|
||||
2026/09/20 16:57:09 [INFO] [monitor] Health check: status=ok
|
||||
2026/09/20 16:57:09 [INFO] [sync] Catalog sync complete
|
||||
2026/09/20 16:57:09 [INFO] [sync] Initial sync: Sablonok naprakészek — nincs változás
|
||||
2026/09/20 16:57:09 [INFO] [settings] Settings saved
|
||||
2026/09/20 16:57:09 [INFO] [web] FileBrowser mounts synced (recreated) — 0 storage path(s), config updated
|
||||
2026/09/20 16:57:13 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:57:13 [INFO] [report] Building system report
|
||||
2026/09/20 16:57:13 [INFO] Event pushed: controller_started (info) [+household(en)] — Controller elindult (0.258.0)
|
||||
2026/09/20 16:57:14 [INFO] [monitor] Health check: status=ok
|
||||
2026/09/20 16:57:14 [INFO] [report] Hub report pushed successfully (2574 bytes)
|
||||
2026/09/20 16:57:14 [INFO] [settings] Settings saved
|
||||
2026/09/20 16:57:14 [INFO] [settings] Settings saved
|
||||
2026/09/20 16:57:14 [INFO] config-refresh: baseline config_version=1 recorded (no restart)
|
||||
2026/09/20 16:57:14 [INFO] [settings] Settings saved
|
||||
2026/09/20 16:57:14 [INFO] [claim-sync] hub claim code cached (generation 1) — hash first 8: $2a$10$o…
|
||||
2026/09/20 16:57:14 [INFO] Startup hub report sent
|
||||
2026/09/20 16:57:18 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:57:18 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:57:23 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:57:23 [INFO] [bootrecon] boot window: fleet settled after 10s (3 identical samples 5s apart) — sweeping
|
||||
2026/09/20 16:57:23 [INFO] [bootrecon] Boot reconciliation: no boot-orphaned apps (nothing to start)
|
||||
2026/09/20 16:57:28 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:57:38 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:57:39 [INFO] [selfupdate] Current version 0.258.0 is up to date
|
||||
2026/09/20 16:57:48 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:57:58 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:58:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:58:08 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/09/20 16:58:09 [INFO] [scheduler] Job agent-channel-health completed (took 199ms)
|
||||
2026/09/20 16:58:18 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:58:28 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:58:38 [INFO] [fillwatch] checked 2 filesystem(s), 0 unreadable/skipped, 0 notification(s); bands: all ok
|
||||
2026/09/20 16:58:38 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:58:48 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:58:58 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:59:08 [INFO] [scheduler] Running job: stack-scan
|
||||
2026/09/20 16:59:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:59:08 [INFO] [stacks] ScanStacks complete: 55 stacks found (0 deployed, 55 available)
|
||||
2026/09/20 16:59:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:59:08 [INFO] [scheduler] Job stack-scan completed (took 62ms)
|
||||
2026/09/20 16:59:08 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/09/20 16:59:09 [INFO] [scheduler] Job agent-channel-health completed (took 179ms)
|
||||
2026/09/20 16:59:18 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:59:28 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:59:38 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:59:48 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 16:59:58 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:00:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:00:08 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/09/20 17:00:09 [INFO] [scheduler] Job agent-channel-health completed (took 182ms)
|
||||
2026/09/20 17:00:18 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:00:28 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:00:38 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:00:48 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:00:58 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:01:08 [INFO] [scheduler] Running job: stack-scan
|
||||
2026/09/20 17:01:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:01:08 [INFO] [stacks] ScanStacks complete: 55 stacks found (0 deployed, 55 available)
|
||||
2026/09/20 17:01:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:01:08 [INFO] [scheduler] Job stack-scan completed (took 67ms)
|
||||
2026/09/20 17:01:08 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/09/20 17:01:09 [INFO] [scheduler] Job agent-channel-health completed (took 170ms)
|
||||
2026/09/20 17:01:18 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:01:28 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:01:38 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:01:48 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:01:58 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:02:08 [INFO] [scheduler] Running job: backup-cache
|
||||
2026/09/20 17:02:08 [INFO] [scheduler] Running job: system-health
|
||||
2026/09/20 17:02:08 [INFO] [scheduler] Running job: offsite-credential-retry
|
||||
2026/09/20 17:02:08 [INFO] [scheduler] Job offsite-credential-retry completed (took 0s)
|
||||
2026/09/20 17:02:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:02:08 [INFO] [backup] Found 0 DB dump files across drives
|
||||
2026/09/20 17:02:08 [INFO] [backup] Discovered 0 databases
|
||||
2026/09/20 17:02:08 [INFO] [backup] Discovered app data: 0 apps
|
||||
2026/09/20 17:02:08 [INFO] [backup] Backup status cache refreshed
|
||||
2026/09/20 17:02:08 [INFO] [scheduler] Job backup-cache completed (took 67ms)
|
||||
2026/09/20 17:02:08 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/09/20 17:02:09 [WARN] [quiesce] tier felhom-pbs skipped: its storage does not exist on the host — no app is stopped for it
|
||||
2026/09/20 17:02:09 [INFO] Event pushed: backup_tier_skipped (warning) [hu-only] — Whole-guest backup tier felhom-pbs skipped: its storage does not exist on the host (never provisioned or removed). No app was stopped for it.
|
||||
2026/09/20 17:02:09 [INFO] [monitor] Health check: status=ok
|
||||
2026/09/20 17:02:09 [INFO] [scheduler] Job system-health completed (took 219ms)
|
||||
2026/09/20 17:02:09 [INFO] [scheduler] Job agent-channel-health completed (took 173ms)
|
||||
2026/09/20 17:02:09 [INFO] [infra] cloudflared skipped — no cf_tunnel_token configured (LAN-only node)
|
||||
2026/09/20 17:02:09 [INFO] [quiesce] backup due on 1 tier(s) — quiescing 0 stack(s): []
|
||||
2026/09/20 17:02:09 [INFO] [quiesce] tier local: backup job backup-9201-1789923729186721684 started — polling
|
||||
2026/09/20 17:02:09 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:02:09 [INFO] [infra] filebrowser: admin/admin replaced by a generated password (value never logged); verified new=200 admin=401
|
||||
2026/09/20 17:02:18 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:02:19 [INFO] [quiesce] tier local: job backup-9201-1789923729186721684 snapshotted — resuming app early (8B.2)
|
||||
2026/09/20 17:02:19 [INFO] [quiesce] unquiescing (snapshotted (early resume, last tier)): restarting 0 stack(s)
|
||||
2026/09/20 17:02:28 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:02:38 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:02:44 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:02:44 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:02:44 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:02:44 [INFO] [web] All sessions invalidated (cleared 0)
|
||||
2026/09/20 17:02:44 [INFO] [web] dashboard claimed by the customer from 172.17.0.1 (code generation 1 consumed)
|
||||
2026/09/20 17:02:46 [INFO] [report] Building system report
|
||||
2026/09/20 17:02:46 [INFO] [monitor] Health check: status=ok
|
||||
2026/09/20 17:02:46 [INFO] [report] Hub report pushed successfully (2783 bytes)
|
||||
2026/09/20 17:02:46 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:02:48 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:02:49 [INFO] [quiesce] tier local: backup job backup-9201-1789923729186721684 done
|
||||
2026/09/20 17:02:58 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:03:05 [INFO] [web] Login from 172.17.0.1:53256
|
||||
2026/09/20 17:03:08 [INFO] [scheduler] Running job: stack-scan
|
||||
2026/09/20 17:03:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:03:08 [INFO] [stacks] ScanStacks complete: 55 stacks found (0 deployed, 55 available)
|
||||
2026/09/20 17:03:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:03:08 [INFO] [scheduler] Job stack-scan completed (took 66ms)
|
||||
2026/09/20 17:03:08 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/09/20 17:03:09 [INFO] [scheduler] Job agent-channel-health completed (took 178ms)
|
||||
2026/09/20 17:03:18 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:03:28 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:03:38 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:03:48 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:03:58 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:04:01 [INFO] [web] Login from 172.17.0.1:51030
|
||||
2026/09/20 17:04:01 [INFO] [api] Deploy requested for stack: bookstack
|
||||
2026/09/20 17:04:01 [INFO] [api] Deploy requested for stack: privatebin
|
||||
2026/09/20 17:04:08 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:04:08 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/09/20 17:04:09 [INFO] [scheduler] Job agent-channel-health completed (took 191ms)
|
||||
2026/09/20 17:04:18 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:04:24 [INFO] [web] Login from 172.17.0.1:47894
|
||||
2026/09/20 17:04:24 [INFO] [api] Deploy requested for stack: bookstack
|
||||
2026/09/20 17:04:24 [INFO] [stacks] Memory check: total=4096MB, reserved=384MB, usable=3712MB, committed_used=0MB, new_req=150MB, remaining=3562MB
|
||||
2026/09/20 17:04:24 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/bookstack — 4 env vars, 2 encrypted, 2 sensitive fields
|
||||
2026/09/20 17:04:24 [INFO] [stacks] Deploying stack bookstack with 4 env vars: [DB_PASSWORD, DOMAIN, SUBDOMAIN, APP_KEY]
|
||||
2026/09/20 17:04:24 [INFO] [stacks] SaveAppConfig: saved config for bookstack
|
||||
2026/09/20 17:04:24 [INFO] [api] Deploy requested for stack: privatebin
|
||||
2026/09/20 17:04:24 [INFO] Event pushed: app_deploy_started (info) [+household(en)] — Alkalmazás telepítése elindult: BookStack
|
||||
2026/09/20 17:04:24 [INFO] [stacks] Memory check: total=4096MB, reserved=384MB, usable=3712MB, committed_used=150MB, new_req=30MB, remaining=3532MB
|
||||
2026/09/20 17:04:24 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/privatebin — 2 env vars, 0 encrypted, 0 sensitive fields
|
||||
2026/09/20 17:04:24 [INFO] [stacks] SaveAppConfig: saved config for privatebin
|
||||
2026/09/20 17:04:24 [INFO] [stacks] Deploying stack privatebin with 2 env vars: [DOMAIN, SUBDOMAIN]
|
||||
2026/09/20 17:04:24 [INFO] Event pushed: app_deploy_started (info) [+household(en)] — Alkalmazás telepítése elindult: PrivateBin
|
||||
2026/09/20 17:04:26 [INFO] [report] Building system report
|
||||
2026/09/20 17:04:26 [INFO] [monitor] Health check: status=ok
|
||||
2026/09/20 17:04:26 [INFO] [report] Hub report pushed successfully (2905 bytes)
|
||||
2026/09/20 17:04:26 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:04:28 [INFO] [stacks] Status refresh: 2 containers across 55 stacks
|
||||
2026/09/20 17:04:33 [INFO] [stacks] Stack privatebin deployed successfully (took 8.6s)
|
||||
2026/09/20 17:04:33 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/privatebin — 2 env vars, 0 encrypted, 0 sensitive fields
|
||||
2026/09/20 17:04:33 [INFO] [stacks] SaveAppConfig: saved config for privatebin
|
||||
2026/09/20 17:04:33 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/privatebin — 2 env vars, 0 encrypted, 0 sensitive fields
|
||||
2026/09/20 17:04:33 [INFO] [stacks] installed-images privatebin: recorded 1 service(s) (privatebin=privatebin/pdo:2.0.5 (sha256:8a2cac16eff6…))
|
||||
2026/09/20 17:04:33 [INFO] [stacks] SaveAppConfig: saved config for privatebin
|
||||
2026/09/20 17:04:33 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/privatebin — 2 env vars, 0 encrypted, 0 sensitive fields
|
||||
2026/09/20 17:04:33 [INFO] [stacks] pin privatebin: privatebin=privatebin/pdo:2.0.5
|
||||
2026/09/20 17:04:33 [INFO] [stacks] SaveAppConfig: saved config for privatebin
|
||||
2026/09/20 17:04:33 [INFO] [stacks] Status refresh: 3 containers across 55 stacks
|
||||
2026/09/20 17:04:33 [INFO] Event pushed: app_deployed (info) [+household(en)] — Alkalmazás telepítve: PrivateBin
|
||||
2026/09/20 17:04:36 [INFO] [stacks] Stack privatebin post-start status:
|
||||
2026/09/20 17:04:36 [INFO] [stacks] privatebin privatebin/pdo:2.0.5 running Up 3 seconds (health: starting)
|
||||
2026/09/20 17:04:38 [INFO] [stacks] Status refresh: 3 containers across 55 stacks
|
||||
2026/09/20 17:04:38 [INFO] Health probes: 1 ok (of 1 probed)
|
||||
2026/09/20 17:04:48 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:04:54 [INFO] [web] Login from 172.17.0.1:56322
|
||||
2026/09/20 17:04:56 [INFO] [stacks] Stack bookstack deployed successfully (took 31.9s)
|
||||
2026/09/20 17:04:56 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/bookstack — 4 env vars, 2 encrypted, 2 sensitive fields
|
||||
2026/09/20 17:04:56 [INFO] [stacks] SaveAppConfig: saved config for bookstack
|
||||
2026/09/20 17:04:56 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/bookstack — 4 env vars, 0 encrypted, 2 sensitive fields
|
||||
2026/09/20 17:04:56 [INFO] [stacks] installed-images bookstack: recorded 2 service(s) (bookstack=lscr.io/linuxserver/bookstack:26.05.2 (sha256:3db259db5828…), bookstack-db=mariadb:12.3 (sha256:805c8e104bd5…))
|
||||
2026/09/20 17:04:56 [INFO] [stacks] SaveAppConfig: saved config for bookstack
|
||||
2026/09/20 17:04:56 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/bookstack — 4 env vars, 0 encrypted, 2 sensitive fields
|
||||
2026/09/20 17:04:56 [INFO] [stacks] pin bookstack: bookstack=lscr.io/linuxserver/bookstack:26.05.2, bookstack-db=mariadb:12.3
|
||||
2026/09/20 17:04:56 [INFO] [stacks] SaveAppConfig: saved config for bookstack
|
||||
2026/09/20 17:04:56 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:04:56 [INFO] Event pushed: app_deployed (info) [+household(en)] — Alkalmazás telepítve: BookStack
|
||||
2026/09/20 17:04:58 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:04:59 [INFO] [stacks] Stack bookstack post-start status:
|
||||
2026/09/20 17:04:59 [INFO] [stacks] bookstack lscr.io/linuxserver/bookstack:26.05.2 running Up 3 seconds (health: starting)
|
||||
2026/09/20 17:04:59 [INFO] [stacks] bookstack-db mariadb:12.3 running Up 14 seconds (healthy)
|
||||
2026/09/20 17:05:08 [INFO] [scheduler] Running job: stack-scan
|
||||
2026/09/20 17:05:08 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:05:08 [INFO] [stacks] ScanStacks complete: 55 stacks found (2 deployed, 53 available)
|
||||
2026/09/20 17:05:08 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:05:08 [INFO] [scheduler] Job stack-scan completed (took 80ms)
|
||||
2026/09/20 17:05:08 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/09/20 17:05:09 [INFO] [scheduler] Job agent-channel-health completed (took 196ms)
|
||||
2026/09/20 17:05:18 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:05:28 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:05:28 [INFO] Health probes: 1 ok (of 1 probed)
|
||||
2026/09/20 17:05:32 [INFO] [web] Login from 172.17.0.1:45876
|
||||
2026/09/20 17:05:32 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:05:32 [INFO] [web] language switch: household language set to hu
|
||||
2026/09/20 17:05:34 [INFO] [report] Building system report
|
||||
2026/09/20 17:05:34 [INFO] [monitor] Health check: status=ok
|
||||
2026/09/20 17:05:34 [INFO] [report] Hub report pushed successfully (5501 bytes)
|
||||
2026/09/20 17:05:34 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:05:36 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:05:36 [INFO] [web] language switch: household language set to en
|
||||
2026/09/20 17:05:38 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:05:48 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:05:49 [INFO] [report] Building system report
|
||||
2026/09/20 17:05:50 [INFO] [monitor] Health check: status=ok
|
||||
2026/09/20 17:05:50 [INFO] [report] Hub report pushed successfully (5503 bytes)
|
||||
2026/09/20 17:05:50 [INFO] [settings] Settings saved
|
||||
2026/09/20 17:05:58 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:06:08 [INFO] [stacks] Status refresh: 5 containers across 55 stacks
|
||||
2026/09/20 17:06:08 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/09/20 17:06:09 [INFO] [scheduler] Job agent-channel-health completed (took 198ms)
|
||||
@@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Download — the Felhom installer | Felhom.eu</title>
|
||||
<meta name="description" content="The Felhom home server installer: download, checksum, and what to check before you install.">
|
||||
<meta name="robots" content="noindex, follow">
|
||||
<link rel="canonical" href="https://felhom.eu/en/download">
|
||||
<link rel="alternate" hreflang="hu" href="https://felhom.eu/letoltes">
|
||||
<link rel="alternate" hreflang="en" href="https://felhom.eu/en/download">
|
||||
<link rel="alternate" hreflang="x-default" href="https://felhom.eu/letoltes">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://felhom.eu/en/download">
|
||||
<meta property="og:title" content="Download the Felhom installer">
|
||||
<meta property="og:description" content="Download, checksum, and what to check before you install.">
|
||||
<meta property="og:image" content="https://felhom.eu/assets/og-image.png">
|
||||
<meta property="og:locale" content="en_GB">
|
||||
<meta property="og:site_name" content="Felhom.eu">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Download the Felhom installer">
|
||||
<meta name="twitter:description" content="Download, checksum, and what to check before you install.">
|
||||
<meta name="twitter:image" content="https://felhom.eu/assets/og-image.png">
|
||||
<!-- shared assets: bump ?v= together on any site.css/icons.svg change -->
|
||||
<link rel="stylesheet" href="/assets/site.css?v=2">
|
||||
<link rel="icon" type="image/png" href="/assets/logo_favicon_2.svg">
|
||||
<script defer src="https://stats.felhom.eu/script.js" data-website-id="d419db57-5e1c-48df-bdb3-c29f25fb1661"></script>
|
||||
</head>
|
||||
<body class="page-letoltes">
|
||||
<nav>
|
||||
<div class="container">
|
||||
<a href="/" class="logo">
|
||||
<img src="/assets/logo.png" alt="Felhom.eu logo">
|
||||
<span class="logo-text">felhom.eu</span>
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
</ul>
|
||||
<button class="hamburger" id="hamburger" aria-label="Open menu" aria-expanded="false">
|
||||
<span class="hamburger-box">
|
||||
<span class="hamburger-line"></span>
|
||||
<span class="hamburger-line"></span>
|
||||
<span class="hamburger-line"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="mobile-overlay" id="mobileOverlay"></div>
|
||||
|
||||
<main>
|
||||
<section class="page-header">
|
||||
<div class="container">
|
||||
<h1>The Felhom <span>installer</span></h1>
|
||||
<p>This page is for someone who has received an installation guide from their operator. The installer turns a whole machine into a Felhom server.</p>
|
||||
<p><a href="/letoltes" lang="hu" hreflang="hu">Magyar</a></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<div class="container">
|
||||
<div class="info-card">
|
||||
<h3><svg class="ico-lg" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-triangle-alert"/></svg> Before you download</h3>
|
||||
<p>Installing <strong>erases every piece of data</strong> on the disk you select. The installer never chooses for you: it lists the disks with their size and type, and overwrites the one you pick.</p>
|
||||
<p>Unplug the external drive you use for backups before you start. If you are not certain which disk is the right one, <strong>stop and call your operator</strong>.</p>
|
||||
<p>The guide from your operator describes every step, screen by screen. Do not start without it.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section alt-bg">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2>Download</h2>
|
||||
<p>Felhom installer 1.29.0 (based on Proxmox VE 9.2)</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<p><a href="https://iso.felhom.eu/felhom-installer-1.29.0-pve9.2-1.iso"><strong>felhom-installer-1.29.0-pve9.2-1.iso</strong></a> — 1.6 GB (1,705,324,544 bytes)</p>
|
||||
<p>Checksum (SHA-256):</p>
|
||||
<div class="highlight-box">
|
||||
<p><code>dceacae5da247d76cad065bf6c0d3bbefac8d8a5f8e571db2fd8449a97e94829</code></p>
|
||||
</div>
|
||||
<p>The same checksum as a file: <a href="https://iso.felhom.eu/felhom-installer-1.29.0-pve9.2-1.iso.sha256">felhom-installer-1.29.0-pve9.2-1.iso.sha256</a></p>
|
||||
<p>To check it on Linux or macOS: <code>sha256sum felhom-installer-1.29.0-pve9.2-1.iso</code>. On Windows, in PowerShell: <code>Get-FileHash felhom-installer-1.29.0-pve9.2-1.iso</code>. The string it prints must match the one above. If it does not match, do not use the file — download it again.</p>
|
||||
<p>The downloaded file has to be written to a USB stick of at least 4 GB (with Balena Etcher or Rufus, for example). The guide covers that too.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2026 felhom.eu — Your own cloud, your own rules</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
(function() {
|
||||
var hamburger = document.getElementById('hamburger');
|
||||
var navLinks = document.querySelector('.nav-links');
|
||||
var overlay = document.getElementById('mobileOverlay');
|
||||
var body = document.body;
|
||||
|
||||
if (!hamburger || !navLinks || !overlay) {
|
||||
console.warn('[mobile-menu] Missing elements:', {
|
||||
hamburger: !!hamburger,
|
||||
navLinks: !!navLinks,
|
||||
overlay: !!overlay
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
hamburger.classList.add('is-active');
|
||||
navLinks.classList.add('is-open');
|
||||
overlay.classList.add('is-visible');
|
||||
body.classList.add('menu-open');
|
||||
hamburger.setAttribute('aria-expanded', 'true');
|
||||
hamburger.setAttribute('aria-label', 'Close menu');
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
hamburger.classList.remove('is-active');
|
||||
navLinks.classList.remove('is-open');
|
||||
overlay.classList.remove('is-visible');
|
||||
body.classList.remove('menu-open');
|
||||
hamburger.setAttribute('aria-expanded', 'false');
|
||||
hamburger.setAttribute('aria-label', 'Open menu');
|
||||
}
|
||||
|
||||
hamburger.addEventListener('click', function() {
|
||||
if (navLinks.classList.contains('is-open')) {
|
||||
closeMenu();
|
||||
} else {
|
||||
openMenu();
|
||||
}
|
||||
});
|
||||
|
||||
overlay.addEventListener('click', closeMenu);
|
||||
|
||||
navLinks.querySelectorAll('a').forEach(function(link) {
|
||||
link.addEventListener('click', closeMenu);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && navLinks.classList.contains('is-open')) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
if (window.innerWidth > 900 && navLinks.classList.contains('is-open')) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,134 @@
|
||||
# DRILL — an English speaker's first hour on 0.258.0 (2026-09-20)
|
||||
|
||||
Every observable, in order. Times are UTC. "Language met" is the column the 2026-09-14 walk did not
|
||||
have. Classification: **(F)** Felhom-owned → a row · **(A)** the app's own UI → not ours ·
|
||||
**(K)** known and deferred → its row number.
|
||||
|
||||
## Phase 0 — what the operator does before the volunteer starts
|
||||
|
||||
| | observable | language met |
|
||||
|---|---|---|
|
||||
| 0.1 | Golden **0.258.0** baked, published (`…/felhom-golden/0.258.0/golden.tar.zst` → 200), vouched (`Artifact manifest set: agent=0.132.0 golden=0.258.0 min_agent="0.131.0"`). One run, no aborted attempts. Record: `documentation/tests/golden-0.258.0-2026-09-20/` | n/a |
|
||||
| 0.2 | Customer `drill-en-0920` created through the hub's own page **with `language=en`**; `dr_tier` and off-site OFF (ep0 is fenced for this task, as on 2026-09-14) | — |
|
||||
| 0.3 | The hub auto-minted and sent the bind mail on creation, with no button pressed: `self-bind link auto-minted for drill-en-0920 on customer creation` | — |
|
||||
| 0.4 | **The stranger's first Felhom text is the bind mail, and it is ENGLISH**: subject *"[Felhom] Link your Felhom box to your account"*, body *"Dear Customer, Your Felhom box is ready and waiting to be linked…"*, including the 5-word owner-passphrase explanation and the 7-day/5-attempt limits | **English** ✓ |
|
||||
|
||||
**The operator steps the guide silently depends on**, all still true: create the customer (with the
|
||||
language), hand over the owner passphrase out of band, create the Cloudflare tunnel (**not done here —
|
||||
R-494, the known intervention**).
|
||||
|
||||
## Step 1 — download the installer
|
||||
|
||||
| | observable | language met |
|
||||
|---|---|---|
|
||||
| 1 | `https://felhom.eu/en/download` → **HTTP 200**, 7 565 B, 0.12 s. Names `felhom-installer-1.29.0-pve9.2-1.iso`, 1 705 324 544 B, sha256 `dceacae5…e94829`, and gives the check command for Linux/macOS and PowerShell | **English — 0 Hungarian lines on the page** ✓ |
|
||||
| 1b | The ISO the drill VM boots hashes to **exactly** the published checksum, and its size matches the page to the byte. So this walk installs what a volunteer downloads, not a look-alike | — |
|
||||
|
||||
## Step 2 — install (the GRAPHICAL installer, which the 2026-09-14 walk did NOT exercise)
|
||||
|
||||
| | observable | language met |
|
||||
|---|---|---|
|
||||
| 2.0 | VM 9301 on demo-hp, 8 GB / 4 cores / 120 GB on `nvme-scratch` (`/mnt/hdd_1`), ISO 1.29.0 as ide2, `--boot order=scsi0;ide2` set in a SEPARATE `qm set` after the disk existed | — |
|
||||
| 2.1 | Boot: the **graphical** entry is the default and fired on its own after the menu timeout | English (Proxmox's own) |
|
||||
| 2.2 | EULA screen → **I agree** | English (Proxmox's own) |
|
||||
| 2.3 | Target harddisk: `/dev/sda (120.00GiB, QEMU HARDDISK)`, listed with size and type as the guide says | English |
|
||||
| 2.4 | Location: prefilled **Hungary / Europe/Budapest / Hungarian keyboard** | English screen, Hungarian DEFAULTS |
|
||||
| 2.5 | Keyboard layout changed to **U.S. English** — see F1 | — |
|
||||
| 2.6 | Password screen: password twice, e-mail prefilled `mail@example.invalid` → replaced with `drill-en@felhom.eu`, exactly as the guide says | English |
|
||||
| 2.7 | Network: hostname prefilled `pve.example.invalid` (refused by the installer), changed to `felhom.drill-en-0920.felhom.eu`; DHCP gave 192.168.0.155/24, gw 192.168.0.1, DNS 192.168.0.250 — left alone as the guide says | English |
|
||||
| 2.8 | Summary: ext4 · /dev/sda · Hungary · Europe/Budapest · **Keymap en-us** · drill-en@felhom.eu · hostname felhom · 192.168.0.155/24. **"Automatically reboot after successful installation" is TICKED by default** — see F2 | English |
|
||||
| 2.9 | Install started 16:41:48Z | — |
|
||||
|
||||
**Keyboard note, for honesty:** `qm sendkey` speaks US scancodes, so the layout had to be U.S.
|
||||
English for this harness to type at all. On 2026-09-14 that was recorded as a harness substitution
|
||||
(H2). **This time it is not one** — it is what an English-speaking volunteer would genuinely choose,
|
||||
and the fact that the guide told them to do the opposite is finding F1.
|
||||
|
||||
## Step 3 — the first screen (the console)
|
||||
|
||||
| | observable | language met |
|
||||
|---|---|---|
|
||||
| 3.1 | Install started **16:41:48Z**; the appliance registered itself at the hub **16:44:33Z** (`appliance registered: new unclaimed box`); the console showed the pairing banner by **16:52:53Z** — about **11 minutes** from pressing Install | — |
|
||||
| 3.2 | `/etc/issue`, top of screen: *"There is nothing to do on this machine, and no need to log in. Follow the guide you received from Felhom."* | **English** ✓ |
|
||||
| 3.3 | The pairing banner is **bilingual**, Hungarian block then English block, inside one frame. **Pairing code `P3G-JGW`, printed once in each language.** The English half reads: *"Open the link from your e-mail and enter this code together with your Owner passphrase (the 5 words you received from your Felhom operator). This screen refreshes itself. There is nothing to do at the box; you can leave it switched on."* | **bilingual, English readable** ✓ |
|
||||
| 3.4 | The install auto-rebooted (the Summary's default) — the guide's *"Installation finished — reboot now?"* prompt never appeared. See **F2** | — |
|
||||
|
||||
## Step 4 — connecting the box (the self-bind page) — and A2, the typo
|
||||
|
||||
**This is what 2026-09-14 could NOT do (its H1: no mailbox, so the operator's bind fallback was used
|
||||
and the mailed link and self-bind page were unproven).** This walk did the real customer path.
|
||||
|
||||
| | observable | language met |
|
||||
|---|---|---|
|
||||
| 4.1 | The link from the e-mail opens `hub.felhom.eu/bind/<token>` → **HTTP 200**. The page: *"Link your Felhom box"*, *"Pairing code — It appears on the box's monitor, after the install"*, *"Owner passphrase — The five-word phrase you received during setup. It proves the account is yours."*, *"For safety the link locks after 5 failed attempts."* | **English — 0 Hungarian lines** ✓ |
|
||||
| A2 | **Typo first:** `P3G-JGX` instead of `P3G-JGW` → refused with *"Those details are not right. Check the pairing code and the owner passphrase, then try again."* It does not say WHICH was wrong, which is right. Hub log: `self-bind attempt 1/5 failed` | **English — 0 Hungarian lines** ✓ |
|
||||
| 4.2 | Correct code + passphrase at **16:54:42Z** → *"Linked successfully. The box carries on installing within about a minute. You can close this page — the setup finishes in the background."* Hub log: `self-bind SUCCESS: appliance 34 bound to customer drill-en-0920 by customer self-service` | **English — 0 Hungarian lines** ✓ |
|
||||
|
||||
**Harness slip, recorded:** while looking for the owner passphrase I printed it to stdout in a probe,
|
||||
against the project's own rule (never print a one-time secret; file→file). It belongs to this
|
||||
throwaway drill customer and is invalidated by the teardown that deletes the customer; every later
|
||||
use read it from a 0600 file. Recorded because a tidy-up nobody is told about teaches the next
|
||||
session nothing.
|
||||
|
||||
## Step 5 — the setup code, and the claim
|
||||
|
||||
| | observable | language met |
|
||||
|---|---|---|
|
||||
| 5.1 | The setup-code mail arrived **31 seconds** after the bind (16:55:13Z). Body fully English: *"Your Felhom server is up. To use the dashboard for the first time, enter the setup code below, then choose your own password"*, the 72-hour/one-use limits, the dashboard address, and the "Request a new code" fallback | **English** ✓ |
|
||||
| 5.2 | **The setup code itself is three HUNGARIAN words with five accented characters** — see **F4**. 20 characters, 3 words, 5 of them outside ASCII | **HUNGARIAN inside an English mail** ✗ |
|
||||
| 5.3 | The dashboard address in the mail does **not resolve** — `getent hosts felhom.drill-en-0920.felhom.eu` finds nothing, because no Cloudflare tunnel was created. **This is R-494, filed long before this walk, and it is intervention I1** | — |
|
||||
| 5.4 | The install-time root password **no longer works** on the box (`Permission denied`) — exactly what the guide promises ("you do not need to remember it") | — PASS |
|
||||
| 5.5 | The claim page itself: *"Set up the server"*, *"Enter the setup code you got by e-mail…"*, *"New password (at least 12 characters)"*, *"Set up and sign in"*, *"Did not get the code? Ask for a new one"* | **English chrome** ✓ |
|
||||
| 5.6 | …but its MESSAGES are Hungarian. A short password answered **„A jelszónak legalább 12 karakter hosszúnak kell lennie"**; a wrong code answered **„Hibás vagy lejárt kód"** — the message the guide quotes in English. See **F3** | **HUNGARIAN** ✗ |
|
||||
| 5.7 | The correct code claimed the dashboard: **302 → /**. Power-on to a claimed dashboard: **≈23 minutes**, including two deliberate typo tests | — |
|
||||
|
||||
## Step 4 (continued) — the dashboard's first language
|
||||
|
||||
| | observable | language met |
|
||||
|---|---|---|
|
||||
| 4.3 | Nobody touched a language switch. The dashboard came up **in English**, from `customer.language` alone: titles *"Apps — Felhom.eu"*, *"Backup — Felhom.eu"*, *"Settings — Felhom.eu"* | **English** ✓ |
|
||||
| 4.4 | Apps page: **0 Hungarian lines in 409**. Settings: **0 in 87**. Launcher: **0 in 39** | ✓ |
|
||||
| 4.5 | Backup page: **4 Hungarian strings in 73 lines** — see **F5** | ✗ |
|
||||
|
||||
## Steps 5–6 — the two apps the guide names
|
||||
|
||||
| | observable | language met |
|
||||
|---|---|---|
|
||||
| 6.1 | BookStack and PrivateBin installed from the Apps page with the default subdomains, exactly as the guide says. The API answered **in English**: *"The install has started – the card shows how it is going"* | ✓ |
|
||||
| 6.2 | Both healthy: PrivateBin ~40 s, BookStack ~50 s after the request | — |
|
||||
| 6.3 | `/apps/bookstack`: **0 Hungarian lines**. `/apps/privatebin`: **0 Hungarian lines** — the whole catalog block (description, tagline, use cases, first steps, deploy labels) in English on a box that was installed from scratch today | ✓ |
|
||||
| 6.4 | The update badge read **"Up to date"** and the backup age **"0 hours ago — within the backup window"** — R-589 and the time helpers, both fixed in 0.258.0 this morning, both proven on a fresh box | ✓ |
|
||||
|
||||
## Step 13 — the language switch
|
||||
|
||||
| | observable | language met |
|
||||
|---|---|---|
|
||||
| 13.1 | The globe switched the household to Hungarian: the Backup page title became **„Biztonsági mentés — Felhom.eu"** | ✓ both ways |
|
||||
| 13.2 | A VISITOR's switch on the sign-in page (`POST /lang`) returned 303 and set **only** the display cookie `felhom_lang=en`; the household's own setting was untouched | ✓ |
|
||||
| 13.3 | Switching the household back made the title **"Backup — Felhom.eu"** again | ✓ |
|
||||
|
||||
## Step 14 — the mailbox
|
||||
|
||||
**Every customer mail this walk produced was in English.** Four mails in the hour:
|
||||
|
||||
| to | subject | language |
|
||||
|---|---|---|
|
||||
| the customer | *[Felhom] Link your Felhom box to your account* | **English** ✓ |
|
||||
| the customer | *[Felhom] Your Felhom server is up — setup code* | **English** ✓ (the code inside is Hungarian — F4) |
|
||||
| the customer | *[Felhom] Your dashboard is now password protected* | **English** ✓ |
|
||||
| the operator | *[Felhom] ⚠️ drill-en-0920: backup_tier_skipped* | operator-tier, English body |
|
||||
|
||||
The box's own log carries the observable R-558 built for exactly this question:
|
||||
`Event pushed: controller_started (info) **[+household(en)]** — Controller elindult (0.258.0)` — the
|
||||
operator's copy is Hungarian, and a separate English household sentence was attached.
|
||||
|
||||
## R-214 — CLOSED by this walk, and it was OPEN before it
|
||||
|
||||
The 2026-09-14 drill recorded *"R-214 still reproduces"*: the console kept showing the pairing code
|
||||
and „a doboz … a párosításra vár" long after a successful bind. **It no longer does.** The console's
|
||||
LAST paint is the bound banner, bilingual, in one frame:
|
||||
|
||||
> Felhom — a doboz össze van kötve. ✓ / A beállítás magától folytatódik… / Ezen a gépen nincs több teendőd.
|
||||
> Felhom — the box is linked. / Setup carries on by itself; this takes a few minutes. / Your dashboard address is in the e-mail you received. / There is nothing left to do on this machine.
|
||||
|
||||
That is R-535 (slice 4, ISO 1.28.0) working on a real install, seen rather than reasoned about.
|
||||
@@ -0,0 +1,738 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Backup — Felhom.eu</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.258.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=0.258.0">
|
||||
<meta name="csrf-token" content="7abf4311860c5747616d8ce12a0c70c3cac0779b17b080e5b6e5f717f73f5aac">
|
||||
<script>
|
||||
|
||||
|
||||
document.documentElement.className=document.documentElement.className.replace('no-js','js');
|
||||
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function felhomConfirm(el,question,onYes){
|
||||
if(!el||el.dataset.fcOpen)return;
|
||||
el.dataset.fcOpen='1';
|
||||
var wrap=document.createElement('span');wrap.className='inline-confirm';
|
||||
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
|
||||
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Yes';
|
||||
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Cancel';
|
||||
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
|
||||
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
|
||||
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
|
||||
no.addEventListener('click',close);
|
||||
yes.addEventListener('click',function(){close();onYes();});
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
|
||||
if(!btn)return;
|
||||
e.preventDefault();
|
||||
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
|
||||
var form=btn.closest('form');
|
||||
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
|
||||
if(!btn)return;
|
||||
var group=btn.closest('.nav-group');
|
||||
if(!group)return;
|
||||
var willOpen=!group.classList.contains('is-open');
|
||||
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
|
||||
g.classList.remove('is-open');
|
||||
var t=g.querySelector('.nav-group-toggle');
|
||||
if(t)t.setAttribute('aria-expanded','false');
|
||||
});
|
||||
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
|
||||
});
|
||||
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Message</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
|
||||
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
|
||||
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
|
||||
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
|
||||
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
|
||||
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
|
||||
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
|
||||
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
|
||||
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
|
||||
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
|
||||
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
|
||||
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
|
||||
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
|
||||
</svg>
|
||||
<header class="mobile-topbar">
|
||||
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu"></a>
|
||||
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
|
||||
<svg class="ico"><use href="#i-menu"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="nav-backdrop" hidden></div>
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu" class="sidebar-logo">
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Launcher</a></li>
|
||||
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Dashboard</a></li>
|
||||
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Apps</a></li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Storage<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/storage" class="">Drives</a></li>
|
||||
<li><a href="/storage/network" class="">Network storage</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group is-open">
|
||||
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Backup<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/backups" class="active">Overview</a></li>
|
||||
<li><a href="/backups/remote" class="">Remote backup</a></li>
|
||||
<li><a href="/backups/apps" class="">Apps</a></li>
|
||||
<li><a href="/backups/restore" class="">Restore</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Sharing<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/sharing" class="">Network sharing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>System monitor</a></li>
|
||||
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="nav-group-label">Settings</div>
|
||||
<ul class="nav-links nav-links-sub">
|
||||
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>System</a></li>
|
||||
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Notifications</a></li>
|
||||
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Security and access</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.258.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="7abf4311860c5747616d8ce12a0c70c3cac0779b17b080e5b6e5f717f73f5aac"><input type="hidden" name="back" value="/backups"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="7abf4311860c5747616d8ce12a0c70c3cac0779b17b080e5b6e5f717f73f5aac"><input type="hidden" name="back" value="/backups"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item current" aria-current="true">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Sign out ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Backup</h2>
|
||||
<span class="domain-badge">drill-en-0920.felhom.eu</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="alert alert-warning">Csak egy másolat készül (nincs második meghajtó) — a 3-2-1 mentéshez csatlakoztasson egy második meghajtót vagy offsite tárolót.</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="alert alert-warning" id="backup-target-alert">A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer — így hibás fájlok ellen véd, lemezhiba ellen nem. Csatlakoztass egy második meghajtót a teljes védelemhez.</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="backup-section-card">
|
||||
<h3>Storage overview</h3>
|
||||
<div class="storage-overview-grid">
|
||||
<div class="storage-bars">
|
||||
|
||||
<div class="storage-item meter nominal">
|
||||
<div class="storage-header">
|
||||
<span class="storage-label">System (/)</span>
|
||||
<span class="storage-value">0.97 GB / 68.7 GB (1%)</span>
|
||||
</div>
|
||||
<div class="meter-track">
|
||||
<div class="meter-fill" style="width:1.4%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="storage-stats">
|
||||
<div class="storage-stat-row">
|
||||
<span class="storage-stat-label">Database backups</span>
|
||||
<span class="storage-stat-value mono">–</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="backup-section-card">
|
||||
<h3>System backup (full backup)</h3>
|
||||
<p class="form-hint" style="margin-bottom:1rem">A regular backup of the whole server — apps, settings and databases together — that can bring back the entire device. The host agent makes and manages it.</p>
|
||||
|
||||
<div class="stats-grid backup-page-cards">
|
||||
<div class="stat-card stat-running">
|
||||
<div class="stat-value">✓</div>
|
||||
<div class="stat-label">Last full backup
|
||||
<br><span class="relative-time">2026-09-20 19:02 (just now)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-total">
|
||||
<div class="stat-value">624.7 MB</div>
|
||||
<div class="stat-label">Helyi tároló (local)</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="font-size:1.15rem">Up to date</div>
|
||||
<div class="stat-label">Next backup
|
||||
<br><span class="relative-time">0 hours ago — within the backup window</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">–</div>
|
||||
<div class="stat-label">Restore checked
|
||||
<br><span class="relative-time">Not run yet</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="backup-tier-table" style="width:100%;margin-top:1rem;border-collapse:collapse">
|
||||
|
||||
<tr style="border-top:1px solid var(--border)">
|
||||
<td style="padding:.5rem .75rem .5rem 0;white-space:nowrap;vertical-align:top"><strong>Helyi tároló (local)</strong></td>
|
||||
<td style="padding:.5rem 0;vertical-align:top">
|
||||
|
||||
|
||||
<div>✓ Last successful backup: 2026-09-20 19:02 (just now) · 624.7 MB
|
||||
<span class="tag tag-run" style="margin-left:.4rem">Up to date</span></div>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="border-top:1px solid var(--border)">
|
||||
<td style="padding:.5rem .75rem .5rem 0;white-space:nowrap;vertical-align:top"><strong>Biztonsági szerver – külön hardver (PBS)</strong></td>
|
||||
<td style="padding:.5rem 0;vertical-align:top">
|
||||
|
||||
<span class="tag"><span class="dot"></span>not set up</span>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<div class="schedule-actions" style="margin-top:1rem">
|
||||
<button class="btn btn-sm btn-primary" id="wg-backup-btn" onclick="triggerGuestBackup()" >Back up now</button>
|
||||
<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Your apps stop during the backup — usually for a few minutes, longer with more data.</span>
|
||||
<div id="wg-backup-result" style="margin-top:.6rem"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="backup-section-card">
|
||||
<h3>Backup window</h3>
|
||||
<p class="form-hint" style="margin-bottom:1rem">The backups run one after the other: database backup, local copy, remote backup, then the full system backup.</p>
|
||||
<form method="POST" action="/backups/window" class="schedule-actions" style="display:flex;align-items:flex-end;gap:.75rem;flex-wrap:wrap">
|
||||
<input type="hidden" name="_csrf" value="7abf4311860c5747616d8ce12a0c70c3cac0779b17b080e5b6e5f717f73f5aac">
|
||||
<div class="form-group" style="margin:0">
|
||||
<label for="window_start">Backup window start</label>
|
||||
<input type="time" id="window_start" name="window_start" value="02:30" class="form-control" style="max-width:9rem" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Save</button>
|
||||
</form>
|
||||
<ul class="form-hint" style="margin-top:1rem;margin-bottom:0;list-style:none;padding:0;line-height:1.9">
|
||||
<li>Database backup: <strong>02:30</strong></li>
|
||||
<li>Local copy: <strong>03:30</strong></li>
|
||||
<li>Remote backup: <strong>04:15</strong></li>
|
||||
<li>Full system backup: about <strong>04:30–08:30</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stats-grid backup-page-cards">
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">–</div>
|
||||
<div class="stat-label">Data backup</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="stat-card" style="border-left-color: var(--text-3);">
|
||||
<div class="stat-value" style="background:var(--text-3);-webkit-background-clip:text;background-clip:text;">–</div>
|
||||
<div class="stat-label">Remote system backup<br><span class="relative-time">not set up</span></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stat-card stat-total">
|
||||
|
||||
<div class="stat-value">–</div>
|
||||
<div class="stat-label">Databases backed up<br><span class="relative-time">embedded databases, inside the volume backup</span></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
function triggerGuestBackup() {
|
||||
var btn = document.getElementById('wg-backup-btn');
|
||||
felhomConfirm(btn, 'Start the full system backup now? Your apps stop during the backup — usually for a few minutes, longer with more data.', function () {
|
||||
var out = document.getElementById('wg-backup-result');
|
||||
btn.disabled = true;
|
||||
out.innerHTML = '<span class="form-hint">Starting the backup…</span>';
|
||||
fetch('/api/guest-backup/trigger', { method: 'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()) })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.ok) { out.innerHTML = '<div class="alert alert-error">' + (data.error || 'Error') + '</div>'; btn.disabled = false; return; }
|
||||
out.innerHTML = '<span class="form-hint">Backup in progress…</span>';
|
||||
pollGuestBackup(out, btn);
|
||||
})
|
||||
.catch(e => { out.innerHTML = '<div class="alert alert-error">Error: ' + e + '</div>'; btn.disabled = false; });
|
||||
});
|
||||
}
|
||||
|
||||
function pollGuestBackup(out, btn) {
|
||||
var tries = 0;
|
||||
var iv = setInterval(function () {
|
||||
tries++;
|
||||
fetch('/api/guest-backup/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok && data.data) {
|
||||
var ph = data.data.phase;
|
||||
out.innerHTML = '<span class="form-hint">Status: ' + ph + '</span>';
|
||||
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">The system backup is done.</div>'; setTimeout(() => window.location.reload(), 1500); }
|
||||
else if (ph === 'failed') { clearInterval(iv); out.innerHTML = '<div class="alert alert-error">The backup failed.</div>'; btn.disabled = false; }
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
if (tries > 180) { clearInterval(iv); btn.disabled = false; }
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
(function () {
|
||||
var btn = document.getElementById('backup-target-assign');
|
||||
if (!btn) { return; }
|
||||
btn.addEventListener('click', function () {
|
||||
var out = document.getElementById('backup-target-assign-result');
|
||||
btn.disabled = true;
|
||||
out.innerHTML = '<p class="form-hint">Assigning the drive…</p>';
|
||||
fetch('/api/storage/backup-target/assign', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()),
|
||||
body: JSON.stringify({ path: btn.getAttribute('data-path') })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) {
|
||||
if (!j.ok) {
|
||||
out.innerHTML = '<div class="alert alert-error">The drive could not be assigned: ' + (j.error || '') + '</div>';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (j.data && j.data.restart_required) {
|
||||
out.innerHTML = '<div class="flash flash-success">The drive is assigned. The setting takes effect after the host agent next restarts.</div>';
|
||||
} else {
|
||||
out.innerHTML = '<div class="flash flash-success">The drive is assigned.</div>';
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
out.innerHTML = '<div class="alert alert-error">Error: ' + e.message + '</div>';
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
var burger=document.querySelector('.nav-burger');
|
||||
var sidebar=document.getElementById('sidebar');
|
||||
var backdrop=document.querySelector('.nav-backdrop');
|
||||
if(!burger||!sidebar||!backdrop)return;
|
||||
function setOpen(open){
|
||||
sidebar.classList.toggle('is-open',open);
|
||||
document.body.classList.toggle('nav-open',open);
|
||||
backdrop.hidden=!open;
|
||||
burger.setAttribute('aria-expanded',open?'true':'false');
|
||||
}
|
||||
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
|
||||
backdrop.addEventListener('click',function(){setOpen(false);});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
|
||||
});
|
||||
})();
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
|
||||
var card = e.target.closest('[data-href]');
|
||||
if (card) window.location.href = card.dataset.href;
|
||||
});
|
||||
async function checkBeforeDeploy(e, name) {
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name);
|
||||
var data = await resp.json();
|
||||
if (data.ok && data.data && data.data.deployed) {
|
||||
e.preventDefault();
|
||||
showAlert('This app is already installed.');
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
} catch(err) {}
|
||||
return true;
|
||||
}
|
||||
async function syncTemplates() {
|
||||
const btn = document.getElementById('sync-btn');
|
||||
const toast = document.getElementById('sync-toast');
|
||||
if (!btn) return;
|
||||
const origText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '↻ Refreshing...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/sync', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (toast) {
|
||||
toast.textContent = data.ok ? (data.message || 'Templates refreshed') : ('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
|
||||
setTimeout(function() { window.location.reload(); }, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
if (toast) {
|
||||
toast.textContent = 'Network error: ' + err.message;
|
||||
toast.className = 'sync-toast sync-toast-err';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
}
|
||||
btn.innerHTML = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
async function stackAction(event, name, action) {
|
||||
const btn = event.currentTarget;
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'In progress...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/stacks/' + name + '/' + action, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (action === 'update') {
|
||||
followUpdate(name, btn);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
function followUpdate(name, btn) {
|
||||
var started = Date.now();
|
||||
var timer = setInterval(async function() {
|
||||
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/stacks/' + name);
|
||||
var d = await r.json();
|
||||
if (!d.ok || !d.data) return;
|
||||
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
|
||||
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
|
||||
} catch (e) {}
|
||||
}, 3000);
|
||||
}
|
||||
async function deleteOrphanStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'delete-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
|
||||
var data = await resp.json();
|
||||
var hddInfo = '';
|
||||
var checkboxHTML = '';
|
||||
if (data.ok && data.data && data.data.has_hdd_data) {
|
||||
hddInfo = '<div class="modal-hdd-info"><strong>Your data on the hard drive:</strong>';
|
||||
data.data.hdd_paths.forEach(function(p) {
|
||||
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
hddInfo += '</div>';
|
||||
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Delete your data from the hard drive</label>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Delete app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">This removes the containers, the volumes and the configuration files.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
hddInfo + checkboxHTML +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Delete</button>' +
|
||||
'</div>';
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeDeleteModal() {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmDelete(name) {
|
||||
var btn = document.getElementById('confirm-delete-btn');
|
||||
var checkbox = document.getElementById('delete-hdd-check');
|
||||
var removeHDD = checkbox ? checkbox.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Deleting...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name, {
|
||||
method: 'DELETE',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Deleted!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was deleted.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
}
|
||||
async function removeStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'remove-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var [hddResp, backupResp] = await Promise.all([
|
||||
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
|
||||
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
|
||||
]);
|
||||
var sections = '';
|
||||
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Always deleted:</strong>' +
|
||||
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
|
||||
'<li>Docker volumes (database, app configuration)</li>' +
|
||||
'<li>Install configuration (app.yaml)</li>' +
|
||||
'<li>Secondary backup schedule</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
var hddCheckbox = '';
|
||||
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
|
||||
var hddPaths = '';
|
||||
hddResp.data.hdd_paths.forEach(function(p) {
|
||||
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Your data on the hard drive:</strong>' + hddPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Delete your data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
|
||||
'If you reinstall the app, you need to import the data again, because the database is deleted. Data you keep is NOT backed up automatically any more.' +
|
||||
'</div></div>';
|
||||
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
|
||||
}
|
||||
|
||||
var backupCheckbox = '';
|
||||
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
|
||||
var bkPaths = '';
|
||||
backupResp.data.backup_paths.forEach(function(p) {
|
||||
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
|
||||
});
|
||||
if (bkPaths) {
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Backup data:</strong>' + bkPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Delete backup data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
|
||||
'The nightly restic snapshots cannot be deleted one by one — they expire automatically under the retention rules.' +
|
||||
'</div></div>';
|
||||
}
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Remove app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">The app goes back to "Not installed". The template stays, and you can install it again.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
sections +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeRemoveModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Remove</button>' +
|
||||
'</div>' + hddCheckbox;
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeRemoveModal() {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmRemoveStack(name) {
|
||||
var btn = document.getElementById('confirm-remove-btn');
|
||||
var hddCheck = document.getElementById('remove-hdd-check');
|
||||
var backupCheck = document.getElementById('remove-backup-check');
|
||||
var removeHDD = hddCheck ? hddCheck.checked : false;
|
||||
var removeBackups = backupCheck ? backupCheck.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Removing...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/remove', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted backups: ' + data.data.backup_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Backups not deleted: ' + data.data.backup_paths_refused.join('; ') + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Removed!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was removed. You can install it again with the Install button.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<a href="/launcher">Found</a>.
|
||||
|
||||
@@ -0,0 +1,872 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings — Felhom.eu</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.258.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=0.258.0">
|
||||
<meta name="csrf-token" content="7abf4311860c5747616d8ce12a0c70c3cac0779b17b080e5b6e5f717f73f5aac">
|
||||
<script>
|
||||
|
||||
|
||||
document.documentElement.className=document.documentElement.className.replace('no-js','js');
|
||||
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function felhomConfirm(el,question,onYes){
|
||||
if(!el||el.dataset.fcOpen)return;
|
||||
el.dataset.fcOpen='1';
|
||||
var wrap=document.createElement('span');wrap.className='inline-confirm';
|
||||
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
|
||||
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Yes';
|
||||
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Cancel';
|
||||
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
|
||||
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
|
||||
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
|
||||
no.addEventListener('click',close);
|
||||
yes.addEventListener('click',function(){close();onYes();});
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
|
||||
if(!btn)return;
|
||||
e.preventDefault();
|
||||
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
|
||||
var form=btn.closest('form');
|
||||
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
|
||||
if(!btn)return;
|
||||
var group=btn.closest('.nav-group');
|
||||
if(!group)return;
|
||||
var willOpen=!group.classList.contains('is-open');
|
||||
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
|
||||
g.classList.remove('is-open');
|
||||
var t=g.querySelector('.nav-group-toggle');
|
||||
if(t)t.setAttribute('aria-expanded','false');
|
||||
});
|
||||
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
|
||||
});
|
||||
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Message</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
|
||||
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
|
||||
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
|
||||
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
|
||||
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
|
||||
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
|
||||
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
|
||||
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
|
||||
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
|
||||
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
|
||||
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
|
||||
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
|
||||
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
|
||||
</svg>
|
||||
<header class="mobile-topbar">
|
||||
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu"></a>
|
||||
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
|
||||
<svg class="ico"><use href="#i-menu"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="nav-backdrop" hidden></div>
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu" class="sidebar-logo">
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Launcher</a></li>
|
||||
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Dashboard</a></li>
|
||||
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Apps</a></li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Storage<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/storage" class="">Drives</a></li>
|
||||
<li><a href="/storage/network" class="">Network storage</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Backup<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/backups" class="">Overview</a></li>
|
||||
<li><a href="/backups/remote" class="">Remote backup</a></li>
|
||||
<li><a href="/backups/apps" class="">Apps</a></li>
|
||||
<li><a href="/backups/restore" class="">Restore</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Sharing<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/sharing" class="">Network sharing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>System monitor</a></li>
|
||||
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="nav-group-label">Settings</div>
|
||||
<ul class="nav-links nav-links-sub">
|
||||
<li><a href="/settings" class="active"><svg class="ico"><use href="#i-settings"/></svg>System</a></li>
|
||||
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Notifications</a></li>
|
||||
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Security and access</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.258.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="7abf4311860c5747616d8ce12a0c70c3cac0779b17b080e5b6e5f717f73f5aac"><input type="hidden" name="back" value="/settings"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="7abf4311860c5747616d8ce12a0c70c3cac0779b17b080e5b6e5f717f73f5aac"><input type="hidden" name="back" value="/settings"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item current" aria-current="true">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Sign out ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Settings — System</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>System configuration</h3>
|
||||
<p class="settings-card-desc">Values set by the operator. Ask the operator to change them.</p>
|
||||
<div class="settings-grid">
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Customer ID</span>
|
||||
<span class="settings-value mono">drill-en-0920</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Customer name</span>
|
||||
<span class="settings-value">Drill EN 2026-09-20</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Domain</span>
|
||||
<span class="settings-value mono">drill-en-0920.felhom.eu</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">App template source</span>
|
||||
<span class="settings-value mono settings-value-truncate">https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Template sync</span>
|
||||
<span class="settings-value mono">15m</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Backup</span>
|
||||
<span class="settings-value"><span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Active</span></span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Backup schedule</span>
|
||||
<span class="settings-value mono">02:30 / 03:00</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Monitoring</span>
|
||||
<span class="settings-value"><span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Active</span></span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Healthchecks URL</span>
|
||||
<span class="settings-value mono settings-value-truncate">https://status.felhom.eu</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Hub reporting</span>
|
||||
<span class="settings-value"><span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Active</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Version and update</h3>
|
||||
<div class="settings-grid">
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Current version</span>
|
||||
<span class="settings-value mono">0.258.0</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Latest version</span>
|
||||
<span class="settings-value mono">
|
||||
0.258.0
|
||||
|
||||
<span style="margin-left:0.5em; color:var(--text-3);">— up to date</span>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Last check</span>
|
||||
<span class="settings-value mono">2026-09-20T16:57:39Z</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Registry</span>
|
||||
<span class="settings-value">public (no authentication)</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Automatic update</span>
|
||||
<span class="settings-value">
|
||||
–
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Minimum version (operator)</span>
|
||||
<span class="settings-value mono">
|
||||
0.257.0
|
||||
<span style="margin-left:0.5em; color:#888;">— the system updates to this version automatically if it is older</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="settings-row" style="padding-top: 0.5em;">
|
||||
<span class="settings-label"></span>
|
||||
<span class="settings-value">
|
||||
<button class="btn btn-secondary btn-sm" id="btn-check-update" onclick="checkUpdate()">Check for updates</button>
|
||||
|
||||
<span id="update-status-msg" style="margin-left:0.5em; display:none;"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Network</h3>
|
||||
<div class="settings-grid">
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Local address (LAN)</span>
|
||||
<span class="settings-value mono">— <span style="color:var(--text-3);">cannot be determined</span></span>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Gateway</span>
|
||||
<span class="settings-value mono">— <span style="color:var(--text-3);">cannot be determined</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="settings-card-desc" style="margin-top:0.5em;">These values depend on the local network and can change. For remote troubleshooting, we ask you to read out this page.</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Server memory (RAM)</h3>
|
||||
|
||||
|
||||
<div class="settings-grid">
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Current memory</span>
|
||||
<span class="settings-value mono">4096 MB</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Used</span>
|
||||
<span class="settings-value mono">151 MB</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Allowed range</span>
|
||||
<span class="settings-value mono">2048 MB – 5891 MB</span>
|
||||
</div>
|
||||
<div class="settings-row" style="padding-top: 0.5em;">
|
||||
<span class="settings-label">New size (MB)</span>
|
||||
<span class="settings-value">
|
||||
<input type="number" id="mem-input" class="input mono" style="width:8em;"
|
||||
value="4096" min="2048" max="5891" step="256"
|
||||
data-current="4096">
|
||||
<button class="btn btn-primary btn-sm" id="btn-mem-resize" onclick="resizeMemory()" style="margin-left:0.5em;">Resize</button>
|
||||
<span id="mem-status-msg" style="margin-left:0.5em; display:none;"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div id="dialog-root"></div>
|
||||
<script>
|
||||
|
||||
function escText(s){var d=document.createElement('div');d.textContent=String(s==null?'':s);return d.innerHTML;}
|
||||
function closeDialog(){ var r=document.getElementById('dialog-root'); if(r) r.innerHTML=''; }
|
||||
function openDialog(opts){
|
||||
var root=document.getElementById('dialog-root');
|
||||
if(!root) return;
|
||||
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">'
|
||||
+'<h3>'+escText(opts.title||'Confirm')+'</h3>'
|
||||
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
|
||||
+'<div class="form-actions"><button id="dialog-go" class="btn btn-primary">'+escText(opts.confirmLabel||'Confirm')+'</button>'
|
||||
+'<button type="button" class="btn btn-outline" id="dialog-cancel">Cancel</button></div>'
|
||||
+'</div></div>';
|
||||
document.getElementById('dialog-go').onclick=function(){ closeDialog(); if(opts.onConfirm) opts.onConfirm(); };
|
||||
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
|
||||
}
|
||||
|
||||
function checkUpdate() {
|
||||
var btn = document.getElementById('btn-check-update');
|
||||
var msg = document.getElementById('update-status-msg');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Checking...';
|
||||
msg.style.display = 'none';
|
||||
fetch('/api/selfupdate/check', {method:'POST', headers: csrfHeaders()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
msg.textContent = data.error || 'An error occurred';
|
||||
msg.style.display = 'inline';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Check for updates';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
msg.textContent = 'Connection error';
|
||||
msg.style.display = 'inline';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Check for updates';
|
||||
});
|
||||
}
|
||||
|
||||
function triggerUpdate() {
|
||||
openDialog({title:'Update controller', confirmLabel:'Update',
|
||||
message:'Update the controller?\n\nThe dashboard is unreachable for a short time during the update.',
|
||||
onConfirm:doTriggerUpdate});
|
||||
}
|
||||
function doTriggerUpdate() {
|
||||
var btn = document.getElementById('btn-trigger-update');
|
||||
var checkBtn = document.getElementById('btn-check-update');
|
||||
var msg = document.getElementById('update-status-msg');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Updating...';
|
||||
if (checkBtn) checkBtn.disabled = true;
|
||||
msg.textContent = 'Update in progress...';
|
||||
msg.style.display = 'inline';
|
||||
fetch('/api/selfupdate/update', {method:'POST', headers: csrfHeaders()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
msg.textContent = 'Restarting...';
|
||||
pollUntilBack();
|
||||
} else {
|
||||
msg.textContent = data.error || 'An error occurred';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Install update';
|
||||
if (checkBtn) checkBtn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
msg.textContent = 'Connection error';
|
||||
pollUntilBack();
|
||||
});
|
||||
}
|
||||
|
||||
function resizeMemory() {
|
||||
var input = document.getElementById('mem-input');
|
||||
var target = parseInt(input.value, 10);
|
||||
var current = parseInt(input.getAttribute('data-current'), 10);
|
||||
if (isNaN(target) || target <= 0) {
|
||||
showMemMsg('Invalid memory value', true);
|
||||
return;
|
||||
}
|
||||
if (target < current) {
|
||||
openDialog({title:'Reduce memory', confirmLabel:'Reduce',
|
||||
message:'Reducing memory can affect the performance of running apps. Continue?',
|
||||
onConfirm:function(){ doResizeMemory(target); }});
|
||||
return;
|
||||
}
|
||||
doResizeMemory(target);
|
||||
}
|
||||
function doResizeMemory(target) {
|
||||
var btn = document.getElementById('btn-mem-resize');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Resizing...';
|
||||
fetch('/api/system/memory/resize', {
|
||||
method:'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({memory_mb: target})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
showMemMsg(data.error || data.message || 'Done.', false);
|
||||
setTimeout(function(){ location.reload(); }, 1200);
|
||||
} else {
|
||||
showMemMsg(data.error || 'The operation failed', true);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Resize';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
showMemMsg('Connection error', true);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Resize';
|
||||
});
|
||||
}
|
||||
function showMemMsg(text, isErr) {
|
||||
var msg = document.getElementById('mem-status-msg');
|
||||
if (!msg) return;
|
||||
msg.textContent = text;
|
||||
msg.style.color = isErr ? 'var(--danger, #c0392b)' : 'var(--text-2)';
|
||||
msg.style.display = 'inline';
|
||||
}
|
||||
|
||||
function pollUntilBack() {
|
||||
var iv = setInterval(function() {
|
||||
fetch('/api/health')
|
||||
.then(function(r) {
|
||||
if (r.ok) {
|
||||
clearInterval(iv);
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Restart controller</h3>
|
||||
<p class="settings-card-desc">
|
||||
If the controller misbehaves, you can safely restart it here — no need to restart the whole server.
|
||||
Your apps keep running; only the controller restarts (a few seconds).
|
||||
</p>
|
||||
<div id="restart-status"></div>
|
||||
<button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">Restart controller</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Restart server</h3>
|
||||
<p class="settings-card-desc">
|
||||
Restarts the whole server. Every app stops for a short time, and the dashboard is unreachable for about 30 seconds. Use it when the whole system needs a restart — otherwise “Restart controller” above is enough.
|
||||
</p>
|
||||
<div id="server-restart-status"></div>
|
||||
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">Restart server</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function restartController() {
|
||||
openDialog({title:'Restart controller', confirmLabel:'Restart',
|
||||
message:'Restart the controller? It takes a few seconds, and the page is unreachable for a short time.',
|
||||
onConfirm:doRestartController});
|
||||
}
|
||||
function doRestartController() {
|
||||
var btn = document.getElementById('btn-restart-controller');
|
||||
var status = document.getElementById('restart-status');
|
||||
btn.disabled = true;
|
||||
status.innerHTML = '<div class="alert alert-info">Restarting… reconnecting…</div>';
|
||||
fetch('/api/selfrestart', { method: 'POST', headers: csrfHeaders() })
|
||||
.then(function(){ pollRestart(0); })
|
||||
.catch(function(){ pollRestart(0); });
|
||||
}
|
||||
function restartServer() {
|
||||
openDialog({title:'Restart server', confirmLabel:'Restart',
|
||||
message:'Restart the server? The apps and the dashboard become unreachable for about 30 seconds.',
|
||||
onConfirm:doRestartServer});
|
||||
}
|
||||
function doRestartServer() {
|
||||
var btn = document.getElementById('btn-restart-server');
|
||||
var status = document.getElementById('server-restart-status');
|
||||
if (btn) btn.disabled = true;
|
||||
if (status) status.innerHTML = '<div class="alert alert-info">Restarting… the dashboard reloads in a few seconds.</div>';
|
||||
fetch('/api/server/reboot', { method: 'POST', headers: csrfHeaders() })
|
||||
.then(function(){ pollRestart(0); })
|
||||
.catch(function(){ pollRestart(0); });
|
||||
}
|
||||
function pollRestart(attempt) {
|
||||
if (attempt > 60) {
|
||||
document.getElementById('restart-status').innerHTML =
|
||||
'<div class="alert alert-error">The restart is taking longer than expected. Reload the page by hand.</div>';
|
||||
return;
|
||||
}
|
||||
setTimeout(function(){
|
||||
fetch('/', { method: 'GET', cache: 'no-store' })
|
||||
.then(function(r){ if (r.ok) { window.location.reload(); } else { pollRestart(attempt + 1); } })
|
||||
.catch(function(){ pollRestart(attempt + 1); });
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
var burger=document.querySelector('.nav-burger');
|
||||
var sidebar=document.getElementById('sidebar');
|
||||
var backdrop=document.querySelector('.nav-backdrop');
|
||||
if(!burger||!sidebar||!backdrop)return;
|
||||
function setOpen(open){
|
||||
sidebar.classList.toggle('is-open',open);
|
||||
document.body.classList.toggle('nav-open',open);
|
||||
backdrop.hidden=!open;
|
||||
burger.setAttribute('aria-expanded',open?'true':'false');
|
||||
}
|
||||
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
|
||||
backdrop.addEventListener('click',function(){setOpen(false);});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
|
||||
});
|
||||
})();
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
|
||||
var card = e.target.closest('[data-href]');
|
||||
if (card) window.location.href = card.dataset.href;
|
||||
});
|
||||
async function checkBeforeDeploy(e, name) {
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name);
|
||||
var data = await resp.json();
|
||||
if (data.ok && data.data && data.data.deployed) {
|
||||
e.preventDefault();
|
||||
showAlert('This app is already installed.');
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
} catch(err) {}
|
||||
return true;
|
||||
}
|
||||
async function syncTemplates() {
|
||||
const btn = document.getElementById('sync-btn');
|
||||
const toast = document.getElementById('sync-toast');
|
||||
if (!btn) return;
|
||||
const origText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '↻ Refreshing...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/sync', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (toast) {
|
||||
toast.textContent = data.ok ? (data.message || 'Templates refreshed') : ('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
|
||||
setTimeout(function() { window.location.reload(); }, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
if (toast) {
|
||||
toast.textContent = 'Network error: ' + err.message;
|
||||
toast.className = 'sync-toast sync-toast-err';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
}
|
||||
btn.innerHTML = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
async function stackAction(event, name, action) {
|
||||
const btn = event.currentTarget;
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'In progress...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/stacks/' + name + '/' + action, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (action === 'update') {
|
||||
followUpdate(name, btn);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
function followUpdate(name, btn) {
|
||||
var started = Date.now();
|
||||
var timer = setInterval(async function() {
|
||||
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/stacks/' + name);
|
||||
var d = await r.json();
|
||||
if (!d.ok || !d.data) return;
|
||||
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
|
||||
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
|
||||
} catch (e) {}
|
||||
}, 3000);
|
||||
}
|
||||
async function deleteOrphanStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'delete-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
|
||||
var data = await resp.json();
|
||||
var hddInfo = '';
|
||||
var checkboxHTML = '';
|
||||
if (data.ok && data.data && data.data.has_hdd_data) {
|
||||
hddInfo = '<div class="modal-hdd-info"><strong>Your data on the hard drive:</strong>';
|
||||
data.data.hdd_paths.forEach(function(p) {
|
||||
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
hddInfo += '</div>';
|
||||
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Delete your data from the hard drive</label>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Delete app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">This removes the containers, the volumes and the configuration files.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
hddInfo + checkboxHTML +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Delete</button>' +
|
||||
'</div>';
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeDeleteModal() {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmDelete(name) {
|
||||
var btn = document.getElementById('confirm-delete-btn');
|
||||
var checkbox = document.getElementById('delete-hdd-check');
|
||||
var removeHDD = checkbox ? checkbox.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Deleting...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name, {
|
||||
method: 'DELETE',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Deleted!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was deleted.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
}
|
||||
async function removeStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'remove-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var [hddResp, backupResp] = await Promise.all([
|
||||
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
|
||||
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
|
||||
]);
|
||||
var sections = '';
|
||||
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Always deleted:</strong>' +
|
||||
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
|
||||
'<li>Docker volumes (database, app configuration)</li>' +
|
||||
'<li>Install configuration (app.yaml)</li>' +
|
||||
'<li>Secondary backup schedule</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
var hddCheckbox = '';
|
||||
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
|
||||
var hddPaths = '';
|
||||
hddResp.data.hdd_paths.forEach(function(p) {
|
||||
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Your data on the hard drive:</strong>' + hddPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Delete your data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
|
||||
'If you reinstall the app, you need to import the data again, because the database is deleted. Data you keep is NOT backed up automatically any more.' +
|
||||
'</div></div>';
|
||||
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
|
||||
}
|
||||
|
||||
var backupCheckbox = '';
|
||||
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
|
||||
var bkPaths = '';
|
||||
backupResp.data.backup_paths.forEach(function(p) {
|
||||
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
|
||||
});
|
||||
if (bkPaths) {
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Backup data:</strong>' + bkPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Delete backup data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
|
||||
'The nightly restic snapshots cannot be deleted one by one — they expire automatically under the retention rules.' +
|
||||
'</div></div>';
|
||||
}
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Remove app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">The app goes back to "Not installed". The template stays, and you can install it again.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
sections +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeRemoveModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Remove</button>' +
|
||||
'</div>' + hddCheckbox;
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeRemoveModal() {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmRemoveStack(name) {
|
||||
var btn = document.getElementById('confirm-remove-btn');
|
||||
var hddCheck = document.getElementById('remove-hdd-check');
|
||||
var backupCheck = document.getElementById('remove-backup-check');
|
||||
var removeHDD = hddCheck ? hddCheck.checked : false;
|
||||
var removeBackups = backupCheck ? backupCheck.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Removing...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/remove', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted backups: ' + data.data.backup_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Backups not deleted: ' + data.data.backup_paths_refused.join('; ') + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Removed!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was removed. You can install it again with the Install button.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BookStack — Felhom.eu</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.258.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=0.258.0">
|
||||
<meta name="csrf-token" content="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df">
|
||||
<script>
|
||||
|
||||
|
||||
document.documentElement.className=document.documentElement.className.replace('no-js','js');
|
||||
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function felhomConfirm(el,question,onYes){
|
||||
if(!el||el.dataset.fcOpen)return;
|
||||
el.dataset.fcOpen='1';
|
||||
var wrap=document.createElement('span');wrap.className='inline-confirm';
|
||||
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
|
||||
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Yes';
|
||||
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Cancel';
|
||||
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
|
||||
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
|
||||
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
|
||||
no.addEventListener('click',close);
|
||||
yes.addEventListener('click',function(){close();onYes();});
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
|
||||
if(!btn)return;
|
||||
e.preventDefault();
|
||||
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
|
||||
var form=btn.closest('form');
|
||||
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
|
||||
if(!btn)return;
|
||||
var group=btn.closest('.nav-group');
|
||||
if(!group)return;
|
||||
var willOpen=!group.classList.contains('is-open');
|
||||
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
|
||||
g.classList.remove('is-open');
|
||||
var t=g.querySelector('.nav-group-toggle');
|
||||
if(t)t.setAttribute('aria-expanded','false');
|
||||
});
|
||||
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
|
||||
});
|
||||
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Message</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
|
||||
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
|
||||
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
|
||||
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
|
||||
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
|
||||
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
|
||||
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
|
||||
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
|
||||
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
|
||||
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
|
||||
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
|
||||
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
|
||||
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
|
||||
</svg>
|
||||
<header class="mobile-topbar">
|
||||
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu"></a>
|
||||
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
|
||||
<svg class="ico"><use href="#i-menu"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="nav-backdrop" hidden></div>
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu" class="sidebar-logo">
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Launcher</a></li>
|
||||
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Dashboard</a></li>
|
||||
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Apps</a></li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Storage<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/storage" class="">Drives</a></li>
|
||||
<li><a href="/storage/network" class="">Network storage</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Backup<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/backups" class="">Overview</a></li>
|
||||
<li><a href="/backups/remote" class="">Remote backup</a></li>
|
||||
<li><a href="/backups/apps" class="">Apps</a></li>
|
||||
<li><a href="/backups/restore" class="">Restore</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Sharing<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/sharing" class="">Network sharing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>System monitor</a></li>
|
||||
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="nav-group-label">Settings</div>
|
||||
<ul class="nav-links nav-links-sub">
|
||||
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>System</a></li>
|
||||
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Notifications</a></li>
|
||||
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Security and access</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.258.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df"><input type="hidden" name="back" value="/apps/bookstack"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df"><input type="hidden" name="back" value="/apps/bookstack"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item current" aria-current="true">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Sign out ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<div style="display:flex;align-items:center;gap:1rem">
|
||||
<a href="/stacks" class="btn btn-sm btn-outline">← Apps</a>
|
||||
<h2>BookStack</h2>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.5rem">
|
||||
|
||||
<span class="stack-state-badge state-warn">Partly stopped</span>
|
||||
|
||||
|
||||
|
||||
<a href="https://wiki.drill-en-0920.felhom.eu" target="_blank" class="btn btn-sm btn-outline">Open ↗</a>
|
||||
<a href="/stacks/bookstack/logs" class="btn btn-sm btn-outline">Log</a>
|
||||
|
||||
<a href="/stacks/bookstack/export" class="btn btn-sm btn-outline">Export</a>
|
||||
<a href="/stacks/bookstack/deploy" class="btn btn-sm btn-outline">Settings</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="app-info-hero">
|
||||
<img class="app-info-logo" src="/static/assets/bookstack-logo.svg"
|
||||
alt="BookStack" data-fallback="/static/app-placeholder.svg"
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/bookstack-logo.png';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
||||
<div class="app-info-hero-text">
|
||||
|
||||
<p class="app-info-tagline">A simple wiki platform - shelves, books, chapters and pages</p>
|
||||
|
||||
<div class="stack-meta-badges">
|
||||
<span class="meta-badge">~150M RAM</span>
|
||||
<span class="meta-badge">productivity</span>
|
||||
|
||||
<span class="meta-badge meta-badge-ok">Runs on Pi</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="app-screenshots" id="screenshots">
|
||||
<img src="/static/assets/bookstack-screenshot-1.webp" alt="" class="app-screenshot"
|
||||
onerror="this.style.display='none'">
|
||||
<img src="/static/assets/bookstack-screenshot-2.webp" alt="" class="app-screenshot"
|
||||
onerror="this.style.display='none'">
|
||||
<img src="/static/assets/bookstack-screenshot-3.webp" alt="" class="app-screenshot"
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="app-info-grid">
|
||||
|
||||
<div class="app-info-card">
|
||||
<h3>What is it for?</h3>
|
||||
<ul class="app-info-list">
|
||||
<li>A family knowledge base - recipes, manuals, house rules</li><li>Project documentation with a structure you can follow</li><li>Book > Chapter > Page, so a large set stays readable</li><li>Full text search and tags</li><li>A visual editor and a Markdown editor</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="app-info-card">
|
||||
<h3>First steps</h3>
|
||||
<ol class="app-info-list">
|
||||
<li>Open wiki.DOMAIN in your browser</li><li>Sign in: admin@admin.com / password</li><li>Change the admin password and e-mail address straight away</li><li>Create your first shelf and book</li><li>Start writing</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="app-info-card">
|
||||
<h3>Default sign-in</h3>
|
||||
<p class="app-info-creds">admin@admin.com / password</p>
|
||||
<p class="app-info-creds-warn">Change it right after your first sign-in!</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="app-info-card">
|
||||
<h3>Documentation</h3>
|
||||
<p><a href="https://www.bookstackapp.com/docs/" target="_blank" class="app-info-link">Official documentation ↗</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
var burger=document.querySelector('.nav-burger');
|
||||
var sidebar=document.getElementById('sidebar');
|
||||
var backdrop=document.querySelector('.nav-backdrop');
|
||||
if(!burger||!sidebar||!backdrop)return;
|
||||
function setOpen(open){
|
||||
sidebar.classList.toggle('is-open',open);
|
||||
document.body.classList.toggle('nav-open',open);
|
||||
backdrop.hidden=!open;
|
||||
burger.setAttribute('aria-expanded',open?'true':'false');
|
||||
}
|
||||
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
|
||||
backdrop.addEventListener('click',function(){setOpen(false);});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
|
||||
});
|
||||
})();
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
|
||||
var card = e.target.closest('[data-href]');
|
||||
if (card) window.location.href = card.dataset.href;
|
||||
});
|
||||
async function checkBeforeDeploy(e, name) {
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name);
|
||||
var data = await resp.json();
|
||||
if (data.ok && data.data && data.data.deployed) {
|
||||
e.preventDefault();
|
||||
showAlert('This app is already installed.');
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
} catch(err) {}
|
||||
return true;
|
||||
}
|
||||
async function syncTemplates() {
|
||||
const btn = document.getElementById('sync-btn');
|
||||
const toast = document.getElementById('sync-toast');
|
||||
if (!btn) return;
|
||||
const origText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '↻ Refreshing...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/sync', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (toast) {
|
||||
toast.textContent = data.ok ? (data.message || 'Templates refreshed') : ('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
|
||||
setTimeout(function() { window.location.reload(); }, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
if (toast) {
|
||||
toast.textContent = 'Network error: ' + err.message;
|
||||
toast.className = 'sync-toast sync-toast-err';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
}
|
||||
btn.innerHTML = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
async function stackAction(event, name, action) {
|
||||
const btn = event.currentTarget;
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'In progress...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/stacks/' + name + '/' + action, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (action === 'update') {
|
||||
followUpdate(name, btn);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
function followUpdate(name, btn) {
|
||||
var started = Date.now();
|
||||
var timer = setInterval(async function() {
|
||||
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/stacks/' + name);
|
||||
var d = await r.json();
|
||||
if (!d.ok || !d.data) return;
|
||||
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
|
||||
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
|
||||
} catch (e) {}
|
||||
}, 3000);
|
||||
}
|
||||
async function deleteOrphanStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'delete-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
|
||||
var data = await resp.json();
|
||||
var hddInfo = '';
|
||||
var checkboxHTML = '';
|
||||
if (data.ok && data.data && data.data.has_hdd_data) {
|
||||
hddInfo = '<div class="modal-hdd-info"><strong>Your data on the hard drive:</strong>';
|
||||
data.data.hdd_paths.forEach(function(p) {
|
||||
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
hddInfo += '</div>';
|
||||
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Delete your data from the hard drive</label>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Delete app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">This removes the containers, the volumes and the configuration files.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
hddInfo + checkboxHTML +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Delete</button>' +
|
||||
'</div>';
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeDeleteModal() {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmDelete(name) {
|
||||
var btn = document.getElementById('confirm-delete-btn');
|
||||
var checkbox = document.getElementById('delete-hdd-check');
|
||||
var removeHDD = checkbox ? checkbox.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Deleting...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name, {
|
||||
method: 'DELETE',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Deleted!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was deleted.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
}
|
||||
async function removeStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'remove-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var [hddResp, backupResp] = await Promise.all([
|
||||
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
|
||||
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
|
||||
]);
|
||||
var sections = '';
|
||||
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Always deleted:</strong>' +
|
||||
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
|
||||
'<li>Docker volumes (database, app configuration)</li>' +
|
||||
'<li>Install configuration (app.yaml)</li>' +
|
||||
'<li>Secondary backup schedule</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
var hddCheckbox = '';
|
||||
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
|
||||
var hddPaths = '';
|
||||
hddResp.data.hdd_paths.forEach(function(p) {
|
||||
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Your data on the hard drive:</strong>' + hddPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Delete your data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
|
||||
'If you reinstall the app, you need to import the data again, because the database is deleted. Data you keep is NOT backed up automatically any more.' +
|
||||
'</div></div>';
|
||||
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
|
||||
}
|
||||
|
||||
var backupCheckbox = '';
|
||||
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
|
||||
var bkPaths = '';
|
||||
backupResp.data.backup_paths.forEach(function(p) {
|
||||
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
|
||||
});
|
||||
if (bkPaths) {
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Backup data:</strong>' + bkPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Delete backup data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
|
||||
'The nightly restic snapshots cannot be deleted one by one — they expire automatically under the retention rules.' +
|
||||
'</div></div>';
|
||||
}
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Remove app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">The app goes back to "Not installed". The template stays, and you can install it again.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
sections +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeRemoveModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Remove</button>' +
|
||||
'</div>' + hddCheckbox;
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeRemoveModal() {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmRemoveStack(name) {
|
||||
var btn = document.getElementById('confirm-remove-btn');
|
||||
var hddCheck = document.getElementById('remove-hdd-check');
|
||||
var backupCheck = document.getElementById('remove-backup-check');
|
||||
var removeHDD = hddCheck ? hddCheck.checked : false;
|
||||
var removeBackups = backupCheck ? backupCheck.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Removing...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/remove', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted backups: ' + data.data.backup_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Backups not deleted: ' + data.data.backup_paths_refused.join('; ') + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Removed!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was removed. You can install it again with the Install button.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,594 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PrivateBin — Felhom.eu</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.258.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=0.258.0">
|
||||
<meta name="csrf-token" content="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df">
|
||||
<script>
|
||||
|
||||
|
||||
document.documentElement.className=document.documentElement.className.replace('no-js','js');
|
||||
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function felhomConfirm(el,question,onYes){
|
||||
if(!el||el.dataset.fcOpen)return;
|
||||
el.dataset.fcOpen='1';
|
||||
var wrap=document.createElement('span');wrap.className='inline-confirm';
|
||||
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
|
||||
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Yes';
|
||||
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Cancel';
|
||||
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
|
||||
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
|
||||
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
|
||||
no.addEventListener('click',close);
|
||||
yes.addEventListener('click',function(){close();onYes();});
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
|
||||
if(!btn)return;
|
||||
e.preventDefault();
|
||||
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
|
||||
var form=btn.closest('form');
|
||||
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
|
||||
if(!btn)return;
|
||||
var group=btn.closest('.nav-group');
|
||||
if(!group)return;
|
||||
var willOpen=!group.classList.contains('is-open');
|
||||
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
|
||||
g.classList.remove('is-open');
|
||||
var t=g.querySelector('.nav-group-toggle');
|
||||
if(t)t.setAttribute('aria-expanded','false');
|
||||
});
|
||||
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
|
||||
});
|
||||
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Message</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
|
||||
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
|
||||
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
|
||||
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
|
||||
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
|
||||
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
|
||||
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
|
||||
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
|
||||
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
|
||||
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
|
||||
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
|
||||
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
|
||||
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
|
||||
</svg>
|
||||
<header class="mobile-topbar">
|
||||
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu"></a>
|
||||
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
|
||||
<svg class="ico"><use href="#i-menu"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="nav-backdrop" hidden></div>
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu" class="sidebar-logo">
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Launcher</a></li>
|
||||
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Dashboard</a></li>
|
||||
<li><a href="/stacks" class="active"><svg class="ico"><use href="#i-cloud"/></svg>Apps</a></li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Storage<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/storage" class="">Drives</a></li>
|
||||
<li><a href="/storage/network" class="">Network storage</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Backup<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/backups" class="">Overview</a></li>
|
||||
<li><a href="/backups/remote" class="">Remote backup</a></li>
|
||||
<li><a href="/backups/apps" class="">Apps</a></li>
|
||||
<li><a href="/backups/restore" class="">Restore</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Sharing<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/sharing" class="">Network sharing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>System monitor</a></li>
|
||||
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="nav-group-label">Settings</div>
|
||||
<ul class="nav-links nav-links-sub">
|
||||
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>System</a></li>
|
||||
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Notifications</a></li>
|
||||
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Security and access</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.258.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df"><input type="hidden" name="back" value="/apps/privatebin"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df"><input type="hidden" name="back" value="/apps/privatebin"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item current" aria-current="true">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Sign out ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<div style="display:flex;align-items:center;gap:1rem">
|
||||
<a href="/stacks" class="btn btn-sm btn-outline">← Apps</a>
|
||||
<h2>PrivateBin</h2>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.5rem">
|
||||
|
||||
<span class="stack-state-badge state-run">Running</span>
|
||||
|
||||
|
||||
|
||||
<a href="https://paste.drill-en-0920.felhom.eu" target="_blank" class="btn btn-sm btn-outline">Open ↗</a>
|
||||
<a href="/stacks/privatebin/logs" class="btn btn-sm btn-outline">Log</a>
|
||||
|
||||
<a href="/stacks/privatebin/export" class="btn btn-sm btn-outline">Export</a>
|
||||
<a href="/stacks/privatebin/deploy" class="btn btn-sm btn-outline">Settings</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="app-info-hero">
|
||||
<img class="app-info-logo" src="/static/assets/privatebin-logo.svg"
|
||||
alt="PrivateBin" data-fallback="/static/app-placeholder.svg"
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
||||
<div class="app-info-hero-text">
|
||||
|
||||
<p class="app-info-tagline">Encrypted text sharing - the server never sees the content</p>
|
||||
|
||||
<div class="stack-meta-badges">
|
||||
<span class="meta-badge">~30M RAM</span>
|
||||
<span class="meta-badge">security</span>
|
||||
|
||||
<span class="meta-badge meta-badge-ok">Runs on Pi</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="app-screenshots" id="screenshots">
|
||||
<img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot"
|
||||
onerror="this.style.display='none'">
|
||||
<img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot"
|
||||
onerror="this.style.display='none'">
|
||||
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
|
||||
onerror="this.style.display='none'">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="app-info-grid">
|
||||
|
||||
<div class="app-info-card">
|
||||
<h3>What is it for?</h3>
|
||||
<ul class="app-info-list">
|
||||
<li>Share sensitive text safely</li><li>End-to-end encryption - the server cannot reach the content</li><li>You choose how long it lasts (5 minutes to 1 year, or never)</li><li>Optional: delete it automatically after it is read</li><li>Password protection for extra safety</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="app-info-card">
|
||||
<h3>First steps</h3>
|
||||
<ol class="app-info-list">
|
||||
<li>Open paste.DOMAIN in your browser</li><li>Type your text and select Send</li><li>Share the link you get - the encryption key is part of the URL</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="app-info-card">
|
||||
<h3>Documentation</h3>
|
||||
<p><a href="https://github.com/PrivateBin/PrivateBin/wiki" target="_blank" class="app-info-link">Official documentation ↗</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
var burger=document.querySelector('.nav-burger');
|
||||
var sidebar=document.getElementById('sidebar');
|
||||
var backdrop=document.querySelector('.nav-backdrop');
|
||||
if(!burger||!sidebar||!backdrop)return;
|
||||
function setOpen(open){
|
||||
sidebar.classList.toggle('is-open',open);
|
||||
document.body.classList.toggle('nav-open',open);
|
||||
backdrop.hidden=!open;
|
||||
burger.setAttribute('aria-expanded',open?'true':'false');
|
||||
}
|
||||
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
|
||||
backdrop.addEventListener('click',function(){setOpen(false);});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
|
||||
});
|
||||
})();
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
|
||||
var card = e.target.closest('[data-href]');
|
||||
if (card) window.location.href = card.dataset.href;
|
||||
});
|
||||
async function checkBeforeDeploy(e, name) {
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name);
|
||||
var data = await resp.json();
|
||||
if (data.ok && data.data && data.data.deployed) {
|
||||
e.preventDefault();
|
||||
showAlert('This app is already installed.');
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
} catch(err) {}
|
||||
return true;
|
||||
}
|
||||
async function syncTemplates() {
|
||||
const btn = document.getElementById('sync-btn');
|
||||
const toast = document.getElementById('sync-toast');
|
||||
if (!btn) return;
|
||||
const origText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '↻ Refreshing...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/sync', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (toast) {
|
||||
toast.textContent = data.ok ? (data.message || 'Templates refreshed') : ('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
|
||||
setTimeout(function() { window.location.reload(); }, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
if (toast) {
|
||||
toast.textContent = 'Network error: ' + err.message;
|
||||
toast.className = 'sync-toast sync-toast-err';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
}
|
||||
btn.innerHTML = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
async function stackAction(event, name, action) {
|
||||
const btn = event.currentTarget;
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'In progress...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/stacks/' + name + '/' + action, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (action === 'update') {
|
||||
followUpdate(name, btn);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
function followUpdate(name, btn) {
|
||||
var started = Date.now();
|
||||
var timer = setInterval(async function() {
|
||||
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/stacks/' + name);
|
||||
var d = await r.json();
|
||||
if (!d.ok || !d.data) return;
|
||||
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
|
||||
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
|
||||
} catch (e) {}
|
||||
}, 3000);
|
||||
}
|
||||
async function deleteOrphanStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'delete-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
|
||||
var data = await resp.json();
|
||||
var hddInfo = '';
|
||||
var checkboxHTML = '';
|
||||
if (data.ok && data.data && data.data.has_hdd_data) {
|
||||
hddInfo = '<div class="modal-hdd-info"><strong>Your data on the hard drive:</strong>';
|
||||
data.data.hdd_paths.forEach(function(p) {
|
||||
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
hddInfo += '</div>';
|
||||
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Delete your data from the hard drive</label>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Delete app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">This removes the containers, the volumes and the configuration files.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
hddInfo + checkboxHTML +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Delete</button>' +
|
||||
'</div>';
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeDeleteModal() {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmDelete(name) {
|
||||
var btn = document.getElementById('confirm-delete-btn');
|
||||
var checkbox = document.getElementById('delete-hdd-check');
|
||||
var removeHDD = checkbox ? checkbox.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Deleting...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name, {
|
||||
method: 'DELETE',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Deleted!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was deleted.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
}
|
||||
async function removeStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'remove-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var [hddResp, backupResp] = await Promise.all([
|
||||
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
|
||||
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
|
||||
]);
|
||||
var sections = '';
|
||||
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Always deleted:</strong>' +
|
||||
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
|
||||
'<li>Docker volumes (database, app configuration)</li>' +
|
||||
'<li>Install configuration (app.yaml)</li>' +
|
||||
'<li>Secondary backup schedule</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
var hddCheckbox = '';
|
||||
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
|
||||
var hddPaths = '';
|
||||
hddResp.data.hdd_paths.forEach(function(p) {
|
||||
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Your data on the hard drive:</strong>' + hddPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Delete your data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
|
||||
'If you reinstall the app, you need to import the data again, because the database is deleted. Data you keep is NOT backed up automatically any more.' +
|
||||
'</div></div>';
|
||||
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
|
||||
}
|
||||
|
||||
var backupCheckbox = '';
|
||||
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
|
||||
var bkPaths = '';
|
||||
backupResp.data.backup_paths.forEach(function(p) {
|
||||
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
|
||||
});
|
||||
if (bkPaths) {
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Backup data:</strong>' + bkPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Delete backup data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
|
||||
'The nightly restic snapshots cannot be deleted one by one — they expire automatically under the retention rules.' +
|
||||
'</div></div>';
|
||||
}
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Remove app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">The app goes back to "Not installed". The template stays, and you can install it again.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
sections +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeRemoveModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Remove</button>' +
|
||||
'</div>' + hddCheckbox;
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeRemoveModal() {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmRemoveStack(name) {
|
||||
var btn = document.getElementById('confirm-remove-btn');
|
||||
var hddCheck = document.getElementById('remove-hdd-check');
|
||||
var backupCheck = document.getElementById('remove-backup-check');
|
||||
var removeHDD = hddCheck ? hddCheck.checked : false;
|
||||
var removeBackups = backupCheck ? backupCheck.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Removing...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/remove', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted backups: ' + data.data.backup_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Backups not deleted: ' + data.data.backup_paths_refused.join('; ') + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Removed!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was removed. You can install it again with the Install button.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,738 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Backup — Felhom.eu</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.258.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=0.258.0">
|
||||
<meta name="csrf-token" content="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df">
|
||||
<script>
|
||||
|
||||
|
||||
document.documentElement.className=document.documentElement.className.replace('no-js','js');
|
||||
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function felhomConfirm(el,question,onYes){
|
||||
if(!el||el.dataset.fcOpen)return;
|
||||
el.dataset.fcOpen='1';
|
||||
var wrap=document.createElement('span');wrap.className='inline-confirm';
|
||||
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
|
||||
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Yes';
|
||||
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Cancel';
|
||||
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
|
||||
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
|
||||
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
|
||||
no.addEventListener('click',close);
|
||||
yes.addEventListener('click',function(){close();onYes();});
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
|
||||
if(!btn)return;
|
||||
e.preventDefault();
|
||||
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
|
||||
var form=btn.closest('form');
|
||||
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
|
||||
if(!btn)return;
|
||||
var group=btn.closest('.nav-group');
|
||||
if(!group)return;
|
||||
var willOpen=!group.classList.contains('is-open');
|
||||
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
|
||||
g.classList.remove('is-open');
|
||||
var t=g.querySelector('.nav-group-toggle');
|
||||
if(t)t.setAttribute('aria-expanded','false');
|
||||
});
|
||||
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
|
||||
});
|
||||
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Message</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
|
||||
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
|
||||
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
|
||||
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
|
||||
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
|
||||
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
|
||||
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
|
||||
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
|
||||
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
|
||||
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
|
||||
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
|
||||
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
|
||||
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
|
||||
</svg>
|
||||
<header class="mobile-topbar">
|
||||
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu"></a>
|
||||
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
|
||||
<svg class="ico"><use href="#i-menu"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="nav-backdrop" hidden></div>
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu" class="sidebar-logo">
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/launcher" class=""><svg class="ico"><use href="#i-rocket"/></svg>Launcher</a></li>
|
||||
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Dashboard</a></li>
|
||||
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Apps</a></li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Storage<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/storage" class="">Drives</a></li>
|
||||
<li><a href="/storage/network" class="">Network storage</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group is-open">
|
||||
<button type="button" class="nav-group-toggle active" aria-expanded="true" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Backup<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/backups" class="active">Overview</a></li>
|
||||
<li><a href="/backups/remote" class="">Remote backup</a></li>
|
||||
<li><a href="/backups/apps" class="">Apps</a></li>
|
||||
<li><a href="/backups/restore" class="">Restore</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Sharing<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/sharing" class="">Network sharing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>System monitor</a></li>
|
||||
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="nav-group-label">Settings</div>
|
||||
<ul class="nav-links nav-links-sub">
|
||||
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>System</a></li>
|
||||
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Notifications</a></li>
|
||||
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Security and access</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.258.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df"><input type="hidden" name="back" value="/backups"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df"><input type="hidden" name="back" value="/backups"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item current" aria-current="true">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Sign out ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Backup</h2>
|
||||
<span class="domain-badge">drill-en-0920.felhom.eu</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="alert alert-warning">Csak egy másolat készül (nincs második meghajtó) — a 3-2-1 mentéshez csatlakoztasson egy második meghajtót vagy offsite tárolót.</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="alert alert-warning" id="backup-target-alert">A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer — így hibás fájlok ellen véd, lemezhiba ellen nem. Csatlakoztass egy második meghajtót a teljes védelemhez.</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="backup-section-card">
|
||||
<h3>Storage overview</h3>
|
||||
<div class="storage-overview-grid">
|
||||
<div class="storage-bars">
|
||||
|
||||
<div class="storage-item meter nominal">
|
||||
<div class="storage-header">
|
||||
<span class="storage-label">System (/)</span>
|
||||
<span class="storage-value">1.95 GB / 68.7 GB (3%)</span>
|
||||
</div>
|
||||
<div class="meter-track">
|
||||
<div class="meter-fill" style="width:2.8%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="storage-stats">
|
||||
<div class="storage-stat-row">
|
||||
<span class="storage-stat-label">Database backups</span>
|
||||
<span class="storage-stat-value mono">–</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="backup-section-card">
|
||||
<h3>System backup (full backup)</h3>
|
||||
<p class="form-hint" style="margin-bottom:1rem">A regular backup of the whole server — apps, settings and databases together — that can bring back the entire device. The host agent makes and manages it.</p>
|
||||
|
||||
<div class="stats-grid backup-page-cards">
|
||||
<div class="stat-card stat-running">
|
||||
<div class="stat-value">✓</div>
|
||||
<div class="stat-label">Last full backup
|
||||
<br><span class="relative-time">2026-09-20 19:02 (2 minutes ago)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-total">
|
||||
<div class="stat-value">624.7 MB</div>
|
||||
<div class="stat-label">Helyi tároló (local)</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="font-size:1.15rem">Up to date</div>
|
||||
<div class="stat-label">Next backup
|
||||
<br><span class="relative-time">0 hours ago — within the backup window</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">–</div>
|
||||
<div class="stat-label">Restore checked
|
||||
<br><span class="relative-time">Not run yet</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="backup-tier-table" style="width:100%;margin-top:1rem;border-collapse:collapse">
|
||||
|
||||
<tr style="border-top:1px solid var(--border)">
|
||||
<td style="padding:.5rem .75rem .5rem 0;white-space:nowrap;vertical-align:top"><strong>Helyi tároló (local)</strong></td>
|
||||
<td style="padding:.5rem 0;vertical-align:top">
|
||||
|
||||
|
||||
<div>✓ Last successful backup: 2026-09-20 19:02 (2 minutes ago) · 624.7 MB
|
||||
<span class="tag tag-run" style="margin-left:.4rem">Up to date</span></div>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr style="border-top:1px solid var(--border)">
|
||||
<td style="padding:.5rem .75rem .5rem 0;white-space:nowrap;vertical-align:top"><strong>Biztonsági szerver – külön hardver (PBS)</strong></td>
|
||||
<td style="padding:.5rem 0;vertical-align:top">
|
||||
|
||||
<span class="tag"><span class="dot"></span>not set up</span>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<div class="schedule-actions" style="margin-top:1rem">
|
||||
<button class="btn btn-sm btn-primary" id="wg-backup-btn" onclick="triggerGuestBackup()" >Back up now</button>
|
||||
<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> Your apps stop during the backup — usually for a few minutes, longer with more data.</span>
|
||||
<div id="wg-backup-result" style="margin-top:.6rem"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="backup-section-card">
|
||||
<h3>Backup window</h3>
|
||||
<p class="form-hint" style="margin-bottom:1rem">The backups run one after the other: database backup, local copy, remote backup, then the full system backup.</p>
|
||||
<form method="POST" action="/backups/window" class="schedule-actions" style="display:flex;align-items:flex-end;gap:.75rem;flex-wrap:wrap">
|
||||
<input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df">
|
||||
<div class="form-group" style="margin:0">
|
||||
<label for="window_start">Backup window start</label>
|
||||
<input type="time" id="window_start" name="window_start" value="02:30" class="form-control" style="max-width:9rem" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Save</button>
|
||||
</form>
|
||||
<ul class="form-hint" style="margin-top:1rem;margin-bottom:0;list-style:none;padding:0;line-height:1.9">
|
||||
<li>Database backup: <strong>02:30</strong></li>
|
||||
<li>Local copy: <strong>03:30</strong></li>
|
||||
<li>Remote backup: <strong>04:15</strong></li>
|
||||
<li>Full system backup: about <strong>04:30–08:30</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stats-grid backup-page-cards">
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">–</div>
|
||||
<div class="stat-label">Data backup</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="stat-card" style="border-left-color: var(--text-3);">
|
||||
<div class="stat-value" style="background:var(--text-3);-webkit-background-clip:text;background-clip:text;">–</div>
|
||||
<div class="stat-label">Remote system backup<br><span class="relative-time">not set up</span></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="stat-card stat-total">
|
||||
|
||||
<div class="stat-value">–</div>
|
||||
<div class="stat-label">Databases backed up<br><span class="relative-time">embedded databases, inside the volume backup</span></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
function triggerGuestBackup() {
|
||||
var btn = document.getElementById('wg-backup-btn');
|
||||
felhomConfirm(btn, 'Start the full system backup now? Your apps stop during the backup — usually for a few minutes, longer with more data.', function () {
|
||||
var out = document.getElementById('wg-backup-result');
|
||||
btn.disabled = true;
|
||||
out.innerHTML = '<span class="form-hint">Starting the backup…</span>';
|
||||
fetch('/api/guest-backup/trigger', { method: 'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()) })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.ok) { out.innerHTML = '<div class="alert alert-error">' + (data.error || 'Error') + '</div>'; btn.disabled = false; return; }
|
||||
out.innerHTML = '<span class="form-hint">Backup in progress…</span>';
|
||||
pollGuestBackup(out, btn);
|
||||
})
|
||||
.catch(e => { out.innerHTML = '<div class="alert alert-error">Error: ' + e + '</div>'; btn.disabled = false; });
|
||||
});
|
||||
}
|
||||
|
||||
function pollGuestBackup(out, btn) {
|
||||
var tries = 0;
|
||||
var iv = setInterval(function () {
|
||||
tries++;
|
||||
fetch('/api/guest-backup/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok && data.data) {
|
||||
var ph = data.data.phase;
|
||||
out.innerHTML = '<span class="form-hint">Status: ' + ph + '</span>';
|
||||
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">The system backup is done.</div>'; setTimeout(() => window.location.reload(), 1500); }
|
||||
else if (ph === 'failed') { clearInterval(iv); out.innerHTML = '<div class="alert alert-error">The backup failed.</div>'; btn.disabled = false; }
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
if (tries > 180) { clearInterval(iv); btn.disabled = false; }
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
(function () {
|
||||
var btn = document.getElementById('backup-target-assign');
|
||||
if (!btn) { return; }
|
||||
btn.addEventListener('click', function () {
|
||||
var out = document.getElementById('backup-target-assign-result');
|
||||
btn.disabled = true;
|
||||
out.innerHTML = '<p class="form-hint">Assigning the drive…</p>';
|
||||
fetch('/api/storage/backup-target/assign', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()),
|
||||
body: JSON.stringify({ path: btn.getAttribute('data-path') })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) {
|
||||
if (!j.ok) {
|
||||
out.innerHTML = '<div class="alert alert-error">The drive could not be assigned: ' + (j.error || '') + '</div>';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (j.data && j.data.restart_required) {
|
||||
out.innerHTML = '<div class="flash flash-success">The drive is assigned. The setting takes effect after the host agent next restarts.</div>';
|
||||
} else {
|
||||
out.innerHTML = '<div class="flash flash-success">The drive is assigned.</div>';
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
out.innerHTML = '<div class="alert alert-error">Error: ' + e.message + '</div>';
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
var burger=document.querySelector('.nav-burger');
|
||||
var sidebar=document.getElementById('sidebar');
|
||||
var backdrop=document.querySelector('.nav-backdrop');
|
||||
if(!burger||!sidebar||!backdrop)return;
|
||||
function setOpen(open){
|
||||
sidebar.classList.toggle('is-open',open);
|
||||
document.body.classList.toggle('nav-open',open);
|
||||
backdrop.hidden=!open;
|
||||
burger.setAttribute('aria-expanded',open?'true':'false');
|
||||
}
|
||||
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
|
||||
backdrop.addEventListener('click',function(){setOpen(false);});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
|
||||
});
|
||||
})();
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
|
||||
var card = e.target.closest('[data-href]');
|
||||
if (card) window.location.href = card.dataset.href;
|
||||
});
|
||||
async function checkBeforeDeploy(e, name) {
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name);
|
||||
var data = await resp.json();
|
||||
if (data.ok && data.data && data.data.deployed) {
|
||||
e.preventDefault();
|
||||
showAlert('This app is already installed.');
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
} catch(err) {}
|
||||
return true;
|
||||
}
|
||||
async function syncTemplates() {
|
||||
const btn = document.getElementById('sync-btn');
|
||||
const toast = document.getElementById('sync-toast');
|
||||
if (!btn) return;
|
||||
const origText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '↻ Refreshing...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/sync', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (toast) {
|
||||
toast.textContent = data.ok ? (data.message || 'Templates refreshed') : ('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
|
||||
setTimeout(function() { window.location.reload(); }, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
if (toast) {
|
||||
toast.textContent = 'Network error: ' + err.message;
|
||||
toast.className = 'sync-toast sync-toast-err';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
}
|
||||
btn.innerHTML = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
async function stackAction(event, name, action) {
|
||||
const btn = event.currentTarget;
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'In progress...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/stacks/' + name + '/' + action, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (action === 'update') {
|
||||
followUpdate(name, btn);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
function followUpdate(name, btn) {
|
||||
var started = Date.now();
|
||||
var timer = setInterval(async function() {
|
||||
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/stacks/' + name);
|
||||
var d = await r.json();
|
||||
if (!d.ok || !d.data) return;
|
||||
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
|
||||
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
|
||||
} catch (e) {}
|
||||
}, 3000);
|
||||
}
|
||||
async function deleteOrphanStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'delete-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
|
||||
var data = await resp.json();
|
||||
var hddInfo = '';
|
||||
var checkboxHTML = '';
|
||||
if (data.ok && data.data && data.data.has_hdd_data) {
|
||||
hddInfo = '<div class="modal-hdd-info"><strong>Your data on the hard drive:</strong>';
|
||||
data.data.hdd_paths.forEach(function(p) {
|
||||
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
hddInfo += '</div>';
|
||||
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Delete your data from the hard drive</label>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Delete app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">This removes the containers, the volumes and the configuration files.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
hddInfo + checkboxHTML +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Delete</button>' +
|
||||
'</div>';
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeDeleteModal() {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmDelete(name) {
|
||||
var btn = document.getElementById('confirm-delete-btn');
|
||||
var checkbox = document.getElementById('delete-hdd-check');
|
||||
var removeHDD = checkbox ? checkbox.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Deleting...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name, {
|
||||
method: 'DELETE',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Deleted!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was deleted.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
}
|
||||
async function removeStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'remove-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var [hddResp, backupResp] = await Promise.all([
|
||||
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
|
||||
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
|
||||
]);
|
||||
var sections = '';
|
||||
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Always deleted:</strong>' +
|
||||
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
|
||||
'<li>Docker volumes (database, app configuration)</li>' +
|
||||
'<li>Install configuration (app.yaml)</li>' +
|
||||
'<li>Secondary backup schedule</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
var hddCheckbox = '';
|
||||
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
|
||||
var hddPaths = '';
|
||||
hddResp.data.hdd_paths.forEach(function(p) {
|
||||
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Your data on the hard drive:</strong>' + hddPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Delete your data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
|
||||
'If you reinstall the app, you need to import the data again, because the database is deleted. Data you keep is NOT backed up automatically any more.' +
|
||||
'</div></div>';
|
||||
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
|
||||
}
|
||||
|
||||
var backupCheckbox = '';
|
||||
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
|
||||
var bkPaths = '';
|
||||
backupResp.data.backup_paths.forEach(function(p) {
|
||||
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
|
||||
});
|
||||
if (bkPaths) {
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Backup data:</strong>' + bkPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Delete backup data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
|
||||
'The nightly restic snapshots cannot be deleted one by one — they expire automatically under the retention rules.' +
|
||||
'</div></div>';
|
||||
}
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Remove app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">The app goes back to "Not installed". The template stays, and you can install it again.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
sections +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeRemoveModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Remove</button>' +
|
||||
'</div>' + hddCheckbox;
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeRemoveModal() {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmRemoveStack(name) {
|
||||
var btn = document.getElementById('confirm-remove-btn');
|
||||
var hddCheck = document.getElementById('remove-hdd-check');
|
||||
var backupCheck = document.getElementById('remove-backup-check');
|
||||
var removeHDD = hddCheck ? hddCheck.checked : false;
|
||||
var removeBackups = backupCheck ? backupCheck.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Removing...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/remove', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted backups: ' + data.data.backup_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Backups not deleted: ' + data.data.backup_paths_refused.join('; ') + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Removed!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was removed. You can install it again with the Install button.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Launcher — Felhom.eu</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.258.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=0.258.0">
|
||||
<meta name="csrf-token" content="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df">
|
||||
<script>
|
||||
|
||||
|
||||
document.documentElement.className=document.documentElement.className.replace('no-js','js');
|
||||
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function felhomConfirm(el,question,onYes){
|
||||
if(!el||el.dataset.fcOpen)return;
|
||||
el.dataset.fcOpen='1';
|
||||
var wrap=document.createElement('span');wrap.className='inline-confirm';
|
||||
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
|
||||
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Yes';
|
||||
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Cancel';
|
||||
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
|
||||
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
|
||||
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
|
||||
no.addEventListener('click',close);
|
||||
yes.addEventListener('click',function(){close();onYes();});
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
|
||||
if(!btn)return;
|
||||
e.preventDefault();
|
||||
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
|
||||
var form=btn.closest('form');
|
||||
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
|
||||
if(!btn)return;
|
||||
var group=btn.closest('.nav-group');
|
||||
if(!group)return;
|
||||
var willOpen=!group.classList.contains('is-open');
|
||||
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
|
||||
g.classList.remove('is-open');
|
||||
var t=g.querySelector('.nav-group-toggle');
|
||||
if(t)t.setAttribute('aria-expanded','false');
|
||||
});
|
||||
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
|
||||
});
|
||||
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Message</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
|
||||
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
|
||||
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
|
||||
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
|
||||
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
|
||||
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
|
||||
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
|
||||
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
|
||||
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
|
||||
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
|
||||
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
|
||||
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
|
||||
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
|
||||
</svg>
|
||||
<header class="mobile-topbar">
|
||||
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu"></a>
|
||||
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
|
||||
<svg class="ico"><use href="#i-menu"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="nav-backdrop" hidden></div>
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu" class="sidebar-logo">
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/launcher" class="active"><svg class="ico"><use href="#i-rocket"/></svg>Launcher</a></li>
|
||||
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Dashboard</a></li>
|
||||
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Apps</a></li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Storage<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/storage" class="">Drives</a></li>
|
||||
<li><a href="/storage/network" class="">Network storage</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Backup<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/backups" class="">Overview</a></li>
|
||||
<li><a href="/backups/remote" class="">Remote backup</a></li>
|
||||
<li><a href="/backups/apps" class="">Apps</a></li>
|
||||
<li><a href="/backups/restore" class="">Restore</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Sharing<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/sharing" class="">Network sharing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>System monitor</a></li>
|
||||
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="nav-group-label">Settings</div>
|
||||
<ul class="nav-links nav-links-sub">
|
||||
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>System</a></li>
|
||||
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Notifications</a></li>
|
||||
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Security and access</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.258.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df"><input type="hidden" name="back" value="/launcher"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df"><input type="hidden" name="back" value="/launcher"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item current" aria-current="true">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Sign out ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Launcher</h2>
|
||||
<span class="domain-badge">drill-en-0920.felhom.eu</span>
|
||||
<button type="button" class="btn btn-outline share-open-btn" onclick="openShareModal()"><svg class="ico"><use href="#i-share"/></svg>Share the launcher</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="launch-grid">
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://wiki.drill-en-0920.felhom.eu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(112, 45%, 38%)">
|
||||
<span class="launch-mono">B</span>
|
||||
<img class="launch-logo" src="/static/assets/bookstack-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/bookstack-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">BookStack</span>
|
||||
<span class="tag tag-warn"><span class="dot"></span>Partly stopped</span>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://files.drill-en-0920.felhom.eu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(217, 45%, 38%)">
|
||||
<span class="launch-mono">F</span>
|
||||
<img class="launch-logo" src="/static/assets/filebrowser-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/filebrowser-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Filebrowser</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://paste.drill-en-0920.felhom.eu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(125, 45%, 38%)">
|
||||
<span class="launch-mono">P</span>
|
||||
<img class="launch-logo" src="/static/assets/privatebin-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">PrivateBin</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-overlay" id="share-modal" style="display:none">
|
||||
<div class="modal-card">
|
||||
<h3>Share the launcher</h3>
|
||||
<p class="share-lede">A link you can share. It only shows your apps and opens them in a new tab. No account and no admin access: every app stays behind its own sign-in.</p>
|
||||
|
||||
<p class="share-note">Sharing is off.</p>
|
||||
<form method="POST" action="/launcher/share/enable">
|
||||
<input type="hidden" name="_csrf" value="611cf996d4b3aefae766d8471194dfb9af88c7e88195a94f112157d6524a61df">
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeShareModal()">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Turn on sharing</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openShareModal(){var m=document.getElementById('share-modal');if(m)m.style.display='flex';}
|
||||
function closeShareModal(){var m=document.getElementById('share-modal');if(m)m.style.display='none';}
|
||||
function copyShareLink(){
|
||||
var inp=document.getElementById('share-link-input');
|
||||
var btn=document.getElementById('share-copy-btn');
|
||||
if(!inp)return;
|
||||
function done(){if(btn){var o=btn.textContent;btn.textContent='Copied';setTimeout(function(){btn.textContent=o;},1500);}}
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(inp.value).then(done,function(){inp.select();done();});}
|
||||
else{inp.select();try{document.execCommand('copy');}catch(e){}done();}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function nativeShare(){
|
||||
var inp=document.getElementById('share-link-input');
|
||||
if(!inp||!navigator.share)return;
|
||||
navigator.share({title:'Launcher — drill-en-0920.felhom.eu',text:'Your home apps in one place.',url:inp.value})
|
||||
.catch(function(e){if(e&&e.name==='AbortError')return;copyShareLink();});
|
||||
}
|
||||
(function(){
|
||||
var m=document.getElementById('share-modal');
|
||||
if(m){m.addEventListener('click',function(e){if(e.target===m)closeShareModal();});}
|
||||
if(navigator.share){var nb=document.getElementById('share-native-btn');if(nb)nb.style.display='';}
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
var burger=document.querySelector('.nav-burger');
|
||||
var sidebar=document.getElementById('sidebar');
|
||||
var backdrop=document.querySelector('.nav-backdrop');
|
||||
if(!burger||!sidebar||!backdrop)return;
|
||||
function setOpen(open){
|
||||
sidebar.classList.toggle('is-open',open);
|
||||
document.body.classList.toggle('nav-open',open);
|
||||
backdrop.hidden=!open;
|
||||
burger.setAttribute('aria-expanded',open?'true':'false');
|
||||
}
|
||||
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
|
||||
backdrop.addEventListener('click',function(){setOpen(false);});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
|
||||
});
|
||||
})();
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
|
||||
var card = e.target.closest('[data-href]');
|
||||
if (card) window.location.href = card.dataset.href;
|
||||
});
|
||||
async function checkBeforeDeploy(e, name) {
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name);
|
||||
var data = await resp.json();
|
||||
if (data.ok && data.data && data.data.deployed) {
|
||||
e.preventDefault();
|
||||
showAlert('This app is already installed.');
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
} catch(err) {}
|
||||
return true;
|
||||
}
|
||||
async function syncTemplates() {
|
||||
const btn = document.getElementById('sync-btn');
|
||||
const toast = document.getElementById('sync-toast');
|
||||
if (!btn) return;
|
||||
const origText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '↻ Refreshing...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/sync', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (toast) {
|
||||
toast.textContent = data.ok ? (data.message || 'Templates refreshed') : ('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
|
||||
setTimeout(function() { window.location.reload(); }, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
if (toast) {
|
||||
toast.textContent = 'Network error: ' + err.message;
|
||||
toast.className = 'sync-toast sync-toast-err';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
}
|
||||
btn.innerHTML = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
async function stackAction(event, name, action) {
|
||||
const btn = event.currentTarget;
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'In progress...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/stacks/' + name + '/' + action, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (action === 'update') {
|
||||
followUpdate(name, btn);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
function followUpdate(name, btn) {
|
||||
var started = Date.now();
|
||||
var timer = setInterval(async function() {
|
||||
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/stacks/' + name);
|
||||
var d = await r.json();
|
||||
if (!d.ok || !d.data) return;
|
||||
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
|
||||
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
|
||||
} catch (e) {}
|
||||
}, 3000);
|
||||
}
|
||||
async function deleteOrphanStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'delete-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
|
||||
var data = await resp.json();
|
||||
var hddInfo = '';
|
||||
var checkboxHTML = '';
|
||||
if (data.ok && data.data && data.data.has_hdd_data) {
|
||||
hddInfo = '<div class="modal-hdd-info"><strong>Your data on the hard drive:</strong>';
|
||||
data.data.hdd_paths.forEach(function(p) {
|
||||
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
hddInfo += '</div>';
|
||||
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Delete your data from the hard drive</label>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Delete app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">This removes the containers, the volumes and the configuration files.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
hddInfo + checkboxHTML +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Delete</button>' +
|
||||
'</div>';
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeDeleteModal() {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmDelete(name) {
|
||||
var btn = document.getElementById('confirm-delete-btn');
|
||||
var checkbox = document.getElementById('delete-hdd-check');
|
||||
var removeHDD = checkbox ? checkbox.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Deleting...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name, {
|
||||
method: 'DELETE',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Deleted!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was deleted.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
}
|
||||
async function removeStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'remove-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var [hddResp, backupResp] = await Promise.all([
|
||||
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
|
||||
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
|
||||
]);
|
||||
var sections = '';
|
||||
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Always deleted:</strong>' +
|
||||
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
|
||||
'<li>Docker volumes (database, app configuration)</li>' +
|
||||
'<li>Install configuration (app.yaml)</li>' +
|
||||
'<li>Secondary backup schedule</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
var hddCheckbox = '';
|
||||
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
|
||||
var hddPaths = '';
|
||||
hddResp.data.hdd_paths.forEach(function(p) {
|
||||
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Your data on the hard drive:</strong>' + hddPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Delete your data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
|
||||
'If you reinstall the app, you need to import the data again, because the database is deleted. Data you keep is NOT backed up automatically any more.' +
|
||||
'</div></div>';
|
||||
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
|
||||
}
|
||||
|
||||
var backupCheckbox = '';
|
||||
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
|
||||
var bkPaths = '';
|
||||
backupResp.data.backup_paths.forEach(function(p) {
|
||||
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
|
||||
});
|
||||
if (bkPaths) {
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Backup data:</strong>' + bkPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Delete backup data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
|
||||
'The nightly restic snapshots cannot be deleted one by one — they expire automatically under the retention rules.' +
|
||||
'</div></div>';
|
||||
}
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Remove app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">The app goes back to "Not installed". The template stays, and you can install it again.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
sections +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeRemoveModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Remove</button>' +
|
||||
'</div>' + hddCheckbox;
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeRemoveModal() {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmRemoveStack(name) {
|
||||
var btn = document.getElementById('confirm-remove-btn');
|
||||
var hddCheck = document.getElementById('remove-hdd-check');
|
||||
var backupCheck = document.getElementById('remove-backup-check');
|
||||
var removeHDD = hddCheck ? hddCheck.checked : false;
|
||||
var removeBackups = backupCheck ? backupCheck.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Removing...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/remove', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted backups: ' + data.data.backup_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Backups not deleted: ' + data.data.backup_paths_refused.join('; ') + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Removed!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was removed. You can install it again with the Install button.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,712 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Launcher — Felhom.eu</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.258.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=0.258.0">
|
||||
<meta name="csrf-token" content="c4c59641559c686a99e86783d9c5823dcb1d6a8e610840df223e5da04f4e12bb">
|
||||
<script>
|
||||
|
||||
|
||||
document.documentElement.className=document.documentElement.className.replace('no-js','js');
|
||||
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function felhomConfirm(el,question,onYes){
|
||||
if(!el||el.dataset.fcOpen)return;
|
||||
el.dataset.fcOpen='1';
|
||||
var wrap=document.createElement('span');wrap.className='inline-confirm';
|
||||
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
|
||||
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Yes';
|
||||
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Cancel';
|
||||
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
|
||||
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
|
||||
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
|
||||
no.addEventListener('click',close);
|
||||
yes.addEventListener('click',function(){close();onYes();});
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
|
||||
if(!btn)return;
|
||||
e.preventDefault();
|
||||
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
|
||||
var form=btn.closest('form');
|
||||
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
|
||||
if(!btn)return;
|
||||
var group=btn.closest('.nav-group');
|
||||
if(!group)return;
|
||||
var willOpen=!group.classList.contains('is-open');
|
||||
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
|
||||
g.classList.remove('is-open');
|
||||
var t=g.querySelector('.nav-group-toggle');
|
||||
if(t)t.setAttribute('aria-expanded','false');
|
||||
});
|
||||
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
|
||||
});
|
||||
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Message</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
|
||||
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
|
||||
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
|
||||
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
|
||||
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
|
||||
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
|
||||
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
|
||||
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
|
||||
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
|
||||
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
|
||||
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
|
||||
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
|
||||
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
|
||||
</svg>
|
||||
<header class="mobile-topbar">
|
||||
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu"></a>
|
||||
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menu">
|
||||
<svg class="ico"><use href="#i-menu"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="nav-backdrop" hidden></div>
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu" class="sidebar-logo">
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/launcher" class="active"><svg class="ico"><use href="#i-rocket"/></svg>Launcher</a></li>
|
||||
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Dashboard</a></li>
|
||||
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Apps</a></li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Storage<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/storage" class="">Drives</a></li>
|
||||
<li><a href="/storage/network" class="">Network storage</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Backup<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/backups" class="">Overview</a></li>
|
||||
<li><a href="/backups/remote" class="">Remote backup</a></li>
|
||||
<li><a href="/backups/apps" class="">Apps</a></li>
|
||||
<li><a href="/backups/restore" class="">Restore</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Sharing<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/sharing" class="">Network sharing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>System monitor</a></li>
|
||||
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="nav-group-label">Settings</div>
|
||||
<ul class="nav-links nav-links-sub">
|
||||
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>System</a></li>
|
||||
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Notifications</a></li>
|
||||
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Security and access</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.258.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Language" title="Language"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="c4c59641559c686a99e86783d9c5823dcb1d6a8e610840df223e5da04f4e12bb"><input type="hidden" name="back" value="/launcher"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="c4c59641559c686a99e86783d9c5823dcb1d6a8e610840df223e5da04f4e12bb"><input type="hidden" name="back" value="/launcher"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item current" aria-current="true">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Sign out ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Launcher</h2>
|
||||
<span class="domain-badge">enkisfelhom.hu</span>
|
||||
<button type="button" class="btn btn-outline share-open-btn" onclick="openShareModal()"><svg class="ico"><use href="#i-share"/></svg>Share the launcher</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="launch-grid">
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://travel.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(155, 45%, 38%)">
|
||||
<span class="launch-mono">A</span>
|
||||
<img class="launch-logo" src="/static/assets/adventurelog-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/adventurelog-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">AdventureLog</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://pdf.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(287, 45%, 38%)">
|
||||
<span class="launch-mono">B</span>
|
||||
<img class="launch-logo" src="/static/assets/bentopdf-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/bentopdf-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">BentoPDF</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://wiki.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(112, 45%, 38%)">
|
||||
<span class="launch-mono">B</span>
|
||||
<img class="launch-logo" src="/static/assets/bookstack-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/bookstack-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">BookStack</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://books.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(274, 45%, 38%)">
|
||||
<span class="launch-mono">C</span>
|
||||
<img class="launch-logo" src="/static/assets/calibre-web-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/calibre-web-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Calibre-Web Automated</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://docs.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(234, 45%, 38%)">
|
||||
<span class="launch-mono">D</span>
|
||||
<img class="launch-logo" src="/static/assets/docmost-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/docmost-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Docmost</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://files.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(217, 45%, 38%)">
|
||||
<span class="launch-mono">F</span>
|
||||
<img class="launch-logo" src="/static/assets/filebrowser-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/filebrowser-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Filebrowser</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://time.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(346, 45%, 38%)">
|
||||
<span class="launch-mono">K</span>
|
||||
<img class="launch-logo" src="/static/assets/kimai-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/kimai-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Kimai</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://gist.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(142, 45%, 38%)">
|
||||
<span class="launch-mono">O</span>
|
||||
<img class="launch-logo" src="/static/assets/opengist-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">OpenGist</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://papir.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(328, 45%, 38%)">
|
||||
<span class="launch-mono">P</span>
|
||||
<img class="launch-logo" src="/static/assets/paperless-ngx-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/paperless-ngx-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Paperless-ngx</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://paste.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(125, 45%, 38%)">
|
||||
<span class="launch-mono">P</span>
|
||||
<img class="launch-logo" src="/static/assets/privatebin-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">PrivateBin</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://jatek.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(44, 45%, 38%)">
|
||||
<span class="launch-mono">R</span>
|
||||
<img class="launch-logo" src="/static/assets/romm-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/romm-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">RomM</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-overlay" id="share-modal" style="display:none">
|
||||
<div class="modal-card">
|
||||
<h3>Share the launcher</h3>
|
||||
<p class="share-lede">A link you can share. It only shows your apps and opens them in a new tab. No account and no admin access: every app stays behind its own sign-in.</p>
|
||||
|
||||
<p class="share-note">Sharing is off.</p>
|
||||
<form method="POST" action="/launcher/share/enable">
|
||||
<input type="hidden" name="_csrf" value="c4c59641559c686a99e86783d9c5823dcb1d6a8e610840df223e5da04f4e12bb">
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeShareModal()">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Turn on sharing</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openShareModal(){var m=document.getElementById('share-modal');if(m)m.style.display='flex';}
|
||||
function closeShareModal(){var m=document.getElementById('share-modal');if(m)m.style.display='none';}
|
||||
function copyShareLink(){
|
||||
var inp=document.getElementById('share-link-input');
|
||||
var btn=document.getElementById('share-copy-btn');
|
||||
if(!inp)return;
|
||||
function done(){if(btn){var o=btn.textContent;btn.textContent='Copied';setTimeout(function(){btn.textContent=o;},1500);}}
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(inp.value).then(done,function(){inp.select();done();});}
|
||||
else{inp.select();try{document.execCommand('copy');}catch(e){}done();}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function nativeShare(){
|
||||
var inp=document.getElementById('share-link-input');
|
||||
if(!inp||!navigator.share)return;
|
||||
navigator.share({title:'Launcher — enkisfelhom.hu',text:'Your home apps in one place.',url:inp.value})
|
||||
.catch(function(e){if(e&&e.name==='AbortError')return;copyShareLink();});
|
||||
}
|
||||
(function(){
|
||||
var m=document.getElementById('share-modal');
|
||||
if(m){m.addEventListener('click',function(e){if(e.target===m)closeShareModal();});}
|
||||
if(navigator.share){var nb=document.getElementById('share-native-btn');if(nb)nb.style.display='';}
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
var burger=document.querySelector('.nav-burger');
|
||||
var sidebar=document.getElementById('sidebar');
|
||||
var backdrop=document.querySelector('.nav-backdrop');
|
||||
if(!burger||!sidebar||!backdrop)return;
|
||||
function setOpen(open){
|
||||
sidebar.classList.toggle('is-open',open);
|
||||
document.body.classList.toggle('nav-open',open);
|
||||
backdrop.hidden=!open;
|
||||
burger.setAttribute('aria-expanded',open?'true':'false');
|
||||
}
|
||||
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
|
||||
backdrop.addEventListener('click',function(){setOpen(false);});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
|
||||
});
|
||||
})();
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
|
||||
var card = e.target.closest('[data-href]');
|
||||
if (card) window.location.href = card.dataset.href;
|
||||
});
|
||||
async function checkBeforeDeploy(e, name) {
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name);
|
||||
var data = await resp.json();
|
||||
if (data.ok && data.data && data.data.deployed) {
|
||||
e.preventDefault();
|
||||
showAlert('This app is already installed.');
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
} catch(err) {}
|
||||
return true;
|
||||
}
|
||||
async function syncTemplates() {
|
||||
const btn = document.getElementById('sync-btn');
|
||||
const toast = document.getElementById('sync-toast');
|
||||
if (!btn) return;
|
||||
const origText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '↻ Refreshing...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/sync', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (toast) {
|
||||
toast.textContent = data.ok ? (data.message || 'Templates refreshed') : ('Error: ' + (data.error || 'Unknown error'));
|
||||
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
|
||||
setTimeout(function() { window.location.reload(); }, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
if (toast) {
|
||||
toast.textContent = 'Network error: ' + err.message;
|
||||
toast.className = 'sync-toast sync-toast-err';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
}
|
||||
btn.innerHTML = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
async function stackAction(event, name, action) {
|
||||
const btn = event.currentTarget;
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'In progress...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/stacks/' + name + '/' + action, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (action === 'update') {
|
||||
followUpdate(name, btn);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
function followUpdate(name, btn) {
|
||||
var started = Date.now();
|
||||
var timer = setInterval(async function() {
|
||||
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/stacks/' + name);
|
||||
var d = await r.json();
|
||||
if (!d.ok || !d.data) return;
|
||||
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
|
||||
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
|
||||
} catch (e) {}
|
||||
}, 3000);
|
||||
}
|
||||
async function deleteOrphanStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'delete-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
|
||||
var data = await resp.json();
|
||||
var hddInfo = '';
|
||||
var checkboxHTML = '';
|
||||
if (data.ok && data.data && data.data.has_hdd_data) {
|
||||
hddInfo = '<div class="modal-hdd-info"><strong>Your data on the hard drive:</strong>';
|
||||
data.data.hdd_paths.forEach(function(p) {
|
||||
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
hddInfo += '</div>';
|
||||
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Delete your data from the hard drive</label>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Delete app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">This removes the containers, the volumes and the configuration files.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
hddInfo + checkboxHTML +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeDeleteModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Delete</button>' +
|
||||
'</div>';
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeDeleteModal() {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmDelete(name) {
|
||||
var btn = document.getElementById('confirm-delete-btn');
|
||||
var checkbox = document.getElementById('delete-hdd-check');
|
||||
var removeHDD = checkbox ? checkbox.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Deleting...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name, {
|
||||
method: 'DELETE',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Deleted!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was deleted.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
}
|
||||
async function removeStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'remove-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Loading...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var [hddResp, backupResp] = await Promise.all([
|
||||
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
|
||||
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
|
||||
]);
|
||||
var sections = '';
|
||||
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Always deleted:</strong>' +
|
||||
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
|
||||
'<li>Docker volumes (database, app configuration)</li>' +
|
||||
'<li>Install configuration (app.yaml)</li>' +
|
||||
'<li>Secondary backup schedule</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
var hddCheckbox = '';
|
||||
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
|
||||
var hddPaths = '';
|
||||
hddResp.data.hdd_paths.forEach(function(p) {
|
||||
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'does not exist') + ')</div>';
|
||||
});
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Your data on the hard drive:</strong>' + hddPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Delete your data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
|
||||
'If you reinstall the app, you need to import the data again, because the database is deleted. Data you keep is NOT backed up automatically any more.' +
|
||||
'</div></div>';
|
||||
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
|
||||
}
|
||||
|
||||
var backupCheckbox = '';
|
||||
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
|
||||
var bkPaths = '';
|
||||
backupResp.data.backup_paths.forEach(function(p) {
|
||||
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
|
||||
});
|
||||
if (bkPaths) {
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Backup data:</strong>' + bkPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Delete backup data</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
|
||||
'The nightly restic snapshots cannot be deleted one by one — they expire automatically under the retention rules.' +
|
||||
'</div></div>';
|
||||
}
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Remove app: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">The app goes back to "Not installed". The template stays, and you can install it again.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">You cannot undo this!</div>' +
|
||||
sections +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeRemoveModal()">Cancel</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Remove</button>' +
|
||||
'</div>' + hddCheckbox;
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Error</h3><p style="color:var(--text-2)">Could not load the details: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Close</button></div>';
|
||||
}
|
||||
}
|
||||
function closeRemoveModal() {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmRemoveStack(name) {
|
||||
var btn = document.getElementById('confirm-remove-btn');
|
||||
var hddCheck = document.getElementById('remove-hdd-check');
|
||||
var backupCheck = document.getElementById('remove-backup-check');
|
||||
var removeHDD = hddCheck ? hddCheck.checked : false;
|
||||
var removeBackups = backupCheck ? backupCheck.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Removing...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/remove', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted data: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Deleted backups: ' + data.data.backup_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Kept data: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Backups not deleted: ' + data.data.backup_paths_refused.join('; ') + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Removed!</h3>' +
|
||||
'<p style="color:var(--text-2)">The app (' + name + ') was removed. You can install it again with the Install button.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Close</button></div>';
|
||||
} else {
|
||||
showAlert('Error: ' + (data.error || 'Unknown error'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Remove';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,712 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="hu" class="no-js">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Indítópult — Felhom.eu</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v=0.258.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=0.258.0">
|
||||
<meta name="csrf-token" content="c4c59641559c686a99e86783d9c5823dcb1d6a8e610840df223e5da04f4e12bb">
|
||||
<script>
|
||||
|
||||
|
||||
document.documentElement.className=document.documentElement.className.replace('no-js','js');
|
||||
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function felhomConfirm(el,question,onYes){
|
||||
if(!el||el.dataset.fcOpen)return;
|
||||
el.dataset.fcOpen='1';
|
||||
var wrap=document.createElement('span');wrap.className='inline-confirm';
|
||||
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
|
||||
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
|
||||
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
|
||||
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
|
||||
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
|
||||
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
|
||||
no.addEventListener('click',close);
|
||||
yes.addEventListener('click',function(){close();onYes();});
|
||||
}
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
|
||||
if(!btn)return;
|
||||
e.preventDefault();
|
||||
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
|
||||
var form=btn.closest('form');
|
||||
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click',function(e){
|
||||
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
|
||||
if(!btn)return;
|
||||
var group=btn.closest('.nav-group');
|
||||
if(!group)return;
|
||||
var willOpen=!group.classList.contains('is-open');
|
||||
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
|
||||
g.classList.remove('is-open');
|
||||
var t=g.querySelector('.nav-group-toggle');
|
||||
if(t)t.setAttribute('aria-expanded','false');
|
||||
});
|
||||
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
|
||||
});
|
||||
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
|
||||
<symbol id="i-memory-stick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12v-2" /> <path d="M12 18v-2" /> <path d="M16 12v-2" /> <path d="M16 18v-2" /> <path d="M2 11h1.5" /> <path d="M20 18v-2" /> <path d="M20.5 11H22" /> <path d="M4 18v-2" /> <path d="M8 12v-2" /> <path d="M8 18v-2" /> <rect x="2" y="6" width="20" height="10" rx="2" /></symbol>
|
||||
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z" /></symbol>
|
||||
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||
<symbol id="i-cloud" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></symbol>
|
||||
<symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||
<symbol id="i-rotate-cw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /> <path d="M21 3v5h-5" /></symbol>
|
||||
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> <path d="M14 2v5a1 1 0 0 0 1 1h5" /> <path d="M10 9H8" /> <path d="M16 13H8" /> <path d="M16 17H8" /></symbol>
|
||||
<symbol id="i-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /> <path d="M10 14 21 3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /></symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||
<symbol id="i-share" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3" /> <circle cx="6" cy="12" r="3" /> <circle cx="18" cy="19" r="3" /> <line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /> <line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
|
||||
<symbol id="i-layout-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /> <rect width="7" height="7" x="14" y="3" rx="1" /> <rect width="7" height="7" x="14" y="14" rx="1" /> <rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91 0z" /> <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" /> <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" /> <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" /></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
||||
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||
<symbol id="i-pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /> <path d="m15 5 4 4" /></symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <path d="m7 10 5 5 5-5" /></symbol>
|
||||
<symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12" /> <path d="m17 8-5-5-5 5" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></symbol>
|
||||
<symbol id="i-trash-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /> <path d="M14 11v6" /> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /> <path d="M3 6h18" /> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></symbol>
|
||||
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" /></symbol>
|
||||
<symbol id="i-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" /></symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /> <path d="M12 5v14" /></symbol>
|
||||
<symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>
|
||||
<symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /> <rect x="5" y="3" width="5" height="18" rx="1" /></symbol>
|
||||
<symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16" /> <path d="M4 6h16" /> <path d="M4 18h16" /></symbol>
|
||||
</svg>
|
||||
<header class="mobile-topbar">
|
||||
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu"></a>
|
||||
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
|
||||
<svg class="ico"><use href="#i-menu"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="nav-backdrop" hidden></div>
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<img src="/static/felhom-logo.svg?v=0.258.0" alt="Felhom.eu" class="sidebar-logo">
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/launcher" class="active"><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
|
||||
<li><a href="/dashboard" class=""><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
|
||||
<li><a href="/stacks" class=""><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/storage" class="">Meghajtók</a></li>
|
||||
<li><a href="/storage/network" class="">Hálózati tárhely</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/backups" class="">Áttekintés</a></li>
|
||||
<li><a href="/backups/remote" class="">Távoli mentés</a></li>
|
||||
<li><a href="/backups/apps" class="">Alkalmazások</a></li>
|
||||
<li><a href="/backups/restore" class="">Visszaállítás</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-group">
|
||||
<button type="button" class="nav-group-toggle" aria-expanded="false" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
|
||||
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
|
||||
<li><a href="/sharing" class="">Hálózati megosztás</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/monitoring" class=""><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
|
||||
<li><a href="/debug" class=""><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="nav-group-label">Beállítások</div>
|
||||
<ul class="nav-links nav-links-sub">
|
||||
<li><a href="/settings" class=""><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
|
||||
<li><a href="/settings/notifications" class=""><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
|
||||
<li><a href="/settings/security" class=""><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.258.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="c4c59641559c686a99e86783d9c5823dcb1d6a8e610840df223e5da04f4e12bb"><input type="hidden" name="back" value="/launcher"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="c4c59641559c686a99e86783d9c5823dcb1d6a8e610840df223e5da04f4e12bb"><input type="hidden" name="back" value="/launcher"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="content">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Indítópult</h2>
|
||||
<span class="domain-badge">enkisfelhom.hu</span>
|
||||
<button type="button" class="btn btn-outline share-open-btn" onclick="openShareModal()"><svg class="ico"><use href="#i-share"/></svg>Indítópult megosztása</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="launch-grid">
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://travel.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(155, 45%, 38%)">
|
||||
<span class="launch-mono">A</span>
|
||||
<img class="launch-logo" src="/static/assets/adventurelog-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/adventurelog-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">AdventureLog</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://pdf.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(287, 45%, 38%)">
|
||||
<span class="launch-mono">B</span>
|
||||
<img class="launch-logo" src="/static/assets/bentopdf-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/bentopdf-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">BentoPDF</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://wiki.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(112, 45%, 38%)">
|
||||
<span class="launch-mono">B</span>
|
||||
<img class="launch-logo" src="/static/assets/bookstack-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/bookstack-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">BookStack</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://books.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(274, 45%, 38%)">
|
||||
<span class="launch-mono">C</span>
|
||||
<img class="launch-logo" src="/static/assets/calibre-web-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/calibre-web-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Calibre-Web Automated</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://docs.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(234, 45%, 38%)">
|
||||
<span class="launch-mono">D</span>
|
||||
<img class="launch-logo" src="/static/assets/docmost-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/docmost-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Docmost</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://files.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(217, 45%, 38%)">
|
||||
<span class="launch-mono">F</span>
|
||||
<img class="launch-logo" src="/static/assets/filebrowser-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/filebrowser-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Filebrowser</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://time.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(346, 45%, 38%)">
|
||||
<span class="launch-mono">K</span>
|
||||
<img class="launch-logo" src="/static/assets/kimai-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/kimai-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Kimai</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://gist.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(142, 45%, 38%)">
|
||||
<span class="launch-mono">O</span>
|
||||
<img class="launch-logo" src="/static/assets/opengist-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/opengist-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">OpenGist</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://papir.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(328, 45%, 38%)">
|
||||
<span class="launch-mono">P</span>
|
||||
<img class="launch-logo" src="/static/assets/paperless-ngx-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/paperless-ngx-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">Paperless-ngx</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://paste.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(125, 45%, 38%)">
|
||||
<span class="launch-mono">P</span>
|
||||
<img class="launch-logo" src="/static/assets/privatebin-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/privatebin-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">PrivateBin</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<a class="launch-cell" href="https://jatek.enkisfelhom.hu" target="_blank" rel="noopener">
|
||||
|
||||
<span class="launch-tile" style="background: hsl(44, 45%, 38%)">
|
||||
<span class="launch-mono">R</span>
|
||||
<img class="launch-logo" src="/static/assets/romm-logo.svg" alt=""
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='\/static\/assets\/romm-logo.png';}else{this.onerror=null;this.style.display='none';this.parentElement.classList.add('launch-tile--noimg');}">
|
||||
</span>
|
||||
|
||||
<span class="launch-name">RomM</span>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-overlay" id="share-modal" style="display:none">
|
||||
<div class="modal-card">
|
||||
<h3>Indítópult megosztása</h3>
|
||||
<p class="share-lede">Egy megosztható link, amely csak megmutatja az alkalmazásokat és új lapon megnyitja őket. Fiók nélkül, felügyeleti hozzáférés nélkül — minden alkalmazás a saját bejelentkezése mögött marad.</p>
|
||||
|
||||
<p class="share-note">A megosztás jelenleg ki van kapcsolva.</p>
|
||||
<form method="POST" action="/launcher/share/enable">
|
||||
<input type="hidden" name="_csrf" value="c4c59641559c686a99e86783d9c5823dcb1d6a8e610840df223e5da04f4e12bb">
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeShareModal()">Bezárás</button>
|
||||
<button type="submit" class="btn btn-primary">Megosztás bekapcsolása</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openShareModal(){var m=document.getElementById('share-modal');if(m)m.style.display='flex';}
|
||||
function closeShareModal(){var m=document.getElementById('share-modal');if(m)m.style.display='none';}
|
||||
function copyShareLink(){
|
||||
var inp=document.getElementById('share-link-input');
|
||||
var btn=document.getElementById('share-copy-btn');
|
||||
if(!inp)return;
|
||||
function done(){if(btn){var o=btn.textContent;btn.textContent='Másolva';setTimeout(function(){btn.textContent=o;},1500);}}
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(inp.value).then(done,function(){inp.select();done();});}
|
||||
else{inp.select();try{document.execCommand('copy');}catch(e){}done();}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function nativeShare(){
|
||||
var inp=document.getElementById('share-link-input');
|
||||
if(!inp||!navigator.share)return;
|
||||
navigator.share({title:'Indítópult — enkisfelhom.hu',text:'Az otthoni alkalmazások egy helyen.',url:inp.value})
|
||||
.catch(function(e){if(e&&e.name==='AbortError')return;copyShareLink();});
|
||||
}
|
||||
(function(){
|
||||
var m=document.getElementById('share-modal');
|
||||
if(m){m.addEventListener('click',function(e){if(e.target===m)closeShareModal();});}
|
||||
if(navigator.share){var nb=document.getElementById('share-native-btn');if(nb)nb.style.display='';}
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
</main>
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
(function(){
|
||||
var burger=document.querySelector('.nav-burger');
|
||||
var sidebar=document.getElementById('sidebar');
|
||||
var backdrop=document.querySelector('.nav-backdrop');
|
||||
if(!burger||!sidebar||!backdrop)return;
|
||||
function setOpen(open){
|
||||
sidebar.classList.toggle('is-open',open);
|
||||
document.body.classList.toggle('nav-open',open);
|
||||
backdrop.hidden=!open;
|
||||
burger.setAttribute('aria-expanded',open?'true':'false');
|
||||
}
|
||||
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
|
||||
backdrop.addEventListener('click',function(){setOpen(false);});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
|
||||
});
|
||||
})();
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
|
||||
var card = e.target.closest('[data-href]');
|
||||
if (card) window.location.href = card.dataset.href;
|
||||
});
|
||||
async function checkBeforeDeploy(e, name) {
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name);
|
||||
var data = await resp.json();
|
||||
if (data.ok && data.data && data.data.deployed) {
|
||||
e.preventDefault();
|
||||
showAlert('Ez az alkalmazás már telepítve van.');
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
} catch(err) {}
|
||||
return true;
|
||||
}
|
||||
async function syncTemplates() {
|
||||
const btn = document.getElementById('sync-btn');
|
||||
const toast = document.getElementById('sync-toast');
|
||||
if (!btn) return;
|
||||
const origText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '↻ Frissítés...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/sync', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (toast) {
|
||||
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
|
||||
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
|
||||
setTimeout(function() { window.location.reload(); }, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
if (toast) {
|
||||
toast.textContent = 'Hálózati hiba: ' + err.message;
|
||||
toast.className = 'sync-toast sync-toast-err';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(function() { toast.style.display = 'none'; }, 5000);
|
||||
}
|
||||
}
|
||||
btn.innerHTML = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
async function stackAction(event, name, action) {
|
||||
const btn = event.currentTarget;
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Folyamatban...';
|
||||
btn.classList.add('loading');
|
||||
try {
|
||||
const resp = await fetch('/api/stacks/' + name + '/' + action, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (action === 'update') {
|
||||
followUpdate(name, btn);
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showAlert('Hálózati hiba: ' + err.message);
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
function followUpdate(name, btn) {
|
||||
var started = Date.now();
|
||||
var timer = setInterval(async function() {
|
||||
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
|
||||
try {
|
||||
var r = await fetch('/api/stacks/' + name);
|
||||
var d = await r.json();
|
||||
if (!d.ok || !d.data) return;
|
||||
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
|
||||
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
|
||||
} catch (e) {}
|
||||
}, 3000);
|
||||
}
|
||||
async function deleteOrphanStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'delete-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
|
||||
var data = await resp.json();
|
||||
var hddInfo = '';
|
||||
var checkboxHTML = '';
|
||||
if (data.ok && data.data && data.data.has_hdd_data) {
|
||||
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
|
||||
data.data.hdd_paths.forEach(function(p) {
|
||||
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
|
||||
});
|
||||
hddInfo += '</div>';
|
||||
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
|
||||
hddInfo + checkboxHTML +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
|
||||
'</div>';
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
|
||||
}
|
||||
}
|
||||
function closeDeleteModal() {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmDelete(name) {
|
||||
var btn = document.getElementById('confirm-delete-btn');
|
||||
var checkbox = document.getElementById('delete-hdd-check');
|
||||
var removeHDD = checkbox ? checkbox.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Törlés folyamatban...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name, {
|
||||
method: 'DELETE',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('delete-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Sikeresen törölve!</h3>' +
|
||||
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
|
||||
} else {
|
||||
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Törlés';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Hálózati hiba: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Törlés';
|
||||
}
|
||||
}
|
||||
async function removeStack(name) {
|
||||
var modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'remove-modal';
|
||||
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
|
||||
document.body.appendChild(modal);
|
||||
try {
|
||||
var [hddResp, backupResp] = await Promise.all([
|
||||
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
|
||||
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
|
||||
]);
|
||||
var sections = '';
|
||||
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Mindig törlődik:</strong>' +
|
||||
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
|
||||
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
|
||||
'<li>Telepítési konfiguráció (app.yaml)</li>' +
|
||||
'<li>Másodlagos mentés ütemezése</li>' +
|
||||
'</ul></div>';
|
||||
|
||||
var hddCheckbox = '';
|
||||
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
|
||||
var hddPaths = '';
|
||||
hddResp.data.hdd_paths.forEach(function(p) {
|
||||
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
|
||||
});
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
|
||||
'Ha újratelepíti az alkalmazást, az adatokat újra importálnia kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
|
||||
'</div></div>';
|
||||
hddCheckbox = '\x3Cscript>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
|
||||
}
|
||||
|
||||
var backupCheckbox = '';
|
||||
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
|
||||
var bkPaths = '';
|
||||
backupResp.data.backup_paths.forEach(function(p) {
|
||||
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
|
||||
});
|
||||
if (bkPaths) {
|
||||
sections += '<div class="modal-section">' +
|
||||
'<strong>Mentési adatok:</strong>' + bkPaths +
|
||||
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
|
||||
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
|
||||
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
|
||||
'</div></div>';
|
||||
}
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
|
||||
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
|
||||
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
|
||||
sections +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
|
||||
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
|
||||
'</div>' + hddCheckbox;
|
||||
} catch (err) {
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
|
||||
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
|
||||
}
|
||||
}
|
||||
function closeRemoveModal() {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
async function confirmRemoveStack(name) {
|
||||
var btn = document.getElementById('confirm-remove-btn');
|
||||
var hddCheck = document.getElementById('remove-hdd-check');
|
||||
var backupCheck = document.getElementById('remove-backup-check');
|
||||
var removeHDD = hddCheck ? hddCheck.checked : false;
|
||||
var removeBackups = backupCheck ? backupCheck.checked : false;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Eltávolítás folyamatban...';
|
||||
try {
|
||||
var resp = await fetch('/api/stacks/' + name + '/remove', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.ok) {
|
||||
var modal = document.getElementById('remove-modal');
|
||||
var removedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
|
||||
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
|
||||
}
|
||||
var preservedInfo = '';
|
||||
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
|
||||
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
|
||||
}
|
||||
if (data.data && data.data.hdd_note) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
|
||||
}
|
||||
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
|
||||
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Nem törölt mentések: ' + data.data.backup_paths_refused.join('; ') + '</p>';
|
||||
}
|
||||
modal.querySelector('.modal-card').innerHTML =
|
||||
'<h3>Sikeresen eltávolítva!</h3>' +
|
||||
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
|
||||
removedInfo + preservedInfo +
|
||||
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
|
||||
} else {
|
||||
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Eltávolítás';
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Hálózati hiba: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Eltávolítás';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||