nuke all old UIs — templates, React SPA, /v2 mount, legacy routes

Removed:
- src/web/templates/*.html (all 6 Jinja2 template files)
- src/web/ui-dist/ (old React SPA build)
- /v2 duplicate static mount (same as /)
- /ui static mount (old React SPA)

Server.py changes:
- Root handler now directly serves v2 UI (no fallback chain)
- Old template routes (/presets, /models, /irs, /settings) → redirect to /
- Removed Jinja2Templates, TEMPLATES_DIR, TEMPLATE_VERSION, UI_DIST_DIR
This commit is contained in:
2026-06-16 18:45:04 -04:00
parent 9cc08d16da
commit 7078217783
12 changed files with 23 additions and 860 deletions
+23 -81
View File
@@ -25,9 +25,8 @@ from typing import Any, Optional
import uvicorn
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from ..dsp.nam_host import NAMHost
from ..dsp.ir_loader import IRLoader
@@ -51,38 +50,14 @@ logger = logging.getLogger(__name__)
# ── Cache buster version (bumped on deploy) ─────────────────────────
# Increment this when template/static changes need to bypass browser cache
TEMPLATE_VERSION = "8"
# ── Defaults ─────────────────────────────────────────────────────────────────
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8080
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
STATIC_DIR = Path(__file__).resolve().parent / "static"
# React UI dist — built frontend served at /ui/
# Can be overridden via UI_DIST_DIR env var
_ui_dist_env = os.environ.get("UI_DIST_DIR")
if _ui_dist_env:
UI_DIST_DIR = Path(_ui_dist_env).resolve()
else:
# Priority: src/web/ui-dist is the canonical UI
_candidates = [
Path(__file__).resolve().parent / "ui-dist",
Path(__file__).resolve().parent.parent.parent / "frontend-react" / "dist",
]
UI_DIST_DIR = next((d for d in _candidates if d.is_dir()), None)
# ── Channel support ──────────────────────────────────────────────────────────────
VALID_CHANNELS: frozenset = frozenset({"guitar", "bass", "keys", "vocals", "backing_tracks"})
def _resolve_channel(channel: str = "guitar") -> str:
"""Normalize a channel string; fall back to 'guitar' for invalid values."""
if isinstance(channel, str) and channel.lower() in VALID_CHANNELS:
return channel.lower()
return "guitar"
# v2 UI dist (the only UI we serve)
V2_DIST_DIR = Path(__file__).resolve().parent / "ui-v2-dist"
# ── Channel mode persistence ────────────────────────────────────────────────
@@ -315,10 +290,6 @@ class WebServer:
def _build_app(self) -> FastAPI:
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
# Inject version into all templates (cache buster for CSS/JS)
templates.env.globals["version"] = TEMPLATE_VERSION
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Web server lifespan started")
@@ -330,63 +301,34 @@ class WebServer:
# Mount static files
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# Mount React UI (if dist exists)
if UI_DIST_DIR and UI_DIST_DIR.is_dir():
logger.info("Mounting React UI from %s at /ui", UI_DIST_DIR)
app.mount("/ui", StaticFiles(directory=str(UI_DIST_DIR), html=True), name="ui")
# Mount V2 React UI (if dist exists)
V2_DIST_DIR = Path(__file__).resolve().parent / "ui-v2-dist"
if V2_DIST_DIR.is_dir():
logger.info("Mounting V2 React UI from %s at /v2", V2_DIST_DIR)
app.mount("/v2", StaticFiles(directory=str(V2_DIST_DIR), html=True), name="ui-v2")
# ── HTML pages ──────────────────────────────────────────────
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
"""Serve the v2 UI (or React SPA / legacy dashboard as fallback)."""
# Prefer the v2 UI if available
v2_dir = Path(__file__).resolve().parent / "ui-v2-dist"
if v2_dir.is_dir():
v2_index = v2_dir / "index.html"
if v2_index.exists():
content = v2_index.read_text()
return HTMLResponse(content)
# Fallback: React build
if UI_DIST_DIR and UI_DIST_DIR.is_dir():
index_html = UI_DIST_DIR / "index.html"
if index_html.exists():
content = index_html.read_text()
return HTMLResponse(content)
# Fallback: legacy dashboard (guitar channel state)
state = self._gather_state(channel="guitar")
ctx = {"request": request}
ctx.update(state)
return templates.TemplateResponse(request, "dashboard.html", ctx)
"""Serve the v2 UI."""
v2_index = V2_DIST_DIR / "index.html"
if v2_index.exists():
content = v2_index.read_text()
return HTMLResponse(content)
return HTMLResponse("<h1>Pi Multi-FX Pedal</h1><p>UI build not found.</p>")
@app.get("/presets", response_class=HTMLResponse)
async def preset_manager(request: Request):
"""Preset manager page — browse banks, load/save/edit."""
return templates.TemplateResponse(request, "presets.html", {"request": request})
# Legacy routes — redirect to the v2 SPA
@app.get("/models", response_class=HTMLResponse)
async def model_manager(request: Request):
"""NAM model manager page."""
return templates.TemplateResponse(request, "models.html", {"request": request})
@app.get("/presets")
async def presets_redirect():
return RedirectResponse(url="/")
@app.get("/irs", response_class=HTMLResponse)
async def ir_manager(request: Request):
"""IR manager page."""
return templates.TemplateResponse(request, "irs.html", {"request": request})
@app.get("/models")
async def models_redirect():
return RedirectResponse(url="/")
@app.get("/settings", response_class=HTMLResponse)
async def settings_page(request: Request):
"""Settings page."""
state = self._gather_state(channel="guitar")
ctx = {"request": request}
ctx.update(state)
return templates.TemplateResponse(request, "settings.html", ctx)
@app.get("/irs")
async def irs_redirect():
return RedirectResponse(url="/")
@app.get("/settings")
async def settings_redirect():
return RedirectResponse(url="/")
# ── REST API ────────────────────────────────────────────────
-37
View File
@@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>{% block title %}Pi Multi-FX Pedal{% endblock %}</title>
<link rel="stylesheet" href="/static/style.css?v={{ version }}">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text y='28' font-size='28'>🎸</text></svg>">
</head>
<body>
<div class="app-container">
<header class="app-header">
<div class="header-left">
<a href="/" class="logo">🎸 Pi Pedal</a>
</div>
<nav class="header-nav">
<a href="/" class="nav-link {% if request.url.path == '/' %}active{% endif %}">Dashboard</a>
<a href="/presets" class="nav-link {% if request.url.path == '/presets' %}active{% endif %}">Presets</a>
<a href="/models" class="nav-link {% if request.url.path == '/models' %}active{% endif %}">Models</a>
<a href="/irs" class="nav-link {% if request.url.path == '/irs' %}active{% endif %}">IRs</a>
<a href="/settings" class="nav-link {% if request.url.path == '/settings' %}active{% endif %}">Settings</a>
</nav>
<div class="header-right">
<span class="connection-indicator" id="connection-status">Disconnected</span>
</div>
</header>
<main class="app-main">
{% block content %}{% endblock %}
</main>
</div>
<script src="/static/websocket.js?v={{ version }}"></script>
<script src="/static/app.js?v={{ version }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
-114
View File
@@ -1,114 +0,0 @@
{% extends "base.html" %}
{% block title %}Dashboard — Pi Multi-FX Pedal{% endblock %}
{% block content %}
<div class="dashboard">
<!-- Current Preset Card -->
<div class="card">
<div class="card-header">
<h2>Current Preset</h2>
</div>
<div class="card-body">
<div class="preset-name-display" id="preset-name">{{ current_preset.name if current_preset else "—" }}</div>
<div class="preset-meta" id="preset-meta">
Bank {{ current_preset.bank if current_preset else 0 }} — Program {{ current_preset.program if current_preset else 0 }}
</div>
<div class="preset-controls">
<button class="btn btn-primary" onclick="activatePreset('prev', -1)">◀ Prev</button>
<button class="btn btn-primary" onclick="activatePreset('next', 1)">Next ▶</button>
</div>
</div>
</div>
<!-- Status Row -->
<div class="status-row">
<div class="card status-card">
<div class="status-label">Bypass</div>
<div class="status-value">
<button class="btn toggle-btn {% if bypass %}active{% endif %}" id="bypass-btn" onclick="toggleBypass()">
{% if bypass %}BYPASSED{% else %}ACTIVE{% endif %}
</button>
</div>
</div>
<div class="card status-card">
<div class="status-label">Tuner</div>
<div class="status-value">
<button class="btn toggle-btn {% if tuner_enabled %}active{% endif %}" id="tuner-btn" onclick="toggleTuner()">
{% if tuner_enabled %}ON{% else %}OFF{% endif %}
</button>
</div>
</div>
<div class="card status-card">
<div class="status-label">Volume</div>
<div class="status-value">
<input type="range" class="slider" id="volume-slider"
min="0" max="100" value="{{ (master_volume * 100)|int }}"
oninput="setVolume(this.value)">
<span class="volume-text" id="volume-text">{{ (master_volume * 100)|int }}%</span>
</div>
</div>
</div>
<!-- FX Chain -->
<div class="card">
<div class="card-header">
<h2>FX Chain</h2>
</div>
<div class="card-body">
<div class="fx-chain" id="fx-chain">
<div class="fx-block-chain-placeholder" id="fx-chain-placeholder">
<p>Connect to pedal to see active FX chain.</p>
</div>
</div>
</div>
</div>
<!-- NAM Model Status -->
<div class="card">
<div class="card-header">
<h2>NAM Model</h2>
</div>
<div class="card-body">
<div class="model-status" id="model-status">
<span class="model-name" id="model-name">{{ nam_model if nam_model else "None loaded" }}</span>
</div>
</div>
</div>
<!-- IR Status -->
<div class="card">
<div class="card-header">
<h2>IR Cabinet</h2>
</div>
<div class="card-body">
<div class="ir-status" id="ir-status">
<span class="ir-name" id="ir-name">{{ ir_name if ir_name else "None loaded" }}</span>
</div>
</div>
</div>
<!-- 4CM Routing Status -->
<div class="card">
<div class="card-header">
<h2>Routing</h2>
</div>
<div class="card-body">
<div class="routing-status">
<span class="routing-indicator" id="routing-mode-indicator" data-mode="{{ routing_mode }}">
{% if routing_mode == '4cm' %}
<span class="badge badge-4cm">4CM</span>
{% else %}
<span class="badge badge-mono">Mono</span>
{% endif %}
</span>
<span class="routing-info" id="routing-info">
{% if routing_mode == '4cm' %}
Breakpoint: {{ routing_breakpoint }}
{% else %}
Full chain
{% endif %}
</span>
</div>
</div>
</div>
</div>
{% endblock %}
-69
View File
@@ -1,69 +0,0 @@
{% extends "base.html" %}
{% block title %}IR Cabinet — Pi Multi-FX Pedal{% endblock %}
{% block content %}
<div class="page irs-page">
<div class="page-header">
<h1>IR Manager</h1>
<button class="btn btn-secondary" onclick="loadIRs()">⟳ Refresh</button>
</div>
<div class="card">
<div class="card-header">
<h2>Current IR</h2>
</div>
<div class="card-body">
<div class="current-ir" id="current-ir">
<span class="ir-name">None loaded</span>
</div>
<div class="ir-controls">
<button class="btn btn-danger" onclick="unloadIR()" id="unload-ir-btn" style="display:none">Unload</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Available IR Files</h2>
</div>
<div class="card-body">
<div class="ir-list" id="ir-list">
<p class="loading">Loading IR files...</p>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Upload IR</h2>
</div>
<div class="card-body">
<p class="hint">Upload .wav IR files to the pedal's IR directory.</p>
<input type="file" id="ir-upload-input" accept=".wav" class="file-input">
<button class="btn btn-primary" onclick="uploadIR()">Upload</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Browse Tone3000 <span class="badge badge-tone3000">BETA</span></h2>
</div>
<div class="card-body">
<p class="hint">Search and download impulse responses directly from the Tone3000 community.</p>
<div class="tonedownload-search-row">
<input type="text" id="ir-tone-search" class="file-input tonedownload-input"
placeholder="Search IRs (e.g. Celestion V30)..." onkeydown="if(event.key==='Enter')searchToneIRs()">
<button class="btn btn-primary" onclick="searchToneIRs()">Search</button>
<button class="btn btn-secondary" onclick="loadTrendingIRs()">Trending</button>
</div>
<div id="ir-tone-loader" class="loading" style="display:none">Searching Tone3000...</div>
<div id="ir-tone-offline" class="tonedownload-offline" style="display:none">
<span>⚠ Offline — search unavailable</span>
</div>
<div id="ir-tone-results" class="tonedownload-results"></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/irs.js"></script>
{% endblock %}
-69
View File
@@ -1,69 +0,0 @@
{% extends "base.html" %}
{% block title %}NAM Models — Pi Multi-FX Pedal{% endblock %}
{% block content %}
<div class="page models-page">
<div class="page-header">
<h1>NAM Model Manager</h1>
<button class="btn btn-secondary" onclick="loadModels()">⟳ Refresh</button>
</div>
<div class="card">
<div class="card-header">
<h2>Current Model</h2>
</div>
<div class="card-body">
<div class="current-model" id="current-model">
<span class="model-name">None loaded</span>
</div>
<div class="model-controls">
<button class="btn btn-danger" onclick="unloadModel()" id="unload-model-btn" style="display:none">Unload</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Available Models</h2>
</div>
<div class="card-body">
<div class="model-list" id="model-list">
<p class="loading">Loading models...</p>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Upload Model</h2>
</div>
<div class="card-body">
<p class="hint">Upload .nam model files to the pedal's model directory.</p>
<input type="file" id="model-upload-input" accept=".nam" class="file-input">
<button class="btn btn-primary" onclick="uploadModel()">Upload</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Browse Tone3000 <span class="badge badge-tone3000">BETA</span></h2>
</div>
<div class="card-body">
<p class="hint">Search and download NAM models directly from the Tone3000 community.</p>
<div class="tonedownload-search-row">
<input type="text" id="model-tone-search" class="file-input tonedownload-input"
placeholder="Search models (e.g. Marshall JCM800)..." onkeydown="if(event.key==='Enter')searchToneModels()">
<button class="btn btn-primary" onclick="searchToneModels()">Search</button>
<button class="btn btn-secondary" onclick="loadTrendingModels()">Trending</button>
</div>
<div id="model-tone-loader" class="loading" style="display:none">Searching Tone3000...</div>
<div id="model-tone-offline" class="tonedownload-offline" style="display:none">
<span>⚠ Offline — search unavailable</span>
</div>
<div id="model-tone-results" class="tonedownload-results"></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/models.js"></script>
{% endblock %}
-34
View File
@@ -1,34 +0,0 @@
{% extends "base.html" %}
{% block title %}Presets — Pi Multi-FX Pedal{% endblock %}
{% block content %}
<div class="page presets-page">
<div class="page-header">
<h1>Preset Manager</h1>
<button class="btn btn-secondary" onclick="loadPresets()">⟳ Refresh</button>
</div>
<div class="bank-list" id="bank-list">
<p class="loading">Loading presets...</p>
</div>
</div>
<div class="modal-overlay" id="preset-modal" style="display:none" onclick="closeModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modal-title">Edit Preset</h2>
<button class="btn-close" onclick="closeModal()"></button>
</div>
<div class="modal-body" id="modal-body">
<!-- Dynamic content loaded by JS -->
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" id="save-preset-btn" onclick="savePreset()">Save</button>
<button class="btn btn-danger" onclick="deletePreset()">Delete</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/presets.js"></script>
{% endblock %}
-381
View File
@@ -1,381 +0,0 @@
{% extends "base.html" %}
{% block title %}Settings — Pi Multi-FX Pedal{% endblock %}
{% block content %}
<div class="page settings-page">
<div class="page-header">
<h1>Settings</h1>
</div>
<div class="settings-layout">
<!-- Left sidebar navigation -->
<nav class="settings-sidebar">
<a class="settings-nav-item active" href="#connection" data-tab="connection" onclick="switchTab('connection')">
<span class="settings-nav-icon">🔌</span>
<span class="settings-nav-label">Connection</span>
</a>
<a class="settings-nav-item" href="#audio" data-tab="audio" onclick="switchTab('audio')">
<span class="settings-nav-icon">🎚️</span>
<span class="settings-nav-label">Audio</span>
</a>
<a class="settings-nav-item" href="#routing" data-tab="routing" onclick="switchTab('routing')">
<span class="settings-nav-icon">🔀</span>
<span class="settings-nav-label">4CM Routing</span>
</a>
<a class="settings-nav-item" href="#network" data-tab="network" onclick="switchTab('network')">
<span class="settings-nav-icon">📶</span>
<span class="settings-nav-label">Network</span>
</a>
<a class="settings-nav-item" href="#bluetooth" data-tab="bluetooth" onclick="switchTab('bluetooth')">
<span class="settings-nav-icon">🔵</span>
<span class="settings-nav-label">Bluetooth</span>
</a>
<a class="settings-nav-item" href="#about" data-tab="about" onclick="switchTab('about')">
<span class="settings-nav-icon"></span>
<span class="settings-nav-label">About</span>
</a>
</nav>
<!-- Content area -->
<div class="settings-content">
<!-- Tab: Connection -->
<div class="settings-panel active" id="tab-connection">
<div class="card">
<div class="card-header"><h2>Connection</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">Hostname</div>
<div class="setting-value"><code id="hostname">pedal.local</code></div>
</div>
<div class="setting-row">
<div class="setting-label">Web Server</div>
<div class="setting-value"><code id="web-url">http://pedal.local:8080</code></div>
</div>
<div class="setting-row">
<div class="setting-label">Connected Clients</div>
<div class="setting-value"><span id="ws-count">0</span></div>
</div>
</div>
</div>
</div>
<!-- Tab: Audio -->
<div class="settings-panel" id="tab-audio">
<div class="card">
<div class="card-header"><h2>Audio</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">Master Volume</div>
<div class="setting-value">
<input type="range" class="slider" id="settings-volume"
min="0" max="100" value="{{ (master_volume * 100)|int }}"
oninput="setVolume(this.value)">
<span id="settings-volume-text">{{ (master_volume * 100)|int }}%</span>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Tuner</div>
<div class="setting-value">
<button class="btn toggle-btn {% if tuner_enabled %}active{% endif %}"
id="settings-tuner-btn" onclick="toggleTuner()">
{% if tuner_enabled %}ON{% else %}OFF{% endif %}
</button>
</div>
</div>
<div class="setting-row" style="border-bottom:none">
<div class="setting-label">Latency Profile</div>
<div class="setting-value">
<select id="audio-profile-select" class="profile-select"
onchange="switchAudioProfile(this.value)">
<option value="">Loading...</option>
</select>
</div>
</div>
<div class="setting-row" style="border-bottom:none">
<div class="setting-label">JACK Status</div>
<div class="setting-value">
<span id="jack-status" class="jack-check"></span>
<button class="btn btn-secondary btn-sm" onclick="refreshAudioProfile()"></button>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: 4CM Routing -->
<div class="settings-panel" id="tab-routing">
<div class="card">
<div class="card-header"><h2>4-Cable Method (4CM)</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">4CM Mode</div>
<div class="setting-value">
<button class="btn toggle-btn {% if routing_mode == '4cm' %}active{% endif %}"
id="4cm-toggle-btn" onclick="toggle4cm()">
{% if routing_mode == '4cm' %}ON{% else %}OFF{% endif %}
</button>
</div>
</div>
<div class="setting-row" id="breakpoint-row"
{% if routing_mode != '4cm' %}style="opacity: 0.4; pointer-events: none;"{% endif %}>
<div class="setting-label">Breakpoint</div>
<div class="setting-value">
<input type="range" class="slider" id="4cm-breakpoint"
min="0" max="16" value="{{ routing_breakpoint }}"
oninput="set4cmBreakpoint(this.value)">
<span id="4cm-breakpoint-text">{{ routing_breakpoint }}</span>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Routing</div>
<div class="setting-value">
<span id="4cm-routing-desc" class="routing-desc">
{% if routing_mode == '4cm' %}
Input 1 (Guitar) → Pre blocks [0..{{ routing_breakpoint }}) → Send
&nbsp;|&nbsp; Input 2 (Return) → Post blocks [{{ routing_breakpoint }}..] → Output
{% else %}
Mono: Guitar → Full chain → Output
{% endif %}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: Network -->
<div class="settings-panel" id="tab-network">
<!-- WiFi Client Status -->
<div class="card" id="wifi-status-card">
<div class="card-header"><h2>WiFi Status</h2></div>
<div class="card-body">
<div class="network-status-row">
<div class="network-status-icon" id="wifi-icon">📶</div>
<div class="network-status-info">
<div class="network-ssid" id="wifi-ssid"></div>
<div class="network-ip" id="wifi-ip"></div>
<div class="network-signal"><span id="wifi-signal-text"></span></div>
</div>
<button class="btn btn-secondary" onclick="refreshWiFiStatus()">Refresh</button>
</div>
</div>
</div>
<!-- WiFi Scan & Connect -->
<div class="card">
<div class="card-header">
<h2>WiFi Networks</h2>
<button class="btn btn-secondary" onclick="scanWiFi()" id="wifi-scan-btn">Scan</button>
</div>
<div class="card-body" id="wifi-scan-results">
<div class="hint">Click Scan to find available WiFi networks.</div>
</div>
</div>
<!-- Known Networks -->
<div class="card" id="wifi-saved-card">
<div class="card-header"><h2>Known Networks</h2></div>
<div class="card-body" id="wifi-saved-list">
<div class="loading">Loading...</div>
</div>
</div>
<!-- Hotspot -->
<div class="card">
<div class="card-header"><h2>WiFi Hotspot</h2></div>
<div class="card-body">
<div class="hotspot-controls">
<div class="setting-row">
<div class="setting-label">Hotspot Status</div>
<div class="setting-value">
<span id="hotspot-status-indicator" class="hotspot-indicator hotspot-off">Off</span>
</div>
</div>
<div class="setting-row" id="hotspot-ssid-row">
<div class="setting-label">SSID</div>
<div class="setting-value">
<input type="text" class="network-input" id="hotspot-ssid" value="Pi-Pedal" maxlength="32">
</div>
</div>
<div class="setting-row" id="hotspot-psk-row">
<div class="setting-label">Password</div>
<div class="setting-value">
<input type="password" class="network-input" id="hotspot-psk" value="pedal1234" maxlength="63">
</div>
</div>
<div class="setting-row">
<div class="setting-label">Connected Clients</div>
<div class="setting-value"><span id="hotspot-clients">0</span></div>
</div>
<div class="hotspot-actions">
<button class="btn btn-primary" id="hotspot-enable-btn" onclick="enableHotspot()">Enable Hotspot</button>
<button class="btn btn-danger" id="hotspot-disable-btn" onclick="disableHotspot()">Disable Hotspot</button>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: Bluetooth -->
<div class="settings-panel" id="tab-bluetooth">
<!-- BT Adapter Status -->
<div class="card">
<div class="card-header"><h2>Bluetooth</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">Bluetooth</div>
<div class="setting-value">
<button class="btn toggle-btn" id="bt-power-btn" onclick="toggleBTPower()">OFF</button>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Discoverable</div>
<div class="setting-value">
<button class="btn toggle-btn" id="bt-discoverable-btn" onclick="toggleBTDiscoverable()">OFF</button>
</div>
</div>
<div class="setting-row">
<div class="setting-label">Adapter Name</div>
<div class="setting-value"><span id="bt-adapter-name"></span></div>
</div>
<div class="setting-row">
<div class="setting-label">Address</div>
<div class="setting-value"><code id="bt-address"></code></div>
</div>
</div>
</div>
<!-- BT Scan -->
<div class="card">
<div class="card-header">
<h2>Discover Devices</h2>
<button class="btn btn-secondary" onclick="scanBT()" id="bt-scan-btn">Scan</button>
</div>
<div class="card-body" id="bt-scan-results">
<div class="hint">Click Scan to find discoverable Bluetooth devices.</div>
</div>
</div>
<!-- Paired Devices -->
<div class="card">
<div class="card-header"><h2>Paired Devices</h2></div>
<div class="card-body" id="bt-paired-list">
<div class="loading">Loading...</div>
</div>
</div>
<!-- BT MIDI Service -->
<div class="card">
<div class="card-header"><h2>Bluetooth MIDI</h2></div>
<div class="card-body">
<div class="setting-row">
<div class="setting-label">BT MIDI Service</div>
<div class="setting-value">
<span class="bt-midi-indicator" id="bt-midi-indicator">Off</span>
</div>
</div>
<div class="bt-midi-actions">
<button class="btn btn-primary" id="bt-midi-enable-btn" onclick="enableBTMIDI()">Enable</button>
<button class="btn btn-danger" id="bt-midi-disable-btn" onclick="disableBTMIDI()">Disable</button>
</div>
<div class="setting-row mt-8">
<div class="setting-label">Connected MIDI Devices</div>
<div class="setting-value" id="bt-midi-devices">
<span class="hint">None connected</span>
</div>
</div>
</div>
</div>
</div>
<!-- Tab: About -->
<div class="settings-panel" id="tab-about">
<div class="card">
<div class="card-header"><h2>About</h2></div>
<div class="card-body">
<div class="about-info">
<p><strong>Pi Multi-FX Pedal</strong> v0.1.0</p>
<p>Raspberry Pi 4B — Python 3.11 — JACK Audio</p>
<p><a href="https://gitea.ourpad.casa/shawn/pi-multifx-pedal" target="_blank">Gitea Repository</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- WiFi connect modal -->
<div class="modal-overlay" id="wifi-connect-modal" style="display:none;" onclick="if(event.target===this)closeWiFiModal()">
<div class="modal">
<div class="modal-header">
<h2>Connect to <span id="wifi-modal-ssid"></span></h2>
<button class="btn-close" onclick="closeWiFiModal()">&times;</button>
</div>
<div class="modal-body">
<div class="setting-row">
<div class="setting-label">Password</div>
<div class="setting-value">
<input type="password" class="network-input" id="wifi-modal-password" placeholder="Enter WiFi password">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="connectWiFi()">Connect</button>
<button class="btn btn-secondary" onclick="closeWiFiModal()">Cancel</button>
</div>
<div id="wifi-modal-error" class="network-error mt-8"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/network.js"></script>
<script src="/static/bluetooth.js"></script>
<script>
/* ── Tab switching (left nav) ──────────────────────────────────── */
function switchTab(tabId) {
// Update nav items
document.querySelectorAll('.settings-nav-item').forEach(a => a.classList.remove('active'));
document.querySelector(`.settings-nav-item[data-tab="${tabId}"]`).classList.add('active');
// Update panels
document.querySelectorAll('.settings-panel').forEach(p => p.classList.remove('active'));
document.getElementById(`tab-${tabId}`).classList.add('active');
// Sync URL hash
if (window.location.hash !== `#${tabId}`) {
history.pushState(null, '', `#${tabId}`);
}
// Refresh tab data when switching to it
if (tabId === 'network') {
refreshWiFiStatus();
loadSavedNetworks();
} else if (tabId === 'bluetooth') {
refreshBTStatus();
refreshBTMIDIStatus();
loadPairedDevices();
} else if (tabId === 'audio') {
refreshAudioProfile();
}
}
/* ── Initial load & hash routing ───────────────────────────────── */
document.addEventListener('DOMContentLoaded', () => {
const hash = window.location.hash.replace('#', '');
const valid = ['connection', 'audio', 'routing', 'network', 'bluetooth', 'about'];
if (valid.includes(hash)) {
switchTab(hash);
}
});
/* ── Listen for back/forward navigation ───────────────────────── */
window.addEventListener('popstate', () => {
const hash = window.location.hash.replace('#', '');
const valid = ['connection', 'audio', 'routing', 'network', 'bluetooth', 'about'];
if (valid.includes(hash)) {
switchTab(hash);
}
});
</script>
{% endblock %}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

-24
View File
@@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

-14
View File
@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ui</title>
<script type="module" crossorigin src="/ui/assets/index-DFFBeBW_.js"></script>
<link rel="stylesheet" crossorigin href="/ui/assets/index-D3aC6klh.css">
</head>
<body>
<div id="root"></div>
</body>
</html>