From 5be49b67c66aac8375dbb6e51428f508a0a1b92d Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 28 May 2026 14:12:29 +0100 Subject: [PATCH] feat(setup): dev-mode easter egg on the Experience step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tap the Advanced card 10 times and a full-width "Dev mode activated" strip slides in beneath the two cards — the same 10-tap pattern as the topbar logo and services-manager unlocks, now at install time. The choice rides the setup payload (dev_mode) so setup_apply.sh persists CFG_DEV_MODE=true, and it's mirrored in-process via LpUi.dev so the next surface already reflects it. 10 more taps toggles it back off. Counting the Advanced radio's click (not the label's) sidesteps the label->input double-fire; the radio is pointer-events:none, so each tap reaches it exactly once. The strip is [hidden] by default (no phantom gap in the flex column) and replays its entrance keyframes each reveal. Signed-off-by: librelad --- .../libreportal/frontend/css/setup-wizard.css | 31 ++++++++++++++ .../frontend/js/system/setup-wizard.js | 42 +++++++++++++++++++ scripts/setup/setup_apply.sh | 8 ++++ 3 files changed, 81 insertions(+) diff --git a/containers/libreportal/frontend/css/setup-wizard.css b/containers/libreportal/frontend/css/setup-wizard.css index 993c5d6..ec9fb96 100755 --- a/containers/libreportal/frontend/css/setup-wizard.css +++ b/containers/libreportal/frontend/css/setup-wizard.css @@ -1116,3 +1116,34 @@ body.setup-wizard-open { color: rgba(var(--text-rgb), 0.65); line-height: 1.45; } + +/* Dev-mode easter egg strip — revealed by 10 taps on the Advanced card + (see setup-wizard.js). [hidden] keeps it out of layout (no phantom gap + from the parent flex column) until JS clears the attribute, at which + point the entrance keyframes play. */ +.setup-dev-strip { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + border-radius: 12px; + background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.16) 0%, rgba(var(--accent-rgb), 0.05) 100%); + border: 1px solid rgba(var(--accent-rgb), 0.5); + box-shadow: 0 6px 22px rgba(var(--accent-rgb), 0.16); + overflow: hidden; + animation: swDevStripIn .4s cubic-bezier(.2, .7, .3, 1) both; +} +/* Author display:flex above outranks the UA [hidden] rule, so re-hide. */ +.setup-dev-strip[hidden] { display: none; } +.setup-dev-strip-icon { font-size: 1.5rem; line-height: 1; } +.setup-dev-strip-text { display: flex; flex-direction: column; gap: 2px; } +.setup-dev-strip-text strong { font-size: 0.95rem; font-weight: 700; color: var(--text-primary); } +.setup-dev-strip-text span { font-size: 0.8rem; color: rgba(var(--text-rgb), 0.65); } + +@keyframes swDevStripIn { + from { opacity: 0; transform: translateY(-6px); max-height: 0; padding-top: 0; padding-bottom: 0; } + to { opacity: 1; transform: translateY(0); max-height: 160px; padding-top: 12px; padding-bottom: 12px; } +} +@media (prefers-reduced-motion: reduce) { + .setup-dev-strip { animation-duration: .01ms; } +} diff --git a/containers/libreportal/frontend/js/system/setup-wizard.js b/containers/libreportal/frontend/js/system/setup-wizard.js index def089d..cbc7716 100755 --- a/containers/libreportal/frontend/js/system/setup-wizard.js +++ b/containers/libreportal/frontend/js/system/setup-wizard.js @@ -23,6 +23,7 @@ class SetupWizard { this.installLevel = 'beginner'; this.totalSteps = this._effectiveTotalSteps(); this.domainCount = 0; // tracked dynamically as the user adds rows + this.devMode = false; // unlocked by the Advanced-card 10-tap easter egg } _effectiveTotalSteps() { @@ -118,6 +119,13 @@ class SetupWizard { + @@ -270,6 +278,34 @@ class SetupWizard { }); }); + // Easter egg: 10 taps on the Advanced card toggles Developer mode and + // reveals a strip beneath the cards — same 10-tap pattern as the topbar + // logo and services-manager unlocks (CFG_DEV_MODE), persisted on install + // via the setup payload's dev_mode field. Counting the radio's click + // (not the label's) avoids the label→input double-fire: the radio is + // pointer-events:none, so each tap reaches it exactly once via the label. + // A 3s idle gap resets the streak. + const advRadio = this.container.querySelector('input[name="sw-level"][value="advanced"]'); + const devStrip = this.container.querySelector('#sw-dev-strip'); + if (advRadio && devStrip) { + const TARGET = 10; + let taps = 0; + let resetTimer = null; + advRadio.addEventListener('click', () => { + taps++; + if (resetTimer) clearTimeout(resetTimer); + resetTimer = setTimeout(() => { taps = 0; }, 3000); + if (taps >= TARGET) { + taps = 0; + clearTimeout(resetTimer); + this.devMode = !this.devMode; + // Toggling [hidden] (display:none → flex) replays the entrance + // animation each time it's revealed. + devStrip.hidden = !this.devMode; + } + }); + } + $('#setup-form').addEventListener('submit', (e) => { e.preventDefault(); console.log('[setup] form submit fired'); @@ -676,6 +712,7 @@ class SetupWizard { install_name: $('#sw-name').value.trim(), timezone: $('#sw-timezone').value, install_level: this.installLevel, + dev_mode: this.devMode, domains, apps, appOptions, @@ -692,6 +729,11 @@ class SetupWizard { try { localStorage.setItem('lp.ui.seeded', '1'); } catch {} } catch {} } + // Same in-process mirror for the dev-mode easter egg (LpUi.dev enabling + // dev also enables advanced); the bash applier persists CFG_DEV_MODE. + if (this.devMode && window.LpUi?.dev) { + try { window.LpUi.dev.set(true); } catch {} + } const submitBtn = $('#sw-submit'); submitBtn.disabled = true; diff --git a/scripts/setup/setup_apply.sh b/scripts/setup/setup_apply.sh index f7e6d04..87b15ac 100644 --- a/scripts/setup/setup_apply.sh +++ b/scripts/setup/setup_apply.sh @@ -21,6 +21,7 @@ setupApplyConfig() local install_name=$(echo "$payload" | jq -r '.install_name // empty') local timezone=$(echo "$payload" | jq -r '.timezone // empty') local install_level=$(echo "$payload" | jq -r '.install_level // empty') + local dev_mode=$(echo "$payload" | jq -r '.dev_mode // empty') local traefik_email=$(echo "$payload" | jq -r '.traefik_email // empty') local domains_json=$(echo "$payload" | jq -c '.domains // []') @@ -43,6 +44,13 @@ setupApplyConfig() isSuccessful "Experience level set to '$install_level'" fi + # Developer mode — opt-in via the wizard's Advanced-card easter egg (10 + # taps). Unlocks the **DEV**-marked CFG_* fields across the WebUI. + if [[ "$dev_mode" == "true" ]]; then + updateConfigOption "CFG_DEV_MODE" "true" + isSuccessful "Developer mode enabled" + fi + local domains_count=$(echo "$domains_json" | jq -r 'length') if [[ "$domains_count" -gt 0 ]]; then local i=0