v2 UI at root, instrument type selector, Gitea push

This commit is contained in:
2026-06-15 05:53:11 +00:00
parent e2f81fd309
commit a69aee357c
2 changed files with 1385 additions and 46 deletions
+730
View File
@@ -0,0 +1,730 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>Pedal v2</title>
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
:root{--bg:#0A0A0E;--surface:#16161E;--panel:#1E1E2A;--border:#2A2A3E;--text:#EAEAFF;--text-dim:#6A6A8A;--text-sec:#9A9AB0;--amber:#E8A030;--green:#50D090;--red:#E05050;--blue:#4090E0;--radius:10px;--font:'Space Grotesk','Inter',system-ui,-apple-system,sans-serif}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--font);overflow:hidden;touch-action:manipulation}
body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webkit-user-select:none}
/* Status Bar */
.status-bar{display:flex;align-items:center;gap:6px;padding:3px 8px;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0;min-height:34px}
.status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.status-dot.on{background:var(--green);box-shadow:0 0 6px var(--green)}
.status-dot.off{background:var(--red)}
.status-channel{font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--amber);padding:2px 6px;border-radius:4px;background:rgba(232,160,48,.12);white-space:nowrap}
.status-preset{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:600;min-width:0}
.status-vol{font-size:10px;font-weight:700;color:var(--amber);white-space:nowrap;font-variant-numeric:tabular-nums}
.status-btn{width:40px;height:40px;border:none;background:transparent;color:var(--text-dim);font-size:16px;border-radius:6px;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;transition:all .15s}
.status-btn:active{background:var(--border);color:var(--amber)}
/* Main content */
.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;padding:4px 6px;gap:4px}
/* FX Chain */
.fx-strip{display:flex;gap:6px;overflow-x:auto;overflow-y:hidden;flex-shrink:0;padding:4px 0;min-height:72px;scrollbar-width:none;-ms-overflow-style:none}
.fx-strip::-webkit-scrollbar{display:none}
.fx-block{flex-shrink:0;width:100px;border-radius:var(--radius);background:var(--surface);border:1.5px solid var(--border);padding:4px 6px;display:flex;flex-direction:column;cursor:pointer;transition:all .15s;position:relative}
.fx-block:active{transform:scale(.96)}
.fx-block.on{border-color:var(--green);background:rgba(80,208,144,.06)}
.fx-block.off{opacity:.4}
.fx-block-add{flex-shrink:0;width:72px;border-radius:var(--radius);border:1.5px dashed var(--border);background:transparent;display:flex;align-items:center;justify-content:center;font-size:22px;color:var(--text-dim);cursor:pointer;transition:all .15s}
.fx-block-add:active{background:var(--surface);color:var(--amber);border-color:var(--amber)}
.fx-type{font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin-bottom:1px}
.fx-name{font-size:10px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.fx-indicator{display:flex;gap:2px;margin-top:auto;padding-top:2px}
.fx-dot{width:6px;height:6px;border-radius:50%;background:var(--border)}
.fx-dot.active{background:var(--amber)}
/* Volume */
.vol-row{display:flex;align-items:center;gap:8px;flex-shrink:0;min-height:36px;padding:0 4px}
.vol-label{font-size:9px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em;width:44px;flex-shrink:0}
.vol-track{flex:1;height:36px;display:flex;align-items:center;cursor:pointer;touch-action:none;position:relative}
.vol-track-bg{width:100%;height:6px;border-radius:3px;background:var(--border);overflow:hidden;position:relative}
.vol-track-fill{height:100%;border-radius:3px;background:var(--amber);transition:width .05s linear}
.vol-track-thumb{position:absolute;top:50%;transform:translate(-50%,-50%);width:24px;height:24px;border-radius:50%;background:var(--amber);border:2px solid var(--bg);box-shadow:0 1px 6px rgba(0,0,0,.5);pointer-events:none}
.vol-pct{font-size:12px;font-weight:700;color:var(--amber);min-width:38px;text-align:right;font-variant-numeric:tabular-nums}
/* Bottom Tab Bar */
.tab-bar{display:flex;flex-shrink:0;background:var(--panel);border-top:1px solid var(--border);min-height:44px}
.tab-btn{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;border:none;background:transparent;color:var(--text-dim);cursor:pointer;padding:4px 2px;transition:all .1s;min-height:44px}
.tab-btn .tab-icon{font-size:14px;line-height:1}
.tab-btn .tab-label{font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}
.tab-btn.active{color:var(--amber);background:rgba(232,160,48,.08)}
.tab-btn:active{opacity:.7}
/* Footswitch */
.footswitch-row{flex-shrink:0;padding:3px 6px}
.footswitch{width:100%;min-height:64px;border-radius:12px;border:none;font-family:var(--font);font-size:16px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;cursor:pointer;transition:all .12s;display:flex;align-items:center;justify-content:center;gap:8px;box-shadow:0 3px 16px rgba(0,0,0,.4)}
.footswitch:active{transform:scale(.97)}
.footswitch.on{background:linear-gradient(180deg,#60D898,#30A068);color:#003020}
.footswitch.off{background:linear-gradient(180deg,#E86868,#B03030);color:#200000}
/* Overlays */
.overlay{position:fixed;inset:0;z-index:100;background:var(--bg);display:none;flex-direction:column;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
.overlay.open{display:flex}
.overlay-hdr{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0;min-height:38px}
.overlay-title{font-size:13px;font-weight:700}
.overlay-close{width:38px;height:38px;border:none;background:transparent;color:var(--text-dim);font-size:16px;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.overlay-close:active{background:var(--border)}
.overlay-body{flex:1;overflow-y:auto;padding:6px 10px}
/* Preset Grid */
.preset-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:6px 0}
.preset-card{border-radius:var(--radius);border:1.5px solid var(--border);background:var(--surface);padding:10px 8px;cursor:pointer;transition:all .12s;min-height:52px;display:flex;flex-direction:column;gap:2px}
.preset-card:active{transform:scale(.97);border-color:var(--amber)}
.preset-card.active{border-color:var(--amber);background:rgba(232,160,48,.08)}
.preset-card .pc-num{font-size:9px;font-weight:700;color:var(--text-dim)}
.preset-card .pc-name{font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Tuner */
.tuner-view{position:fixed;inset:0;z-index:90;background:var(--bg);display:none;flex-direction:column;align-items:center;justify-content:center}
.tuner-view.open{display:flex}
.tuner-note{font-size:64px;font-weight:800;color:var(--text);line-height:1;margin-bottom:4px}
.tuner-cents{height:44px;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px}
.tuner-cents-bar{width:100px;height:6px;border-radius:3px;background:var(--border);overflow:hidden;position:relative}
.tuner-cents-fill{height:100%;border-radius:3px;position:absolute}
.tuner-cents-fill.in-tune{background:var(--green);left:calc(50% - 3px);width:6px}
.tuner-cents-fill.sharp{background:var(--red)}
.tuner-cents-fill.flat{background:var(--blue)}
.tuner-freq{font-size:11px;color:var(--text-dim);margin-bottom:12px}
.tuner-exit{width:60%;max-width:260px;min-height:50px;border-radius:var(--radius);border:1.5px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font);font-size:14px;font-weight:700;cursor:pointer}
.tuner-exit:active{background:var(--panel);border-color:var(--amber)}
/* Settings */
.setting-group{margin-bottom:12px}
.setting-group-title{font-size:10px;font-weight:700;color:var(--text-sec);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px;padding:0 4px}
.setting-row{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:var(--surface);border-radius:6px;border:1px solid var(--border);margin-bottom:3px;min-height:42px}
.setting-row .sr-label{font-size:11px;font-weight:600}
.setting-row .sr-value{font-size:10px;color:var(--text-sec)}
.setting-row .sr-action{min-width:52px;min-height:32px;border-radius:6px;border:1px solid var(--border);background:var(--panel);color:var(--text);font-family:var(--font);font-size:10px;font-weight:600;cursor:pointer;padding:0 8px;display:flex;align-items:center;justify-content:center}
.setting-row .sr-action:active{background:var(--border)}
.setting-row .sr-action.primary{background:var(--amber);color:#000;border-color:var(--amber)}
.setting-row .sr-action.danger{background:var(--red);color:#fff;border-color:var(--red)}
.wifi-list{display:flex;flex-direction:column;gap:3px}
.wifi-item{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--surface);border-radius:6px;border:1px solid var(--border);cursor:pointer;min-height:40px}
.wifi-item:active{background:var(--panel)}
.wifi-item .wi-ssid{font-size:11px;font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wifi-connect-pw{display:flex;gap:4px;padding:6px 0}
.wifi-connect-pw input{flex:1;padding:6px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font);font-size:11px;outline:none;min-height:36px}
.wifi-connect-pw input:focus{border-color:var(--amber)}
.wifi-connect-pw button{min-width:52px;min-height:36px;border-radius:6px;border:1px solid var(--amber);background:var(--amber);color:#000;font-family:var(--font);font-size:11px;font-weight:700;cursor:pointer}
.audio-profile-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px}
.ap-card{border-radius:6px;border:1px solid var(--border);background:var(--surface);padding:6px;cursor:pointer;text-align:center;transition:all .12s;min-height:40px}
.ap-card:active{transform:scale(.97)}
.ap-card.active{border-color:var(--amber);background:rgba(232,160,48,.1)}
.ap-card .ap-key{font-size:10px;font-weight:700;text-transform:capitalize}
.ap-card .ap-desc{font-size:8px;color:var(--text-dim);margin-top:1px}
.snap-grid{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px}
.snap-slot{border-radius:6px;border:1px solid var(--border);background:var(--surface);padding:6px 3px;cursor:pointer;text-align:center;transition:all .12s;min-height:40px;display:flex;flex-direction:column;align-items:center;justify-content:center}
.snap-slot:active{transform:scale(.95)}
.snap-slot.used{border-color:var(--amber);background:rgba(232,160,48,.06)}
.snap-slot.current{border-color:var(--green);background:rgba(80,208,144,.1)}
.snap-slot .ss-num{font-size:9px;font-weight:700;color:var(--text-dim)}
.snap-slot .ss-name{font-size:9px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50px}
.spinner{width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--amber);border-radius:50%;animation:spin .6s linear infinite;display:inline-block;vertical-align:middle}
@keyframes spin{to{transform:rotate(360deg)}}
.toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:200;background:var(--panel);border:1px solid var(--amber);border-radius:8px;padding:6px 14px;font-size:11px;font-weight:600;color:var(--text);display:none;white-space:nowrap;box-shadow:0 4px 20px rgba(0,0,0,.5)}
.toast.show{display:block;animation:toastIn .2s ease}
@keyframes toastIn{from{opacity:0;transform:translateX(-50%) translateY(10px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
</style>
</head>
<body>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-dot on" id="statusDot"></div>
<div class="status-channel" id="statusChannel" onclick="cycleInstrument()" style="cursor:pointer">Guitar</div>
<div class="status-preset" id="statusPreset"></div>
<div class="status-vol" id="statusVolPct">80%</div>
<button class="status-btn" id="btnChannel" onclick="cycleInstrument()">🎛</button>
<button class="status-btn" id="btnSettings"></button>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="fx-strip" id="fxStrip">
<div class="fx-block-add" id="btnAddBlock">+</div>
</div>
<div class="vol-row">
<span class="vol-label">Vol</span>
<div class="vol-track" id="volTrack">
<div class="vol-track-bg"><div class="vol-track-fill" id="volFill" style="width:80%"></div></div>
<div class="vol-track-thumb" id="volThumb" style="left:80%"></div>
</div>
<span class="vol-pct" id="volPct">80%</span>
</div>
</div>
<!-- Tab Bar -->
<div class="tab-bar" id="tabBar">
<button class="tab-btn active" data-view="main"><span class="tab-icon">🎛</span><span class="tab-label">FX</span></button>
<button class="tab-btn" data-view="presets"><span class="tab-icon">🎸</span><span class="tab-label">Presets</span></button>
<button class="tab-btn" data-view="snapshots"><span class="tab-icon">📸</span><span class="tab-label">Snapshots</span></button>
<button class="tab-btn" data-view="downloads"><span class="tab-icon"></span><span class="tab-label">Downloads</span></button>
<button class="tab-btn" id="tabTuner"><span class="tab-icon">🎵</span><span class="tab-label">Tuner</span></button>
<button class="tab-btn" data-view="settings"><span class="tab-icon"></span><span class="tab-label">Settings</span></button>
</div>
<!-- Footswitch -->
<div class="footswitch-row">
<button class="footswitch on" id="footswitch">BYpass</button>
</div>
<!-- Presets Overlay -->
<div class="overlay" id="overlayPresets">
<div class="overlay-hdr"><span class="overlay-title">🎸 Presets</span><button class="overlay-close" id="closePresets"></button></div>
<div class="overlay-body"><div class="preset-grid" id="presetGrid"></div></div>
</div>
<!-- Snapshots Overlay -->
<div class="overlay" id="overlaySnapshots">
<div class="overlay-hdr"><span class="overlay-title">📸 Snapshots</span><button class="overlay-close" id="closeSnapshots"></button></div>
<div class="overlay-body">
<div style="display:flex;gap:6px;margin-bottom:6px"><button class="sr-action primary" id="snapSaveCurrent" style="flex:1">💾 Save Current</button></div>
<div class="snap-grid" id="snapGrid"></div>
</div>
</div>
<!-- Downloads Overlay -->
<div class="overlay" id="overlayDownloads">
<div class="overlay-hdr"><span class="overlay-title">⬇ Downloads</span><button class="overlay-close" id="closeDownloads"></button></div>
<div class="overlay-body">
<div style="display:flex;gap:6px;margin-bottom:6px">
<input id="dlSearch" placeholder="Search Tone3000..." style="flex:1;padding:6px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font);font-size:11px;outline:none;min-height:36px">
<button class="sr-action primary" id="dlSearchBtn">🔍</button>
</div>
<div id="dlResults"></div>
</div>
</div>
<!-- Settings Overlay -->
<div class="overlay" id="overlaySettings">
<div class="overlay-hdr"><span class="overlay-title">⚙ Settings</span><button class="overlay-close" id="closeSettings"></button></div>
<div class="overlay-body">
<div class="setting-group"><div class="setting-group-title">Network</div>
<div class="setting-row"><span class="sr-label">🌐 Eth IP</span><span class="sr-value" id="netEthIp"></span></div>
<div class="setting-row"><span class="sr-label">🔗 MAC</span><span class="sr-value" id="netEthMac"></span></div>
<div class="setting-row"><span class="sr-label">🚪 Gateway</span><span class="sr-value" id="netGateway"></span></div>
<div class="setting-row"><span class="sr-label">🌐 DNS</span><span class="sr-value" id="netDns"></span></div>
<div class="setting-row"><span class="sr-label">🌍 IP Mode</span><span class="sr-value" id="netDhcpStatus">DHCP</span><button class="sr-action" id="netModeToggle">Static</button></div>
<div class="setting-row"><span class="sr-label">🔗 Hostname</span><span class="sr-value" id="netHostname">pedal</span></div>
<div class="setting-row"><span class="sr-label">📶 WiFi</span><span class="sr-value" id="wifiStatus"></span><button class="sr-action" id="wifiScanBtn">Scan</button></div>
<div id="wifiNetworks" class="wifi-list"></div>
<div class="wifi-connect-pw" id="wifiConnectPw" style="display:none"><input type="password" id="wifiPw" placeholder="WiFi password"><button id="wifiConnectBtn">Connect</button></div>
<div class="setting-row"><span class="sr-label">🔥 Hotspot</span><span class="sr-value" id="hotspotStatus">Off</span><button class="sr-action" id="hotspotToggle">Enable</button></div>
</div>
<div class="setting-group"><div class="setting-group-title">Audio</div>
<div class="setting-row"><span class="sr-label">🎛 Profile</span><span class="sr-value" id="audioProfileLabel"></span></div>
<div class="audio-profile-grid" id="audioProfileGrid"></div>
<div class="setting-row"><span class="sr-label">🎸 Instrument</span><span class="sr-value" id="instrumentLabel">Guitar</span><button class="sr-action" id="instrumentCycleBtn" style="min-width:44px;padding:4px 10px">Cycle</button></div>
<div class="setting-row"><span class="sr-label">🔄 Channel Mode</span><span class="sr-value" id="channelModeLabel">Dual Mono</span><button class="sr-action" id="channelModeToggle">Toggle</button></div>
<div class="setting-row"><span class="sr-label">🔗 Routing</span><span class="sr-value" id="routingModeLabel">4CM</span><button class="sr-action" id="routingModeToggle">Toggle</button></div>
</div>
<div class="setting-group"><div class="setting-group-title">Bluetooth</div>
<div class="setting-row"><span class="sr-label">🔵 Status</span><span class="sr-value" id="btStatus"></span></div>
<div class="setting-row"><span class="sr-label">🎛 MIDI</span><span class="sr-value" id="btMidiStatus">Off</span><button class="sr-action" id="btMidiToggle">Enable</button></div>
<div class="setting-row"><span class="sr-label">🔓 Discoverable</span><span class="sr-value" id="btDiscStatus">On</span><button class="sr-action" id="btDiscToggle">Disable</button></div>
</div>
<div class="setting-group"><div class="setting-group-title">System</div>
<div class="setting-row"><span class="sr-label">🔧 CPU</span><span class="sr-value" id="cpuLabel">0%</span></div>
<div class="setting-row"><span class="sr-label">🌡 Temp</span><span class="sr-value" id="sysTemp"></span></div>
<div class="setting-row"><span class="sr-label">⏱ Uptime</span><span class="sr-value" id="sysUptime"></span></div>
<div class="setting-row"><span class="sr-label">💾 Disk</span><span class="sr-value" id="sysDisk"></span></div>
<div class="setting-row"><span class="sr-label">🧠 RAM</span><span class="sr-value" id="sysMemory"></span></div>
<div class="setting-row"><span class="sr-label">↺ Reset</span><button class="sr-action danger" id="btnResetDefaults">Reset to Defaults</button></div>
<div class="setting-row"><span class="sr-label">🔄 Service</span><button class="sr-action danger" id="btnRestartSvc">Restart</button></div>
<div class="setting-row"><span class="sr-label">⏻ Reboot</span><button class="sr-action danger" id="btnReboot">Reboot</button></div>
</div>
</div>
</div>
<!-- Tuner -->
<div class="tuner-view" id="tunerView">
<div class="tuner-note" id="tunerNote"></div>
<div class="tuner-cents"><span style="font-size:10px;color:var(--text-dim)"></span><div class="tuner-cents-bar"><div class="tuner-cents-fill in-tune" id="tunerCentsFill"></div></div><span style="font-size:10px;color:var(--text-dim)"></span></div>
<div class="tuner-freq" id="tunerFreq">0.0 Hz</div>
<button class="tuner-exit" id="tunerExit">✕ Close Tuner</button>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
// ═══ API Client ═══
const API = {
_base:'',
async _fetch(path,opts={}){
const ac=new AbortController();
const to=setTimeout(()=>ac.abort(),opts.timeout||6000);
try{
const r=await fetch(this._base+path,{...opts,signal:ac.signal,headers:{'Content-Type':'application/json',...opts.headers}});
clearTimeout(to);
if(!r.ok)throw new Error(r.status+' '+r.statusText);
return r.status===204?null:r.json();
}catch(e){clearTimeout(to);if(e.name==='AbortError')throw new Error('timeout');throw e;}
},
getState:(ch)=>API._fetch('/api/state?channel='+(ch||'guitar')),
getTunerPitch:(ch)=>API._fetch('/api/tuner/pitch?channel='+(ch||'guitar'),{timeout:3000}),
bypassToggle:(ch)=>API._fetch('/api/bypass?channel='+(ch||'guitar'),{method:'POST'}),
tunerToggle:(ch,on)=>API._fetch('/api/tuner?channel='+(ch||'guitar'),{method:'POST',body:JSON.stringify({on})}),
setVolume:(ch,v)=>API._fetch('/api/volume?channel='+(ch||'guitar'),{method:'PUT',body:JSON.stringify({volume:v})}),
toggleBlock:(ch,id,en)=>API._fetch('/api/blocks?channel='+(ch||'guitar'),{method:'PATCH',body:JSON.stringify({block_id:id,enabled:en})}),
listPresets:(ch)=>API._fetch('/api/presets?channel='+(ch||'guitar')),
activatePreset:(ch,b,p)=>API._fetch('/api/presets/'+b+'/'+p+'/activate?channel='+(ch||'guitar'),{method:'POST'}),
listSnapshots:(ch)=>API._fetch('/api/snapshots?channel='+(ch||'guitar')),
saveSnapshot:(ch,s,data)=>API._fetch('/api/snapshots/'+s+'/save?channel='+(ch||'guitar'),{method:'POST',body:data?JSON.stringify(data):undefined}),
recallSnapshot:(ch,s)=>API._fetch('/api/snapshots/'+s+'/recall?channel='+(ch||'guitar'),{method:'POST'}),
wifiScan:()=>API._fetch('/api/wifi/scan',{timeout:15000}),
wifiStatus:()=>API._fetch('/api/wifi/status'),
wifiConnect:(s,p)=>API._fetch('/api/wifi/connect',{method:'POST',body:JSON.stringify({ssid:s,password:p}),timeout:20000}),
hotspotEnable:()=>API._fetch('/api/wifi/hotspot/enable',{method:'POST',timeout:15000}),
hotspotDisable:()=>API._fetch('/api/wifi/hotspot/disable',{method:'POST'}),
hotspotStatus:()=>API._fetch('/api/wifi/hotspot'),
btStatus:()=>API._fetch('/api/bluetooth/status'),
btPaired:()=>API._fetch('/api/bluetooth/paired'),
btMidiStatus:()=>API._fetch('/api/bluetooth/midi-status'),
btMidiEnable:()=>API._fetch('/api/bluetooth/midi-enable',{method:'POST'}),
btMidiDisable:()=>API._fetch('/api/bluetooth/midi-disable',{method:'POST'}),
btDiscoverableSet:(on)=>API._fetch('/api/bluetooth/discoverable',{method:'POST',body:JSON.stringify({on})}),
listAudioProfiles:()=>API._fetch('/api/audio/profiles'),
setAudioProfile:(p)=>API._fetch('/api/audio/profile',{method:'POST',body:JSON.stringify({profile:p})}),
getChannelMode:()=>API._fetch('/api/channel-mode'),
setChannelMode:(m)=>API._fetch('/api/channel-mode',{method:'POST',body:JSON.stringify({channel_mode:m})}),
getRouting:(ch)=>API._fetch('/api/routing?channel='+(ch||'guitar')),
setRouting:(ch,data)=>API._fetch('/api/routing?channel='+(ch||'guitar'),{method:'POST',body:JSON.stringify(data)}),
saveSettings:(d)=>API._fetch('/api/settings',{method:'POST',body:JSON.stringify(d)}),
getSettings:()=>API._fetch('/api/settings'),
getSystem:()=>API._fetch('/api/system'),
getSystemLogs:(n)=>API._fetch('/api/system/logs?lines='+(n||50)),
setHostname:(h)=>API._fetch('/api/system/hostname',{method:'POST',body:JSON.stringify({hostname:h})}),
getNetwork:()=>API._fetch('/api/network'),
setNetworkStatic:(d)=>API._fetch('/api/network/static',{method:'POST',body:JSON.stringify(d),timeout:25000}),
setNetworkDhcp:()=>API._fetch('/api/network/dhcp',{method:'POST',timeout:25000}),
listAudioDevices:()=>API._fetch('/api/audio/devices'),
getDiagnostics:()=>API._fetch('/api/diagnostics'),
createBackup:()=>API._fetch('/api/backup',{method:'POST',timeout:35000}),
listBackups:()=>API._fetch('/api/backup/list'),
};
// ═══ State ═══
let state={channel:'guitar',connected:false,bypass:false,master_volume:0.8,blocks:[],routing_mode:'4cm',tuner_enabled:false,cpu_percent:0};
let currentChannel='guitar';
let channels=['guitar','bass','keys','vocals','backing_tracks'];
let presets=[];
let snapshots={};
let audioProfiles=[];
let currentBank=0;
let pollTimer=null;
let tunerPollTimer=null;
let volDragging=false;
// ═══ Helpers ═══
const $=(s)=>document.querySelector(s);
const $$=(s)=>document.querySelectorAll(s);
function cycleInstrument(){
const idx=channels.indexOf(currentChannel);
currentChannel=channels[(idx+1)%channels.length];
document.getElementById('statusChannel').textContent=currentChannel.charAt(0).toUpperCase()+currentChannel.slice(1).replace('_',' ');
localStorage.setItem('pedalCurrentChannel',currentChannel);
toast(currentChannel);
}
function toast(msg,t){const e=$('#toast');e.textContent=msg;e.classList.add('show');clearTimeout(e._h);e._h=setTimeout(()=>e.classList.remove('show'),t||2000);}
// ═══ Overlay Controller ═══
function openOverlay(id){document.getElementById('overlay'+id).classList.add('open');}
function closeOverlay(id){document.getElementById('overlay'+id).classList.remove('open');}
document.querySelectorAll('.overlay-close').forEach(b=>{
b.addEventListener('click',function(){this.closest('.overlay').classList.remove('open');});
});
// ═══ Init Volume Slider ═══
function initVolSlider(){
const track=$('#volTrack'),fill=$('#volFill'),thumb=$('#volThumb'),pct=$('#volPct');
function setV(p){const v=Math.max(0,Math.min(1,p/100));fill.style.width=(v*100)+'%';thumb.style.left=(v*100)+'%';pct.textContent=Math.round(v*100)+'%';}
function upd(e){const r=track.getBoundingClientRect();const x=(e.touches?e.touches[0].clientX:e.clientX)-r.left;setV((x/r.width)*100);}
track.addEventListener('mousedown',(e)=>{volDragging=true;upd(e);});
track.addEventListener('touchstart',(e)=>{volDragging=true;upd(e);},{passive:true});
document.addEventListener('mousemove',(e)=>{if(volDragging){e.preventDefault();upd(e);}});
document.addEventListener('touchmove',(e)=>{if(volDragging)upd(e);},{passive:true});
document.addEventListener('mouseup',()=>{if(!volDragging)return;volDragging=false;API.setVolume(currentChannel,parseFloat($('#volFill').style.width)/100).catch(()=>{});});
document.addEventListener('touchend',()=>{if(!volDragging)return;volDragging=false;API.setVolume(currentChannel,parseFloat($('#volFill').style.width)/100).catch(()=>{});});
}
// ═══ State Loading ═══
async function loadState(){
try{
const s=await API.getState(currentChannel);
if(!s)return;
state=s;
document.getElementById('statusChannel').textContent=currentChannel.charAt(0).toUpperCase()+currentChannel.slice(1).replace('_',' ');
document.getElementById('statusDot').className='status-dot '+(s.connected?'on':'off');
document.getElementById('statusPreset').textContent=(s.current_preset&&s.current_preset.name)||'—';
const vp=Math.round((s.master_volume||0)*100);
document.getElementById('statusVolPct').textContent=vp+'%';
document.getElementById('volFill').style.width=vp+'%';
document.getElementById('volThumb').style.left=vp+'%';
document.getElementById('volPct').textContent=vp+'%';
const fs=document.getElementById('footswitch');
if(s.bypass){fs.className='footswitch off';fs.textContent='MUTE';}
else{fs.className='footswitch on';fs.textContent='BYpass';}
document.getElementById('cpuLabel').textContent=(s.cpu_percent!=null?s.cpu_percent:'—')+'%';
renderFxBlocks();
const tb=document.getElementById('tabTuner');
if(s.tuner_enabled){tb.querySelector('.tab-label').textContent='Tuning';tb.classList.add('active');}
else{tb.querySelector('.tab-label').textContent='Tuner';tb.classList.remove('active');}
}catch(e){document.getElementById('statusDot').className='status-dot off';}
}
// ═══ FX Blocks ═══
const BLOCK_COLORS={nam_amp:'#E8A030',ir:'#4090E0',gate:'#50D090',compressor:'#E0E060',eq:'#A080E0',reverb:'#60C0E0',delay:'#80D0A0',distortion:'#E06060'};
function renderFxBlocks(){
const strip=document.getElementById('fxStrip');
const blocks=state.blocks||[];
const addBtn=document.getElementById('btnAddBlock');
strip.innerHTML='';
strip.appendChild(addBtn);
blocks.forEach((b,i)=>{
const div=document.createElement('div');
const en=b.enabled!==false&&!b.bypass;
div.className='fx-block '+(en?'on':'off');
div.innerHTML='<div class="fx-type">'+(b.fx_type||'fx')+'</div><div class="fx-name">'+(b.name||b.fx_type||'Block')+'</div><div class="fx-indicator">'+[...Array(4)].map((_,j)=>'<div class="fx-dot'+(j<3?' active':'')+'"></div>').join('')+'</div>';
strip.insertBefore(div,addBtn);
});
}
// ═══ Presets ═══
async function loadPresets(){
try{
presets=await API.listPresets(currentChannel);
const grid=document.getElementById('presetGrid');
grid.innerHTML='';
const bank=(presets.banks||[])[currentBank];
const slots=bank?(bank.presets||[]):[];
for(let i=0;i<4;i++){
const p=slots[i];
const div=document.createElement('div');
const active=state.current_preset&&state.current_preset.bank===currentBank&&state.current_preset.program===i;
div.className='preset-card'+(active?' active':'');
if(p){
div.innerHTML='<div class="pc-num">'+currentBank+'.'+i+'</div><div class="pc-name">'+(p.name||'Preset')+'</div>';
div.onclick=()=>{API.activatePreset(currentChannel,currentBank,i).then(()=>{closeOverlay('Presets');toast('Loaded: '+(p.name||'Preset'));}).catch(e=>toast(e.message));};
}else{
div.innerHTML='<div class="pc-num">'+currentBank+'.'+i+'</div><div class="pc-name" style="color:var(--text-dim);font-weight:400">Empty</div>';
div.style.opacity='.35';
}
grid.appendChild(div);
}
}catch(e){}
}
// ═══ Snapshots ═══
async function loadSnapshots(){
try{
snapshots=await API.listSnapshots(currentChannel);
const grid=document.getElementById('snapGrid');
const slots=snapshots.snapshots||{};
const cur=snapshots.current_snapshot||0;
grid.innerHTML='';
for(let i=1;i<=8;i++){
const s=slots[i];
const div=document.createElement('div');
div.className='snap-slot'+(s?' used':'')+(cur===i?' current':'');
if(s){div.innerHTML='<div class="ss-num">'+i+'</div><div class="ss-name">'+(s.name||'Slot '+i)+'</div>';
div.onclick=()=>{API.recallSnapshot(currentChannel,i).then(()=>toast('Recalled: '+(s.name||'Slot '+i))).catch(e=>toast(e.message));};}
else{div.innerHTML='<div class="ss-num">'+i+'</div><div class="ss-name" style="color:var(--text-dim)">Empty</div>';}
grid.appendChild(div);
}
}catch(e){}
}
// ═══ Settings Loaders ═══
async function loadNetworkInfo(){
try{
const n=await API.getNetwork();
if(n.ethernet){
document.getElementById('netEthIp').textContent=n.ethernet.ip||'—';
document.getElementById('netEthMac').textContent=n.ethernet.mac||'—';
document.getElementById('netGateway').textContent=n.ethernet.gateway||'—';
}
if(n.dns)document.getElementById('netDns').textContent=n.dns.join(', ');
}catch(e){}
try{
const w=await API.wifiStatus();
if(w.ssid)document.getElementById('wifiStatus').textContent=w.ssid+(w.signal?' ('+w.signal+'dB)':'');
else if(w.connected)document.getElementById('wifiStatus').textContent='Connected';
else document.getElementById('wifiStatus').textContent='—';
}catch(e){}
}
async function loadSystemInfo(){
try{
const s=await API.getSystem();
if(s.cpu_temp_c)document.getElementById('sysTemp').textContent=s.cpu_temp_c+'°C';
document.getElementById('sysUptime').textContent=s.uptime||'—';
document.getElementById('sysDisk').textContent=s.disk?s.disk.pct+' ('+s.disk.avail+' free)':'—';
document.getElementById('sysMemory').textContent=s.memory_mb?Math.round(s.memory_mb.used/s.memory_mb.total*100)+'% ('+s.memory_mb.used+'/'+s.memory_mb.total+'MB)':'—';
document.getElementById('netHostname').textContent=s.hostname||'pedal';
}catch(e){}
}
async function loadAudioProfiles(){
try{
audioProfiles=await API.listAudioProfiles();
const grid=document.getElementById('audioProfileGrid');
const cp=audioProfiles.current||'standard';
grid.innerHTML='';
(audioProfiles.profiles||[]).forEach(p=>{
const d=document.createElement('div');
d.className='ap-card'+(p.key===cp?' active':'');
d.innerHTML='<div class="ap-key">'+p.key.replace(/_/g,' ')+'</div><div class="ap-desc">'+p.period+'fr / '+p.rate+'Hz</div>'+((p.key===cp)?'<div style="font-size:8px;color:var(--amber)">● Active</div>':'');
d.onclick=()=>{API.setAudioProfile(p.key).then(()=>{toast('Profile: '+p.key);loadAudioProfiles();}).catch(e=>toast(e.message));};
grid.appendChild(d);
});
document.getElementById('audioProfileLabel').textContent=cp;
const cm=await API.getChannelMode();
if(cm.channel_mode)document.getElementById('channelModeLabel').textContent=cm.channel_mode.replace(/-/g,' ').replace(/\b\w/g,l=>l.toUpperCase());
const rt=await API.getRouting(currentChannel);
if(rt.routing_mode)document.getElementById('routingModeLabel').textContent=rt.routing_mode.toUpperCase();
}catch(e){}
}
async function loadBtStatus(){
try{
const st=await API.btStatus();
document.getElementById('btStatus').textContent=st.powered?'Powered On':'Powered Off';
document.getElementById('btDiscStatus').textContent=st.discoverable?'On':'Off';
document.getElementById('btDiscToggle').textContent=st.discoverable?'Disable':'Enable';
}catch(e){}
}
async function loadBtMidi(){
try{
const st=await API.btMidiStatus();
const r=st.running||st.enabled||false;
document.getElementById('btMidiStatus').textContent=r?'Running':'Off';
document.getElementById('btMidiToggle').textContent=r?'Disable':'Enable';
}catch(e){}
}
async function loadHotspotStatus(){
try{
const hs=await API.hotspotStatus();
const a=hs.active||false;
document.getElementById('hotspotStatus').textContent=a?'On':'Off';
document.getElementById('hotspotToggle').textContent=a?'Disable':'Enable';
document.getElementById('hotspotToggle').className='sr-action'+(a?' danger':'');
}catch(e){}
}
async function doWifiScan(){
const list=document.getElementById('wifiNetworks');
list.innerHTML='<div style="text-align:center;padding:10px;color:var(--text-dim);font-size:11px">Scanning...</div>';
try{
const r=await API.wifiScan();
const nets=r.networks||[];
list.innerHTML='';
if(nets.length===0){list.innerHTML='<div style="text-align:center;padding:10px;color:var(--text-dim);font-size:11px">No networks found</div>';return;}
nets.forEach(n=>{
const item=document.createElement('div');
item.className='wifi-item';
const sec=n.secured!==false;
item.innerHTML='<span class="wi-ssid">'+(n.ssid||n.name||'Unknown')+'</span>'+(sec?'<span style="font-size:8px;color:var(--text-dim)">🔒</span>':'<span style="font-size:8px;color:var(--green)">Open</span>')+'<span style="font-size:8px;color:var(--text-dim)">'+(n.signal||'')+'dB</span>';
item.onclick=()=>{
if(sec){
const pwDiv=document.getElementById('wifiConnectPw');
pwDiv.style.display='flex';
document.getElementById('wifiPw').value='';
document.getElementById('wifiPw').focus();
document.getElementById('wifiConnectBtn').onclick=async()=>{
const pw=document.getElementById('wifiPw').value;
if(!pw){toast('Enter password');return;}
pwDiv.style.display='none';
try{await API.wifiConnect(n.ssid||n.name,pw);toast('Connected!');setTimeout(doWifiScan,3000);}catch(e){toast(e.message);}
};
}else{
API.wifiConnect(n.ssid||n.name,'').then(()=>toast('Connected!')).catch(e=>toast(e.message));
}
};
list.appendChild(item);
});
document.getElementById('wifiStatus').textContent=nets.length+' networks';
}catch(e){list.innerHTML='<div style="text-align:center;padding:10px;color:var(--text-dim);font-size:11px">Scan failed</div>';}
}
// ═══ Tuner ═══
function startTunerPolling(){stopTunerPolling();if(state.tuner_enabled)tunerPollTimer=setInterval(pollTuner,150);}
function stopTunerPolling(){if(tunerPollTimer){clearInterval(tunerPollTimer);tunerPollTimer=null;}}
async function pollTuner(){
if(!state.tuner_enabled)return;
try{
const d=await API.getTunerPitch(currentChannel);
if(!d)return;
document.getElementById('tunerNote').textContent=d.note||'—';
const fill=document.getElementById('tunerCentsFill');
const c=d.cents||0;
if(Math.abs(c)<3){fill.className='tuner-cents-fill in-tune';fill.style.left='calc(50% - 3px)';fill.style.width='6px';}
else if(c>0){fill.className='tuner-cents-fill sharp';fill.style.left='calc(50% + '+Math.min(c,50)*1.2+'px)';fill.style.width=Math.min(Math.abs(c),50)+'px';}
else{fill.className='tuner-cents-fill flat';fill.style.left='calc(50% - '+Math.min(Math.abs(c),50)*1.2+'px)';fill.style.width=Math.min(Math.abs(c),50)+'px';}
document.getElementById('tunerFreq').textContent=(d.frequency||0).toFixed(1)+' Hz';
}catch(e){}
}
// ═══ Main Poll Loop ═══
function startPolling(){stopPolling();loadState();pollTimer=setInterval(loadState,2000);}
function stopPolling(){if(pollTimer){clearInterval(pollTimer);pollTimer=null;}}
// ═══ Init! ═══
function init(){
// Restore saved channel
const savedChannel=localStorage.getItem('pedalCurrentChannel');
if(savedChannel && channels.includes(savedChannel)) currentChannel=savedChannel;
initVolSlider();
// Footswitch
document.getElementById('footswitch').onclick=()=>API.bypassToggle(currentChannel).catch(()=>{});
// Tab bar navigation
document.querySelectorAll('.tab-btn[data-view]').forEach(btn=>{
btn.onclick=()=>{
const view=btn.dataset.view;
if(view==='main'){document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');}
else if(view==='settings')openOverlay('Settings');
else if(view==='presets'){openOverlay('Presets');loadPresets();}
else if(view==='snapshots'){openOverlay('Snapshots');loadSnapshots();}
else if(view==='downloads')openOverlay('Downloads');
};
});
// Status bar: settings
document.getElementById('btnSettings').onclick=async()=>{
document.getElementById('instrumentLabel').textContent=currentChannel.charAt(0).toUpperCase()+currentChannel.slice(1).replace('_',' ');
openOverlay('Settings');
await loadAudioProfiles();await loadBtStatus();await loadBtMidi();await loadHotspotStatus();
await loadNetworkInfo();await loadSystemInfo();await doWifiScan();
};
// Close all overlays
document.querySelectorAll('.overlay').forEach(o=>{o.onclick=function(e){if(e.target===this)this.classList.remove('open');};});
// Settings: Toggles
document.getElementById('instrumentCycleBtn').onclick=()=>{
const idx=channels.indexOf(currentChannel);
currentChannel=channels[(idx+1)%channels.length];
const displayName=currentChannel.charAt(0).toUpperCase()+currentChannel.slice(1).replace('_',' ');
document.getElementById('instrumentLabel').textContent=displayName;
document.getElementById('statusChannel').textContent=displayName;
localStorage.setItem('pedalCurrentChannel',currentChannel);
toast(currentChannel);
loadState();
};
document.getElementById('channelModeToggle').onclick=async()=>{
const cur=document.getElementById('channelModeLabel').textContent.toLowerCase();
const next=cur.includes('dual')?'stereo':'dual-mono';
try{await API.setChannelMode(next);loadAudioProfiles();toast('Mode: '+next);}catch(e){toast(e.message);}
};
document.getElementById('routingModeToggle').onclick=async()=>{
const cur=document.getElementById('routingModeLabel').textContent;
try{await API.setRouting(currentChannel,{routing_mode:cur==='MONO'?'4cm':'mono'});loadAudioProfiles();}catch(e){toast(e.message);}
};
document.getElementById('netModeToggle').onclick=async()=>{
const lbl=document.getElementById('netDhcpStatus');
if(lbl.textContent==='DHCP'){
const ip=prompt('Enter static IP:','192.168.0.100');
if(!ip)return;
const gw=prompt('Gateway:','192.168.0.1');
try{await API.setNetworkStatic({ip,netmask:'24',gateway:gw});lbl.textContent='Static';toast('Static: '+ip);}catch(e){toast(e.message);}
}else{
try{await API.setNetworkDhcp();lbl.textContent='DHCP';toast('DHCP enabled');}catch(e){toast(e.message);}
}
};
document.getElementById('wifiScanBtn').onclick=doWifiScan;
document.getElementById('hotspotToggle').onclick=async()=>{
const on=document.getElementById('hotspotToggle').textContent==='Disable';
try{if(on)await API.hotspotDisable();else await API.hotspotEnable();loadHotspotStatus();toast(on?'Hotspot off':'Hotspot on');}catch(e){toast(e.message);}
};
document.getElementById('btMidiToggle').onclick=async()=>{
const on=document.getElementById('btMidiToggle').textContent==='Disable';
try{if(on)await API.btMidiDisable();else await API.btMidiEnable();loadBtMidi();toast('MIDI '+(on?'off':'on'));}catch(e){toast(e.message);}
};
document.getElementById('btDiscToggle').onclick=async()=>{
const on=document.getElementById('btDiscToggle').textContent==='Disable';
try{await API.btDiscoverableSet(!on);loadBtStatus();toast(on?'Hidden':'Visible');}catch(e){toast(e.message);}
};
// System
document.getElementById('btnResetDefaults').onclick=async()=>{
if(!confirm('Reset all to factory defaults?'))return;
toast('Resetting...');
try{
await API.saveSettings({channel_mode:'dual-mono',brightness:8,tap_tempo:120});
await API.setRouting(currentChannel,{routing_mode:'mono'}).catch(()=>{});
toast('Defaults restored');
setTimeout(()=>{closeOverlay('Settings');loadAudioProfiles();},1000);
}catch(e){toast('Failed: '+e.message);}
};
document.getElementById('btnRestartSvc').onclick=()=>{toast('Restarting service...');fetch('/api/settings',{method:'POST',body:JSON.stringify({_restart:true})}).catch(()=>{});};
document.getElementById('btnReboot').onclick=()=>{if(confirm('Reboot pedal?')){toast('Rebooting...');fetch('/api/settings',{method:'POST',body:JSON.stringify({_reboot:true})}).catch(()=>{});}};
// Tuner
document.getElementById('tabTuner').onclick=async()=>{
state.tuner_enabled=!state.tuner_enabled;
if(state.tuner_enabled){document.getElementById('tunerView').classList.add('open');startTunerPolling();}
else{document.getElementById('tunerView').classList.remove('open');stopTunerPolling();}
API.tunerToggle(currentChannel,state.tuner_enabled).catch(()=>{});
};
document.getElementById('tunerExit').onclick=()=>{
state.tuner_enabled=false;
document.getElementById('tunerView').classList.remove('open');
stopTunerPolling();
API.tunerToggle(currentChannel,false).catch(()=>{});
};
// Snapshots save
document.getElementById('snapSaveCurrent').onclick=async()=>{
const slots=snapshots.snapshots||{};
let slot=1;
for(let i=1;i<=8;i++){if(!slots[i]){slot=i;break;}}
try{await API.saveSnapshot(currentChannel,slot);toast('Saved to slot '+slot);loadSnapshots();}catch(e){toast(e.message);}
};
// Downloads search
document.getElementById('dlSearchBtn').onclick=async()=>{
const q=document.getElementById('dlSearch').value.trim();
if(!q)return;
const res=document.getElementById('dlResults');
res.innerHTML='<div style="text-align:center;padding:16px"><span class="spinner"></span></div>';
try{
const data=await API._fetch('/api/models/tonedownload/search?q='+encodeURIComponent(q)+'&page=0');
const items=data.results||[];
res.innerHTML='';
if(items.length===0){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim);font-size:11px">No results</div>';return;}
items.forEach(item=>{
const d=document.createElement('div');
d.className='preset-card';
d.style.cursor='default';
d.innerHTML='<div class="pc-name">'+(item.name||'Unknown')+'</div><div class="pc-num">'+(item.author||'')+'</div><button class="sr-action primary" style="margin-top:4px;align-self:flex-end">Get</button>';
d.querySelector('button').onclick=async()=>{
d.querySelector('button').textContent='...';
try{await API._fetch('/api/models/tonedownload/install',{method:'POST',body:JSON.stringify({download_url:item.download_url,name:item.name}),timeout:60000});toast('Downloaded!');d.querySelector('button').textContent='✓';}catch(e){toast(e.message);d.querySelector('button').textContent='Retry';}
};
res.appendChild(d);
});
}catch(e){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim)">Error</div>';}
};
document.getElementById('dlSearch').onkeydown=(e)=>{if(e.key==='Enter')document.getElementById('dlSearchBtn').click();};
// Start polling
startPolling();
loadPresets();
loadSnapshots();
}
document.addEventListener('DOMContentLoaded',init);
</script>
</body>
</html>