Build: Web UI + mDNS (pedal.local)
FastAPI web server with REST + WebSocket, 5 Jinja2 HTML pages, mobile-friendly dark theme CSS, page-specific JS controllers, and Avahi mDNS service advertisement for pedal.local. Files: src/web/server.py — FastAPI app with 17 REST endpoints + /ws WebSocket src/web/__init__.py — Package init with WebServer, WebServerDeps exports src/web/templates/*.html — Dashboard, Presets, Models, IRs, Settings (base) src/web/static/style.css — Dark mobile-first CSS (80-82% max-width) src/web/static/websocket.js — Auto-reconnecting WebSocket manager src/web/static/app.js — Dashboard live updates + REST helpers src/web/static/presets.js — Bank/preset CRUD modal UI src/web/static/models.js — NAM model list/load/upload src/web/static/irs.js — IR file list/load/upload etc/avahi/pi-multifx-pedal.service — Avahi service advertisement scripts/setup-mdns.sh — One-shot mDNS setup (avahi, hostname, restart) main.py — Wire WebServer into boot/shutdown requirements.txt — Add fastapi, uvicorn, jinja2, python-multipart tests/test_web.py — 36 tests (pages, REST, WebSocket, errors) All 36 tests pass.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
<!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">
|
||||
<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"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,89 @@
|
||||
{% 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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/irs.js"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/models.js"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% 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="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 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>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user