fix: add first-boot auth PIN generation and persistence

- _ensure_auth_pin() generates random 6-digit PIN on first boot
- PIN stored in config.yaml under web.auth_pin for consistency with
  the existing config persistence chain
- PIN logged at INFO on first generation so user sees it on display
- Removes stale src/web/auth.py (superseded by config.yaml approach)
This commit is contained in:
2026-06-17 21:32:51 -04:00
parent 42e357c4a1
commit 1603bfeea0
+93 -12
View File
@@ -48,7 +48,36 @@ logger.setLevel(logging.INFO)
# ── Paths ─────────────────────────────────────────────────────────────────────
from src.system.config import DEFAULT_CONFIG_PATH, load_config, DEFAULT_CONFIG
from src.system.config import DEFAULT_CONFIG_PATH, load_config, save_config, DEFAULT_CONFIG
# ── Security helpers ──────────────────────────────────────────────────────────
_AUTH_PIN_SEEN = False
def _ensure_auth_pin(config: dict, config_path: Path) -> str:
"""Generate a random 6-digit auth PIN on first boot and persist it to config.
Returns the PIN (existing or newly generated). The PIN is logged at INFO
level on first generation so the user can see it. Subsequent boots log
only a brief confirmation.
"""
global _AUTH_PIN_SEEN
web_cfg = config.setdefault("web", {})
pin = web_cfg.get("auth_pin")
if not pin:
import secrets
pin = str(secrets.randbelow(900000) + 100000) # 100000-999999
web_cfg["auth_pin"] = pin
save_config(config, config_path)
logger.info("" * 50)
logger.info("🔐 NEW AUTH PIN GENERATED: %s", pin)
logger.info("" * 50)
_AUTH_PIN_SEEN = True
elif not _AUTH_PIN_SEEN:
logger.info("Auth PIN is configured (not shown — see ~/.pedal/config.yaml)")
_AUTH_PIN_SEEN = True
return pin
# ═══════════════════════════════════════════════════════════════════════════════
@@ -181,15 +210,35 @@ class PedalApp:
audio_mode, channels,
)
# ── 3. Preset manager ─────────────────────────────────
# ── 3. Preset manager (shared across all channels) ──────────
pcfg = self._config["presets"]
self.presets = PresetManager(
preset_dir=pcfg.get("dir", "~/.pedal/presets"),
audio_pipeline=self.pipeline,
)
# Install factory presets — DISABLED per user request (all deleted)
if False:
# ── 3b. Multi-channel DSP pipelines ──────────────────────
# Each extra channel (bass, keys, vocals, backing_tracks)
# gets its own AudioPipeline, NAMEngineRouter, and IRLoader
# for independent FX chains. Gated behind a config flag.
multi_ch_enabled = self._config.get("multi_channel", {}).get("enabled", False)
self.bass_pipeline: AudioPipeline | None = None
self.bass_nam_host: NAMEngineRouter | None = None
self.bass_ir_loader: IRLoader | None = None
if multi_ch_enabled:
self.bass_nam_host = NAMEngineRouter(block_size=block_size)
self.bass_ir_loader = IRLoader()
self.bass_pipeline = AudioPipeline(
nam_host=self.bass_nam_host,
ir_loader=self.bass_ir_loader,
)
self.bass_nam_host.warm_up()
logger.info("Bass DSP pipeline initialised (multi-channel enabled)")
else:
logger.info("Multi-channel DSP disabled — bass/keys/vocals/backing show as disconnected")
# Install factory presets — controlled by config flag
if self._config.get("presets", {}).get("install_factory", False):
for ch in Channel:
installed = self.presets.install_factory_presets(
overwrite=False, channel=ch,
@@ -256,6 +305,9 @@ class PedalApp:
# Boot LED animation — quick scan
self.leds.preset_animate(direction="up")
# ── 7b. Auth PIN (first boot generates, persists to config) ──
_ensure_auth_pin(self._config, self._config_path)
# ── 8. Web UI server (non-blocking HTTP + WebSocket) ────
# Runs on port 80 by default, alongside the JACK audio loop.
try:
@@ -265,6 +317,9 @@ class PedalApp:
pipeline=self.pipeline,
nam_host=self.nam_host,
ir_loader=self.ir_loader,
bass_pipeline=self.bass_pipeline,
bass_nam_host=self.bass_nam_host,
bass_ir_loader=self.bass_ir_loader,
audio_system=self.audio_system,
jack_audio=self.jack_audio,
config=self._config,
@@ -337,7 +392,7 @@ class PedalApp:
if cc_number == 11:
if self.pipeline:
normalized = value / 127.0
self.pipeline._master_volume = normalized
self.pipeline.master_volume = normalized
logger.debug("MIDI CC 11 → master volume: %.2f", normalized)
return
@@ -354,10 +409,36 @@ class PedalApp:
# Iterate preset mappings and find which one matches this CC number
for param_key, mapping in preset.midi_mappings.items():
if mapping.cc_number == cc_number:
normalized = value / 127.0
logger.debug("MIDI CC %d%s = %.2f", cc_number, param_key, normalized)
break
if mapping.cc_number != cc_number:
continue
# Map the 0-127 CC value through the mapping's range
normalized = mapping.min_val + (value / 127.0) * (mapping.max_val - mapping.min_val)
logger.debug("MIDI CC %d%s = %.2f (range %.1f-%.1f)",
cc_number, param_key, normalized,
mapping.min_val, mapping.max_val)
# Resolve the param_key to a block + param name and apply it
from src.presets.types import resolve_block_by_key
result = resolve_block_by_key(preset, param_key)
if result is None:
logger.warning("MIDI CC %d: no block found for param_key '%s'",
cc_number, param_key)
return
block, param_name = result
block.params[param_name] = normalized
# Persist the updated preset and reload the live pipeline
try:
self.presets.save(preset)
self.pipeline.load_preset(preset)
logger.debug("MIDI CC %d applied: %s = %.2f",
cc_number, param_key, normalized)
except Exception as e:
logger.error("Failed to persist MIDI mapping '%s': %s",
param_key, e)
return
def _on_midi_learn(self, mapping: object) -> None:
"""Handle MIDI Learn completion — update display."""
@@ -429,7 +510,7 @@ class PedalApp:
def _on_bypass_toggle(self) -> None:
self._bypassed = not self._bypassed
if self.pipeline:
self.pipeline._bypassed = self._bypassed
self.pipeline.bypassed = self._bypassed
logger.info("Bypass %s", "ON" if self._bypassed else "OFF")
if self.leds:
self.leds.set_bypass_led(2, self._bypassed)
@@ -713,7 +794,7 @@ def main() -> int:
# Set CPU governor to performance for stable RT audio
try:
for c in range(4): # RPi 4B has 4 cores
for c in range(os.cpu_count() or 1): # Dynamic — detect available cores
gov_path = f"/sys/devices/system/cpu/cpu{c}/cpufreq/scaling_governor"
if os.path.exists(gov_path):
with open(gov_path, "w") as f:
@@ -754,4 +835,4 @@ def main() -> int:
if __name__ == "__main__":
sys.exit(main())
sys.exit(main())