d170a2aecf
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
745 lines
37 KiB
HTML
745 lines
37 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 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. */}}
|
|
{{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>
|
|
<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>{{T "app_info.az_uzemelteto_allitotta_be"}}</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}}
|
|
</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). */}}
|
|
<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>
|
|
</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}}
|