v0.319.0: the app page in panels, the login in one place, a password the box never had is said in words
gates / gates (push) Successful in 52s

- style A (operator 2026-10-16): hero, tab bar and every section are panels on --bg-1, one width, 16px apart;
  the install page's form groups use the same rule; the settings tab's 700px inner wrapper is gone
- „Belépés az alkalmazásba": name + password side by side, fetched on demand (R-254, R-709); pairing in app_login.go
- the empty-value rule: no stored value -> a sentence, never an eye; page and reveal endpoint share storedValuePresent
- „Cím és kulcsok" is rows (no Domain input, no disabled storage select, no install form); hint under the title
- tests red-proved: app_page_tuning_test.go (4); 36 app-page parity fixtures re-captured, 4 new; install fixtures unchanged

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-16 12:12:06 +02:00
parent 8d44416610
commit d170a2aecf
58 changed files with 6046 additions and 1776 deletions
+31
View File
@@ -1,3 +1,34 @@
## v0.319.0 — the app page in panels, the login in one place, a password the box never had is said in words (2026-10-16)
**MinAgent: 0.157.0** — unchanged; nothing here calls the agent. No golden (`09` §3 decision 250). Design: felhom.eu
`architecture/02-controller-module-map.md` „The app page" (`[DESIGN]`, operator-approved 2026-10-16, section style A).
- **Panels (style A).** The hero, the tab bar and every section of the four tabs are panels on `--bg-1`, the column's
full width, 16px apart; the open tab is filled `--bg-2` with a 2px `--blue-bright` underline; a panel's title sits 18px
from its top edge with a soft rule under it. The install page's form groups and its progress panel use the same rule.
The settings tab's inner 700px wrapper is gone (it made the settings panels narrower than the column).
- **„Belépés az alkalmazásba"** — one panel with the app's login name and password side by side (stacked under 720px),
both fetched on demand as before (R-254, R-709). Pairing: a `type: password` field plus the `secret` or `text` field with
the same prefix and `USER`/`USERNAME`/`EMAIL` (`web/app_login.go`). Pairs today: calibre-web, kimai, lubelogger,
nextcloud, paperless-ngx, radicale; the other 14 password apps show the password alone.
- **The empty-value rule.** A login field the box holds no value for is a sentence („A doboz ennél a telepítésnél nem
mentett jelszót…"), never an eye that answers 404. The page and the reveal endpoint decide „stored" with one function
(`storedValuePresent`). Measured cause on demo-hp: Calibre-Web was installed 2026-08-21, its template gained
`ADMIN_PASSWORD` on 2026-09-29, and `InjectMissingFields` never fills a `type: password` field
(felhom.eu `audits/app-page-tuning-2026-10-16/P0`). Its stored user name is the real one (renamed in the app 2026-10-01).
- **„Cím és kulcsok"** — the settings tab no longer re-renders the install form: no Domain input, no disabled storage
select, no „Automatikusan generált értékek" block. The read-only hint sits under the title; the storage is a row
„Tárhely: <név>"; the other install answers and generated keys are rows (keys keep their eye).
- Guide: the screenshots are a panel that hides itself when no screenshot loads; the missing-guide line is a panel. Data:
the export's progress and done boxes are panels. Logs: the log box is one panel.
- `/apps/{slug}/install` opened for an INSTALLED app still renders the install form read-only with its secret guards;
two parity cases now render it (`deploy_installed`, `deploy_installed_restored`).
- Tests red-proved: `app_page_tuning_test.go` (TestAppPage_LoginPairPanel, _NoStoredValueShowsSentence,
_SettingsHasNoDuplicateDomain, _OneSectionStyleEverywhere); the storage-row tests re-targeted from the old select.
Parity fixtures: 36 app-page fixtures re-captured (the per-task exception, markup only); 4 new
(`app_settings_login_not_stored`, `app_settings_login_text_name`, `deploy_installed`, `deploy_installed_restored`);
every install-page fixture and every other fixture byte-identical.
## v0.318.0 — the internal data place, partitions, an honest whole-box backup offer, sys_drive never offered (2026-10-16) ## v0.318.0 — the internal data place, partitions, an honest whole-box backup offer, sys_drive never offered (2026-10-16)
**MinAgent: 0.157.0** — nothing here calls a new agent endpoint. The internal place and the partitions appear only when the **MinAgent: 0.157.0** — nothing here calls a new agent endpoint. The internal place and the partitions appear only when the
+1
View File
@@ -204,6 +204,7 @@
| `maskRepoURL` | controller/internal/sync/sync.go | `(url) string` | Logging git URLs | Strips embedded credentials | | `maskRepoURL` | controller/internal/sync/sync.go | `(url) string` | Logging git URLs | Strips embedded credentials |
| `metrics.RedactLine` | controller/internal/metrics/redact.go | `(s string) string` | ANY log line shipped off-box (issue context, log tails) | Masks password/passwd/secret/token/api-key/authorization/bearer values + 64-hex; apply BEFORE the line leaves the box — controller-side redaction is authoritative | | `metrics.RedactLine` | controller/internal/metrics/redact.go | `(s string) string` | ANY log line shipped off-box (issue context, log tails) | Masks password/passwd/secret/token/api-key/authorization/bearer values + 64-hex; apply BEFORE the line leaves the box — controller-side redaction is authoritative |
| `settingsRetrievalPasswordRevealHandler` | controller/internal/web/handlers.go | `POST /settings/retrieval-password/reveal` | **THE PATTERN for showing a secret in the UI** — an XHR that returns only the value | **Never template a secret into a page and hide it with CSS.** `display:none` / `hidden` / `type="password"` stop a browser DRAWING the value; the plaintext is still in the response body, so a `curl` of the page returns it, and it reaches caches, history and any screen-share of the source. R-249 shipped exactly that for two months and was found by it landing in a transcript. The page carries a **boolean** (`HasRetrievalPassword`); the value comes from a POST (CSRF-covered, uncacheable) and the reveal is **logged as an act**. `escrow_handlers.go` states the same rule for R. **Test on the RESPONSE BODY** — a test asserting what the customer *sees* cannot see this class at all. **Both R-254 sites are now FIXED the same way** — `POST /apps/<slug>/initial-credentials/reveal` (re-reads the container, never a cached copy) and `POST /stacks/<name>/auto-field/reveal` (authorised on the field being a `type: secret` auto-field of that stack). **Per-secret, never one generic reveal-any-named-secret endpoint.** The PRE-DEPLOY hidden input is deliberate and untouched — a form must carry what it submits (README §318). Enforced by `scripts/secret_in_markup_gate.py`, whose measured blind spot (a secret under a neutral page-data key) is in its docstring; runtime body-assertion covers 4 of 27 pages — R-255. | | `settingsRetrievalPasswordRevealHandler` | controller/internal/web/handlers.go | `POST /settings/retrieval-password/reveal` | **THE PATTERN for showing a secret in the UI** — an XHR that returns only the value | **Never template a secret into a page and hide it with CSS.** `display:none` / `hidden` / `type="password"` stop a browser DRAWING the value; the plaintext is still in the response body, so a `curl` of the page returns it, and it reaches caches, history and any screen-share of the source. R-249 shipped exactly that for two months and was found by it landing in a transcript. The page carries a **boolean** (`HasRetrievalPassword`); the value comes from a POST (CSRF-covered, uncacheable) and the reveal is **logged as an act**. `escrow_handlers.go` states the same rule for R. **Test on the RESPONSE BODY** — a test asserting what the customer *sees* cannot see this class at all. **Both R-254 sites are now FIXED the same way** — `POST /apps/<slug>/initial-credentials/reveal` (re-reads the container, never a cached copy) and `POST /stacks/<name>/auto-field/reveal` (authorised on the field being a `type: secret` auto-field of that stack). **Per-secret, never one generic reveal-any-named-secret endpoint.** The PRE-DEPLOY hidden input is deliberate and untouched — a form must carry what it submits (README §318). Enforced by `scripts/secret_in_markup_gate.py`, whose measured blind spot (a secret under a neutral page-data key) is in its docstring; runtime body-assertion covers 4 of 27 pages — R-255. |
| `storedValuePresent` / `appLoginFields` | controller/internal/web/app_login.go | the settings tab's login panel and „Cím és kulcsok" rows (v0.319.0) | **A field the box holds no value for is a sentence, never an eye.** `storedValuePresent` is the ONE definition of „stored", read by the page AND by `appAutoFieldRevealHandler`'s 404 branch — so the page cannot offer a reveal the endpoint refuses. The page gets a boolean per login field, never the value (R-254/R-709). Pinned by `TestAppPage_NoStoredValueShowsSentence`. |
### Storage registry + mount detection ### Storage registry + mount detection
+9 -3
View File
@@ -344,7 +344,7 @@ card for moving data), and every „Vissza" went to the app LIST, so there was n
| URL | Tab | What is on it | | URL | Tab | What is on it |
|---|---|---| |---|---|---|
| `/apps/{slug}` | — | the default: the settings tab when installed, the guide when not. **Rendered, not redirected** — the app's address stays its address | | `/apps/{slug}` | — | the default: the settings tab when installed, the guide when not. **Rendered, not redirected** — the app's address stays its address |
| `/apps/{slug}/settings` | „Beállítások" | „Működés" (Start / Restart / Stop, Stop confirms), the cards that want something from the household (setup gate, after-start fields, sign-up window, first login), then the generated values, the install-time values read-only, the optional settings, e-mail, integrations, geo | | `/apps/{slug}/settings` | „Beállítások" | „Működés" (Start / Restart / Stop, Stop confirms), the cards that want something from the household (setup gate, after-start fields, sign-up window, first login), „Belépés az alkalmazásba" (the login name and password side by side, v0.319.0), the optional settings, e-mail, integrations, geo, then „Cím és kulcsok" (address, storage, the other install answers and generated keys as rows) |
| `/apps/{slug}/data` | „Adatok és mentés" | „Hova tegyem a fájlokat?", move to another drive, the nightly copy, the 2nd-copy setting, export | | `/apps/{slug}/data` | „Adatok és mentés" | „Hova tegyem a fájlokat?", move to another drive, the nightly copy, the 2nd-copy setting, export |
| `/apps/{slug}/guide` | „Útmutató" | tagline, screenshots, what it is for, first steps, the catalog's guide, the FAQ, the docs link. For a NOT-installed app this is the whole page, with „Telepítés" | | `/apps/{slug}/guide` | „Útmutató" | tagline, screenshots, what it is for, first steps, the catalog's guide, the FAQ, the docs link. For a NOT-installed app this is the whole page, with „Telepítés" |
| `/apps/{slug}/logs` | „Napló" | the log, and the only tab whose poller runs | | `/apps/{slug}/logs` | „Napló" | the log, and the only tab whose poller runs |
@@ -362,8 +362,14 @@ app-email toggle and the reveal endpoints keep their URLs and their handlers.
Code: `internal/web/app_page.go` (routing, the tabs, the redirects), `templates/app_shell.html` (the one header, the Code: `internal/web/app_page.go` (routing, the tabs, the redirects), `templates/app_shell.html` (the one header, the
banners, the tab bar), `app_settings.html` / `app_data.html` / `app_guide.html` / `app_logs.html`, and banners, the tab bar), `app_settings.html` / `app_data.html` / `app_guide.html` / `app_logs.html`, and
`app_fields.html` — the configuration fields, shared byte-for-byte by the install wizard and the settings tab so the `app_fields.html` — the install wizard's configuration fields (since v0.319.0 the settings tab shows rows and the login
two cannot drift. panel instead, `internal/web/app_login.go`).
**Layout (v0.319.0, operator-approved 2026-10-16, section style A):** the hero, the tab bar and every section are panels
on `--bg-1`, the column's full width, 16px apart; the open tab is filled `--bg-2` with a 2px `--blue-bright` underline;
the install page's form groups use the same panel rule. **A login field the box holds no value for is a sentence, never an
eye** — the page and the reveal endpoint share `storedValuePresent`. Design: felhom.eu `architecture/02-controller-module-map.md`
„The app page's layout".
#### „Még egy lépés" — a value asked AFTER the app starts (v0.315.0) #### „Még egy lépés" — a value asked AFTER the app starts (v0.315.0)
+5
View File
@@ -118,6 +118,11 @@
"app_page.address_and_keys": "Address and keys", "app_page.address_and_keys": "Address and keys",
"app_page.address": "Address", "app_page.address": "Address",
"app_page.installed_readonly": "These were created at install time and cannot be changed.", "app_page.installed_readonly": "These were created at install time and cannot be changed.",
"app_page.login_title": "Signing in to the app",
"app_page.login_desc": "For the first sign-in. If you change the password in the app, the old one stays here.",
"app_page.login_no_password": "The box saved no password for this install. Sign in with the one you set in the app.",
"app_page.login_no_name": "The box saved no user name for this install. Sign in with the one you set in the app.",
"app_page.value_no": "No",
"app_page.after_start_btn": "Save", "app_page.after_start_btn": "Save",
"app_page.after_start_title": "One more step", "app_page.after_start_title": "One more step",
"app_page.faq": "Frequently asked questions", "app_page.faq": "Frequently asked questions",
+5
View File
@@ -114,6 +114,11 @@
"app_page.address_and_keys": "Cím és kulcsok", "app_page.address_and_keys": "Cím és kulcsok",
"app_page.address": "Cím", "app_page.address": "Cím",
"app_page.installed_readonly": "Ezek telepítéskor jöttek létre, és nem módosíthatók.", "app_page.installed_readonly": "Ezek telepítéskor jöttek létre, és nem módosíthatók.",
"app_page.login_title": "Belépés az alkalmazásba",
"app_page.login_desc": "Az első bejelentkezéshez. Ha az alkalmazásban megváltoztatod a jelszót, itt a régi marad.",
"app_page.login_no_password": "A doboz ennél a telepítésnél nem mentett jelszót. A belépéshez azt használd, amit az alkalmazásban beállítottál.",
"app_page.login_no_name": "A doboz ennél a telepítésnél nem mentett felhasználónevet. A belépéshez azt használd, amit az alkalmazásban beállítottál.",
"app_page.value_no": "Nem",
"app_page.after_start_btn": "Mentés", "app_page.after_start_btn": "Mentés",
"app_page.after_start_title": "Még egy lépés", "app_page.after_start_title": "Még egy lépés",
"app_page.faq": "Gyakori kérdések", "app_page.faq": "Gyakori kérdések",
+159
View File
@@ -0,0 +1,159 @@
package web
import (
"strings"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// THE LOGIN PANEL AND THE „CIM ES KULCSOK" ROWS (v0.319.0) — the settings tab's view of what was
// set at install.
//
// Until v0.318.0 the settings tab re-rendered the install wizard's form with every input disabled:
// the generated login name sat in „Automatikusan generalt ertekek" next to a disabled Domain input,
// and the password sat three sections lower among the other install answers. The household had to
// find two halves of one login. Now one panel holds the pair, and the rest are label/value rows.
//
// THE EMPTY-VALUE RULE. A field the box holds no value for is SAID, never offered an eye: the reveal
// endpoint answers 404 for it („Ehhez a mezohoz nincs mentett ertek"), which is a refusal the
// household could only meet by pressing. Measured 2026-10-16 on demo-hp: Calibre-Web was installed
// on 2026-08-21, its template gained ADMIN_PASSWORD on 2026-09-29, and InjectMissingFields never
// fills a `type: password` field — so the box never had one (audits/app-page-tuning-2026-10-16/P0).
// The page and the endpoint decide „stored" with ONE function, storedValuePresent, so the page
// cannot offer what the endpoint would refuse. Pinned by TestAppPage_NoStoredValueShowsSentence.
//
// SECRETS. A `type: secret` name and every `type: password` value stay off the page (R-254, R-709);
// only the boolean Stored reaches the template. A `type: text` name is not a secret — the old
// disabled input carried it as its value — and is shown as text.
// storedValuePresent is the one definition of „the box holds a value for this field", shared by the
// settings tab and appAutoFieldRevealHandler's 404 branch.
func storedValuePresent(val string) bool { return strings.TrimSpace(val) != "" }
// AppLoginField is one half of the login panel. Value is set ONLY for a plain-text name; a secret
// or a password never carries its value here.
type AppLoginField struct {
EnvVar string
Label string
IsName bool // the name half; false for the password half
OnDemand bool // fetched on demand through /stacks/{name}/auto-field/reveal
Value string // a `type: text` name only
Stored bool // the box holds a non-blank value
Restored bool // R-694: a restore generated it, so it is not the app's login and is never shown
}
// AppSettingRow is one label/value row of „Cim es kulcsok". An OnDemand row carries no value; the
// eye fetches it.
type AppSettingRow struct {
EnvVar string
Label string
Value string
OnDemand bool
}
// loginNameSuffixes are the env-var endings the catalog uses for the name next to a password:
// ADMIN_USER + ADMIN_PASSWORD (calibre-web), ADMIN_EMAIL + ADMIN_PASSWORD (kimai),
// NEXTCLOUD_ADMIN_USER + NEXTCLOUD_ADMIN_PASSWORD, and so on — measured over the catalog on
// 2026-10-16, every pair shares the prefix.
var loginNameSuffixes = []string{"USER", "USERNAME", "EMAIL"}
// loginFieldIndexes returns, in deploy_fields order, the indexes of the login fields: every
// `type: password` field (not asked after start), each preceded by its name field when the
// template has one with the same prefix and type `secret` or `text`. A password with no clean
// pair stands alone.
func loginFieldIndexes(meta *stacks.Metadata) []int {
if meta == nil {
return nil
}
byEnv := make(map[string]int, len(meta.DeployFields))
for i, f := range meta.DeployFields {
byEnv[f.EnvVar] = i
}
var out []int
for i, f := range meta.DeployFields {
if f.Type != "password" || f.AfterStart {
continue
}
// ADMIN_PASSWORD -> prefix "ADMIN_" -> ADMIN_USER / ADMIN_USERNAME / ADMIN_EMAIL.
prefix := strings.TrimSuffix(f.EnvVar, "PASSWORD")
if prefix != f.EnvVar {
for _, suf := range loginNameSuffixes {
if j, ok := byEnv[prefix+suf]; ok {
if t := meta.DeployFields[j].Type; (t == "secret" || t == "text") && !meta.DeployFields[j].AfterStart {
out = append(out, j)
break
}
}
}
}
out = append(out, i)
}
return out
}
// appLoginFields builds the panel from the DECRYPTED env (before any value is removed from it).
func appLoginFields(meta *stacks.Metadata, env map[string]string, restored map[string]bool) []AppLoginField {
var out []AppLoginField
for _, i := range loginFieldIndexes(meta) {
f := meta.DeployFields[i]
lf := AppLoginField{EnvVar: f.EnvVar, Label: f.Label, IsName: true, Stored: storedValuePresent(env[f.EnvVar])}
switch f.Type {
case "password":
lf.IsName, lf.OnDemand, lf.Restored = false, true, restored[f.EnvVar]
case "secret":
lf.OnDemand = true
default:
lf.Value = env[f.EnvVar]
}
out = append(out, lf)
}
return out
}
// appSettingRows is everything else an installed app was given at install, as rows: the generated
// secrets (eye) and the plain answers (text). Skipped, because the page says them elsewhere: the
// domain and the subdomain (the „Cim" row), the data folder (the „Tarhely" row), the login fields
// (the login panel), and a field asked after start (its own card).
func appSettingRows(meta *stacks.Metadata, env map[string]string, yes, no string) []AppSettingRow {
if meta == nil {
return nil
}
login := map[int]bool{}
for _, i := range loginFieldIndexes(meta) {
login[i] = true
}
var out []AppSettingRow
for i, f := range meta.DeployFields {
if login[i] || f.AfterStart {
continue
}
val := env[f.EnvVar]
switch f.Type {
case "domain", "subdomain", "path", "password":
continue
case "secret":
if storedValuePresent(val) {
out = append(out, AppSettingRow{EnvVar: f.EnvVar, Label: f.Label, OnDemand: true})
}
continue
case "select":
for _, o := range f.Options {
if o.Value == val {
val = o.Label
break
}
}
case "boolean":
if val == "true" {
val = yes
} else {
val = no
}
}
if !storedValuePresent(val) {
continue
}
out = append(out, AppSettingRow{EnvVar: f.EnvVar, Label: f.Label, Value: val})
}
return out
}
@@ -0,0 +1,434 @@
package web
import (
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// APP PAGE TUNING, ROUND 2 (v0.319.0) — the login panel, the empty-value rule, the settings tab
// without the install form's duplicates, and one section style on every surface.
//
// The page tests go through s.ServeHTTP (the production route switch and handler), because the
// thing under test is what the HANDLER tells the template: a template rendered with hand-built
// LoginFields would assert nothing about whether the box's own values stay out of the page.
// Assertions are on ASCII `data-*` markers and ids, never on accented Hungarian.
// The two values the box holds for an installed Calibre-Web. Neither may appear in any page.
const (
tuningUser = "TESTONLY-user-q7x"
tuningPassword = "TESTONLY-pw-Lm93!"
tuningKey = "TESTONLY-key-Zr55"
)
// calibreMeta is the catalog's calibre-web deploy_fields (templates/calibre-web/.felhom.yml, read
// 2026-10-16), plus one generated key so a non-login secret row is exercised too.
const calibreMeta = "display_name: Calibre-Web\nslug: calibre-web\nsubdomain: books\n" +
"deploy_fields:\n" +
" - env_var: DOMAIN\n label: Domain\n type: domain\n locked_after_deploy: true\n" +
" - env_var: SUBDOMAIN\n label: Aldomain\n type: subdomain\n default: books\n required: true\n" +
" - env_var: HDD_PATH\n label: Konyvtar utvonal\n type: path\n required: true\n" +
" - env_var: ADMIN_USER\n label: Admin nev\n type: secret\n generate: \"hex:5\"\n" +
" - env_var: ADMIN_PASSWORD\n label: Admin jelszo\n type: password\n generate: \"password:24:special\"\n" +
" - env_var: SECRET_KEY\n label: Titkos kulcs\n type: secret\n generate: \"hex:16\"\n"
// tuningHarness: `calibre-web` installed with the given env, and `calibre-new` — the same template,
// not installed — for the install page.
func tuningHarness(t *testing.T, env map[string]string) *Server {
t.Helper()
dir := t.TempDir()
bin := filepath.Join(dir, "bin")
if err := os.MkdirAll(bin, 0o755); err != nil {
t.Fatal(err)
}
stub := "#!/bin/sh\nif [ \"$1\" = ps ]; then\n printf 'calibre-web\\tbusybox\\trunning\\tUp 2 hours\\tcalibre-web\\n'\nfi\nexit 0\n"
if err := os.WriteFile(filepath.Join(bin, "docker"), []byte(stub), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin)
lg := log.New(io.Discard, "", 0)
cfg := config.Default()
cfg.Customer.Domain = "example.hu"
cfg.Paths.StacksDir = filepath.Join(dir, "stacks")
cfg.Paths.DataDir = filepath.Join(dir, "data")
for _, app := range []string{"calibre-web", "calibre-new"} {
d := filepath.Join(cfg.Paths.StacksDir, app)
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
meta := strings.Replace(calibreMeta, "slug: calibre-web", "slug: "+app, 1)
for name, body := range map[string]string{
"docker-compose.yml": "services:\n " + app + ":\n image: busybox\n",
".felhom.yml": meta,
} {
if err := os.WriteFile(filepath.Join(d, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
}
if err := stacks.SaveAppConfig(filepath.Join(cfg.Paths.StacksDir, "calibre-web"),
&stacks.AppConfig{Deployed: true, Env: env}, nil, nil); err != nil {
t.Fatal(err)
}
sett, err := settings.Load(filepath.Join(dir, "settings.json"), lg)
if err != nil {
t.Fatal(err)
}
if err := sett.AddStoragePath(settings.StoragePath{Path: "/mnt/felhom-drives/hdd_1", Label: "Kulso lemez", IsDefault: true, Schedulable: true}); err != nil {
t.Fatal(err)
}
mgr, err := stacks.NewManager(cfg, lg)
if err != nil {
t.Fatal(err)
}
_ = mgr.ScanStacks()
if _, ok := mgr.GetStack("calibre-web"); !ok {
t.Fatal("calibre-web was not discovered by ScanStacks")
}
s := &Server{cfg: cfg, settings: sett, stackMgr: mgr, logger: lg, version: "test"}
s.loadTemplates()
return s
}
func tuningEnv() map[string]string {
return map[string]string{
"DOMAIN": "example.hu", "SUBDOMAIN": "books", "HDD_PATH": "/mnt/felhom-drives/hdd_1",
"ADMIN_USER": tuningUser, "ADMIN_PASSWORD": tuningPassword, "SECRET_KEY": tuningKey,
}
}
// revealStatus asks the reveal endpoint exactly as the eye button does, and returns its status.
func revealStatus(s *Server, envVar string) int {
r := httptest.NewRequest(http.MethodPost, "/stacks/calibre-web/auto-field/reveal", strings.NewReader("env_var="+envVar))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.appAutoFieldRevealHandler(w, r, "calibre-web")
return w.Code
}
// sectionOf returns the element that starts at the first `marker` up to its matching </div>.
func sectionOf(t *testing.T, body, marker string) string {
t.Helper()
i := strings.Index(body, marker)
if i < 0 {
t.Fatalf("no %s in the page", marker)
}
start := strings.LastIndex(body[:i], "<div")
depth := 0
for _, m := range divTagRe.FindAllStringIndex(body[start:], -1) {
if strings.HasPrefix(body[start+m[0]:], "</") {
depth--
} else {
depth++
}
if depth == 0 {
return body[start : start+m[1]]
}
}
t.Fatalf("%s: unbalanced divs", marker)
return ""
}
var divTagRe = regexp.MustCompile(`</?div\b[^>]*>`)
var revealCallRe = regexp.MustCompile(`revealAutoField\('calibre-web','([A-Z_]+)'`)
// TestAppPage_LoginPairPanel (Scenario B) — ONE „Belepes az alkalmazasba" panel holds the name and
// the password, name first; both are fetched on demand, so neither value is in the page; and the
// eye on each is one the endpoint answers.
//
// RED-PROOF (REPORT.md): render the panel without the name (drop the pairing loop in
// loginFieldIndexes) -> the ADMIN_USER assertion fails. Reverted.
func TestAppPage_LoginPairPanel(t *testing.T) {
s := tuningHarness(t, tuningEnv())
rec := appGet(t, s, "/apps/calibre-web/settings")
if rec.Code != http.StatusOK {
t.Fatalf("GET settings = %d: %s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
for _, v := range []string{tuningUser, tuningPassword, tuningKey} {
if strings.Contains(body, v) {
t.Fatalf("a stored secret value is in the settings page (R-254/R-709)")
}
}
if n := strings.Count(body, `data-login-panel="1"`); n != 1 {
t.Fatalf("%d login panels, want exactly 1", n)
}
panel := sectionOf(t, body, `data-login-panel="1"`)
u, p := strings.Index(panel, `data-login-field="ADMIN_USER"`), strings.Index(panel, `data-login-field="ADMIN_PASSWORD"`)
if u < 0 || p < 0 {
t.Fatalf("the login panel must hold BOTH halves (user=%d password=%d)", u, p)
}
if u > p {
t.Error("the name must come before the password")
}
if !strings.Contains(panel, `class="app-login-pair"`) {
t.Error("the pair is not laid out side by side (app-login-pair)")
}
for _, f := range []string{"ADMIN_USER", "ADMIN_PASSWORD"} {
if !strings.Contains(panel, `data-login-reveal="`+f+`"`) {
t.Errorf("no eye for %s in the login panel", f)
}
if n := strings.Count(body, `id="field-`+f+`"`); n != 1 {
t.Errorf("field %s shown %d times, want once (Scenario E)", f, n)
}
}
// The login fields are shown ONCE: not also as a „Cim es kulcsok" row.
if strings.Contains(body, `data-setting-row="ADMIN_USER"`) || strings.Contains(body, `data-setting-row="ADMIN_PASSWORD"`) {
t.Error("a login field is shown twice — in the panel and as a row")
}
// Every eye on the page is one the endpoint answers with a value.
for _, m := range revealCallRe.FindAllStringSubmatch(body, -1) {
if code := revealStatus(s, m[1]); code != http.StatusOK {
t.Errorf("the page offers an eye for %s and the endpoint answers %d", m[1], code)
}
}
}
// TestAppPage_NoStoredValueShowsSentence (Scenario C) — the box holds no password (Calibre-Web
// installed before its template had one, measured on demo-hp 2026-10-16): the page says so in a
// sentence and offers no eye, because the endpoint would answer 404 for it.
//
// RED-PROOF (REPORT.md): render the eye regardless (Stored: true in appLoginFields) -> the
// no-eye and the every-eye-answers assertions fail. Reverted.
func TestAppPage_NoStoredValueShowsSentence(t *testing.T) {
env := tuningEnv()
delete(env, "ADMIN_PASSWORD")
env["ADMIN_USER"] = " " // a blank value is no value: the endpoint's own rule
s := tuningHarness(t, env)
body := appGet(t, s, "/apps/calibre-web/settings").Body.String()
for _, f := range []string{"ADMIN_PASSWORD", "ADMIN_USER"} {
if code := revealStatus(s, f); code != http.StatusNotFound {
t.Fatalf("precondition: the endpoint must answer 404 for %s, got %d", f, code)
}
if !strings.Contains(body, `data-login-not-stored="`+f+`"`) {
t.Errorf("%s: no sentence for a field the box holds no value for", f)
}
if strings.Contains(body, `data-login-reveal="`+f+`"`) || strings.Contains(body, `revealAutoField('calibre-web','`+f+`'`) {
t.Errorf("%s: an eye is offered for a value the endpoint answers 404 for", f)
}
}
for _, m := range revealCallRe.FindAllStringSubmatch(body, -1) {
if code := revealStatus(s, m[1]); code != http.StatusOK {
t.Errorf("the page offers an eye for %s and the endpoint answers %d", m[1], code)
}
}
}
// TestAppPage_SettingsHasNoDuplicateDomain (Scenario E) — the settings tab carries no Domain input
// (the address is the „Cim" row), no disabled storage select (it is the „Tarhely" row), no install
// form, and the read-only hint sits under the „Cim es kulcsok" title. The install page keeps its
// fields (Scenario D's byte-identity is the parity fixtures' job; this asserts the fields are there).
//
// RED-PROOF (REPORT.md): put `{{template "app_deploy_fields" .}}` back into the settings tab -> the
// Domain/deploy-form assertions fail. Reverted.
func TestAppPage_SettingsHasNoDuplicateDomain(t *testing.T) {
s := tuningHarness(t, tuningEnv())
body := appGet(t, s, "/apps/calibre-web/settings").Body.String()
for _, gone := range []string{`id="auto-field-DOMAIN"`, `id="field-DOMAIN"`, `id="deploy-form"`, `id="field-HDD_PATH"`, `id="field-SUBDOMAIN"`} {
if strings.Contains(body, gone) {
t.Errorf("the settings tab still renders %s", gone)
}
}
if n := strings.Count(body, `data-app-address-row="1"`); n != 1 {
t.Errorf("%d address rows, want 1", n)
}
keys := sectionOf(t, body, `data-address-keys="1"`)
hint, addr := strings.Index(keys, `data-readonly-hint="1"`), strings.Index(keys, `data-app-address-row="1"`)
if hint < 0 || addr < 0 || hint > addr {
t.Errorf("the read-only hint must sit under the title, above the rows (hint=%d address=%d)", hint, addr)
}
if strings.Count(body, `data-readonly-hint="1"`) != 1 {
t.Error("the read-only hint must appear once, inside the address-and-keys section")
}
if row := sectionOf(t, keys, `data-app-storage-row="1"`); !strings.Contains(row, "Kulso lemez") {
t.Errorf("the storage row must name the app's drive, got %q", row)
}
if !strings.Contains(keys, `data-setting-row="SECRET_KEY"`) {
t.Error("a generated non-login key must still be a row with its eye")
}
inst := appGet(t, s, "/apps/calibre-new/install")
if inst.Code != http.StatusOK {
t.Fatalf("GET install = %d", inst.Code)
}
ib := inst.Body.String()
for _, want := range []string{`id="deploy-form"`, `id="field-SUBDOMAIN"`, `id="field-ADMIN_PASSWORD"`, `id="auto-field-ADMIN_USER"`, `generatePassword('field-ADMIN_PASSWORD'`} {
if !strings.Contains(ib, want) {
t.Errorf("the install page lost %s", want)
}
}
}
// ── one section style ─────────────────────────────────────────────────────────────────────────────
var (
tagRe = regexp.MustCompile(`<(/?)([a-zA-Z][a-zA-Z0-9]*)([^>]*?)(/?)>`)
classAttrRe = regexp.MustCompile(`class="([^"]*)"`)
stripBlockRe = regexp.MustCompile(`(?s)<script\b.*?</script>|<style\b.*?</style>|<!--.*?-->`)
voidTags = map[string]bool{"input": true, "img": true, "br": true, "hr": true, "meta": true, "link": true, "source": true, "use": true, "path": true}
// Wrappers that carry no look of their own: their children are the page's sections.
transparentWrappers = []string{"app-sections", "app-guide", "app-data-tab", "app-settings-tab"}
// Not sections: the save/failure flash lines at the top of a tab.
messageClasses = []string{"flash", "monitoring-banner"}
)
func hasClass(attrs, want string) bool {
m := classAttrRe.FindStringSubmatch(attrs)
if m == nil {
return false
}
for _, c := range strings.Fields(m[1]) {
if c == want {
return true
}
}
return false
}
// tabTopLevelBlocks returns, for a rendered app-page TAB, every block the household sees between the
// tab bar and the end of the column — looking through the transparent wrappers — as its opening tag.
func tabTopLevelBlocks(t *testing.T, where, body string) []string {
t.Helper()
body = stripBlockRe.ReplaceAllString(body, "")
nav := strings.Index(body, `class="app-tabs"`)
if nav < 0 {
t.Fatalf("%s: no tab bar", where)
}
end := strings.Index(body[nav:], "</nav>")
rest := body[nav+end+len("</nav>"):]
var stack []bool // true = transparent wrapper
var out []string
for _, m := range tagRe.FindAllStringSubmatch(rest, -1) {
closing, name, attrs, self := m[1] == "/", strings.ToLower(m[2]), m[3], m[4] == "/"
if closing {
if len(stack) == 0 {
return out // the column itself closed
}
stack = stack[:len(stack)-1]
continue
}
if voidTags[name] || self {
continue
}
level := 0
for _, tr := range stack {
if !tr {
level++
}
}
transparent := false
if name == "div" {
for _, w := range transparentWrappers {
if hasClass(attrs, w) {
transparent = true
}
}
}
if level == 0 && !transparent {
out = append(out, m[0])
}
stack = append(stack, transparent)
}
return out
}
// TestAppPage_OneSectionStyleEverywhere — style A, the operator's choice of 2026-10-16: every section
// on the four tabs is an `.app-section` panel, the install page's form groups and progress panel are
// the same panel by one CSS rule, and no surface keeps the old flat hairline rule or a box of its own.
//
// Rendered through the parity cases (every branch the fixtures carry, both the rich and the bare data)
// and the production route for the bare harness app.
//
// RED-PROOF (REPORT.md): leave the logs tab on its old markup (`<div data-tab-panel="logs">`, no
// app-section) -> the app_logs case fails. Reverted.
func TestAppPage_OneSectionStyleEverywhere(t *testing.T) {
s := i18nTestServer(t)
tabs := map[string]bool{"app_settings": true, "app_data": true, "app_guide": true, "app_logs": true}
checked := map[string]int{}
for _, c := range i18nCases() {
if !tabs[c.tmpl] {
continue
}
body := renderI18nCase(t, s, "hu", c)
if !strings.Contains(body, `class="app-tabs"`) {
continue // a not-installed app's guide has no tab bar; the route test below covers it
}
checked[c.tmpl]++
for _, tag := range tabTopLevelBlocks(t, c.name, body) {
if hasClass(tag, "app-section") {
continue
}
msg := false
for _, mc := range messageClasses {
msg = msg || hasClass(tag, mc)
}
if !msg {
t.Errorf("%s: a block outside the section style: %s", c.name, tag)
}
}
for _, legacy := range []string{`class="app-optional-config"`, `class="deploy-storage-info"`, `class="card"`, `class="deploy-container"`, `id="deploy-form"`} {
if strings.Contains(body, legacy) {
t.Errorf("%s: a tab still carries the old box %s", c.name, legacy)
}
}
}
for tmpl := range tabs {
if checked[tmpl] == 0 {
t.Fatalf("no parity case exercised %s — the check proved nothing for that tab", tmpl)
}
}
// A not-installed app: its one view is the guide, without a tab bar — sections after the hero.
hs := appPageHarness(t)
g := appGet(t, hs, "/apps/grocy").Body.String()
g = stripBlockRe.ReplaceAllString(g, "")
if i := strings.Index(g, `data-guide-missing="1"`); i < 0 || !strings.Contains(g[strings.LastIndex(g[:i], "<div"):i], "app-section") {
t.Error("a not-installed app's guide-missing line is not a section")
}
// The install page: every form group is a .form-section inside the wizard's form, and the
// stylesheet draws it with the SAME rule as .app-section.
for _, c := range i18nCases() {
if c.tmpl != "deploy" {
continue
}
body := renderI18nCase(t, s, "hu", c)
form := body[strings.Index(body, `id="deploy-form"`):]
form = form[:strings.Index(form, "</form>")]
if strings.Count(form, `<h4>`) != strings.Count(form, `<div class="form-section">`) {
t.Errorf("%s: a form group on the install page is not a form-section", c.name)
}
}
css, err := os.ReadFile(filepath.Join("templates", "style.css"))
if err != nil {
t.Fatal(err)
}
panelRule := regexp.MustCompile(`(?s)\.app-section,\s*\.app-page \.deploy-form \.form-section,\s*\.app-page \.deploy-progress\s*\{([^}]*)\}`).FindSubmatch(css)
if panelRule == nil {
t.Fatal("style.css: the one panel rule (.app-section + the install page's form-section and progress) is gone")
}
for _, want := range []string{"background: var(--bg-1)", "border: 1px solid var(--line)", "padding: 18px 24px 22px", "margin: 0 0 16px"} {
if !strings.Contains(string(panelRule[1]), want) {
t.Errorf("style.css: the panel rule lacks %q (style A)", want)
}
}
// No .app-section rule may redraw it the old way: v0.316.0's flat `padding: 28px 0` + hairline.
for _, m := range regexp.MustCompile(`([^{}]*\.app-section[^{}]*)\{([^}]*)\}`).FindAllSubmatch(css, -1) {
if strings.Contains(string(m[2]), "padding: 28px 0") {
t.Errorf("style.css: %s still carries the old flat section rule", strings.TrimSpace(string(m[1])))
}
}
}
+9 -2
View File
@@ -489,9 +489,15 @@ func (s *Server) deployPageData(w http.ResponseWriter, r *http.Request, name str
if appCfg != nil { if appCfg != nil {
for _, n := range appCfg.RestoredLogins { for _, n := range appCfg.RestoredLogins {
restored[n] = true restored[n] = true
delete(decryptedEnv, n)
} }
} }
// v0.319.0: the settings tab's login panel and „Cim es kulcsok" rows, read BEFORE any value
// is dropped below — they learn only whether a value is stored, never the secret itself.
data["LoginFields"] = appLoginFields(meta, decryptedEnv, restored)
data["SettingsRows"] = appSettingRows(meta, decryptedEnv, s.msgLang(lang, "deploy.igen"), s.msgLang(lang, "app_page.value_no"))
for n := range restored {
delete(decryptedEnv, n)
}
// R-709 (v0.280.0): a `type: password` value is never written into this page's HTML either — like a // R-709 (v0.280.0): a `type: password` value is never written into this page's HTML either — like a
// `type: secret` (R-254) it is fetched on demand (/stacks/<n>/auto-field/reveal). // `type: secret` (R-254) it is fetched on demand (/stacks/<n>/auto-field/reveal).
for _, f := range meta.DeployFields { for _, f := range meta.DeployFields {
@@ -578,6 +584,7 @@ func (s *Server) deployPageData(w http.ResponseWriter, r *http.Request, name str
for _, dp := range deployPaths { for _, dp := range deployPaths {
if dp.Path == hdd { if dp.Path == hdd {
inList = true inList = true
data["CurrentHDDLabel"] = dp.Label // v0.319.0: the settings tab's „Tarhely" row
break break
} }
} }
@@ -2626,7 +2633,7 @@ func (s *Server) appAutoFieldRevealHandler(w http.ResponseWriter, r *http.Reques
return return
} }
val := crypto.DecryptMap(s.encKey, appCfg.Env)[envVar] val := crypto.DecryptMap(s.encKey, appCfg.Env)[envVar]
if strings.TrimSpace(val) == "" { if !storedValuePresent(val) { // the settings tab reads the same function, so it never offers this 404
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.no_stored_value")) escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.no_stored_value"))
return return
@@ -187,6 +187,18 @@ func i18nCasesA() []i18nCase {
{"deploy_new_memory_blocked", "deploy", func() map[string]interface{} { {"deploy_new_memory_blocked", "deploy", func() map[string]interface{} {
return i18nDeployData(false, stacks.StateNotDeployed, 3) return i18nDeployData(false, stacks.StateNotDeployed, 3)
}}, }},
// v0.319.0: `/apps/{slug}/install` opened for an app that IS installed — the form read-only, its
// secrets fetched on demand. The settings tab stopped rendering this form, so these two cases are
// what renders its installed-app branches now.
{"deploy_installed", "deploy", func() map[string]interface{} {
return i18nDeployData(true, stacks.StateRunning, 1)
}},
{"deploy_installed_restored", "deploy", func() map[string]interface{} {
d := i18nDeployData(true, stacks.StateRunning, 1)
d["DeployedFieldValues"] = map[string]string{"SUBDOMAIN": "paste"}
d["RestoredLogins"] = map[string]bool{"ADMIN_PASSWORD": true}
return d
}},
{"app_settings_running", "app_settings", func() map[string]interface{} { {"app_settings_running", "app_settings", func() map[string]interface{} {
return i18nDeployData(true, stacks.StateRunning, 1) return i18nDeployData(true, stacks.StateRunning, 1)
}}, }},
@@ -198,6 +210,26 @@ func i18nCasesA() []i18nCase {
d := i18nDeployData(true, stacks.StateRunning, 1) d := i18nDeployData(true, stacks.StateRunning, 1)
d["DeployedFieldValues"] = map[string]string{"SUBDOMAIN": "paste"} d["DeployedFieldValues"] = map[string]string{"SUBDOMAIN": "paste"}
d["RestoredLogins"] = map[string]bool{"ADMIN_PASSWORD": true} d["RestoredLogins"] = map[string]bool{"ADMIN_PASSWORD": true}
d["LoginFields"] = []AppLoginField{{EnvVar: "ADMIN_PASSWORD", Label: "Admin jelszó mező", OnDemand: true, Stored: true, Restored: true}}
return d
}},
// v0.319.0: the box holds no login value (Calibre-Web installed before its template had one) —
// a sentence for each half, never an eye that answers 404.
{"app_settings_login_not_stored", "app_settings", func() map[string]interface{} {
d := i18nDeployData(true, stacks.StateRunning, 1)
d["LoginFields"] = []AppLoginField{
{EnvVar: "ADMIN_USER", Label: "Admin név mező", IsName: true, OnDemand: true},
{EnvVar: "ADMIN_PASSWORD", Label: "Admin jelszó mező", OnDemand: true},
}
return d
}},
// v0.319.0: a plain-text login name (Nextcloud, Paperless) is shown as text beside the password.
{"app_settings_login_text_name", "app_settings", func() map[string]interface{} {
d := i18nDeployData(true, stacks.StateRunning, 2)
d["LoginFields"] = []AppLoginField{
{EnvVar: "PAPERLESS_ADMIN_USER", Label: "Admin név mező", IsName: true, Value: "konyveles", Stored: true},
{EnvVar: "PAPERLESS_ADMIN_PASSWORD", Label: "Admin jelszó mező", OnDemand: true, Stored: true},
}
return d return d
}}, }},
{"settings_system_full", "settings_system", func() map[string]interface{} { {"settings_system_full", "settings_system", func() map[string]interface{} {
@@ -399,5 +431,18 @@ func i18nDeployData(deployed bool, st stacks.ContainerState, variant int) map[st
d["CurrentHDDPathMissing"] = true d["CurrentHDDPathMissing"] = true
d["StoragePaths"] = nil d["StoragePaths"] = nil
} }
// v0.319.0: the settings tab's login panel and „Cim es kulcsok" rows (app_login.go). The install
// page reads neither key, so its fixtures do not move.
if deployed {
d["CurrentHDDLabel"] = "HDD egy"
d["LoginFields"] = []AppLoginField{
{EnvVar: "ADMIN_USER", Label: "Admin név mező", IsName: true, OnDemand: true, Stored: true},
{EnvVar: "ADMIN_PASSWORD", Label: "Admin jelszó mező", OnDemand: true, Stored: true},
}
d["SettingsRows"] = []AppSettingRow{
{EnvVar: "SECRET_KEY", Label: "Titkos kulcs érték", OnDemand: true},
{EnvVar: "LANG_SEL", Label: "Felület nyelve", Value: "Magyar nyelv"},
}
}
return d return d
} }
@@ -115,9 +115,11 @@ func TestAppInfo_HeroPlaceholder_ScreenshotsUntouched(t *testing.T) {
if !strings.Contains(html, canonical3Step) { if !strings.Contains(html, canonical3Step) {
t.Error("app_info hero logo must use the canonical 3-step chain") t.Error("app_info hero logo must use the canonical 3-step chain")
} }
// Screenshots must be byte-identical to before — they still vanish on error, no placeholder. // Screenshots still vanish on error, no placeholder. v0.319.0: through appShotGone, which also
if !strings.Contains(html, `class="app-screenshot"`) || !strings.Contains(html, `onerror="this.style.display='none'"`) { // hides the screenshots panel when the last one is gone — a panel around nothing is not drawn.
t.Error("app_info screenshot imgs must keep their original vanish-on-error onerror") if !strings.Contains(html, `class="app-screenshot"`) || !strings.Contains(html, `onerror="appShotGone(this)"`) ||
!strings.Contains(html, "img.style.display = 'none';") {
t.Error("app_info screenshot imgs must vanish on error")
} }
if strings.Contains(html, `class="app-screenshot" `) && strings.Contains(html, `app-screenshot" data-fallback`) { if strings.Contains(html, `class="app-screenshot" `) && strings.Contains(html, `app-screenshot" data-fallback`) {
t.Error("app_info screenshots must NOT gain a placeholder fallback") t.Error("app_info screenshots must NOT gain a placeholder fallback")
+24 -29
View File
@@ -173,22 +173,11 @@ deploy_fields:
return s return s
} }
// optionTag returns the full <option ...>...</option> block whose value attribute equals path. // Deployed on the NAS → the „Tarhely" row names the NAS, never the default drive.
func optionTag(t *testing.T, body, path string) string { // v0.319.0: the settings tab shows the storage as a row, not a disabled select — the RCA fix 4 rule is
t.Helper() // unchanged: the view states the STORED HDD_PATH.
marker := `value="` + path + `"` // Companion red-proof: make the row read the default drive's label → the first assertion FAILS (the exact
i := strings.Index(body, marker) // S-C lie from the RCA).
if i < 0 {
t.Fatalf("no option with value %q in rendered page", path)
}
start := strings.LastIndex(body[:i], "<option")
end := strings.Index(body[i:], ">")
return body[start : i+end]
}
// Deployed on the NAS → the NAS option carries `selected`; the default drive does NOT.
// Companion red-proof: revert deploy.html to the IsDefault-only selection → the default option is
// selected instead → both assertions FAIL (the exact S-C lie from the RCA).
func TestDeployPage_DeployedSelectShowsStoredHDDPath(t *testing.T) { func TestDeployPage_DeployedSelectShowsStoredHDDPath(t *testing.T) {
s := testDeployPageServer(t, "/mnt/felhom-drives/nas-media") s := testDeployPageServer(t, "/mnt/felhom-drives/nas-media")
// v0.315.0: an installed app's fields are the app page's settings TAB. The old /stacks/…/deploy // v0.315.0: an installed app's fields are the app page's settings TAB. The old /stacks/…/deploy
@@ -197,28 +186,34 @@ func TestDeployPage_DeployedSelectShowsStoredHDDPath(t *testing.T) {
if rec.Code != 200 { if rec.Code != 200 {
t.Fatalf("GET settings tab = %d: %s", rec.Code, rec.Body.String()) t.Fatalf("GET settings tab = %d: %s", rec.Code, rec.Body.String())
} }
body := rec.Body.String() row := storageRow(t, rec.Body.String())
if nas := optionTag(t, body, "/mnt/felhom-drives/nas-media"); !strings.Contains(nas, "selected") { if !strings.Contains(row, "nas-media") {
t.Errorf("the STORED path's option must be selected, got: %s", nas) t.Errorf("the storage row must name the STORED path's drive, got: %s", row)
} }
if usb := optionTag(t, body, "/mnt/felhom-drives/felhom-usb"); strings.Contains(usb, "selected") { if strings.Contains(row, "felhom-usb") {
t.Errorf("the default drive must NOT be selected for a deployed app, got: %s", usb) t.Errorf("the storage row must NOT name the default drive for a deployed app, got: %s", row)
} }
} }
// Stored path absent from the schedulable list → an extra disabled option names it verbatim (the // storageRow returns the settings tab's „Tarhely" value cell.
// view must never silently show a different storage than app.yaml). func storageRow(t *testing.T, body string) string {
t.Helper()
i := strings.Index(body, `data-app-storage-row="1"`)
if i < 0 {
t.Fatalf("no storage row in the rendered settings tab")
}
return body[i : i+strings.Index(body[i:], "</div>")]
}
// Stored path absent from the schedulable list → the row names it verbatim as not available (the view
// must never silently show a different storage than app.yaml).
func TestDeployPage_MissingStoredPathRendersTruthOption(t *testing.T) { func TestDeployPage_MissingStoredPathRendersTruthOption(t *testing.T) {
s := testDeployPageServer(t, "/mnt/felhom-drives/gone") s := testDeployPageServer(t, "/mnt/felhom-drives/gone")
rec := getPage(t, s, "/apps/testapp/settings") rec := getPage(t, s, "/apps/testapp/settings")
if rec.Code != 200 { if rec.Code != 200 {
t.Fatalf("GET settings tab = %d: %s", rec.Code, rec.Body.String()) t.Fatalf("GET settings tab = %d: %s", rec.Code, rec.Body.String())
} }
body := rec.Body.String() if row := storageRow(t, rec.Body.String()); !strings.Contains(row, "/mnt/felhom-drives/gone (nem elérhető)") {
if !strings.Contains(body, "/mnt/felhom-drives/gone (nem elérhető)") { t.Errorf("a missing stored path must render as its own path, not available; got: %s", row)
t.Errorf("missing stored path must render as a disabled truth option")
}
if gone := optionTag(t, body, "/mnt/felhom-drives/gone"); !strings.Contains(gone, "selected") || !strings.Contains(gone, "disabled") {
t.Errorf("the truth option must be selected+disabled, got: %s", gone)
} }
} }
@@ -18,7 +18,7 @@
{{if .FlashError}}<div class="monitoring-banner monitoring-banner-red">{{.FlashError}}</div>{{end}} {{if .FlashError}}<div class="monitoring-banner monitoring-banner-red">{{.FlashError}}</div>{{end}}
{{if .DataPathCards}} {{if .DataPathCards}}
<div class="app-section" style="margin-top:1rem"> <div class="app-section">
<h3>{{T "app_info.hova_tegyem_a_fajlokat"}}</h3> <h3>{{T "app_info.hova_tegyem_a_fajlokat"}}</h3>
<p class="form-hint">{{T "app_info.ezeket_a_mappakat_a_fajlkezeloben"}}</p> <p class="form-hint">{{T "app_info.ezeket_a_mappakat_a_fajlkezeloben"}}</p>
<div class="datapath-list"> <div class="datapath-list">
@@ -36,7 +36,7 @@
{{end}} {{end}}
{{if and .Stack.Deployed .MigrateTargets}} {{if and .Stack.Deployed .MigrateTargets}}
<div class="app-section" style="margin-top:1rem"> <div class="app-section">
<h3>{{T "app_info.athelyezes_masik_tarhelyre"}}</h3> <h3>{{T "app_info.athelyezes_masik_tarhelyre"}}</h3>
<p class="form-hint">{{T "app_info.ennek_az_alkalmazasnak_az_adatait"}}</p> <p class="form-hint">{{T "app_info.ennek_az_alkalmazasnak_az_adatait"}}</p>
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.5rem"> <div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.5rem">
@@ -87,7 +87,7 @@ function appMigrate(btn,app,label){
{{/* The nightly copy, stated as a fact. It was a card on the settings page; the files it is about {{/* The nightly copy, stated as a fact. It was a card on the settings page; the files it is about
live here. */}} live here. */}}
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>{{T "deploy.biztonsagi_mentes"}}</h3> <h3>{{T "deploy.biztonsagi_mentes"}}</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -255,13 +255,13 @@ function appMigrate(btn,app,label){
</button> </button>
</div> </div>
<div id="progressCard" class="card" style="max-width:700px;display:none"> <div id="progressCard" class="app-section" style="display:none">
<h3>{{T "app_export.folyamat"}}</h3> <h3>{{T "app_export.folyamat"}}</h3>
<div id="progressSteps"></div> <div id="progressSteps"></div>
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div> <div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
</div> </div>
<div id="doneCard" class="card" style="max-width:700px;display:none"> <div id="doneCard" class="app-section" style="display:none">
<h3 style="color:var(--blue)">{{T "app_export.kesz"}}</h3> <h3 style="color:var(--blue)">{{T "app_export.kesz"}}</h3>
<div style="margin-bottom:1rem"> <div style="margin-bottom:1rem">
<span id="doneFile" style="font-weight:600"></span> <span id="doneFile" style="font-weight:600"></span>
@@ -1,9 +1,11 @@
{{/* THE APP'S CONFIGURATION FIELDS — shared by the install wizard and the settings tab. {{/* THE APP'S CONFIGURATION FIELDS — the install wizard's form (deploy.html).
ONE copy of this markup, rendered by two surfaces, because they are the same fields: the v0.319.0: the settings tab no longer renders this — it shows the install answers as rows and
install page submits them, the settings tab shows what was submitted. `.AlreadyDeployed` is the login as one panel (app_settings.html, app_login.go). The `.AlreadyDeployed` branches below
the single flag every branch below reads, exactly as it did when both surfaces were one page — are NOT dead: `/apps/{slug}/install` still answers for an app that is installed, and renders
that is what keeps the install form and the settings view from drifting apart. this form read-only. Their guards are what keep an installed app's secrets out of that page
(TestDeployPage_DeployedApp_DoesNotCarryTheSecret, TestR709_AnInstalledAppsPasswordIsNotInThePage)
and the parity case `deploy_installed` renders them.
The secret rules are unchanged and load-bearing: The secret rules are unchanged and load-bearing:
R-254 — an INSTALLED app's `type: secret` value is not in the page; the eye fetches it. R-254 — an INSTALLED app's `type: secret` value is not in the page; the eye fetches it.
@@ -14,21 +14,33 @@
Everything here is PLAIN TEXT through html/template. No markdown, no raw HTML: a step that Everything here is PLAIN TEXT through html/template. No markdown, no raw HTML: a step that
contains a tag is shown as the characters the author typed. */}} contains a tag is shown as the characters the author typed. */}}
<!-- Screenshots (graceful — hidden if assets don't exist) --> <script>
// Defined BEFORE the images: an error event can fire as soon as an <img> is parsed.
function appShotGone(img) {
img.style.display = 'none';
var box = img.parentNode, left = false;
box.querySelectorAll('img').forEach(function (i) { if (i.style.display !== 'none') { left = true; } });
if (!left) { box.closest('.app-section').style.display = 'none'; }
}
</script>
<!-- Screenshots (graceful — each hidden if its asset does not exist, and the section with the last
one, so a panel is never drawn around nothing) -->
<div class="app-section" data-screenshots-section="1">
<div class="app-screenshots" id="screenshots"> <div class="app-screenshots" id="screenshots">
<img src="{{screenshotURL .Meta.Slug 1}}" alt="" class="app-screenshot" <img src="{{screenshotURL .Meta.Slug 1}}" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="{{screenshotURL .Meta.Slug 2}}" alt="" class="app-screenshot" <img src="{{screenshotURL .Meta.Slug 2}}" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="{{screenshotURL .Meta.Slug 3}}" alt="" class="app-screenshot" <img src="{{screenshotURL .Meta.Slug 3}}" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
</div>
</div> </div>
{{if not .Deployed}} {{if not .Deployed}}
{{/* The install button lives on this tab, because this tab IS the page for a not-installed app. {{/* The install button lives on this tab, because this tab IS the page for a not-installed app.
The server refuses a non-installable template regardless; hiding the button keeps the page The server refuses a non-installable template regardless; hiding the button keeps the page
from offering an action that would be rejected. */}} from offering an action that would be rejected. */}}
<div class="app-section" style="margin-top:1rem"> <div class="app-section">
{{if canInstall .Meta}} {{if canInstall .Meta}}
<a href="/apps/{{.Meta.Slug}}/install" class="btn btn-primary" data-install-cta="1" <a href="/apps/{{.Meta.Slug}}/install" class="btn btn-primary" data-install-cta="1"
onclick="return checkBeforeDeploy(event, '{{.Stack.Name}}')">{{T "app_info.telepites"}}</a> onclick="return checkBeforeDeploy(event, '{{.Stack.Name}}')">{{T "app_info.telepites"}}</a>
@@ -71,7 +83,7 @@
{{if .AppInfo.Guide.Rendered}} {{if .AppInfo.Guide.Rendered}}
<div class="app-guide" data-guide="1" data-guide-checked="{{.AppInfo.Guide.CheckedVersion}}"> <div class="app-guide" data-guide="1" data-guide-checked="{{.AppInfo.Guide.CheckedVersion}}">
{{- range .AppInfo.Guide.Sections}} {{- range .AppInfo.Guide.Sections}}
<div class="app-section" style="margin-top:1rem" data-guide-section="1"> <div class="app-section" data-guide-section="1">
<h3>{{.Title}}</h3> <h3>{{.Title}}</h3>
{{if .Intro}}<p>{{.Intro}}</p>{{end}} {{if .Intro}}<p>{{.Intro}}</p>{{end}}
{{if .Steps}} {{if .Steps}}
@@ -82,7 +94,7 @@
</div> </div>
{{- end}} {{- end}}
{{- if .AppInfo.Guide.FAQ}} {{- if .AppInfo.Guide.FAQ}}
<div class="app-section" style="margin-top:1rem" data-guide-faq="1"> <div class="app-section" data-guide-faq="1">
<h3>{{T "app_page.faq"}}</h3> <h3>{{T "app_page.faq"}}</h3>
<dl class="app-faq"> <dl class="app-faq">
{{- range .AppInfo.Guide.FAQ}} {{- range .AppInfo.Guide.FAQ}}
@@ -94,7 +106,7 @@
{{- end}} {{- end}}
</div> </div>
{{else}} {{else}}
<p class="form-hint" style="margin-top:1rem" data-guide-missing="1">{{T "app_page.guide_missing"}}</p> <div class="app-section" data-guide-missing="1"><p>{{T "app_page.guide_missing"}}</p></div>
{{end}} {{end}}
<div class="app-sections"> <div class="app-sections">
@@ -7,7 +7,7 @@
the reason the tabs are four URLs and not four hidden panels: a poller on the settings tab the reason the tabs are four URLs and not four hidden panels: a poller on the settings tab
would ask the box for 200 log lines every three seconds for nothing. */}} would ask the box for 200 log lines every three seconds for nothing. */}}
<div data-tab-panel="logs"> <div class="app-section" data-tab-panel="logs">
<div class="logs-container" id="logs-container"> <div class="logs-container" id="logs-container">
<pre class="logs-output" id="logs-output">{{.Logs}}</pre> <pre class="logs-output" id="logs-output">{{.Logs}}</pre>
</div> </div>
@@ -17,7 +17,7 @@
„Adatok es mentes" holds everything about the FILES, including the backup card that used to „Adatok es mentes" holds everything about the FILES, including the backup card that used to
sit on this page. */}} sit on this page. */}}
<div class="deploy-container"> <div class="app-settings-tab">
{{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</div>{{end}} {{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</div>{{end}}
{{if .FlashError}}<div class="flash flash-error">{{.FlashError}}</div>{{end}} {{if .FlashError}}<div class="flash flash-error">{{.FlashError}}</div>{{end}}
@@ -62,13 +62,13 @@
{{/* 3. THE REST OF WHAT THE APP WANTS FROM THE HOUSEHOLD. */}} {{/* 3. THE REST OF WHAT THE APP WANTS FROM THE HOUSEHOLD. */}}
{{- if .InstallHoldClosed}} {{- if .InstallHoldClosed}}
<div class="app-section" style="margin-top:1rem" id="install-hold-card"> <div class="app-section" id="install-hold-card">
<h3>{{T "app_info.install_hold_title"}}</h3> <h3>{{T "app_info.install_hold_title"}}</h3>
<p>{{T "app_info.install_hold_closed"}}</p> <p>{{T "app_info.install_hold_closed"}}</p>
</div> </div>
{{- end}} {{- end}}
{{- if .SetupGateClosed}} {{- if .SetupGateClosed}}
<div class="app-section" style="margin-top:1rem" id="setup-gate-card"> <div class="app-section" id="setup-gate-card">
<h3>{{T "app_info.setup_gate_title"}}</h3> <h3>{{T "app_info.setup_gate_title"}}</h3>
<p>{{T "app_info.setup_gate_closed"}}</p>{{- if .SignupNative}} <p>{{T "app_info.setup_gate_closed"}}</p>{{- if .SignupNative}}
<p class="form-hint" data-restarts-once="true">{{T "app_info.restarts_once"}}</p>{{- end}} <p class="form-hint" data-restarts-once="true">{{T "app_info.restarts_once"}}</p>{{- end}}
@@ -84,7 +84,7 @@
{{- end}} {{- end}}
{{- if .CloseSignupOffered}} {{- if .CloseSignupOffered}}
<div class="app-section" style="margin-top:1rem" id="close-signup-card"> <div class="app-section" id="close-signup-card">
<h3>{{T "app_info.signup_title"}}</h3> <h3>{{T "app_info.signup_title"}}</h3>
<p>{{T "app_info.close_signup_text"}}</p>{{- if .SignupNative}} <p>{{T "app_info.close_signup_text"}}</p>{{- if .SignupNative}}
<p class="form-hint" data-restarts-once="true">{{T "app_info.restarts_once"}}</p>{{- end}} <p class="form-hint" data-restarts-once="true">{{T "app_info.restarts_once"}}</p>{{- end}}
@@ -93,7 +93,7 @@
</div> </div>
{{- end}} {{- end}}
{{- if or .SignupClosed .SignupOpenUntil}} {{- if or .SignupClosed .SignupOpenUntil}}
<div class="app-section" style="margin-top:1rem" id="signup-card"> <div class="app-section" id="signup-card">
<h3>{{T "app_info.signup_title"}}</h3> <h3>{{T "app_info.signup_title"}}</h3>
{{- if .SignupOpenUntil}} {{- if .SignupOpenUntil}}
<p>{{T "app_info.signup_open_until"}} {{.SignupOpenUntil}}</p> <p>{{T "app_info.signup_open_until"}} {{.SignupOpenUntil}}</p>
@@ -121,6 +121,35 @@
{{/* The logins, each already its own guarded card on the details page. */}} {{/* The logins, each already its own guarded card on the details page. */}}
<div class="app-sections"> <div class="app-sections">
{{/* v0.319.0 — „BELEPES AZ ALKALMAZASBA": the name and the password of the app's first login,
side by side. The values are fetched on demand (R-254, R-709); the page learns only whether
the box holds one (app_login.go), and a field it holds none for is a sentence, not an eye. */}}
{{if .LoginFields}}
<div class="app-section" id="app-login-panel" data-login-panel="1">
<h3>{{T "app_page.login_title"}}</h3>
<p>{{T "app_page.login_desc"}}</p>
<div class="app-login-pair">
{{- range .LoginFields}}
<div class="app-login-field" data-login-field="{{.EnvVar}}">
<label class="app-login-label" for="field-{{.EnvVar}}">{{.Label}}</label>
{{- if .Restored}}
<p class="form-hint" data-login-restored="{{.EnvVar}}">{{T "deploy.login_from_backup"}}</p>
{{- else if not .Stored}}
<p class="form-hint" data-login-not-stored="{{.EnvVar}}">{{if .IsName}}{{T "app_page.login_no_name"}}{{else}}{{T "app_page.login_no_password"}}{{end}}</p>
{{- else if .OnDemand}}
<div class="input-with-button">
<input type="password" id="field-{{.EnvVar}}" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" data-login-reveal="{{.EnvVar}}" onclick="revealAutoField('{{$.Stack.Name}}','{{.EnvVar}}', this, 'field-{{.EnvVar}}')">{{T "deploy.megjelenites"}}</button>
</div>
{{- else}}
<input type="text" id="field-{{.EnvVar}}" class="form-control" value="{{.Value}}" readonly>
{{- end}}
</div>
{{- end}}
</div>
</div>
{{end}}
{{if .InitialCreds}} {{if .InitialCreds}}
<div class="app-section"> <div class="app-section">
<h3>{{T "app_info.kezdeti_belepesi_adatok"}}</h3> <h3>{{T "app_info.kezdeti_belepesi_adatok"}}</h3>
@@ -173,8 +202,8 @@
household reached by pressing „Beallitasok" on an installed app. The hint lives with the household reached by pressing „Beallitasok" on an installed app. The hint lives with the
fields it is about, at the bottom of this tab. */}} fields it is about, at the bottom of this tab. */}}
{{if .StorageInfo}} {{if .StorageInfo}}
<div class="deploy-storage-info"> <div class="app-section">
<h4>{{T "deploy.adattarolas"}}</h4> <h3>{{T "deploy.adattarolas"}}</h3>
<div class="settings-grid"> <div class="settings-grid">
<div class="settings-row"> <div class="settings-row">
<span class="settings-label">{{T "deploy.tarhely"}}</span> <span class="settings-label">{{T "deploy.tarhely"}}</span>
@@ -201,8 +230,8 @@
</div> </div>
{{end}} {{end}}
{{if .StaleData}} {{if .StaleData}}
<div class="deploy-stale-data"> <div class="app-section deploy-stale-data">
<h4>{{T "deploy.korabbi_adatok"}}</h4> <h3>{{T "deploy.korabbi_adatok"}}</h3>
<p class="form-hint" style="margin-bottom:1rem"> <p class="form-hint" style="margin-bottom:1rem">
{{T "deploy.az_alkalmazas_adatainak_masolata_megtala"}} {{T "deploy.az_alkalmazas_adatainak_masolata_megtala"}}
</p> </p>
@@ -230,7 +259,7 @@
</div> </div>
{{end}} {{end}}
{{if .HasIntegrations}} {{if .HasIntegrations}}
<div class="app-optional-config"> <div class="app-section">
<h3>{{T "deploy.integraciok"}}</h3> <h3>{{T "deploy.integraciok"}}</h3>
<p class="config-group-desc"> <p class="config-group-desc">
{{T "deploy.mas_telepitett_alkalmazasokkal_valo_ossz"}} {{T "deploy.mas_telepitett_alkalmazasokkal_valo_ossz"}}
@@ -290,7 +319,7 @@
{{end}} {{end}}
{{if .GeoGlobalEnabled}} {{if .GeoGlobalEnabled}}
<div class="app-optional-config"> <div class="app-section">
<h3>{{T "deploy.foldrajzi_korlatozas"}}</h3> <h3>{{T "deploy.foldrajzi_korlatozas"}}</h3>
<p class="config-group-desc"> <p class="config-group-desc">
{{T "deploy.az_alkalmazas_egyeni_orszagkorlatozas_ne"}} {{T "deploy.az_alkalmazas_egyeni_orszagkorlatozas_ne"}}
@@ -425,7 +454,7 @@
{{end}} {{end}}
{{if .HasOptionalConfig}} {{if .HasOptionalConfig}}
<div class="app-optional-config"> <div class="app-section">
<h3>{{T "deploy.opcionalis_beallitasok"}}</h3> <h3>{{T "deploy.opcionalis_beallitasok"}}</h3>
{{range .OptionalConfig}} {{range .OptionalConfig}}
<div class="config-group"> <div class="config-group">
@@ -519,7 +548,7 @@
{{end}} {{end}}
{{if .AppEmailSupported}} {{if .AppEmailSupported}}
<div class="app-optional-config"> <div class="app-section">
<h3>{{T "deploy.email_kuldes"}}</h3> <h3>{{T "deploy.email_kuldes"}}</h3>
<p class="config-group-desc"> <p class="config-group-desc">
{{T "deploy.ez_az_alkalmazas_tud_emailt"}} {{T "deploy.ez_az_alkalmazas_tud_emailt"}}
@@ -542,18 +571,38 @@
</form> </form>
</div> </div>
{{end}} {{end}}
<div class="app-section"> {{/* „CIM ES KULCSOK" (v0.319.0) — what was set at install, as rows. The hint that says they
cannot be changed sits under the title it is about, not at the bottom of the tab. The domain,
the subdomain and the data folder are the „Cim" and „Tarhely" rows; the login is the panel
above; everything else is a row here (app_login.go appSettingRows). */}}
<div class="app-section" data-address-keys="1">
<h3>{{T "app_page.address_and_keys"}}</h3> <h3>{{T "app_page.address_and_keys"}}</h3>
<p class="app-field-hint" data-readonly-hint="1">{{T "app_page.installed_readonly"}}</p>
{{if .EffectiveSubdomain}} {{if .EffectiveSubdomain}}
<div class="app-field-row"> <div class="app-field-row">
<div class="app-field-label">{{T "app_page.address"}}</div> <div class="app-field-label">{{T "app_page.address"}}</div>
<div class="app-field-value" data-app-address-row="1">{{.EffectiveSubdomain}}.{{.Domain}}</div> <div class="app-field-value" data-app-address-row="1">{{.EffectiveSubdomain}}.{{.Domain}}</div>
</div> </div>
{{end}} {{end}}
<form id="deploy-form" class="deploy-form"> {{if .CurrentHDDPath}}
{{template "app_deploy_fields" .}} <div class="app-field-row">
</form> <div class="app-field-label">{{T "deploy.tarhely"}}</div>
<p class="app-field-hint" data-readonly-hint="1">{{T "app_page.installed_readonly"}}</p> <div class="app-field-value" data-app-storage-row="1">{{if .CurrentHDDPathMissing}}{{T "deploy.nem_elerheto"}}{{else if .CurrentHDDLabel}}{{.CurrentHDDLabel}}{{else}}<span class="mono">{{.CurrentHDDPath}}</span>{{end}}</div>
</div>
{{end}}
{{- range .SettingsRows}}
<div class="app-field-row" data-setting-row="{{.EnvVar}}">
<div class="app-field-label">{{.Label}}</div>
{{- if .OnDemand}}
<div class="app-field-value input-with-button">
<input type="password" id="auto-field-{{.EnvVar}}" class="form-control" value="" placeholder="••••••••••••" readonly aria-label="{{.Label}}">
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('{{$.Stack.Name}}','{{.EnvVar}}', this)">{{T "deploy.megjelenites"}}</button>
</div>
{{- else}}
<div class="app-field-value">{{.Value}}</div>
{{- end}}
</div>
{{- end}}
</div> </div>
</div> </div>
@@ -612,46 +661,9 @@ function icCopyPw(btn) {
{{template "app_press_script" .}} {{template "app_press_script" .}}
<script> <script>
function checkStorageSpace(sel) { // R-254 site two: on an already-deployed app no secret is in this page — it is fetched on demand,
var opt = sel.options[sel.selectedIndex]; // and the server records the act. Nothing caches it between presses. Since v0.319.0 the login
var warn = document.getElementById('storage-space-warn'); // panel's name and password use this too (R-709: a `type: password` value, fetched like a secret).
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
// Check on page load
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
// R-254 site two: on an already-deployed app the value is NOT in this page — it is fetched on
// demand, and the server records the act. Nothing caches it between presses.
// R-709 (v0.280.0): an installed app's `type: password` value, fetched on demand like a secret.
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || '{{T "deploy.a_lekeres_nem_sikerult"}}'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('{{T "deploy.a_lekeres_nem_sikerult"}}'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
+63 -35
View File
@@ -1300,13 +1300,17 @@ a.stat-card:hover {
.login-footer a:hover { color: var(--blue); } .login-footer a:hover { color: var(--blue); }
/* --- App Info Page --- */ /* --- App Info Page --- */
/* ── THE APP PAGE (v0.316.0) — one column, hairlines, boxes only where they mean something ─────── /* ── THE APP PAGE (v0.319.0) — one column, the header, the tab bar and every section a panel ─────
Operator-approved mockup, 2026-10-15. The rules, and why each one: Operator-approved mockup „Felhom app oldal — egy oszlop", 2026-10-16, section style A. The rules:
ONE COLUMN of ONE WIDTH. Boxes of different widths read as a page assembled by accident. ONE COLUMN of ONE WIDTH. Every panel is the column's full width; panels of different widths
SECTIONS ARE SEPARATED BY HAIRLINES, not drawn as cards. A card says "this is a thing you act read as a page assembled by accident.
on"; most of an app page is a thing you READ, so a card there is a promise it cannot keep. PANELS ON --bg-1. v0.316.0's hairline sections sat on the page background with the header and
A BOX MEANS SOMETHING. Exactly two exist: blue when the app is waiting for the household, amber the tab bar, and the operator found the page flat: nothing told one section from the next.
when something is risky. The exception-colour principle, applied to shape as well as colour. Now the hero, the tab bar and each section are their own panel, 16px apart.
ONE SECTION STYLE ON EVERY SURFACE — the four tabs and the install page. Never mixed. Pinned by
TestAppPage_OneSectionStyleEverywhere.
COLOUR STILL MEANS SOMETHING. A panel is neutral; blue is the app waiting for the household,
amber is a risk. The callouts keep their colours inside or as a panel.
Tokens only — no raw hexes, 2px radius, no shadows. */ Tokens only — no raw hexes, 2px radius, no shadows. */
.app-page { .app-page {
@@ -1330,18 +1334,23 @@ a.stat-card:hover {
} }
.app-back:hover { color: var(--text-1); } .app-back:hover { color: var(--text-1); }
/* HERO — no box, no border. The app's identity is a heading, not a card. */ /* HERO — its own panel. */
.app-hero { .app-hero {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 20px; gap: 20px;
flex-wrap: wrap; flex-wrap: wrap;
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 20px 24px;
margin-top: 16px;
} }
.app-hero-logo { .app-hero-logo {
width: 64px; height: 64px; width: 64px; height: 64px;
min-width: 64px; min-width: 64px;
border: 1px solid var(--line); border: 1px solid var(--line);
background: var(--bg-1); background: var(--bg-2);
border-radius: var(--radius); border-radius: var(--radius);
object-fit: contain; object-fit: contain;
padding: 8px; padding: 8px;
@@ -1364,40 +1373,62 @@ a.stat-card:hover {
worked. A success that needed no decision is not news; it is a date. */ worked. A success that needed no decision is not news; it is a date. */
.app-meta { font-size: 12px; color: var(--text-3); } .app-meta { font-size: 12px; color: var(--text-3); }
/* TABS — links with a line under the open one. No pills, no fills. */ /* TABS — a panel; the open tab is filled and underlined. */
.app-tabs { .app-tabs {
display: flex; display: flex;
gap: 28px; gap: 4px;
flex-wrap: wrap; flex-wrap: wrap;
margin: 28px 0 8px; background: var(--bg-1);
border-bottom: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius);
padding: 4px;
margin: 16px 0 20px;
} }
.app-tab { .app-tab {
font-size: 15px; font-weight: 600; font-size: 15px; font-weight: 600;
padding: 12px 0; padding: 10px 18px;
color: var(--text-2); color: var(--text-2);
text-decoration: none; text-decoration: none;
border-radius: var(--radius);
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
margin-bottom: -1px;
white-space: nowrap; white-space: nowrap;
} }
.app-tab:hover { color: var(--text-1); } .app-tab:hover { color: var(--text-1); }
.app-tab-active { color: var(--text-1); border-bottom-color: var(--blue-bright); } .app-tab-active { color: var(--text-1); background: var(--bg-2); border-bottom-color: var(--blue-bright); }
/* SECTIONS — a hairline between, nothing around. */ /* SECTIONS — style A: each its own panel, the column's width, 16px apart, the title 18px from the
.app-section { top edge with a soft rule under it. The install page's form groups (.form-section inside the
padding: 28px 0; wizard's form) and its progress panel are the same panel: one style on every surface. */
border-bottom: 1px solid var(--line-soft); .app-section,
background: none; .app-page .deploy-form .form-section,
border-left: 0; border-right: 0; border-top: 0; .app-page .deploy-progress {
border-radius: 0; padding: 18px 24px 22px;
margin: 0; background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
margin: 0 0 16px;
} }
.app-section:last-child { border-bottom: 0; } .app-section > h3:first-child, .app-section > h4:first-child,
.app-section > h3, .app-section > h4 { .app-page .deploy-form .form-section > h4:first-child,
font-size: 17px; font-weight: 700; color: var(--text-1); margin: 0 0 6px; .app-page .deploy-progress > h3:first-child {
font-size: 17px; font-weight: 700; color: var(--text-1);
margin: 0 0 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--line-soft);
}
/* A heading further down a panel is a sub-heading, not a second title. */
.app-section > h3:not(:first-child), .app-section > h4:not(:first-child) {
font-size: 14px; font-weight: 600; color: var(--text-1); margin: 16px 0 6px;
} }
.app-section > p { font-size: 14px; color: var(--text-2); line-height: 1.55; margin: 0 0 16px; } .app-section > p { font-size: 14px; color: var(--text-2); line-height: 1.55; margin: 0 0 16px; }
.app-section > :last-child { margin-bottom: 0; }
/* The install wizard's form was one box around all its groups; the groups are the panels now. */
.app-page .deploy-form { background: none; border: 0; padding: 0; }
/* THE LOGIN PAIR — the name and the password side by side, stacked on a phone. */
.app-login-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.app-login-field { min-width: 0; }
.app-login-label { display: block; font-size: 13px; color: var(--text-2); margin-bottom: 6px; }
/* ROWS — label and value, one grid so every value on the page starts at the same x. */ /* ROWS — label and value, one grid so every value on the page starts at the same x. */
.app-field-row { .app-field-row {
@@ -1418,17 +1449,13 @@ a.stat-card:hover {
font-size: 14px; font-size: 14px;
border: 1px solid var(--line); border: 1px solid var(--line);
} }
.app-callout > h3, .app-callout > h4 { font-size: 15px; font-weight: 700; margin: 0 0 6px; color: var(--text-1); } .app-callout > h3, .app-callout > h4 { font-size: 15px; font-weight: 700; margin: 0 0 6px; color: var(--text-1); padding-bottom: 0; border-bottom: 0; }
/* „The app is waiting for you" — a request, never a fault, so blue and never amber. */ /* „The app is waiting for you" — a request, never a fault, so blue and never amber. */
.app-callout-ask { border-color: var(--blue); background: var(--blue-dim); } .app-callout-ask { border-color: var(--blue); background: var(--blue-dim); }
/* „Something here is risky" — the exception colour, and only on a deviation. */ /* „Something here is risky" — the exception colour, and only on a deviation. */
.app-callout-warn { border-color: var(--warn); background: var(--warn-dim); } .app-callout-warn { border-color: var(--warn); background: var(--warn-dim); }
/* The settings tab reuses the install wizard's field markup, which carries that page's boxes: a /* A box inside a panel would be a narrower inner card; the panel is the box. */
card around the whole form and one around every generated value. On a TAB those are sections, so
the boxes come off — the input itself is still a field, which is what tells the household it is a
value. Scoped to .app-section, so the install page (same column, its own form) is untouched. */
.app-section .deploy-form,
.app-section .form-group-auto, .app-section .form-group-auto,
.app-section .app-optional-config, .app-section .app-optional-config,
.app-section .deploy-storage-info, .app-section .deploy-storage-info,
@@ -1450,8 +1477,9 @@ a.stat-card:hover {
.app-page { padding: 0 0 24px; } .app-page { padding: 0 0 24px; }
.app-field-row { grid-template-columns: 1fr; gap: 4px; } .app-field-row { grid-template-columns: 1fr; gap: 4px; }
.app-hero-actions { margin-left: 0; flex-basis: 100%; } .app-hero-actions { margin-left: 0; flex-basis: 100%; }
.app-tabs { gap: 18px; } .app-tab { font-size: 14px; padding: 8px 12px; }
.app-tab { font-size: 14px; } .app-hero, .app-section, .app-page .deploy-form .form-section, .app-page .deploy-progress { padding-left: 16px; padding-right: 16px; }
.app-login-pair { grid-template-columns: 1fr; }
} }
.app-info-hero { .app-info-hero {
@@ -223,7 +223,7 @@
<div class="app-section" style="margin-top:1rem"> <div class="app-section">
<h3>Hova tegyem a fájlokat?</h3> <h3>Hova tegyem a fájlokat?</h3>
<p class="form-hint">Ezeket a mappákat a Fájlkezelőben éred el. Előfordulhat, hogy először be kell jelentkezned a Fájlkezelőbe.</p> <p class="form-hint">Ezeket a mappákat a Fájlkezelőben éred el. Előfordulhat, hogy először be kell jelentkezned a Fájlkezelőbe.</p>
<div class="datapath-list"> <div class="datapath-list">
@@ -241,7 +241,7 @@
<div class="app-section" style="margin-top:1rem"> <div class="app-section">
<h3>Áthelyezés másik tárhelyre</h3> <h3>Áthelyezés másik tárhelyre</h3>
<p class="form-hint">Ennek az alkalmazásnak az adatait másik csatlakoztatott tárhelyre helyezheted át. Az alkalmazás az áthelyezés alatt rövid időre leáll, az adatok pedig csak az ellenőrzés és a sikeres újraindítás után törlődnek a régi helyről.</p> <p class="form-hint">Ennek az alkalmazásnak az adatait másik csatlakoztatott tárhelyre helyezheted át. Az alkalmazás az áthelyezés alatt rövid időre leáll, az adatok pedig csak az ellenőrzés és a sikeres újraindítás után törlődnek a régi helyről.</p>
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.5rem"> <div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-top:.5rem">
@@ -291,7 +291,7 @@ function appMigrate(btn,app,label){
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>Biztonsági mentés</h3> <h3>Biztonsági mentés</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -429,13 +429,13 @@ function appMigrate(btn,app,label){
</button> </button>
</div> </div>
<div id="progressCard" class="card" style="max-width:700px;display:none"> <div id="progressCard" class="app-section" style="display:none">
<h3>Folyamat</h3> <h3>Folyamat</h3>
<div id="progressSteps"></div> <div id="progressSteps"></div>
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div> <div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
</div> </div>
<div id="doneCard" class="card" style="max-width:700px;display:none"> <div id="doneCard" class="app-section" style="display:none">
<h3 style="color:var(--blue)">Kész!</h3> <h3 style="color:var(--blue)">Kész!</h3>
<div style="margin-bottom:1rem"> <div style="margin-bottom:1rem">
<span id="doneFile" style="font-weight:600"></span> <span id="doneFile" style="font-weight:600"></span>
@@ -227,7 +227,7 @@
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>Biztonsági mentés</h3> <h3>Biztonsági mentés</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -309,13 +309,13 @@
</button> </button>
</div> </div>
<div id="progressCard" class="card" style="max-width:700px;display:none"> <div id="progressCard" class="app-section" style="display:none">
<h3>Folyamat</h3> <h3>Folyamat</h3>
<div id="progressSteps"></div> <div id="progressSteps"></div>
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div> <div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
</div> </div>
<div id="doneCard" class="card" style="max-width:700px;display:none"> <div id="doneCard" class="app-section" style="display:none">
<h3 style="color:var(--blue)">Kész!</h3> <h3 style="color:var(--blue)">Kész!</h3>
<div style="margin-bottom:1rem"> <div style="margin-bottom:1rem">
<span id="doneFile" style="font-weight:600"></span> <span id="doneFile" style="font-weight:600"></span>
@@ -227,7 +227,7 @@
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>Biztonsági mentés</h3> <h3>Biztonsági mentés</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -227,7 +227,7 @@
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>Biztonsági mentés</h3> <h3>Biztonsági mentés</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -227,7 +227,7 @@
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>Biztonsági mentés</h3> <h3>Biztonsági mentés</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -227,7 +227,7 @@
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>Biztonsági mentés</h3> <h3>Biztonsági mentés</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -227,7 +227,7 @@
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>Biztonsági mentés</h3> <h3>Biztonsági mentés</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -227,7 +227,7 @@
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>Biztonsági mentés</h3> <h3>Biztonsági mentés</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -227,7 +227,7 @@
<div class="app-section deploy-cross-drive"> <div class="app-section">
<h3>Biztonsági mentés</h3> <h3>Biztonsági mentés</h3>
<div class="cross-drive-nightly"> <div class="cross-drive-nightly">
@@ -207,19 +207,30 @@
<script>
function appShotGone(img) {
img.style.display = 'none';
var box = img.parentNode, left = false;
box.querySelectorAll('img').forEach(function (i) { if (i.style.display !== 'none') { left = true; } });
if (!left) { box.closest('.app-section').style.display = 'none'; }
}
</script>
<div class="app-section" data-screenshots-section="1">
<div class="app-screenshots" id="screenshots"> <div class="app-screenshots" id="screenshots">
<img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot" <img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot" <img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot" <img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
</div>
</div> </div>
<div class="app-section" style="margin-top:1rem"> <div class="app-section">
<p class="form-hint" data-not-installable="1">Ez az alkalmazás most nem telepíthető.</p> <p class="form-hint" data-not-installable="1">Ez az alkalmazás most nem telepíthető.</p>
@@ -236,7 +247,7 @@
<p class="form-hint" style="margin-top:1rem" data-guide-missing="1">Ehhez az alkalmazáshoz még nem készült útmutató.</p> <div class="app-section" data-guide-missing="1"><p>Ehhez az alkalmazáshoz még nem készült útmutató.</p></div>
<div class="app-sections"> <div class="app-sections">
@@ -218,14 +218,25 @@
<script>
function appShotGone(img) {
img.style.display = 'none';
var box = img.parentNode, left = false;
box.querySelectorAll('img').forEach(function (i) { if (i.style.display !== 'none') { left = true; } });
if (!left) { box.closest('.app-section').style.display = 'none'; }
}
</script>
<div class="app-section" data-screenshots-section="1">
<div class="app-screenshots" id="screenshots"> <div class="app-screenshots" id="screenshots">
<img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot" <img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot" <img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot" <img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
</div>
</div> </div>
@@ -262,7 +273,7 @@
<div class="app-guide" data-guide="1" data-guide-checked="2.0.6"> <div class="app-guide" data-guide="1" data-guide-checked="2.0.6">
<div class="app-section" style="margin-top:1rem" data-guide-section="1"> <div class="app-section" data-guide-section="1">
<h3>Első bekapcsolás</h3> <h3>Első bekapcsolás</h3>
<p>Nyisd meg a böngészőben.</p> <p>Nyisd meg a böngészőben.</p>
@@ -270,7 +281,7 @@
</ol> </ol>
</div> </div>
<div class="app-section" style="margin-top:1rem" data-guide-section="1"> <div class="app-section" data-guide-section="1">
<h3>Megosztás másokkal</h3> <h3>Megosztás másokkal</h3>
@@ -278,7 +289,7 @@
</ol> </ol>
</div> </div>
<div class="app-section" style="margin-top:1rem" data-guide-faq="1"> <div class="app-section" data-guide-faq="1">
<h3>Gyakori kérdések</h3> <h3>Gyakori kérdések</h3>
<dl class="app-faq"> <dl class="app-faq">
<dt>Hol vannak a fájlok?</dt> <dt>Hol vannak a fájlok?</dt>
@@ -205,19 +205,30 @@
<script>
function appShotGone(img) {
img.style.display = 'none';
var box = img.parentNode, left = false;
box.querySelectorAll('img').forEach(function (i) { if (i.style.display !== 'none') { left = true; } });
if (!left) { box.closest('.app-section').style.display = 'none'; }
}
</script>
<div class="app-section" data-screenshots-section="1">
<div class="app-screenshots" id="screenshots"> <div class="app-screenshots" id="screenshots">
<img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot" <img src="/static/assets/privatebin-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot" <img src="/static/assets/privatebin-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot" <img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
</div>
</div> </div>
<div class="app-section" style="margin-top:1rem"> <div class="app-section">
<a href="/apps/privatebin/install" class="btn btn-primary" data-install-cta="1" <a href="/apps/privatebin/install" class="btn btn-primary" data-install-cta="1"
onclick="return checkBeforeDeploy(event, 'privatebin')">Telepítés</a> onclick="return checkBeforeDeploy(event, 'privatebin')">Telepítés</a>
@@ -235,7 +246,7 @@
<p class="form-hint" style="margin-top:1rem" data-guide-missing="1">Ehhez az alkalmazáshoz még nem készült útmutató.</p> <div class="app-section" data-guide-missing="1"><p>Ehhez az alkalmazáshoz még nem készült útmutató.</p></div>
<div class="app-sections"> <div class="app-sections">
@@ -205,19 +205,30 @@
<script>
function appShotGone(img) {
img.style.display = 'none';
var box = img.parentNode, left = false;
box.querySelectorAll('img').forEach(function (i) { if (i.style.display !== 'none') { left = true; } });
if (!left) { box.closest('.app-section').style.display = 'none'; }
}
</script>
<div class="app-section" data-screenshots-section="1">
<div class="app-screenshots" id="screenshots"> <div class="app-screenshots" id="screenshots">
<img src="/static/assets/grocy-screenshot-1.webp" alt="" class="app-screenshot" <img src="/static/assets/grocy-screenshot-1.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="/static/assets/grocy-screenshot-2.webp" alt="" class="app-screenshot" <img src="/static/assets/grocy-screenshot-2.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
<img src="/static/assets/grocy-screenshot-3.webp" alt="" class="app-screenshot" <img src="/static/assets/grocy-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'"> onerror="appShotGone(this)">
</div>
</div> </div>
<div class="app-section" style="margin-top:1rem"> <div class="app-section">
<a href="/apps/grocy/install" class="btn btn-primary" data-install-cta="1" <a href="/apps/grocy/install" class="btn btn-primary" data-install-cta="1"
onclick="return checkBeforeDeploy(event, 'grocy')">Telepítés</a> onclick="return checkBeforeDeploy(event, 'grocy')">Telepítés</a>
@@ -235,7 +246,7 @@
<p class="form-hint" style="margin-top:1rem" data-guide-missing="1">Ehhez az alkalmazáshoz még nem készült útmutató.</p> <div class="app-section" data-guide-missing="1"><p>Ehhez az alkalmazáshoz még nem készült útmutató.</p></div>
<div class="app-sections"> <div class="app-sections">
+1 -1
View File
@@ -218,7 +218,7 @@
<div data-tab-panel="logs"> <div class="app-section" data-tab-panel="logs">
<div class="logs-container" id="logs-container"> <div class="logs-container" id="logs-container">
<pre class="logs-output" id="logs-output">2026-09-14 line one <pre class="logs-output" id="logs-output">2026-09-14 line one
line two</pre> line two</pre>
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -249,7 +249,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="setup-gate-card"> <div class="app-section" id="setup-gate-card">
<h3>Első beállítás</h3> <h3>Első beállítás</h3>
<p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p> <p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p>
<p class="form-hint">Ha kész, a doboz magától észreveszi, és mindenkinek megnyitja az alkalmazást.</p> <p class="form-hint">Ha kész, a doboz magától észreveszi, és mindenkinek megnyitja az alkalmazást.</p>
@@ -260,6 +260,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -273,17 +276,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -321,46 +319,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="close-signup-card"> <div class="app-section" id="close-signup-card">
<h3>Regisztráció</h3> <h3>Regisztráció</h3>
<p>Ennél az alkalmazásnál bárki regisztrálhat, aki megtalálja a címét. Zárd le a regisztrációt most — a családtagjaidat utána is fel tudod venni.</p> <p>Ennél az alkalmazásnál bárki regisztrálhat, aki megtalálja a címét. Zárd le a regisztrációt most — a családtagjaidat utána is fel tudod venni.</p>
<button type="button" class="btn btn-sm btn-primary" onclick="appPress(this, '/apps/opengist/close-signup')">Regisztráció lezárása most</button> <button type="button" class="btn btn-sm btn-primary" onclick="appPress(this, '/apps/opengist/close-signup')">Regisztráció lezárása most</button>
@@ -247,6 +247,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -260,17 +263,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -308,46 +306,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="close-signup-card"> <div class="app-section" id="close-signup-card">
<h3>Regisztráció</h3> <h3>Regisztráció</h3>
<p>Ennél az alkalmazásnál bárki regisztrálhat, aki megtalálja a címét. Zárd le a regisztrációt most — a családtagjaidat utána is fel tudod venni.</p> <p>Ennél az alkalmazásnál bárki regisztrálhat, aki megtalálja a címét. Zárd le a regisztrációt most — a családtagjaidat utána is fel tudod venni.</p>
<p class="form-hint" data-restarts-once="true">Ehhez az alkalmazás egyszer újraindul, és közben rövid ideig nem érhető el.</p> <p class="form-hint" data-restarts-once="true">Ehhez az alkalmazás egyszer újraindul, és közben rövid ideig nem érhető el.</p>
@@ -248,6 +248,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -261,17 +264,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -309,46 +307,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -229,7 +229,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -252,6 +252,9 @@
<div class="app-sections"> <div class="app-sections">
<div class="app-section"> <div class="app-section">
<h3>Kezdeti belépési adatok</h3> <h3>Kezdeti belépési adatok</h3>
<table class="initcred-table"> <table class="initcred-table">
@@ -297,22 +300,17 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
<div class="app-field-row"> <div class="app-field-row">
<div class="app-field-label">Cím</div> <div class="app-field-label">Cím</div>
<div class="app-field-value" data-app-address-row="1">paste.example.hu</div> <div class="app-field-value" data-app-address-row="1">paste.example.hu</div>
</div> </div>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -401,46 +399,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -220,7 +220,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -242,6 +242,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -255,17 +258,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -303,46 +301,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="install-hold-card"> <div class="app-section" id="install-hold-card">
<h3>Még csak te éred el</h3> <h3>Még csak te éred el</h3>
<p>A doboz most cseréli le az alkalmazás ismert első jelszavát egy saját, generált jelszóra. Addig csak te éred el, amíg be vagy jelentkezve a vezérlőpultba – más nem léphet be az ismert jelszóval. Ha a csere nem sikerül, változtasd meg a jelszót kézzel, és jelezd itt, hogy megtetted.</p> <p>A doboz most cseréli le az alkalmazás ismert első jelszavát egy saját, generált jelszóra. Addig csak te éred el, amíg be vagy jelentkezve a vezérlőpultba – más nem léphet be az ismert jelszóval. Ha a csere nem sikerül, változtasd meg a jelszót kézzel, és jelezd itt, hogy megtetted.</p>
</div> </div>
@@ -245,6 +245,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -258,17 +261,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -306,46 +304,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -241,6 +241,9 @@
<div class="app-sections"> <div class="app-sections">
<div class="app-section"> <div class="app-section">
@@ -262,17 +265,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -310,46 +308,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -241,6 +241,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -254,17 +257,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -302,46 +300,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -228,7 +228,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -251,6 +251,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -264,17 +267,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -312,46 +310,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -219,7 +219,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -242,6 +242,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -255,17 +258,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -303,46 +301,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -224,7 +224,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -246,6 +246,9 @@
<div class="app-sections"> <div class="app-sections">
<div class="app-section"> <div class="app-section">
<h3>Kezdeti belépési adatok</h3> <h3>Kezdeti belépési adatok</h3>
<table class="initcred-table"> <table class="initcred-table">
@@ -279,17 +282,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -378,46 +376,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -222,7 +222,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
<div class="flash flash-success">A mentés sikeresen elkészült.</div> <div class="flash flash-success">A mentés sikeresen elkészült.</div>
<div class="flash flash-error">A művelet most nem sikerült.</div> <div class="flash flash-error">A művelet most nem sikerült.</div>
@@ -245,6 +245,20 @@
<div class="app-sections"> <div class="app-sections">
<div class="app-section" id="app-login-panel" data-login-panel="1">
<h3>Belépés az alkalmazásba</h3>
<p>Az első bejelentkezéshez. Ha az alkalmazásban megváltoztatod a jelszót, itt a régi marad.</p>
<div class="app-login-pair">
<div class="app-login-field" data-login-field="ADMIN_PASSWORD">
<label class="app-login-label" for="field-ADMIN_PASSWORD">Admin jelszó mező</label>
<p class="form-hint" data-login-restored="ADMIN_PASSWORD">Mentésből töltötted vissza: a belépéshez a mentés idején érvényes jelszavad kell. Az itt tárolt érték nem az, ezért nem mutatjuk.</p>
</div>
</div>
</div>
<div class="app-section"> <div class="app-section">
@@ -256,8 +270,8 @@
<div class="deploy-storage-info"> <div class="app-section">
<h4>Adattárolás</h4> <h3>Adattárolás</h3>
<div class="settings-grid"> <div class="settings-grid">
<div class="settings-row"> <div class="settings-row">
<span class="settings-label">Tárhely</span> <span class="settings-label">Tárhely</span>
@@ -284,8 +298,8 @@
</div> </div>
<div class="deploy-stale-data"> <div class="app-section deploy-stale-data">
<h4>Korábbi adatok</h4> <h3>Korábbi adatok</h3>
<p class="form-hint" style="margin-bottom:1rem"> <p class="form-hint" style="margin-bottom:1rem">
Az alkalmazás adatainak másolata megtalálható egy másik tárolón is. Az alkalmazás adatainak másolata megtalálható egy másik tárolón is.
Ez általában áthelyezés után marad hátra. Ez általában áthelyezés után marad hátra.
@@ -314,7 +328,7 @@
</div> </div>
<div class="app-optional-config"> <div class="app-section">
<h3>Integrációk</h3> <h3>Integrációk</h3>
<p class="config-group-desc"> <p class="config-group-desc">
Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor. Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor.
@@ -438,7 +452,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Földrajzi korlátozás</h3> <h3>Földrajzi korlátozás</h3>
<p class="config-group-desc"> <p class="config-group-desc">
Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi. Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi.
@@ -573,7 +587,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Opcionális beállítások</h3> <h3>Opcionális beállítások</h3>
<div class="config-group"> <div class="config-group">
@@ -672,7 +686,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Email-küldés</h3> <h3>Email-küldés</h3>
<p class="config-group-desc"> <p class="config-group-desc">
Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül, Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,
@@ -697,209 +711,33 @@
</form> </form>
</div> </div>
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
<div class="app-field-row"> <div class="app-field-row">
<div class="app-field-label">Cím</div> <div class="app-field-label">Cím</div>
<div class="app-field-value" data-app-address-row="1">paste.example.hu</div> <div class="app-field-value" data-app-address-row="1">paste.example.hu</div>
</div> </div>
<form id="deploy-form" class="deploy-form">
<div class="form-section"> <div class="app-field-row">
<h4>Automatikusan generált értékek</h4> <div class="app-field-label">Tárhely</div>
<div class="app-field-value" data-app-storage-row="1">HDD egy</div>
<p class="form-section-desc">Ezek az értékek automatikusan jöttek létre a telepítéskor.</p>
<div class="form-group form-group-auto">
<label>Titkos kulcs érték <span class="auto-generated-badge"><svg class="ico ico-sm"><use href="#i-check"/></svg> Automatikusan generálva</span></label>
<div class="input-with-button">
<input type="password" id="auto-field-SECRET_KEY" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('privatebin','SECRET_KEY', this)">Megjelenítés</button>
</div>
</div>
<div class="form-group form-group-auto">
<label>Adatbázis neve itt </label>
<input type="text" id="auto-field-DB_NAME" class="form-control" value="privatebin_db" readonly>
</div>
<div class="form-group form-group-auto">
<label>Üres mező értéke </label>
</div>
</div> </div>
<div class="app-field-row" data-setting-row="SECRET_KEY">
<div class="app-field-label">Titkos kulcs érték</div>
<div class="form-section"> <div class="app-field-value input-with-button">
<h4>Beállítások</h4> <input type="password" id="auto-field-SECRET_KEY" class="form-control" value="" placeholder="••••••••••••" readonly aria-label="Titkos kulcs érték">
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('privatebin','SECRET_KEY', this)">Megjelenítés</button>
<div class="form-group">
<label for="field-LANG_SEL">
Felület nyelve
</label>
<select id="field-LANG_SEL" name="LANG_SEL" class="form-control"
disabled>
<option value="hu">Magyar nyelv</option>
</select>
</div> </div>
<div class="form-group">
<label for="field-ADMIN_PASSWORD">
Admin jelszó mező
<span class="required">*</span>
<span class="locked-hint">telepítés után nem módosítható</span>
</label>
<div class="input-with-button">
<input type="password" id="field-ADMIN_PASSWORD" class="form-control" value="" placeholder="••••••••••••" disabled>
</div>
<span class="form-hint">Mentésből töltötted vissza: a belépéshez a mentés idején érvényes jelszavad kell. Az itt tárolt érték nem az, ezért nem mutatjuk.</span>
</div>
<div class="form-group">
<label for="field-FLAG">
Kapcsoló mező itt
</label>
<label class="toggle">
<input type="checkbox" id="field-FLAG" name="FLAG" value="true"
disabled>
<span class="toggle-label">Igen</span>
</label>
<span class="form-hint">Egy kapcsoló leírása</span>
</div>
<div class="form-group">
<label for="field-HDD_PATH">
Adatok helye itt
<span class="required">*</span>
</label>
<select id="field-HDD_PATH" name="HDD_PATH" class="form-control"
disabled
onchange="checkStorageSpace(this)">
<option value="/mnt/hdd1" data-free-percent="50"
selected>
HDD egy — 500 GB szabad (alapértelmezett)
</option>
<option value="/mnt/nas" data-free-percent="90"
disabled
>
NAS egy — 900 GB szabad (hálózati tárhely nem használható)
</option>
<option value="/mnt/hdd2" data-free-percent="10"
>
HDD kettő — 100 GB szabad
</option>
</select>
<div id="storage-space-warn" class="form-hint" style="color:var(--warn);display:none">
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A kiválasztott tárhely majdnem megtelt.
</div>
<div class="form-hint" style="margin-top:.4rem;opacity:.8">
<svg class="ico ico-sm"><use href="#i-info"/></svg> A kiválasztott meghajtón az alkalmazás <strong>fájljai</strong> (média, dokumentumok) tárolódnak.
Az <strong>adatbázis a gyors belső SSD-n</strong> fut — és az alkalmazással együtt készül róla biztonsági mentés.
</div>
</div>
<div class="form-group">
<label for="field-NOTE">
Megjegyzés mező
</label>
<input type="text" id="field-NOTE" name="NOTE"
class="form-control" value=""
placeholder="Írj ide valamit"
disabled>
</div>
</div> </div>
<div class="app-field-row" data-setting-row="LANG_SEL">
<div class="app-field-label">Felület nyelve</div>
</form> <div class="app-field-value">Magyar nyelv</div>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> </div>
</div> </div>
</div> </div>
@@ -937,46 +775,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -222,7 +222,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
<div class="flash flash-success">A mentés sikeresen elkészült.</div> <div class="flash flash-success">A mentés sikeresen elkészült.</div>
<div class="flash flash-error">A művelet most nem sikerült.</div> <div class="flash flash-error">A művelet most nem sikerült.</div>
@@ -245,6 +245,30 @@
<div class="app-sections"> <div class="app-sections">
<div class="app-section" id="app-login-panel" data-login-panel="1">
<h3>Belépés az alkalmazásba</h3>
<p>Az első bejelentkezéshez. Ha az alkalmazásban megváltoztatod a jelszót, itt a régi marad.</p>
<div class="app-login-pair">
<div class="app-login-field" data-login-field="ADMIN_USER">
<label class="app-login-label" for="field-ADMIN_USER">Admin név mező</label>
<div class="input-with-button">
<input type="password" id="field-ADMIN_USER" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" data-login-reveal="ADMIN_USER" onclick="revealAutoField('privatebin','ADMIN_USER', this, 'field-ADMIN_USER')">Megjelenítés</button>
</div>
</div>
<div class="app-login-field" data-login-field="ADMIN_PASSWORD">
<label class="app-login-label" for="field-ADMIN_PASSWORD">Admin jelszó mező</label>
<div class="input-with-button">
<input type="password" id="field-ADMIN_PASSWORD" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" data-login-reveal="ADMIN_PASSWORD" onclick="revealAutoField('privatebin','ADMIN_PASSWORD', this, 'field-ADMIN_PASSWORD')">Megjelenítés</button>
</div>
</div>
</div>
</div>
<div class="app-section"> <div class="app-section">
@@ -256,8 +280,8 @@
<div class="deploy-storage-info"> <div class="app-section">
<h4>Adattárolás</h4> <h3>Adattárolás</h3>
<div class="settings-grid"> <div class="settings-grid">
<div class="settings-row"> <div class="settings-row">
<span class="settings-label">Tárhely</span> <span class="settings-label">Tárhely</span>
@@ -284,8 +308,8 @@
</div> </div>
<div class="deploy-stale-data"> <div class="app-section deploy-stale-data">
<h4>Korábbi adatok</h4> <h3>Korábbi adatok</h3>
<p class="form-hint" style="margin-bottom:1rem"> <p class="form-hint" style="margin-bottom:1rem">
Az alkalmazás adatainak másolata megtalálható egy másik tárolón is. Az alkalmazás adatainak másolata megtalálható egy másik tárolón is.
Ez általában áthelyezés után marad hátra. Ez általában áthelyezés után marad hátra.
@@ -314,7 +338,7 @@
</div> </div>
<div class="app-optional-config"> <div class="app-section">
<h3>Integrációk</h3> <h3>Integrációk</h3>
<p class="config-group-desc"> <p class="config-group-desc">
Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor. Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor.
@@ -438,7 +462,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Földrajzi korlátozás</h3> <h3>Földrajzi korlátozás</h3>
<p class="config-group-desc"> <p class="config-group-desc">
Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi. Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi.
@@ -573,7 +597,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Opcionális beállítások</h3> <h3>Opcionális beállítások</h3>
<div class="config-group"> <div class="config-group">
@@ -672,7 +696,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Email-küldés</h3> <h3>Email-küldés</h3>
<p class="config-group-desc"> <p class="config-group-desc">
Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül, Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,
@@ -697,212 +721,33 @@
</form> </form>
</div> </div>
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
<div class="app-field-row"> <div class="app-field-row">
<div class="app-field-label">Cím</div> <div class="app-field-label">Cím</div>
<div class="app-field-value" data-app-address-row="1">paste.example.hu</div> <div class="app-field-value" data-app-address-row="1">paste.example.hu</div>
</div> </div>
<form id="deploy-form" class="deploy-form">
<div class="form-section"> <div class="app-field-row">
<h4>Automatikusan generált értékek</h4> <div class="app-field-label">Tárhely</div>
<div class="app-field-value" data-app-storage-row="1">HDD egy</div>
<p class="form-section-desc">Ezek az értékek automatikusan jöttek létre a telepítéskor.</p>
<div class="form-group form-group-auto">
<label>Titkos kulcs érték <span class="auto-generated-badge"><svg class="ico ico-sm"><use href="#i-check"/></svg> Automatikusan generálva</span></label>
<div class="input-with-button">
<input type="password" id="auto-field-SECRET_KEY" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('privatebin','SECRET_KEY', this)">Megjelenítés</button>
</div>
</div>
<div class="form-group form-group-auto">
<label>Adatbázis neve itt </label>
<input type="text" id="auto-field-DB_NAME" class="form-control" value="privatebin_db" readonly>
</div>
<div class="form-group form-group-auto">
<label>Üres mező értéke </label>
</div>
</div> </div>
<div class="app-field-row" data-setting-row="SECRET_KEY">
<div class="app-field-label">Titkos kulcs érték</div>
<div class="form-section"> <div class="app-field-value input-with-button">
<h4>Beállítások</h4> <input type="password" id="auto-field-SECRET_KEY" class="form-control" value="" placeholder="••••••••••••" readonly aria-label="Titkos kulcs érték">
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('privatebin','SECRET_KEY', this)">Megjelenítés</button>
<div class="form-group">
<label for="field-LANG_SEL">
Felület nyelve
</label>
<select id="field-LANG_SEL" name="LANG_SEL" class="form-control"
disabled>
<option value="hu">Magyar nyelv</option>
</select>
</div> </div>
<div class="form-group">
<label for="field-ADMIN_PASSWORD">
Admin jelszó mező
<span class="required">*</span>
<span class="locked-hint">telepítés után nem módosítható</span>
</label>
<div class="input-with-button">
<input type="password" id="field-ADMIN_PASSWORD" class="form-control" value="" placeholder="••••••••••••" disabled>
<button type="button" class="btn btn-sm btn-outline pw-toggle-btn"
onclick="revealPasswordField('privatebin', 'ADMIN_PASSWORD', this)"
title="Megjelenítés">&#128065;</button>
</div>
<span class="form-hint">Telepítéskor beállított kezdeti jelszó — ha az alkalmazásban megváltoztattad, az itt nem frissül.</span>
</div>
<div class="form-group">
<label for="field-FLAG">
Kapcsoló mező itt
</label>
<label class="toggle">
<input type="checkbox" id="field-FLAG" name="FLAG" value="true"
disabled>
<span class="toggle-label">Igen</span>
</label>
<span class="form-hint">Egy kapcsoló leírása</span>
</div>
<div class="form-group">
<label for="field-HDD_PATH">
Adatok helye itt
<span class="required">*</span>
</label>
<select id="field-HDD_PATH" name="HDD_PATH" class="form-control"
disabled
onchange="checkStorageSpace(this)">
<option value="/mnt/hdd1" data-free-percent="50"
selected>
HDD egy — 500 GB szabad (alapértelmezett)
</option>
<option value="/mnt/nas" data-free-percent="90"
disabled
>
NAS egy — 900 GB szabad (hálózati tárhely nem használható)
</option>
<option value="/mnt/hdd2" data-free-percent="10"
>
HDD kettő — 100 GB szabad
</option>
</select>
<div id="storage-space-warn" class="form-hint" style="color:var(--warn);display:none">
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A kiválasztott tárhely majdnem megtelt.
</div>
<div class="form-hint" style="margin-top:.4rem;opacity:.8">
<svg class="ico ico-sm"><use href="#i-info"/></svg> A kiválasztott meghajtón az alkalmazás <strong>fájljai</strong> (média, dokumentumok) tárolódnak.
Az <strong>adatbázis a gyors belső SSD-n</strong> fut — és az alkalmazással együtt készül róla biztonsági mentés.
</div>
</div>
<div class="form-group">
<label for="field-NOTE">
Megjegyzés mező
</label>
<input type="text" id="field-NOTE" name="NOTE"
class="form-control" value=""
placeholder="Írj ide valamit"
disabled>
</div>
</div> </div>
<div class="app-field-row" data-setting-row="LANG_SEL">
<div class="app-field-label">Felület nyelve</div>
</form> <div class="app-field-value">Magyar nyelv</div>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> </div>
</div> </div>
</div> </div>
@@ -940,46 +785,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="setup-gate-card"> <div class="app-section" id="setup-gate-card">
<h3>Első beállítás</h3> <h3>Első beállítás</h3>
<p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p> <p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p>
<p class="form-hint">Ha kész, nyomd meg ezt a gombot. Addig a telefonos alkalmazások és a család többi tagja nem éri el.</p> <p class="form-hint">Ha kész, nyomd meg ezt a gombot. Addig a telefonos alkalmazások és a család többi tagja nem éri el.</p>
@@ -248,6 +248,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -261,17 +264,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -309,46 +307,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="setup-gate-card"> <div class="app-section" id="setup-gate-card">
<h3>Első beállítás</h3> <h3>Első beállítás</h3>
<p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p> <p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p>
<p class="form-hint" data-restarts-once="true">Ehhez az alkalmazás egyszer újraindul, és közben rövid ideig nem érhető el.</p> <p class="form-hint" data-restarts-once="true">Ehhez az alkalmazás egyszer újraindul, és közben rövid ideig nem érhető el.</p>
@@ -249,6 +249,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -262,17 +265,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -310,46 +308,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="setup-gate-card"> <div class="app-section" id="setup-gate-card">
<h3>Első beállítás</h3> <h3>Első beállítás</h3>
<p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p> <p>Most csak te éred el ezt az alkalmazást, amíg be vagy jelentkezve a vezérlőpultba. Így más nem hozhatja létre az első admin fiókot. Nyisd meg, és végezd el az első beállítást.</p>
<p class="form-hint">Ha kész, a doboz magától észreveszi, és mindenkinek megnyitja az alkalmazást.</p> <p class="form-hint">Ha kész, a doboz magától észreveszi, és mindenkinek megnyitja az alkalmazást.</p>
@@ -248,6 +248,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -261,17 +264,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -309,46 +307,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="signup-card"> <div class="app-section" id="signup-card">
<h3>Regisztráció</h3> <h3>Regisztráció</h3>
<p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p> <p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p>
<p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p> <p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
@@ -248,6 +248,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -261,17 +264,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -309,46 +307,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="signup-card"> <div class="app-section" id="signup-card">
<h3>Regisztráció</h3> <h3>Regisztráció</h3>
<p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p> <p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p>
<p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p> <p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
@@ -250,6 +250,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -263,17 +266,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -311,46 +309,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="signup-card"> <div class="app-section" id="signup-card">
<h3>Regisztráció</h3> <h3>Regisztráció</h3>
<p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p> <p>Az első admin fiók után a regisztráció zárva: idegen nem hozhat létre fiókot.</p>
<p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p> <p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
@@ -249,6 +249,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -262,17 +265,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -310,46 +308,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div> </div>
<div class="app-section" style="margin-top:1rem" id="signup-card"> <div class="app-section" id="signup-card">
<h3>Regisztráció</h3> <h3>Regisztráció</h3>
<p>A regisztráció most nyitva, eddig: 14:05</p> <p>A regisztráció most nyitva, eddig: 14:05</p>
<p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p> <p class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
@@ -246,6 +246,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -259,17 +262,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -307,46 +305,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
<div class="flash flash-success">A mentés sikeresen elkészült.</div> <div class="flash flash-success">A mentés sikeresen elkészült.</div>
<div class="flash flash-error">A művelet most nem sikerült.</div> <div class="flash flash-error">A művelet most nem sikerült.</div>
@@ -240,6 +240,30 @@
<div class="app-sections"> <div class="app-sections">
<div class="app-section" id="app-login-panel" data-login-panel="1">
<h3>Belépés az alkalmazásba</h3>
<p>Az első bejelentkezéshez. Ha az alkalmazásban megváltoztatod a jelszót, itt a régi marad.</p>
<div class="app-login-pair">
<div class="app-login-field" data-login-field="ADMIN_USER">
<label class="app-login-label" for="field-ADMIN_USER">Admin név mező</label>
<div class="input-with-button">
<input type="password" id="field-ADMIN_USER" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" data-login-reveal="ADMIN_USER" onclick="revealAutoField('privatebin','ADMIN_USER', this, 'field-ADMIN_USER')">Megjelenítés</button>
</div>
</div>
<div class="app-login-field" data-login-field="ADMIN_PASSWORD">
<label class="app-login-label" for="field-ADMIN_PASSWORD">Admin jelszó mező</label>
<div class="input-with-button">
<input type="password" id="field-ADMIN_PASSWORD" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" data-login-reveal="ADMIN_PASSWORD" onclick="revealAutoField('privatebin','ADMIN_PASSWORD', this, 'field-ADMIN_PASSWORD')">Megjelenítés</button>
</div>
</div>
</div>
</div>
<div class="app-section"> <div class="app-section">
@@ -251,8 +275,8 @@
<div class="deploy-storage-info"> <div class="app-section">
<h4>Adattárolás</h4> <h3>Adattárolás</h3>
<div class="settings-grid"> <div class="settings-grid">
<div class="settings-row"> <div class="settings-row">
<span class="settings-label">Tárhely</span> <span class="settings-label">Tárhely</span>
@@ -266,7 +290,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Integrációk</h3> <h3>Integrációk</h3>
<p class="config-group-desc"> <p class="config-group-desc">
Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor. Más telepített alkalmazásokkal való összekapcsolás. Az integráció automatikusan felfüggesztődik, ha bármelyik alkalmazás leáll, és újraaktiválódik indításkor.
@@ -390,7 +414,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Földrajzi korlátozás</h3> <h3>Földrajzi korlátozás</h3>
<p class="config-group-desc"> <p class="config-group-desc">
Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi. Az alkalmazás egyéni országkorlátozás nélkül a globális beállítást követi.
@@ -525,7 +549,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Opcionális beállítások</h3> <h3>Opcionális beállítások</h3>
<div class="config-group"> <div class="config-group">
@@ -624,7 +648,7 @@
<div class="app-optional-config"> <div class="app-section">
<h3>Email-küldés</h3> <h3>Email-küldés</h3>
<p class="config-group-desc"> <p class="config-group-desc">
Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül, Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,
@@ -644,184 +668,33 @@
</form> </form>
</div> </div>
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
<div class="app-field-row"> <div class="app-field-row">
<div class="app-field-label">Cím</div> <div class="app-field-label">Cím</div>
<div class="app-field-value" data-app-address-row="1">paste.example.hu</div> <div class="app-field-value" data-app-address-row="1">paste.example.hu</div>
</div> </div>
<form id="deploy-form" class="deploy-form">
<div class="form-section"> <div class="app-field-row">
<h4>Automatikusan generált értékek</h4> <div class="app-field-label">Tárhely</div>
<div class="app-field-value" data-app-storage-row="1">/mnt/gone (nem elérhető)</div>
<p class="form-section-desc">Ezek az értékek automatikusan jöttek létre a telepítéskor.</p>
<div class="form-group form-group-auto">
<label>Titkos kulcs érték <span class="auto-generated-badge"><svg class="ico ico-sm"><use href="#i-check"/></svg> Automatikusan generálva</span></label>
<div class="input-with-button">
<input type="password" id="auto-field-SECRET_KEY" class="form-control" value="" placeholder="••••••••••••" readonly>
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('privatebin','SECRET_KEY', this)">Megjelenítés</button>
</div>
</div>
<div class="form-group form-group-auto">
<label>Adatbázis neve itt </label>
<input type="text" id="auto-field-DB_NAME" class="form-control" value="privatebin_db" readonly>
</div>
<div class="form-group form-group-auto">
<label>Üres mező értéke </label>
</div>
</div> </div>
<div class="app-field-row" data-setting-row="SECRET_KEY">
<div class="app-field-label">Titkos kulcs érték</div>
<div class="form-section"> <div class="app-field-value input-with-button">
<h4>Beállítások</h4> <input type="password" id="auto-field-SECRET_KEY" class="form-control" value="" placeholder="••••••••••••" readonly aria-label="Titkos kulcs érték">
<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('privatebin','SECRET_KEY', this)">Megjelenítés</button>
<div class="form-group">
<label for="field-LANG_SEL">
Felület nyelve
</label>
<select id="field-LANG_SEL" name="LANG_SEL" class="form-control"
disabled>
<option value="hu">Magyar nyelv</option>
</select>
</div> </div>
<div class="form-group">
<label for="field-ADMIN_PASSWORD">
Admin jelszó mező
<span class="required">*</span>
<span class="locked-hint">telepítés után nem módosítható</span>
</label>
<div class="input-with-button">
<input type="password" id="field-ADMIN_PASSWORD" class="form-control" value="" placeholder="••••••••••••" disabled>
<button type="button" class="btn btn-sm btn-outline pw-toggle-btn"
onclick="revealPasswordField('privatebin', 'ADMIN_PASSWORD', this)"
title="Megjelenítés">&#128065;</button>
</div>
<span class="form-hint">Telepítéskor beállított kezdeti jelszó — ha az alkalmazásban megváltoztattad, az itt nem frissül.</span>
</div>
<div class="form-group">
<label for="field-FLAG">
Kapcsoló mező itt
</label>
<label class="toggle">
<input type="checkbox" id="field-FLAG" name="FLAG" value="true"
disabled>
<span class="toggle-label">Igen</span>
</label>
<span class="form-hint">Egy kapcsoló leírása</span>
</div>
<div class="form-group">
<label for="field-HDD_PATH">
Adatok helye itt
<span class="required">*</span>
</label>
<input type="text" id="field-HDD_PATH" name="HDD_PATH"
class="form-control" value=""
placeholder="/mnt/valahol"
required
disabled>
<span class="form-hint" style="color:var(--warn)">Nincs regisztrált adattároló — add meg kézzel az útvonalat</span>
</div>
<div class="form-group">
<label for="field-NOTE">
Megjegyzés mező
</label>
<input type="text" id="field-NOTE" name="NOTE"
class="form-control" value=""
placeholder="Írj ide valamit"
disabled>
</div>
</div> </div>
<div class="app-field-row" data-setting-row="LANG_SEL">
<div class="app-field-label">Felület nyelve</div>
</form> <div class="app-field-value">Magyar nyelv</div>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> </div>
</div> </div>
</div> </div>
@@ -859,46 +732,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
@@ -220,7 +220,7 @@
<div class="deploy-container"> <div class="app-settings-tab">
@@ -242,6 +242,9 @@
<div class="app-sections"> <div class="app-sections">
</div> </div>
@@ -255,17 +258,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<h3>Cím és kulcsok</h3> <h3>Cím és kulcsok</h3>
<form id="deploy-form" class="deploy-form">
</form>
<p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p> <p class="app-field-hint" data-readonly-hint="1">Ezek telepítéskor jöttek létre, és nem módosíthatók.</p>
</div> </div>
</div> </div>
@@ -303,46 +301,9 @@ function appAfterStart(btn, slug, envVar) {
</script> </script>
<script> <script>
function checkStorageSpace(sel) {
var opt = sel.options[sel.selectedIndex];
var warn = document.getElementById('storage-space-warn');
if (!warn) return;
var freePct = parseFloat(opt.getAttribute('data-free-percent') || '100');
warn.style.display = freePct < 20 ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', function() {
var sel = document.querySelector('select[onchange="checkStorageSpace(this)"]');
if (sel) checkStorageSpace(sel);
});
function revealPasswordField(stackName, envVar, btn) {
var el = document.getElementById('field-' + envVar);
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
btn.dataset.shown = '';
return;
}
btn.disabled = true;
fetch('/stacks/' + encodeURIComponent(stackName) + '/auto-field/reveal', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, csrfHeaders()),
credentials: 'same-origin',
body: 'env_var=' + encodeURIComponent(envVar)
}).then(function (r) { return r.json(); }).then(function (j) {
btn.disabled = false;
if (!j.ok) { showAlert(j.error || 'A lekérés nem sikerült.'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('A lekérés nem sikerült.'); });
}
function revealAutoField(stackName, envVar, btn, elId) { function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar)); var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return; if (!el) return;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -248,6 +248,8 @@
"app_page.tab_data": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the „Adatok es mentes\" tab's label. New copy: no page carried this name before the six app pages became four tabs.", "app_page.tab_data": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the „Adatok es mentes\" tab's label. New copy: no page carried this name before the six app pages became four tabs.",
"app_page.tab_guide": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the „Utmutato\" tab's label. New copy.", "app_page.tab_guide": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the „Utmutato\" tab's label. New copy.",
"app_page.tab_logs": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the „Naplo\" tab's label. The logs PAGE said „Naplok\" (plural); the tab is one app's log, so the word is new, not moved.", "app_page.tab_logs": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the „Naplo\" tab's label. The logs PAGE said „Naplok\" (plural); the tab is one app's log, so the word is new, not moved.",
"app_page.value_no": "BORN AS A KEY (v0.319.0, 2026-10-16) -- a boolean install answer shown as a row on the settings tab (appSettingRows); no catalog template has a boolean field today. New copy.",
"deploy.igen": "A TEMPLATE KEY (app_fields.html, the install wizard's toggle) read in Go too since v0.319.0 -- the yes of a boolean install answer shown as a row on the settings tab (appSettingRows). Same text as the template's.",
"err.after_start.failed": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the generic refusal for an after-start action that fails without naming its own sentence. Plex's three messages (err.plex_claim.*) are unchanged; this is the fallback a future app gets for free.", "err.after_start.failed": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the generic refusal for an after-start action that fails without naming its own sentence. Plex's three messages (err.plex_claim.*) are unchanged; this is the fallback a future app gets for free.",
"page.title.app_data": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the data tab's page title. It replaces page.title.tier2_config, whose page no longer exists; the old key was removed in the same commit.", "page.title.app_data": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the data tab's page title. It replaces page.title.tier2_config, whose page no longer exists; the old key was removed in the same commit.",
"page.title.app_guide": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the guide tab's page title. New: the details page it replaces had no title key of its own, it used the app's display name.", "page.title.app_guide": "BORN AS A KEY (v0.315.0, 2026-10-15) -- the guide tab's page title. New: the details page it replaces had no title key of its own, it used the app's display name.",