f30a80841e
gates / gates (push) Successful in 50s
FileBrowser (and traefik, cloudflared, samba) has no app.yaml, so Deployed is false. Since v0.315.0 the app page read Deployed alone and showed FileBrowser "not installed", with Install and without its login. A protected infra stack is now installed by definition on the app page (Deployed || Protected, as every other surface already counted): settings/guide/log tabs, Restart only, no install page, no data tab. The Install press would have deployed the infra stack as an app: DeployStack and the deploy API now refuse a protected stack. In the operator state (the box does not know FileBrowser's password - both demo boxes) the login panel offers "Új jelszó beállítása": the file browser's own CLI with the server stopped (spiked on demo-hp: new 200, old 401, users/shares/settings unchanged), verified, stored encrypted, state generated. Every installed app's settings tab has the login panel with exactly one answer: the box's fields, the login the app wrote (InitialCreds), or a sentence chosen by the catalog's new app_info.login_kind (first_visit default, email, none; none behind the family gate says the family sign-in is the login). "Fájlok ↗" opens the library folder, else the first declared folder, else a media app's first present drive's media folder. The data tab lists each drive's media folders; a folder whose drive is not bound into FileBrowser is text, never a link. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
770 lines
39 KiB
HTML
770 lines
39 KiB
HTML
{{define "app_settings"}}
|
|
{{template "layout_start" .}}
|
|
<div class="app-page">
|
|
{{template "app_shell_start" .}}
|
|
|
|
{{/* „BEALLITASOK" — the tab an installed app opens on.
|
|
|
|
Order, and each step of it is a decision:
|
|
1. Mukodes — Start / Restart / Stop. They used to sit in the page header and read as
|
|
menu buttons; a state change is an action on the app, so it is a card.
|
|
2. what the app still wants FROM the household — the setup gate, an after-start value, the
|
|
sign-up window, the first login. Above the form, because an app that is waiting on the
|
|
household is not asking them to review settings.
|
|
3. the settings themselves — generated values, install-time values (read-only), the optional
|
|
ones, e-mail, integrations, geo.
|
|
|
|
„Adatok es mentes" holds everything about the FILES, including the backup card that used to
|
|
sit on this page. */}}
|
|
|
|
<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}}
|
|
|
|
{{/* 1. WHAT THE APP IS WAITING FOR — the generic after-start card (Part 4, D2 = A).
|
|
FIRST on the tab (v0.316.0): a household opening a Plex that wants its claim code should
|
|
meet the code box, not a Restart button. It is the page's one BLUE callout — a request,
|
|
never a fault, so never amber.
|
|
The catalog says a value is asked after the The catalog says a value is asked after the
|
|
app starts and supplies its label; the ACTION is Go code in after_start.go, keyed by slug
|
|
and env var. A field with no registered action renders nothing and POSTs 404 — the catalog
|
|
cannot make the box run anything. */}}
|
|
{{- range .AfterStartFields}}
|
|
<div class="app-section app-callout app-callout-ask" data-afterstart="{{.EnvVar}}">
|
|
<h3>{{T "app_page.after_start_title"}}</h3>
|
|
{{if .Description}}<p>{{.Description}}</p>{{end}}
|
|
<div class="input-with-button">
|
|
<input type="text" id="after-start-{{.EnvVar}}" class="form-control"
|
|
placeholder="{{.Placeholder}}" autocomplete="off" spellcheck="false"
|
|
aria-label="{{.Label}}">
|
|
<button type="button" class="btn btn-sm btn-primary"
|
|
onclick="appAfterStart(this, '{{$.Meta.Slug}}', '{{.EnvVar}}')">{{T "app_page.after_start_btn"}}</button>
|
|
</div>
|
|
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
|
|
</div>
|
|
{{- end}}
|
|
|
|
{{/* 2. MUKODES. Stop asks first — it takes the app away from everyone in the house, and the
|
|
protected stacks refuse it server-side regardless (traefik, cloudflared, the controller
|
|
itself and always samba). */}}
|
|
<div class="app-section" id="app-run-card" data-run-card="1">
|
|
<h3>{{T "app_page.mukodes"}}</h3>
|
|
<p class="form-hint" data-run-state="{{.Stack.State}}">{{stateLabel .Stack.State}}</p>
|
|
<div style="display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem">
|
|
{{if .Infra}}
|
|
{{/* v0.320.0: part of the box — Restart only, as the app list offers it; the box starts it
|
|
again by itself, and Stop is refused server-side for every protected stack. */}}
|
|
{{if isOperational .Stack.State}}<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, '{{.Stack.Name}}', 'restart')">{{T "common.ujrainditas"}}</button>{{end}}
|
|
{{else if isOperational .Stack.State}}
|
|
<button class="btn btn-sm btn-warning" data-run-action="restart" onclick="stackAction(event, '{{.Stack.Name}}', 'restart')">{{T "common.ujrainditas"}}</button>
|
|
<button class="btn btn-sm btn-danger" data-run-action="stop" onclick="var b=this; felhomConfirm(b, '{{T "app_page.stop_confirm"}}', function(){ stackAction(event, '{{$.Stack.Name}}', 'stop'); })">{{T "deploy.leallitas"}}</button>
|
|
{{else}}
|
|
<button class="btn btn-sm btn-success" data-run-action="start" onclick="stackAction(event, '{{.Stack.Name}}', 'start')">{{T "deploy.inditas"}}</button>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
{{/* 3. THE REST OF WHAT THE APP WANTS FROM THE HOUSEHOLD. */}}
|
|
{{- if .InstallHoldClosed}}
|
|
<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" 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}}
|
|
{{- if .SetupGateHasProbe}}
|
|
<p class="form-hint">{{T "app_info.setup_gate_probe"}}</p>
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/{{.Meta.Slug}}/setup-gate/open')">{{T "app_info.setup_gate_done_btn"}}</button>
|
|
{{- else}}
|
|
<p class="form-hint">{{T "app_info.setup_gate_button_hint"}}</p>
|
|
<button type="button" class="btn btn-sm btn-primary" onclick="var b=this; felhomConfirm(b, '{{T "app_info.setup_gate_confirm"}}', function(){ appPress(b, '/apps/{{.Meta.Slug}}/setup-gate/open'); })">{{T "app_info.setup_gate_done_btn"}}</button>
|
|
{{- end}}
|
|
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
|
|
</div>
|
|
{{- end}}
|
|
|
|
{{- if .CloseSignupOffered}}
|
|
<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}}
|
|
<button type="button" class="btn btn-sm btn-primary" onclick="appPress(this, '/apps/{{.Meta.Slug}}/close-signup')">{{T "app_info.close_signup_btn"}}</button>
|
|
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
|
|
</div>
|
|
{{- end}}
|
|
{{- if or .SignupClosed .SignupOpenUntil}}
|
|
<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>
|
|
{{- else}}
|
|
<p>{{T "app_info.signup_closed"}}</p>
|
|
{{- end}}
|
|
{{- if .AppInfo.AddPeople}}
|
|
<p class="form-hint" id="add-people">{{T "app_info.signup_add_how"}} {{.AppInfo.AddPeople}}</p>
|
|
{{- end}}
|
|
{{- if .SignupNativeFailed}}
|
|
<p class="form-hint" id="signup-native-failed">{{T "app_info.signup_native_failed"}}</p>
|
|
{{- end}}
|
|
{{- if .SignupNativeOpenFailed}}
|
|
<p class="form-hint" id="signup-native-open-failed">{{T "app_info.signup_native_open_failed"}}</p>
|
|
{{- end}}
|
|
{{- if .SignupClosed}}
|
|
{{- if .SignupNative}}
|
|
<p class="form-hint">{{T "app_info.signup_window_restart"}}</p>
|
|
{{- end}}
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/{{.Meta.Slug}}/signup-window')">{{T "app_info.signup_window_btn"}}</button>
|
|
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
|
|
{{- end}}
|
|
</div>
|
|
{{- end}}
|
|
|
|
{{/* 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.
|
|
|
|
v0.320.0 — EVERY installed app has this panel, with exactly ONE answer (app_login.go
|
|
loginAnswerFor): the box's fields · the login the app wrote (InitialCreds, FileBrowser's too) ·
|
|
or a sentence for an app the box holds no login for (first visit / e-mail / none). */}}
|
|
{{if .LoginAnswer}}
|
|
<div class="app-section" id="app-login-panel" data-login-panel="1" data-login-answer="{{.LoginAnswer}}">
|
|
<h3>{{T "app_page.login_title"}}</h3>
|
|
{{- if eq .LoginAnswer "fields"}}
|
|
<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>
|
|
{{- else if eq .LoginAnswer "initial"}}
|
|
<table class="initcred-table">
|
|
{{if .InitialCreds.Username}}
|
|
<tr>
|
|
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">{{T "app_info.felhasznalonev"}}</td>
|
|
<td><code class="initcred-user" style="user-select:all">{{.InitialCreds.Username}}</code></td>
|
|
</tr>
|
|
{{end}}
|
|
{{if .InitialCredsOperatorSet}}
|
|
<tr>
|
|
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">{{T "common.jelszo"}}</td>
|
|
<td data-fb-operator="1">{{T "app_page.fb_operator_set"}}</td>
|
|
</tr>
|
|
{{else}}
|
|
<tr>
|
|
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap;vertical-align:middle">{{T "common.jelszo"}}</td>
|
|
<!-- R-254: the value is NOT in this page. It used to be rendered into a `hidden`
|
|
span, which stops a browser drawing it and nothing else — a fetch of this page
|
|
returned a real per-install password. Both buttons now ask the server. -->
|
|
<td style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
|
<code id="initcred-pw">••••••••••••</code>
|
|
<button type="button" class="btn btn-sm btn-outline" id="initcred-reveal" onclick="icRevealPw(this)">{{T "app_info.megjelenites"}}</button>
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="icCopyPw(this)">{{T "app_info.masolas"}}</button>
|
|
<span id="initcred-err" class="form-hint" style="display:none;color:var(--red)"></span>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</table>
|
|
{{if .InitialCreds.Note}}<p class="app-info-creds-warn">{{.InitialCreds.Note}}</p>{{end}}
|
|
{{if not .InitialCredsOperatorSet}}<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal"}}</p>{{end}}
|
|
{{- if .InitialCredsOperatorSet}}
|
|
{{/* v0.320.0: the box does not know this password, so it offers to set a new one (stacks
|
|
ResetFileBrowserAdminPassword — the file browser's own CLI, spiked on demo-hp). POST + CSRF,
|
|
confirmed first: the old password stops working. */}}
|
|
<div style="margin-top:.75rem">
|
|
<button type="button" class="btn btn-sm btn-outline" data-fb-reset="1" onclick="var b=this; felhomConfirm(b, '{{T "app_page.fb_reset_confirm"}}', function(){ appPress(b, '/apps/{{.Meta.Slug}}/initial-credentials/reset'); })">{{T "app_page.fb_reset_btn"}}</button>
|
|
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
|
|
</div>
|
|
{{- end}}
|
|
{{- else if eq .LoginAnswer "email"}}
|
|
<p data-login-kind="email">{{T "app_page.login_kind_email"}}</p>
|
|
{{- else if and (eq .LoginAnswer "none") .Meta.FamilyGate}}
|
|
{{/* No login of its own, behind the family gate (decision 64): the family sign-in IS its login. */}}
|
|
<p data-login-kind="family">{{T "app_page.login_kind_family"}}</p>
|
|
{{- else if eq .LoginAnswer "none"}}
|
|
<p data-login-kind="none">{{T "app_page.login_kind_none"}}</p>
|
|
{{- else}}
|
|
<p data-login-kind="first_visit">{{T "app_page.login_kind_first_visit"}}</p>
|
|
{{- end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if and .AppInfo.DefaultCreds (not .DefaultLoginReplaced)}}
|
|
<div class="app-section">
|
|
<h3>{{T "app_info.alapertelmezett_belepes"}}</h3>
|
|
<p class="app-info-creds">{{.AppInfo.DefaultCreds}}</p>
|
|
{{if .KnownLoginLine}}<p class="app-info-creds-warn" id="known-login-line">{{.KnownLoginLine}}</p>{{else}}<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal_2"}}</p>{{end}}
|
|
{{- if and .Stack.Deployed .KnownLoginLine}}
|
|
<button type="button" class="btn btn-sm btn-outline" onclick="appPress(this, '/apps/{{.Meta.Slug}}/default-login/changed')">{{T "app_info.default_login_changed_btn"}}</button>
|
|
<span class="form-hint app-press-err" style="display:none;color:var(--red)"></span>
|
|
{{- end}}
|
|
</div>
|
|
{{end}}</div>
|
|
|
|
{{/* 3. THE SETTINGS. v0.316.0: the „this app is already installed, the settings below are
|
|
read-only" BANNER is gone — it was a box saying what a one-line hint says, on a tab the
|
|
household reached by pressing „Beallitasok" on an installed app. The hint lives with the
|
|
fields it is about, at the bottom of this tab. */}}
|
|
{{if .StorageInfo}}
|
|
<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>
|
|
<span class="settings-value">{{.StorageInfo.Label}} <span class="mono" style="color:var(--text-2)">({{.StorageInfo.Path}})</span></span>
|
|
</div>
|
|
{{if .StorageInfo.DataSizeHuman}}
|
|
<div class="settings-row">
|
|
<span class="settings-label">{{T "deploy.adatmeret"}}</span>
|
|
<span class="settings-value mono">{{numText .StorageInfo.DataSizeHuman}}</span>
|
|
</div>
|
|
{{end}}
|
|
{{if .StorageInfo.FreeHuman}}
|
|
<div class="settings-row">
|
|
<span class="settings-label">{{T "deploy.szabad_hely"}}</span>
|
|
<span class="settings-value mono">{{T "deploy.szabad"}}</span>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{if .OtherStoragePaths}}
|
|
<span class="btn btn-sm btn-outline" style="margin-top:.75rem;opacity:.45;cursor:not-allowed" title="{{T "deploy.hamarosan"}}">
|
|
<svg class="ico ico-sm"><use href="#i-upload"/></svg> {{T "deploy.mozgatas_masik_tarolora"}}
|
|
</span>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
{{if .StaleData}}
|
|
<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>
|
|
{{range .StaleData}}
|
|
<div class="stale-data-item">
|
|
<div class="settings-grid" style="margin-bottom:.75rem">
|
|
<div class="settings-row">
|
|
<span class="settings-label">{{T "deploy.tarhely"}}</span>
|
|
<span class="settings-value">{{.Label}} <span class="mono" style="color:var(--text-2)">({{.Path}})</span></span>
|
|
</div>
|
|
<div class="settings-row">
|
|
<span class="settings-label">{{T "deploy.meret"}}</span>
|
|
<span class="settings-value mono">{{numText .SizeHuman}}</span>
|
|
</div>
|
|
<div class="settings-row">
|
|
<span class="settings-label">{{T "deploy.mappak"}}</span>
|
|
<span class="settings-value mono" style="font-size:.85rem">{{range .Mounts}}{{.}}<br>{{end}}</span>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteStaleData('{{$.Meta.Slug}}', '{{.Path}}', this)">
|
|
{{T "deploy.korabbi_adatok_torlese"}}
|
|
</button>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
{{if .HasIntegrations}}
|
|
<div class="app-section">
|
|
<h3>{{T "deploy.integraciok"}}</h3>
|
|
<p class="config-group-desc">
|
|
{{T "deploy.mas_telepitett_alkalmazasokkal_valo_ossz"}}
|
|
</p>
|
|
|
|
{{range .Integrations}}
|
|
<div class="config-field" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.75rem 0;border-bottom:1px solid var(--line)">
|
|
<div style="flex:1">
|
|
<strong>{{.Label}}</strong>
|
|
<p class="config-field-help" style="margin:0">{{.Description}}</p>
|
|
{{if not .TargetDeployed}}
|
|
<span class="tag tag-off" style="margin-top:.25rem"><span class="dot"></span>{{T "deploy.nincs_telepitve"}}</span>
|
|
{{else if not .TargetRunning}}
|
|
<span class="tag tag-neutral" style="margin-top:.25rem"><span class="dot"></span>{{T "deploy.celalkalmazas_leallitva"}}</span>
|
|
{{else if eq .Status "error"}}
|
|
<span class="tag tag-crit" style="margin-top:.25rem"><span class="dot"></span>{{T "common.hiba"}}</span>
|
|
{{else if .Enabled}}
|
|
<span class="tag tag-run" style="margin-top:.25rem"><span class="dot"></span>{{T "deploy.aktiv"}}</span>
|
|
{{end}}
|
|
</div>
|
|
<label class="toggle">
|
|
<input type="checkbox"
|
|
{{if .Enabled}}checked{{end}}
|
|
{{if not .TargetDeployed}}disabled title="{{T "deploy.a_celalkalmazas_nincs_telepitve"}}"{{end}}
|
|
{{if and .TargetDeployed (not .TargetRunning)}}disabled title="{{T "deploy.a_celalkalmazas_nem_fut"}}"{{end}}
|
|
onchange="toggleIntegration('{{$.Stack.Name}}', '{{.Target}}', this.checked, this)">
|
|
<span class="toggle-label"></span>
|
|
</label>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<script>
|
|
async function toggleIntegration(provider, target, enable, checkbox) {
|
|
checkbox.disabled = true;
|
|
try {
|
|
var resp = await fetch('/api/integrations/' + provider + '/' + target, {
|
|
method: 'POST',
|
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
body: JSON.stringify({enabled: enable})
|
|
});
|
|
var data = await resp.json();
|
|
if (!data.ok) {
|
|
checkbox.checked = !enable;
|
|
showAlert(data.error || '{{T "common.hiba_tortent"}}');
|
|
} else {
|
|
setTimeout(function(){ location.reload(); }, 500);
|
|
return;
|
|
}
|
|
} catch(err) {
|
|
checkbox.checked = !enable;
|
|
showAlert('{{T "deploy.halozati_hiba"}}');
|
|
}
|
|
checkbox.disabled = false;
|
|
}
|
|
</script>
|
|
{{end}}
|
|
|
|
{{if .GeoGlobalEnabled}}
|
|
<div class="app-section">
|
|
<h3>{{T "deploy.foldrajzi_korlatozas"}}</h3>
|
|
<p class="config-group-desc">
|
|
{{T "deploy.az_alkalmazas_egyeni_orszagkorlatozas_ne"}}
|
|
</p>
|
|
|
|
<label class="toggle" style="margin-bottom:1rem">
|
|
<input type="checkbox" id="app-geo-override-toggle"
|
|
{{if .GeoAppOverride}}checked{{end}}
|
|
onchange="toggleAppGeoOverride(this.checked)">
|
|
<span class="toggle-label">{{T "deploy.egyeni_orszagkorlatozas"}}</span>
|
|
</label>
|
|
|
|
<div id="app-geo-override-details" {{if not .GeoAppOverride}}style="display:none"{{end}}>
|
|
<div class="form-group">
|
|
<label>{{T "deploy.engedelyezett_orszagok_ehhez_az_alkalmaz"}}</label>
|
|
<div class="geo-country-selector">
|
|
<input type="text" id="app-geo-search" class="form-control config-input"
|
|
placeholder="{{T "deploy.orszag_keresese"}}" autocomplete="off"
|
|
oninput="filterAppGeoCountries(this.value)"
|
|
onfocus="showAppGeoList()"
|
|
onblur="setTimeout(function(){hideAppGeoList()},200)">
|
|
<div class="geo-country-list" id="app-geo-country-list"></div>
|
|
</div>
|
|
<div class="geo-selected-tags" id="app-geo-tags"></div>
|
|
</div>
|
|
<div class="config-actions">
|
|
<button class="btn btn-primary" onclick="saveAppGeoOverride()">{{T "deploy.mentes"}}</button>
|
|
<span id="app-geo-status" class="config-save-status"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function(){
|
|
var allCountries = [];
|
|
var appGeoCountries = {{json .GeoAppOverrideCountries}};
|
|
var stackName = '{{.Stack.Name}}';
|
|
|
|
function loadCountries(cb) {
|
|
if (allCountries.length > 0) { cb(); return; }
|
|
fetch('/api/geo/countries', {headers: csrfHeaders()})
|
|
.then(function(r){return r.json()})
|
|
.then(function(d){ if(d.ok) allCountries = d.data; cb(); })
|
|
.catch(function(){ cb(); });
|
|
}
|
|
|
|
window.toggleAppGeoOverride = function(enabled) {
|
|
document.getElementById('app-geo-override-details').style.display = enabled ? '' : 'none';
|
|
if (enabled) {
|
|
if (!appGeoCountries || appGeoCountries.length === 0) {
|
|
appGeoCountries = {{json .GeoGlobalCountries}};
|
|
}
|
|
loadCountries(renderAppGeoTags);
|
|
} else {
|
|
fetch('/api/stacks/' + stackName + '/geo/override', {method:'DELETE', headers:csrfHeaders()});
|
|
}
|
|
};
|
|
|
|
window.showAppGeoList = function() { loadCountries(function(){ filterAppGeoCountries(''); }); };
|
|
window.hideAppGeoList = function() { document.getElementById('app-geo-country-list').style.display='none'; };
|
|
|
|
window.filterAppGeoCountries = function(q) {
|
|
var list = document.getElementById('app-geo-country-list');
|
|
q = q.toLowerCase();
|
|
var html = ''; var count = 0;
|
|
for (var i = 0; i < allCountries.length && count < 15; i++) {
|
|
var c = allCountries[i];
|
|
if (appGeoCountries.indexOf(c.code) >= 0) continue;
|
|
if (q && c.name.toLowerCase().indexOf(q) < 0 && c.code.toLowerCase().indexOf(q) < 0) continue;
|
|
html += '<div class="geo-country-option" onmousedown="addAppGeoCountry(\'' + c.code + '\')">'
|
|
+ esc(c.name) + ' <small>(' + c.code + ')</small></div>';
|
|
count++;
|
|
}
|
|
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">{{T "deploy.nincs_talalat"}}</div>';
|
|
list.style.display = (count > 0 || q) ? '' : 'none';
|
|
};
|
|
|
|
window.addAppGeoCountry = function(code) {
|
|
if (appGeoCountries.indexOf(code) >= 0) return;
|
|
appGeoCountries.push(code);
|
|
renderAppGeoTags();
|
|
document.getElementById('app-geo-search').value = '';
|
|
hideAppGeoList();
|
|
};
|
|
|
|
window.removeAppGeoCountry = function(code) {
|
|
appGeoCountries = appGeoCountries.filter(function(c){return c !== code});
|
|
renderAppGeoTags();
|
|
};
|
|
|
|
function renderAppGeoTags() {
|
|
var el = document.getElementById('app-geo-tags');
|
|
var html = '';
|
|
for (var i = 0; i < appGeoCountries.length; i++) {
|
|
var code = appGeoCountries[i];
|
|
var name = code;
|
|
for (var j = 0; j < allCountries.length; j++) {
|
|
if (allCountries[j].code === code) { name = allCountries[j].name; break; }
|
|
}
|
|
html += '<span class="geo-tag">' + esc(name) + ' (' + code + ') '
|
|
+ '<span class="geo-tag-remove" onclick="removeAppGeoCountry(\'' + code + '\')">×</span></span>';
|
|
}
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
window.saveAppGeoOverride = function() {
|
|
var status = document.getElementById('app-geo-status');
|
|
fetch('/api/stacks/' + stackName + '/geo/override', {
|
|
method: 'POST',
|
|
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
|
|
body: JSON.stringify({allowed_countries: appGeoCountries})
|
|
})
|
|
.then(function(r){return r.json()})
|
|
.then(function(d){
|
|
status.textContent = d.ok ? (d.message || '{{T "deploy.mentve"}}') : (d.error || '{{T "common.hiba"}}');
|
|
status.className = 'config-save-status ' + (d.ok ? 'config-save-ok' : 'config-save-err');
|
|
setTimeout(function(){ status.textContent=''; }, 5000);
|
|
})
|
|
.catch(function(){
|
|
status.textContent = '{{T "deploy.halozati_hiba"}}';
|
|
status.className = 'config-save-status config-save-err';
|
|
});
|
|
};
|
|
|
|
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
|
|
if (document.getElementById('app-geo-override-toggle') && document.getElementById('app-geo-override-toggle').checked) {
|
|
loadCountries(renderAppGeoTags);
|
|
}
|
|
})();
|
|
</script>
|
|
{{end}}
|
|
|
|
{{if .HasOptionalConfig}}
|
|
<div class="app-section">
|
|
<h3>{{T "deploy.opcionalis_beallitasok"}}</h3>
|
|
{{range .OptionalConfig}}
|
|
<div class="config-group">
|
|
<h4>{{.Group}}</h4>
|
|
{{if .Description}}<p class="config-group-desc">{{.Description}}</p>{{end}}
|
|
|
|
<div class="config-fields">
|
|
{{range .Fields}}
|
|
<div class="config-field">
|
|
<label for="opt-{{.EnvVar}}">{{.Label}}</label>
|
|
{{if .HelpText}}<p class="config-field-help">{{.HelpText}}</p>{{end}}
|
|
{{if .HelpURL}}<p class="config-field-help"><a href="{{.HelpURL}}" target="_blank">{{T "deploy.regisztracios_utmutato"}}</a></p>{{end}}
|
|
{{if eq .Type "secret_input"}}
|
|
{{/* R-255 (v0.308.0): a stored secret is NOT in the page; it is fetched on demand like the
|
|
auto-generated ones (R-254). A blank field that was never revealed is not sent, so
|
|
saving other fields keeps it. */}}
|
|
{{$set := index $.CurrentValues .EnvVar}}
|
|
<div class="input-with-button">
|
|
<input type="password" id="opt-{{.EnvVar}}" name="{{.EnvVar}}" class="config-input"
|
|
value="" placeholder="{{if $set}}••••••••••••{{else}}{{.Label}}{{end}}"
|
|
{{if $set}}data-secret-set="1" {{end}}autocomplete="off">
|
|
{{if $set}}<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('{{$.Stack.Name}}','{{.EnvVar}}', this, 'opt-{{.EnvVar}}')">{{T "deploy.megjelenites"}}</button>{{end}}
|
|
</div>
|
|
{{else}}
|
|
<input type="text"
|
|
id="opt-{{.EnvVar}}"
|
|
name="{{.EnvVar}}"
|
|
class="config-input"
|
|
value="{{index $.CurrentValues .EnvVar}}"
|
|
placeholder="{{.Label}}"
|
|
autocomplete="off">
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="config-actions">
|
|
<button class="btn btn-primary" id="save-optional-config" onclick="saveOptionalConfig('{{.Stack.Name}}')">
|
|
{{T "deploy.mentes"}}
|
|
</button>
|
|
<span id="config-save-status" class="config-save-status"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function saveOptionalConfig(stackName) {
|
|
var btn = document.getElementById('save-optional-config');
|
|
var status = document.getElementById('config-save-status');
|
|
var inputs = document.querySelectorAll('.config-input');
|
|
|
|
var values = {};
|
|
inputs.forEach(function(input) {
|
|
// R-255: a stored secret the household never revealed and left blank stays as it is.
|
|
if (input.dataset.secretSet === '1' && input.dataset.revealed !== '1' && input.value === '') return;
|
|
values[input.name] = input.value;
|
|
});
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = '{{T "deploy.mentes_2"}}';
|
|
status.textContent = '';
|
|
status.className = 'config-save-status';
|
|
|
|
try {
|
|
var resp = await fetch('/api/stacks/' + stackName + '/optional-config', {
|
|
method: 'POST',
|
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
body: JSON.stringify({values: values})
|
|
});
|
|
var data = await resp.json();
|
|
|
|
if (data.ok) {
|
|
status.textContent = (data.message || '{{T "deploy.mentve"}}');
|
|
status.className = 'config-save-status config-save-ok';
|
|
} else {
|
|
status.textContent = (data.error || '{{T "common.hiba"}}');
|
|
status.className = 'config-save-status config-save-err';
|
|
}
|
|
} catch(err) {
|
|
status.textContent = '{{T "deploy.halozati_hiba"}}';
|
|
status.className = 'config-save-status config-save-err';
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = '{{T "deploy.mentes"}}';
|
|
|
|
setTimeout(function() { status.textContent = ''; }, 5000);
|
|
}
|
|
</script>
|
|
{{end}}
|
|
|
|
{{if .AppEmailSupported}}
|
|
<div class="app-section">
|
|
<h3>{{T "deploy.email_kuldes"}}</h3>
|
|
<p class="config-group-desc">
|
|
{{T "deploy.ez_az_alkalmazas_tud_emailt"}}
|
|
</p>
|
|
{{if not .AppEmailGlobalOn}}
|
|
<p class="alert alert-warning" style="margin-bottom:1rem">
|
|
{{T "deploy.az_alkalmazas_email_jelenleg_ki"}}
|
|
</p>
|
|
{{end}}
|
|
<form method="POST" action="/stacks/{{.Stack.Name}}/app-email">
|
|
{{$.CSRFField}}
|
|
<label style="display:flex;align-items:center;gap:.5rem;margin-bottom:1rem">
|
|
<input type="checkbox" name="app_email_enabled" value="on" {{if .AppEmailAppOn}}checked{{end}}>
|
|
{{T "deploy.email_kuldes_engedelyezese_ennel_az"}}
|
|
</label>
|
|
<div class="config-actions">
|
|
<button class="btn btn-primary" type="submit">{{T "deploy.mentes"}}</button>
|
|
<span class="config-group-desc" style="margin-left:.5rem">{{T "deploy.a_mentes_ujrainditja_az_alkalmazast"}}</span>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{{end}}
|
|
{{/* „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). */}}
|
|
{{- if not .Infra}}
|
|
<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}}
|
|
{{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>
|
|
{{- end}}
|
|
</div>
|
|
|
|
{{if .InitialCreds}}
|
|
<script>
|
|
// R-254: the password is NOT in this page. It is fetched on demand from the server, which re-reads it
|
|
// live from the running container — so this is the only way it reaches a browser, and every fetch is
|
|
// recorded server-side. Nothing caches it in a variable between presses: each act asks again.
|
|
function icFetchPw() {
|
|
return fetch('/apps/{{.Meta.Slug}}/initial-credentials/reveal', {
|
|
method: 'POST',
|
|
headers: {'X-CSRF-Token': '{{.CSRFToken}}'},
|
|
credentials: 'same-origin'
|
|
}).then(function (r) { return r.json(); }).then(function (j) {
|
|
if (!j.ok) { throw new Error(j.error || '{{T "app_info.a_kezdeti_jelszo_beolvasasa_nem"}}'); }
|
|
return j.data.password;
|
|
});
|
|
}
|
|
function icErr(msg) {
|
|
var e = document.getElementById('initcred-err');
|
|
e.textContent = msg;
|
|
e.style.display = 'inline';
|
|
}
|
|
function icRevealPw(btn) {
|
|
var code = document.getElementById('initcred-pw');
|
|
if (!code) return;
|
|
document.getElementById('initcred-err').style.display = 'none';
|
|
if (code.dataset.shown === '1') { // hide: drop the value out of the DOM again
|
|
code.textContent = '••••••••••••';
|
|
code.dataset.shown = '0';
|
|
btn.textContent = '{{T "app_info.megjelenites"}}';
|
|
return;
|
|
}
|
|
btn.disabled = true;
|
|
icFetchPw().then(function (pw) {
|
|
btn.disabled = false;
|
|
code.textContent = pw;
|
|
code.dataset.shown = '1';
|
|
btn.textContent = '{{T "app_info.elrejtes"}}';
|
|
}).catch(function (e) { btn.disabled = false; icErr(e.message); });
|
|
}
|
|
function icCopyPw(btn) {
|
|
document.getElementById('initcred-err').style.display = 'none';
|
|
btn.disabled = true;
|
|
icFetchPw().then(function (pw) {
|
|
return navigator.clipboard.writeText(pw).then(function () {
|
|
btn.disabled = false;
|
|
var orig = btn.textContent;
|
|
btn.textContent = '{{T "app_info.masolva"}}';
|
|
setTimeout(function () { btn.textContent = orig; }, 1500);
|
|
});
|
|
}).catch(function (e) { btn.disabled = false; icErr(e.message); });
|
|
}
|
|
</script>
|
|
{{end}}
|
|
|
|
{{template "app_press_script" .}}
|
|
<script>
|
|
// 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;
|
|
if (btn.dataset.shown === '1') {
|
|
el.value = '';
|
|
el.type = 'password';
|
|
el.dataset.revealed = '';
|
|
btn.dataset.shown = '';
|
|
btn.textContent = '{{T "deploy.megjelenites"}}';
|
|
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';
|
|
btn.dataset.shown = '1';
|
|
btn.textContent = '{{T "deploy.elrejtes"}}';
|
|
}).catch(function () { btn.disabled = false; showAlert('{{T "deploy.a_lekeres_nem_sikerult"}}'); });
|
|
}
|
|
function deleteStaleData(stackName, stalePath, btn) {
|
|
// Two-step inline confirm (the irreversible delete keeps its double acknowledgement).
|
|
felhomConfirm(btn, '{{T "deploy.biztosan_torolni_szeretned_a_korabbi"}} ' + stalePath + ' {{T "deploy.figyelem_a_muvelet_visszavonhatatlan_elo"}}', function () {
|
|
felhomConfirm(btn, '{{T "deploy.utolso_figyelmeztetes_a_torles_visszavon"}}', function () {
|
|
deleteStaleDataRun(stackName, stalePath, btn);
|
|
});
|
|
});
|
|
}
|
|
|
|
function deleteStaleDataRun(stackName, stalePath, btn) {
|
|
btn.disabled = true;
|
|
btn.textContent = '{{T "deploy.torles_folyamatban"}}';
|
|
|
|
fetch('/api/storage/stale-cleanup', {
|
|
method: 'POST',
|
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
body: JSON.stringify({stack_name: stackName, stale_path: stalePath})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (!data.ok) {
|
|
showAlert('{{T "common.hiba_kettospont"}} ' + (data.error || '{{T "deploy.ismeretlen_hiba"}}'));
|
|
btn.disabled = false;
|
|
btn.textContent = '{{T "deploy.korabbi_adatok_torlese"}}';
|
|
return;
|
|
}
|
|
var msg = '{{T "deploy.korabbi_adatok_torolve_n_nfelszabaditott"}} ' + (data.freed_human || '?');
|
|
if (data.errors && data.errors.length > 0) {
|
|
msg += '{{T "deploy.n_nnehany_hiba_tortent_n"}}' + data.errors.join('\n');
|
|
}
|
|
showAlert(msg);
|
|
// Remove the stale data card from DOM
|
|
var item = btn.closest('.stale-data-item');
|
|
if (item) item.remove();
|
|
// If no more stale items, remove the whole section
|
|
var container = document.querySelector('.deploy-stale-data');
|
|
if (container && container.querySelectorAll('.stale-data-item').length === 0) {
|
|
container.remove();
|
|
}
|
|
})
|
|
.catch(function(e) {
|
|
showAlert('{{T "deploy.halozati_hiba_2"}} ' + e.message);
|
|
btn.disabled = false;
|
|
btn.textContent = '{{T "deploy.korabbi_adatok_torlese"}}';
|
|
});
|
|
}
|
|
|
|
</script>
|
|
|
|
</div>{{/* .app-page */}}
|
|
{{template "layout_end" .}}
|
|
{{end}}
|