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)
**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 |
| `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. |
| `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
+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 |
|---|---|---|
| `/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}/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 |
@@ -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
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
two cannot drift.
`app_fields.html` — the install wizard's configuration fields (since v0.319.0 the settings tab shows rows and the login
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)
+5
View File
@@ -118,6 +118,11 @@
"app_page.address_and_keys": "Address and keys",
"app_page.address": "Address",
"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_title": "One more step",
"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": "Cím",
"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_title": "Még egy lépés",
"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 {
for _, n := range appCfg.RestoredLogins {
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
// `type: secret` (R-254) it is fetched on demand (/stacks/<n>/auto-field/reveal).
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 {
if dp.Path == hdd {
inList = true
data["CurrentHDDLabel"] = dp.Label // v0.319.0: the settings tab's „Tarhely" row
break
}
}
@@ -2626,7 +2633,7 @@ func (s *Server) appAutoFieldRevealHandler(w http.ResponseWriter, r *http.Reques
return
}
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")
escrowJSON(w, http.StatusNotFound, nil, s.msg(r, "escrow.no_stored_value"))
return
@@ -187,6 +187,18 @@ func i18nCasesA() []i18nCase {
{"deploy_new_memory_blocked", "deploy", func() map[string]interface{} {
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{} {
return i18nDeployData(true, stacks.StateRunning, 1)
}},
@@ -198,6 +210,26 @@ func i18nCasesA() []i18nCase {
d := i18nDeployData(true, stacks.StateRunning, 1)
d["DeployedFieldValues"] = map[string]string{"SUBDOMAIN": "paste"}
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
}},
{"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["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
}
@@ -115,9 +115,11 @@ func TestAppInfo_HeroPlaceholder_ScreenshotsUntouched(t *testing.T) {
if !strings.Contains(html, canonical3Step) {
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.
if !strings.Contains(html, `class="app-screenshot"`) || !strings.Contains(html, `onerror="this.style.display='none'"`) {
t.Error("app_info screenshot imgs must keep their original vanish-on-error onerror")
// Screenshots still vanish on error, no placeholder. v0.319.0: through appShotGone, which also
// hides the screenshots panel when the last one is gone — a panel around nothing is not drawn.
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`) {
t.Error("app_info screenshots must NOT gain a placeholder fallback")
+24 -29
View File
@@ -173,22 +173,11 @@ deploy_fields:
return s
}
// optionTag returns the full <option ...>...</option> block whose value attribute equals path.
func optionTag(t *testing.T, body, path string) string {
t.Helper()
marker := `value="` + path + `"`
i := strings.Index(body, marker)
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).
// Deployed on the NAS → the „Tarhely" row names the NAS, never the default drive.
// v0.319.0: the settings tab shows the storage as a row, not a disabled select — the RCA fix 4 rule is
// unchanged: the view states the STORED HDD_PATH.
// Companion red-proof: make the row read the default drive's label → the first assertion FAILS (the exact
// S-C lie from the RCA).
func TestDeployPage_DeployedSelectShowsStoredHDDPath(t *testing.T) {
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
@@ -197,28 +186,34 @@ func TestDeployPage_DeployedSelectShowsStoredHDDPath(t *testing.T) {
if rec.Code != 200 {
t.Fatalf("GET settings tab = %d: %s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
if nas := optionTag(t, body, "/mnt/felhom-drives/nas-media"); !strings.Contains(nas, "selected") {
t.Errorf("the STORED path's option must be selected, got: %s", nas)
row := storageRow(t, rec.Body.String())
if !strings.Contains(row, "nas-media") {
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") {
t.Errorf("the default drive must NOT be selected for a deployed app, got: %s", usb)
if strings.Contains(row, "felhom-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
// view must never silently show a different storage than app.yaml).
// storageRow returns the settings tab's „Tarhely" value cell.
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) {
s := testDeployPageServer(t, "/mnt/felhom-drives/gone")
rec := getPage(t, s, "/apps/testapp/settings")
if rec.Code != 200 {
t.Fatalf("GET settings tab = %d: %s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
if !strings.Contains(body, "/mnt/felhom-drives/gone (nem elérhető)") {
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)
if row := storageRow(t, rec.Body.String()); !strings.Contains(row, "/mnt/felhom-drives/gone (nem elérhető)") {
t.Errorf("a missing stored path must render as its own path, not available; got: %s", row)
}
}
@@ -18,7 +18,7 @@
{{if .FlashError}}<div class="monitoring-banner monitoring-banner-red">{{.FlashError}}</div>{{end}}
{{if .DataPathCards}}
<div class="app-section" style="margin-top:1rem">
<div class="app-section">
<h3>{{T "app_info.hova_tegyem_a_fajlokat"}}</h3>
<p class="form-hint">{{T "app_info.ezeket_a_mappakat_a_fajlkezeloben"}}</p>
<div class="datapath-list">
@@ -36,7 +36,7 @@
{{end}}
{{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>
<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">
@@ -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
live here. */}}
<div class="app-section deploy-cross-drive">
<div class="app-section">
<h3>{{T "deploy.biztonsagi_mentes"}}</h3>
<div class="cross-drive-nightly">
@@ -255,13 +255,13 @@ function appMigrate(btn,app,label){
</button>
</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>
<div id="progressSteps"></div>
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
</div>
<div id="doneCard" class="card" style="max-width:700px;display:none">
<div id="doneCard" class="app-section" style="display:none">
<h3 style="color:var(--blue)">{{T "app_export.kesz"}}</h3>
<div style="margin-bottom:1rem">
<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
install page submits them, the settings tab shows what was submitted. `.AlreadyDeployed` is
the single flag every branch below reads, exactly as it did when both surfaces were one page —
that is what keeps the install form and the settings view from drifting apart.
v0.319.0: the settings tab no longer renders this — it shows the install answers as rows and
the login as one panel (app_settings.html, app_login.go). The `.AlreadyDeployed` branches below
are NOT dead: `/apps/{slug}/install` still answers for an app that is installed, and renders
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:
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
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">
<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"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
<img src="{{screenshotURL .Meta.Slug 3}}" alt="" class="app-screenshot"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
</div>
</div>
{{if not .Deployed}}
{{/* 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
from offering an action that would be rejected. */}}
<div class="app-section" style="margin-top:1rem">
<div class="app-section">
{{if canInstall .Meta}}
<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>
@@ -71,7 +83,7 @@
{{if .AppInfo.Guide.Rendered}}
<div class="app-guide" data-guide="1" data-guide-checked="{{.AppInfo.Guide.CheckedVersion}}">
{{- 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>
{{if .Intro}}<p>{{.Intro}}</p>{{end}}
{{if .Steps}}
@@ -82,7 +94,7 @@
</div>
{{- end}}
{{- 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>
<dl class="app-faq">
{{- range .AppInfo.Guide.FAQ}}
@@ -94,7 +106,7 @@
{{- end}}
</div>
{{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}}
<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
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">
<pre class="logs-output" id="logs-output">{{.Logs}}</pre>
</div>
@@ -17,7 +17,7 @@
„Adatok es mentes" holds everything about the FILES, including the backup card that used to
sit on this page. */}}
<div class="deploy-container">
<div class="app-settings-tab">
{{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</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. */}}
{{- 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>
<p>{{T "app_info.install_hold_closed"}}</p>
</div>
{{- end}}
{{- 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>
<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}}
@@ -84,7 +84,7 @@
{{- end}}
{{- 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>
<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}}
@@ -93,7 +93,7 @@
</div>
{{- end}}
{{- 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>
{{- if .SignupOpenUntil}}
<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. */}}
<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}}
<div class="app-section">
<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
fields it is about, at the bottom of this tab. */}}
{{if .StorageInfo}}
<div class="deploy-storage-info">
<h4>{{T "deploy.adattarolas"}}</h4>
<div class="app-section">
<h3>{{T "deploy.adattarolas"}}</h3>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">{{T "deploy.tarhely"}}</span>
@@ -201,8 +230,8 @@
</div>
{{end}}
{{if .StaleData}}
<div class="deploy-stale-data">
<h4>{{T "deploy.korabbi_adatok"}}</h4>
<div class="app-section deploy-stale-data">
<h3>{{T "deploy.korabbi_adatok"}}</h3>
<p class="form-hint" style="margin-bottom:1rem">
{{T "deploy.az_alkalmazas_adatainak_masolata_megtala"}}
</p>
@@ -230,7 +259,7 @@
</div>
{{end}}
{{if .HasIntegrations}}
<div class="app-optional-config">
<div class="app-section">
<h3>{{T "deploy.integraciok"}}</h3>
<p class="config-group-desc">
{{T "deploy.mas_telepitett_alkalmazasokkal_valo_ossz"}}
@@ -290,7 +319,7 @@
{{end}}
{{if .GeoGlobalEnabled}}
<div class="app-optional-config">
<div class="app-section">
<h3>{{T "deploy.foldrajzi_korlatozas"}}</h3>
<p class="config-group-desc">
{{T "deploy.az_alkalmazas_egyeni_orszagkorlatozas_ne"}}
@@ -425,7 +454,7 @@
{{end}}
{{if .HasOptionalConfig}}
<div class="app-optional-config">
<div class="app-section">
<h3>{{T "deploy.opcionalis_beallitasok"}}</h3>
{{range .OptionalConfig}}
<div class="config-group">
@@ -519,7 +548,7 @@
{{end}}
{{if .AppEmailSupported}}
<div class="app-optional-config">
<div class="app-section">
<h3>{{T "deploy.email_kuldes"}}</h3>
<p class="config-group-desc">
{{T "deploy.ez_az_alkalmazas_tud_emailt"}}
@@ -542,18 +571,38 @@
</form>
</div>
{{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>
<p class="app-field-hint" data-readonly-hint="1">{{T "app_page.installed_readonly"}}</p>
{{if .EffectiveSubdomain}}
<div class="app-field-row">
<div class="app-field-label">{{T "app_page.address"}}</div>
<div class="app-field-value" data-app-address-row="1">{{.EffectiveSubdomain}}.{{.Domain}}</div>
</div>
{{end}}
<form id="deploy-form" class="deploy-form">
{{template "app_deploy_fields" .}}
</form>
<p class="app-field-hint" data-readonly-hint="1">{{T "app_page.installed_readonly"}}</p>
{{if .CurrentHDDPath}}
<div class="app-field-row">
<div class="app-field-label">{{T "deploy.tarhely"}}</div>
<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>
@@ -612,46 +661,9 @@ function icCopyPw(btn) {
{{template "app_press_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';
}
// 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"}}'); });
}
// R-254 site two: on an already-deployed app no secret is in this page — it is fetched on demand,
// and the server records the act. Nothing caches it between presses. Since v0.319.0 the login
// panel's name and password use this too (R-709: a `type: password` value, fetched like a secret).
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
+63 -35
View File
@@ -1300,13 +1300,17 @@ a.stat-card:hover {
.login-footer a:hover { color: var(--blue); }
/* --- App Info Page --- */
/* ── THE APP PAGE (v0.316.0) — one column, hairlines, boxes only where they mean something ───────
Operator-approved mockup, 2026-10-15. The rules, and why each one:
ONE COLUMN of ONE WIDTH. Boxes of different widths read as a page assembled by accident.
SECTIONS ARE SEPARATED BY HAIRLINES, not drawn as cards. A card says "this is a thing you act
on"; most of an app page is a thing you READ, so a card there is a promise it cannot keep.
A BOX MEANS SOMETHING. Exactly two exist: blue when the app is waiting for the household, amber
when something is risky. The exception-colour principle, applied to shape as well as colour.
/* ── THE APP PAGE (v0.319.0) — one column, the header, the tab bar and every section a panel ─────
Operator-approved mockup „Felhom app oldal — egy oszlop", 2026-10-16, section style A. The rules:
ONE COLUMN of ONE WIDTH. Every panel is the column's full width; panels of different widths
read as a page assembled by accident.
PANELS ON --bg-1. v0.316.0's hairline sections sat on the page background with the header and
the tab bar, and the operator found the page flat: nothing told one section from the next.
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. */
.app-page {
@@ -1330,18 +1334,23 @@ a.stat-card:hover {
}
.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 {
display: flex;
align-items: flex-start;
gap: 20px;
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 {
width: 64px; height: 64px;
min-width: 64px;
border: 1px solid var(--line);
background: var(--bg-1);
background: var(--bg-2);
border-radius: var(--radius);
object-fit: contain;
padding: 8px;
@@ -1364,40 +1373,62 @@ a.stat-card:hover {
worked. A success that needed no decision is not news; it is a date. */
.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 {
display: flex;
gap: 28px;
gap: 4px;
flex-wrap: wrap;
margin: 28px 0 8px;
border-bottom: 1px solid var(--line);
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 4px;
margin: 16px 0 20px;
}
.app-tab {
font-size: 15px; font-weight: 600;
padding: 12px 0;
padding: 10px 18px;
color: var(--text-2);
text-decoration: none;
border-radius: var(--radius);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
white-space: nowrap;
}
.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. */
.app-section {
padding: 28px 0;
border-bottom: 1px solid var(--line-soft);
background: none;
border-left: 0; border-right: 0; border-top: 0;
border-radius: 0;
margin: 0;
/* SECTIONS — style A: each its own panel, the column's width, 16px apart, the title 18px from the
top edge with a soft rule under it. The install page's form groups (.form-section inside the
wizard's form) and its progress panel are the same panel: one style on every surface. */
.app-section,
.app-page .deploy-form .form-section,
.app-page .deploy-progress {
padding: 18px 24px 22px;
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, .app-section > h4 {
font-size: 17px; font-weight: 700; color: var(--text-1); margin: 0 0 6px;
.app-section > h3:first-child, .app-section > h4:first-child,
.app-page .deploy-form .form-section > h4:first-child,
.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 > :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. */
.app-field-row {
@@ -1418,17 +1449,13 @@ a.stat-card:hover {
font-size: 14px;
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. */
.app-callout-ask { border-color: var(--blue); background: var(--blue-dim); }
/* „Something here is risky" — the exception colour, and only on a deviation. */
.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
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,
/* A box inside a panel would be a narrower inner card; the panel is the box. */
.app-section .form-group-auto,
.app-section .app-optional-config,
.app-section .deploy-storage-info,
@@ -1450,8 +1477,9 @@ a.stat-card:hover {
.app-page { padding: 0 0 24px; }
.app-field-row { grid-template-columns: 1fr; gap: 4px; }
.app-hero-actions { margin-left: 0; flex-basis: 100%; }
.app-tabs { gap: 18px; }
.app-tab { font-size: 14px; }
.app-tab { font-size: 14px; padding: 8px 12px; }
.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 {
@@ -223,7 +223,7 @@
<div class="app-section" style="margin-top:1rem">
<div class="app-section">
<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>
<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>
<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">
@@ -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>
<div class="cross-drive-nightly">
@@ -429,13 +429,13 @@ function appMigrate(btn,app,label){
</button>
</div>
<div id="progressCard" class="card" style="max-width:700px;display:none">
<div id="progressCard" class="app-section" style="display:none">
<h3>Folyamat</h3>
<div id="progressSteps"></div>
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
</div>
<div id="doneCard" class="card" style="max-width:700px;display:none">
<div id="doneCard" class="app-section" style="display:none">
<h3 style="color:var(--blue)">Kész!</h3>
<div style="margin-bottom:1rem">
<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>
<div class="cross-drive-nightly">
@@ -309,13 +309,13 @@
</button>
</div>
<div id="progressCard" class="card" style="max-width:700px;display:none">
<div id="progressCard" class="app-section" style="display:none">
<h3>Folyamat</h3>
<div id="progressSteps"></div>
<div id="progressError" style="display:none;color:var(--crit);margin-top:1rem;font-weight:600"></div>
</div>
<div id="doneCard" class="card" style="max-width:700px;display:none">
<div id="doneCard" class="app-section" style="display:none">
<h3 style="color:var(--blue)">Kész!</h3>
<div style="margin-bottom:1rem">
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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">
<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"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
</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>
@@ -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">
@@ -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">
<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"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
</div>
</div>
@@ -262,7 +273,7 @@
<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>
<p>Nyisd meg a böngészőben.</p>
@@ -270,7 +281,7 @@
</ol>
</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>
@@ -278,7 +289,7 @@
</ol>
</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>
<dl class="app-faq">
<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">
<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"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
<img src="/static/assets/privatebin-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
</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"
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">
@@ -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">
<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"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
<img src="/static/assets/grocy-screenshot-3.webp" alt="" class="app-screenshot"
onerror="this.style.display='none'">
onerror="appShotGone(this)">
</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"
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">
+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">
<pre class="logs-output" id="logs-output">2026-09-14 line one
line two</pre>
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -249,7 +249,7 @@
</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>
<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>
@@ -260,6 +260,9 @@
<div class="app-sections">
</div>
@@ -273,17 +276,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -321,46 +319,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</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>
<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>
@@ -247,6 +247,9 @@
<div class="app-sections">
</div>
@@ -260,17 +263,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -308,46 +306,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</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>
<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>
@@ -248,6 +248,9 @@
<div class="app-sections">
</div>
@@ -261,17 +264,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -309,46 +307,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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-section">
<h3>Kezdeti belépési adatok</h3>
<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>
<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-label">Cím</div>
<div class="app-field-value" data-app-address-row="1">paste.example.hu</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>
@@ -401,46 +399,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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>
@@ -255,17 +258,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -303,46 +301,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</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>
<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>
@@ -245,6 +245,9 @@
<div class="app-sections">
</div>
@@ -258,17 +261,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -306,46 +304,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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-section">
@@ -262,17 +265,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -310,46 +308,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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>
@@ -254,17 +257,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -302,46 +300,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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>
@@ -264,17 +267,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -312,46 +310,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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>
@@ -255,17 +258,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -303,46 +301,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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-section">
<h3>Kezdeti belépési adatok</h3>
<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>
<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>
@@ -378,46 +376,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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-error">A művelet most nem sikerült.</div>
@@ -245,6 +245,20 @@
<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">
@@ -256,8 +270,8 @@
<div class="deploy-storage-info">
<h4>Adattárolás</h4>
<div class="app-section">
<h3>Adattárolás</h3>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Tárhely</span>
@@ -284,8 +298,8 @@
</div>
<div class="deploy-stale-data">
<h4>Korábbi adatok</h4>
<div class="app-section deploy-stale-data">
<h3>Korábbi adatok</h3>
<p class="form-hint" style="margin-bottom:1rem">
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.
@@ -314,7 +328,7 @@
</div>
<div class="app-optional-config">
<div class="app-section">
<h3>Integrációk</h3>
<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.
@@ -438,7 +452,7 @@
<div class="app-optional-config">
<div class="app-section">
<h3>Földrajzi korlátozás</h3>
<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.
@@ -573,7 +587,7 @@
<div class="app-optional-config">
<div class="app-section">
<h3>Opcionális beállítások</h3>
<div class="config-group">
@@ -672,7 +686,7 @@
<div class="app-optional-config">
<div class="app-section">
<h3>Email-küldés</h3>
<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,
@@ -697,209 +711,33 @@
</form>
</div>
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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-label">Cím</div>
<div class="app-field-value" data-app-address-row="1">paste.example.hu</div>
</div>
<form id="deploy-form" class="deploy-form">
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
<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 class="app-field-row">
<div class="app-field-label">Tárhely</div>
<div class="app-field-value" data-app-storage-row="1">HDD egy</div>
</div>
<div class="form-section">
<h4>Beállítások</h4>
<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 class="app-field-row" data-setting-row="SECRET_KEY">
<div class="app-field-label">Titkos kulcs érték</div>
<div class="app-field-value input-with-button">
<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>
<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>
</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 class="app-field-row" data-setting-row="LANG_SEL">
<div class="app-field-label">Felület nyelve</div>
<div class="app-field-value">Magyar nyelv</div>
</div>
</div>
</div>
@@ -937,46 +775,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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-error">A művelet most nem sikerült.</div>
@@ -245,6 +245,30 @@
<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">
@@ -256,8 +280,8 @@
<div class="deploy-storage-info">
<h4>Adattárolás</h4>
<div class="app-section">
<h3>Adattárolás</h3>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">Tárhely</span>
@@ -284,8 +308,8 @@
</div>
<div class="deploy-stale-data">
<h4>Korábbi adatok</h4>
<div class="app-section deploy-stale-data">
<h3>Korábbi adatok</h3>
<p class="form-hint" style="margin-bottom:1rem">
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.
@@ -314,7 +338,7 @@
</div>
<div class="app-optional-config">
<div class="app-section">
<h3>Integrációk</h3>
<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.
@@ -438,7 +462,7 @@
<div class="app-optional-config">
<div class="app-section">
<h3>Földrajzi korlátozás</h3>
<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.
@@ -573,7 +597,7 @@
<div class="app-optional-config">
<div class="app-section">
<h3>Opcionális beállítások</h3>
<div class="config-group">
@@ -672,7 +696,7 @@
<div class="app-optional-config">
<div class="app-section">
<h3>Email-küldés</h3>
<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,
@@ -697,212 +721,33 @@
</form>
</div>
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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-label">Cím</div>
<div class="app-field-value" data-app-address-row="1">paste.example.hu</div>
</div>
<form id="deploy-form" class="deploy-form">
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
<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 class="app-field-row">
<div class="app-field-label">Tárhely</div>
<div class="app-field-value" data-app-storage-row="1">HDD egy</div>
</div>
<div class="form-section">
<h4>Beállítások</h4>
<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 class="app-field-row" data-setting-row="SECRET_KEY">
<div class="app-field-label">Titkos kulcs érték</div>
<div class="app-field-value input-with-button">
<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>
<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>
</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 class="app-field-row" data-setting-row="LANG_SEL">
<div class="app-field-label">Felület nyelve</div>
<div class="app-field-value">Magyar nyelv</div>
</div>
</div>
</div>
@@ -940,46 +785,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</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>
<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>
@@ -248,6 +248,9 @@
<div class="app-sections">
</div>
@@ -261,17 +264,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -309,46 +307,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</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>
<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>
@@ -249,6 +249,9 @@
<div class="app-sections">
</div>
@@ -262,17 +265,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -310,46 +308,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</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>
<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>
@@ -248,6 +248,9 @@
<div class="app-sections">
</div>
@@ -261,17 +264,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -309,46 +307,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div>
<div class="app-section" style="margin-top:1rem" id="signup-card">
<div class="app-section" id="signup-card">
<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 class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
@@ -248,6 +248,9 @@
<div class="app-sections">
</div>
@@ -261,17 +264,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -309,46 +307,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div>
<div class="app-section" style="margin-top:1rem" id="signup-card">
<div class="app-section" id="signup-card">
<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 class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
@@ -250,6 +250,9 @@
<div class="app-sections">
</div>
@@ -263,17 +266,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -311,46 +309,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div>
<div class="app-section" style="margin-top:1rem" id="signup-card">
<div class="app-section" id="signup-card">
<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 class="form-hint" id="add-people">Családtagot így adhatsz hozzá: Admin panel → Users.</p>
@@ -249,6 +249,9 @@
<div class="app-sections">
</div>
@@ -262,17 +265,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -310,46 +308,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
@@ -218,7 +218,7 @@
<div class="deploy-container">
<div class="app-settings-tab">
@@ -237,7 +237,7 @@
</div>
<div class="app-section" style="margin-top:1rem" id="signup-card">
<div class="app-section" id="signup-card">
<h3>Regisztráció</h3>
<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>
@@ -246,6 +246,9 @@
<div class="app-sections">
</div>
@@ -259,17 +262,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -307,46 +305,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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-error">A művelet most nem sikerült.</div>
@@ -240,6 +240,30 @@
<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">
@@ -251,8 +275,8 @@
<div class="deploy-storage-info">
<h4>Adattárolás</h4>
<div class="app-section">
<h3>Adattárolás</h3>
<div class="settings-grid">
<div class="settings-row">
<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>
<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.
@@ -390,7 +414,7 @@
<div class="app-optional-config">
<div class="app-section">
<h3>Földrajzi korlátozás</h3>
<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.
@@ -525,7 +549,7 @@
<div class="app-optional-config">
<div class="app-section">
<h3>Opcionális beállítások</h3>
<div class="config-group">
@@ -624,7 +648,7 @@
<div class="app-optional-config">
<div class="app-section">
<h3>Email-küldés</h3>
<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,
@@ -644,184 +668,33 @@
</form>
</div>
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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-label">Cím</div>
<div class="app-field-value" data-app-address-row="1">paste.example.hu</div>
</div>
<form id="deploy-form" class="deploy-form">
<div class="form-section">
<h4>Automatikusan generált értékek</h4>
<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 class="app-field-row">
<div class="app-field-label">Tárhely</div>
<div class="app-field-value" data-app-storage-row="1">/mnt/gone (nem elérhető)</div>
</div>
<div class="form-section">
<h4>Beállítások</h4>
<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 class="app-field-row" data-setting-row="SECRET_KEY">
<div class="app-field-label">Titkos kulcs érték</div>
<div class="app-field-value input-with-button">
<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>
<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>
</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 class="app-field-row" data-setting-row="LANG_SEL">
<div class="app-field-label">Felület nyelve</div>
<div class="app-field-value">Magyar nyelv</div>
</div>
</div>
</div>
@@ -859,46 +732,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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>
@@ -255,17 +258,12 @@
<div class="app-section">
<div class="app-section" data-address-keys="1">
<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>
</div>
</div>
@@ -303,46 +301,9 @@ function appAfterStart(btn, slug, envVar) {
</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) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
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_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.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.",
"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.",