feat(setup): dev-mode easter egg on the Experience step

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 <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-28 14:12:29 +01:00
parent af8a4cb22e
commit 5be49b67c6
3 changed files with 81 additions and 0 deletions

View File

@ -1116,3 +1116,34 @@ body.setup-wizard-open {
color: rgba(var(--text-rgb), 0.65); color: rgba(var(--text-rgb), 0.65);
line-height: 1.45; 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; }
}

View File

@ -23,6 +23,7 @@ class SetupWizard {
this.installLevel = 'beginner'; this.installLevel = 'beginner';
this.totalSteps = this._effectiveTotalSteps(); this.totalSteps = this._effectiveTotalSteps();
this.domainCount = 0; // tracked dynamically as the user adds rows this.domainCount = 0; // tracked dynamically as the user adds rows
this.devMode = false; // unlocked by the Advanced-card 10-tap easter egg
} }
_effectiveTotalSteps() { _effectiveTotalSteps() {
@ -118,6 +119,13 @@ class SetupWizard {
</div> </div>
</label> </label>
</div> </div>
<div class="setup-dev-strip" id="sw-dev-strip" role="status" hidden>
<span class="setup-dev-strip-icon" aria-hidden="true">🛠</span>
<div class="setup-dev-strip-text">
<strong>Dev mode activated</strong>
<span>Developer fields will be unlocked after install.</span>
</div>
</div>
</div> </div>
</section> </section>
@ -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) => { $('#setup-form').addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
console.log('[setup] form submit fired'); console.log('[setup] form submit fired');
@ -676,6 +712,7 @@ class SetupWizard {
install_name: $('#sw-name').value.trim(), install_name: $('#sw-name').value.trim(),
timezone: $('#sw-timezone').value, timezone: $('#sw-timezone').value,
install_level: this.installLevel, install_level: this.installLevel,
dev_mode: this.devMode,
domains, domains,
apps, apps,
appOptions, appOptions,
@ -692,6 +729,11 @@ class SetupWizard {
try { localStorage.setItem('lp.ui.seeded', '1'); } catch {} try { localStorage.setItem('lp.ui.seeded', '1'); } catch {}
} 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'); const submitBtn = $('#sw-submit');
submitBtn.disabled = true; submitBtn.disabled = true;

View File

@ -21,6 +21,7 @@ setupApplyConfig()
local install_name=$(echo "$payload" | jq -r '.install_name // empty') local install_name=$(echo "$payload" | jq -r '.install_name // empty')
local timezone=$(echo "$payload" | jq -r '.timezone // empty') local timezone=$(echo "$payload" | jq -r '.timezone // empty')
local install_level=$(echo "$payload" | jq -r '.install_level // 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 traefik_email=$(echo "$payload" | jq -r '.traefik_email // empty')
local domains_json=$(echo "$payload" | jq -c '.domains // []') local domains_json=$(echo "$payload" | jq -c '.domains // []')
@ -43,6 +44,13 @@ setupApplyConfig()
isSuccessful "Experience level set to '$install_level'" isSuccessful "Experience level set to '$install_level'"
fi 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') local domains_count=$(echo "$domains_json" | jq -r 'length')
if [[ "$domains_count" -gt 0 ]]; then if [[ "$domains_count" -gt 0 ]]; then
local i=0 local i=0