fix: move NAM block size sync before JACK client restart

The profile change handler was calling nam_host.set_block_size() AFTER
jack_client.start(), creating a race where the JACK callback fired with
the new buffer size while the old NAM subprocess was still running at
the old block_size. Pipe byte counts desynced => NAM returned passthrough.

+ also: auto_connect disable during restart, connect_fx_ports reorder,
  Tone3000 gear/sort/pagination params, NAM engine switch API endpoints
This commit is contained in:
2026-06-17 17:09:38 -04:00
parent 1b2747abb1
commit 6008776e3c
12 changed files with 646 additions and 190 deletions
+169 -44
View File
@@ -172,7 +172,7 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
<!-- Main Content -->
<div class="main-content">
<div class="nam-hero empty" id="namHero" onclick="switchTab('FX')">
<div class="nam-hero empty" id="namHero" onclick="document.querySelector('.tab-btn[data-view=&quot;downloads&quot;]').click();setTimeout(()=>{var b=document.getElementById('dlTabLocal');if(b)b.click();},100)">
<div class="nam-badge" id="namBadge">NAM</div>
<div class="nam-name" id="namName">No Model Loaded</div>
<div class="nam-detail" id="namDetail"></div>
@@ -228,20 +228,39 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
<div class="overlay-hdr"><span class="overlay-title">⬇ Downloads</span><button class="overlay-close" id="closeDownloads"></button></div>
<!-- Tab + filter nav -->
<div class="settings-nav">
<button class="sn-btn active" id="dlTabNam" onclick="switchDlTab('nam')">🎛 NAM</button>
<button class="sn-btn" id="dlTabIr" onclick="switchDlTab('ir')">📦 IR</button>
<button class="sn-btn active" id="dlTabLocal">📂 Local</button>
<button class="sn-btn" id="dlTabNam">🎛 NAM</button>
<button class="sn-btn" id="dlTabIr">📦 IR</button>
</div>
<div class="settings-nav" style="border-bottom:1px solid var(--border);padding-top:0">
<button class="sn-btn active" data-arch="" onclick="setArch(this,'')">All</button>
<button class="sn-btn" data-arch="nano" onclick="setArch(this,'nano')">Nano</button>
<button class="sn-btn" data-arch="feather" onclick="setArch(this,'feather')">Feather</button>
<button class="sn-btn" data-arch="standard" onclick="setArch(this,'standard')">Standard</button>
<button class="sn-btn active" data-arch="">All</button>
<button class="sn-btn" data-arch="nano">Nano</button>
<button class="sn-btn" data-arch="feather">Feather</button>
<button class="sn-btn" data-arch="standard">Standard</button>
</div>
<!-- Type filter row (NAM only) -->
<div class="settings-nav" id="dlGearRow" style="border-bottom:1px solid var(--border);padding-top:0;padding-bottom:0">
<button class="sn-btn active" data-gear="">All Types</button>
<button class="sn-btn" data-gear="amp">Amp</button>
<button class="sn-btn" data-gear="pedal">Pedal</button>
<button class="sn-btn" data-gear="full-rig">Full Rig</button>
<button class="sn-btn" data-gear="outboard">Outboard</button>
<button class="sn-btn" data-gear="ir">IR</button>
</div>
<div class="overlay-body" style="padding-top:4px">
<div style="display:flex;gap:6px;margin-bottom:6px">
<div style="display:flex;gap:6px;margin-bottom:4px" id="dlSearchRow">
<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 style="display:flex;gap:6px;margin-bottom:6px;align-items:center" id="dlSortRow">
<span style="font-size:10px;color:var(--text-dim);flex-shrink:0">Sort:</span>
<select id="dlSortSelect" style="flex:1;font-size:10px;padding:4px 6px;min-height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-family:var(--font)">
<option value="created_at.desc">Newest</option>
<option value="created_at.asc">Oldest</option>
<option value="name.asc">Name A-Z</option>
<option value="name.desc">Name Z-A</option>
</select>
</div>
<div id="dlResults"></div>
</div>
</div>
@@ -371,6 +390,7 @@ body{display:flex;flex-direction:column;max-height:100vh;user-select:none;-webki
<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">🎛 NAM Engine</span><span class="sr-value" id="namEngineLabel">cpp</span><button class="sr-action" id="btnToggleNamEngine">Switch</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>
@@ -422,9 +442,9 @@ const API = {
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})}),
btMidiEnable:()=>API._fetch('/api/bluetooth/midi-enable',{method:'POST',timeout:15000}),
btMidiDisable:()=>API._fetch('/api/bluetooth/midi-disable',{method:'POST',timeout:15000}),
btDiscoverableSet:(on)=>API._fetch('/api/bluetooth/discoverable',{method:'POST',body:JSON.stringify({on}),timeout:15000}),
listAudioProfiles:()=>API._fetch('/api/audio/profiles'),
getAudioProfile:()=>API._fetch('/api/audio/profile'),
setAudioProfile:(p)=>{
@@ -439,7 +459,7 @@ const API = {
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})}),
setHostname:(h)=>API._fetch('/api/system/hostname',{method:'POST',body:JSON.stringify({hostname:h}),timeout:15000}),
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}),
@@ -518,16 +538,19 @@ async function loadAudioDevices(){
async function setAudioInputDevice(id){
if(!id)return;
showLoading('Switching input...');
await API.saveSettings({input_device:id}).catch(()=>{});
try{
await API.saveSettings({input_device:id});
toast('Input: '+id);
}catch(e){toast('Input switch failed');}
hideLoading();
toast('Input: '+id);
}
async function setAudioOutputDevice(id){
if(!id)return;
showLoading('Switching output...');
await API.saveSettings({output_device:id}).catch(()=>{});
hideLoading();
toast('Output: '+id);
try{
await API.saveSettings({output_device:id});
toast('Output: '+id);
}catch(e){toast('Output switch failed');}
}
async function setChOutput(ch,port){
try{
@@ -567,7 +590,7 @@ function setChInstrument(idx,val){
const arr=JSON.parse(localStorage.getItem('chInstruments')||'["guitar","bass"]');
arr[idx]=val;
localStorage.setItem('chInstruments',JSON.stringify(arr));
API.saveSettings({['ch'+(idx+1)+'_instrument']:val}).catch(()=>{});
API.saveSettings({['ch'+(idx+1)+'_instrument']:val}).catch(e=>toast('Instrument failed'));
}
function toggleChPower(idx){
const btn=document.getElementById('ch'+(idx+1)+'Power');
@@ -576,7 +599,7 @@ function toggleChPower(idx){
btn.style.background=on?'rgba(160,160,188,.1)':'rgba(58,184,122,.2)';
btn.style.borderColor=on?'rgba(160,160,188,.2)':'rgba(58,184,122,.4)';
btn.style.color=on?'var(--text3)':'var(--green)';
API.bypassToggle(channels[idx]||'guitar').catch(()=>{});
API.bypassToggle(channels[idx]||'guitar').catch(e=>toast('Channel toggle failed'));
}
function loadChInstrumentState(){
@@ -604,8 +627,8 @@ function initVolSlider(){
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(()=>{});});
document.addEventListener('mouseup',()=>{if(!volDragging)return;volDragging=false;API.setVolume(currentChannel,parseFloat($('#volFill').style.width)/100).catch(e=>toast('Volume failed'));});
document.addEventListener('touchend',()=>{if(!volDragging)return;volDragging=false;API.setVolume(currentChannel,parseFloat($('#volFill').style.width)/100).catch(e=>toast('Volume failed'));});
}
// ═══ State Loading ═══
@@ -704,10 +727,10 @@ async function toggleBlock(idx){
const b=blocks[idx];
if(!b)return;
const newEn=!(b.enabled!==false);
await API.toggleBlock(currentChannel,b.block_id||idx,newEn).catch(()=>{});
await API.toggleBlock(currentChannel,b.block_id||idx,newEn).catch(e=>toast('Block failed'));
loadState();
}
function showAddBlock(){openOverlay('Downloads');}
function showAddBlock(){document.querySelector('.tab-btn[data-view="downloads"]').click();}
// ═══ Presets ═══
async function loadPresets(){
@@ -731,9 +754,9 @@ async function loadPresets(){
}
grid.appendChild(div);
}
}catch(e){}
if(!bank||slots.length===0)grid.innerHTML='<div style="text-align:center;padding:20px;color:var(--text-dim);font-size:12px">No presets</div>';
}catch(e){toast('Failed to load presets');}
}
// ═══ Snapshots ═══
async function loadSnapshots(){
try{
@@ -751,7 +774,7 @@ async function loadSnapshots(){
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){}
}catch(e){toast('Failed to load snapshots');}
}
// ═══ Settings Loaders ═══
@@ -764,7 +787,7 @@ async function loadNetworkInfo(){
document.getElementById('netGateway').textContent=n.ethernet.gateway||'—';
}
if(n.dns)document.getElementById('netDns').textContent=n.dns.join(', ');
}catch(e){}
}catch(e){toast('Network info failed');}
try{
const w=await API.wifiStatus();
if(w.ssid)document.getElementById('wifiStatus').textContent=w.ssid+(w.signal?' ('+w.signal+'dB)':'');
@@ -804,7 +827,7 @@ async function setAudioParams(){
const btn=document.querySelector('.sn-btn[data-section="audio"]');
if(btn)btn.textContent='⏳ Applying...';
try{
await API._fetch('/api/audio/profile',{method:'POST',body:JSON.stringify({period,rate}),timeout:15000});
await API._fetch('/api/audio/profile',{method:'POST',body:JSON.stringify({period,rate}),timeout:30000});
toast('Audio: '+rate+'Hz / '+period+'fr');
loadAudioParams();
}catch(e){toast('Failed: '+e.message);}
@@ -914,17 +937,17 @@ function init(){
initVolSlider();
// Footswitch
document.getElementById('footswitch').onclick=()=>API.bypassToggle(currentChannel).catch(()=>{});
document.getElementById('footswitch').onclick=()=>API.bypassToggle(currentChannel).catch(e=>toast('Bypass failed'));
// 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');loadNetworkInfo();loadSystemInfo();loadAudioParams();loadAudioDevices();loadChOutput();loadBackingSource();loadHotspotStatus();loadBtStatus();loadBtMidi();return;}
else if(view==='settings'){openOverlay('Settings');loadNetworkInfo();loadSystemInfo();loadAudioParams();loadAudioDevices();loadChOutput();loadBackingSource();loadHotspotStatus();loadBtStatus();loadBtMidi();loadNamEngine();return;}
else if(view==='presets'){openOverlay('Presets');loadPresets();}
else if(view==='snapshots'){openOverlay('Snapshots');loadSnapshots();}
else if(view==='downloads')openOverlay('Downloads');
else if(view==='downloads'){openOverlay('Downloads');if(dlTab==='local')doDlSearch();}
};
});
@@ -940,6 +963,7 @@ function init(){
loadHotspotStatus();
loadBtStatus();
loadBtMidi();
loadNamEngine();
};
// Close all overlays
@@ -1004,18 +1028,38 @@ function init(){
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(()=>{});}};
// NAM engine toggle
document.getElementById('btnToggleNamEngine').onclick=async()=>{
const label=document.getElementById('namEngineLabel');
const cur=label.textContent;
const next=cur==='cpp'?'pytorch':'cpp';
label.textContent='...';
try{
await API._fetch('/api/nam/engine',{method:'POST',body:JSON.stringify({engine_mode:next}),timeout:30000});
label.textContent=next;
toast('NAM engine: '+next);
}catch(e){label.textContent=cur;toast('Switch failed: '+e.message);}
};
// Load current engine on settings open
async function loadNamEngine(){
try{
const r=await API._fetch('/api/nam/engine');
document.getElementById('namEngineLabel').textContent=r.engine_mode||'cpp';
}catch(e){}
}
// 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(()=>{});
API.tunerToggle(currentChannel,state.tuner_enabled).catch(e=>toast('Tuner failed'));
};
document.getElementById('tunerExit').onclick=()=>{
state.tuner_enabled=false;
document.getElementById('tunerView').classList.remove('open');
stopTunerPolling();
API.tunerToggle(currentChannel,false).catch(()=>{});
API.tunerToggle(currentChannel,false).catch(e=>toast('Tuner failed'));
};
// Snapshots save
@@ -1026,34 +1070,103 @@ function init(){
try{await API.saveSnapshot(currentChannel,slot);toast('Saved to slot '+slot);loadSnapshots();}catch(e){toast(e.message);}
};
// Downloads - state
let dlTab='nam', dlArch='';
// Downloads state & wiring
// Downloads state & wiring
let dlTab='local', dlArch='', dlGear='', dlPage=0, dlSort='created_at.desc';
function switchDlTab(tab){
dlTab=tab;
dlTab=tab; dlPage=0;
document.getElementById('dlTabLocal').className='sn-btn'+(tab==='local'?' active':'');
document.getElementById('dlTabNam').className='sn-btn'+(tab==='nam'?' active':'');
document.getElementById('dlTabIr').className='sn-btn'+(tab==='ir'?' active':'');
// Show/hide filter rows per tab
const archRow=document.querySelectorAll('#overlayDownloads > .settings-nav')[1];
if(archRow)archRow.style.display=tab==='nam'?'flex':'none';
const gearRow=document.getElementById('dlGearRow');
const sortRow=document.getElementById('dlSortRow');
const searchRow=document.getElementById('dlSearchRow');
const showNam=tab==='nam';
if(archRow)archRow.style.display=showNam?'flex':'none';
if(gearRow)gearRow.style.display=showNam?'flex':'none';
if(sortRow)sortRow.style.display=showNam?'flex':'none';
if(searchRow)searchRow.style.display=(tab==='nam'||tab==='ir')?'flex':'none';
doDlSearch();
}
document.getElementById('dlTabLocal').onclick=()=>switchDlTab('local');
document.getElementById('dlTabNam').onclick=()=>switchDlTab('nam');
document.getElementById('dlTabIr').onclick=()=>switchDlTab('ir');
function setArch(btn,arch){
document.querySelectorAll('#overlayDownloads .settings-nav')[1].querySelectorAll('.sn-btn').forEach(b=>b.classList.remove('active'));
document.querySelectorAll('#overlayDownloads > .settings-nav')[1].querySelectorAll('.sn-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
dlArch=arch;
dlArch=arch; dlPage=0;
doDlSearch();
}
document.querySelectorAll('#overlayDownloads > .settings-nav')[1].querySelectorAll('.sn-btn[data-arch]').forEach(btn=>{
btn.onclick=()=>setArch(btn,btn.dataset.arch);
});
function setGear(btn,gear){
document.getElementById('dlGearRow').querySelectorAll('.sn-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
dlGear=gear; dlPage=0;
doDlSearch();
}
document.getElementById('dlGearRow').querySelectorAll('.sn-btn[data-gear]').forEach(btn=>{
btn.onclick=()=>setGear(btn,btn.dataset.gear);
});
// Sort dropdown
document.getElementById('dlSortSelect').onchange=function(){
dlSort=this.value; dlPage=0; doDlSearch();
};
async function doDlSearch(){
const q=document.getElementById('dlSearch').value.trim();
const res=document.getElementById('dlResults');
res.innerHTML='<div style="text-align:center;padding:16px"><span class="spinner"></span></div>';
try{
// Local tab: list installed models from /api/models
if(dlTab==='local'){
const data=await API._fetch('/api/models');
const items=data.models||[];
const current=data.current||null;
res.innerHTML='';
if(items.length===0){
res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim);font-size:11px">No models installed — download from the NAM tab</div>';
return;
}
items.forEach(item=>{
const d=document.createElement('div');
d.className='preset-card';
d.style.cursor='pointer';
const isLoaded=item.path===current;
if(isLoaded)d.style.borderColor='var(--green)';
const meta=[item.architecture||'?'];
if(item.size_mb)meta.push(item.size_mb+'MB');
d.innerHTML='<div class="pc-name">'+(item.name||'Unknown')+'</div><div class="pc-num">'+(isLoaded?'✅ Loaded':'')+'</div><div style="font-size:8px;color:var(--text-dim);margin-top:2px">'+meta.join(' · ')+'</div>';
d.onclick=async()=>{
toast('Loading...');
try{
await API._fetch('/api/models/load',{method:'POST',body:JSON.stringify({path:item.path}),timeout:10000});
toast('Loaded: '+item.name);
doDlSearch();
}catch(e){toast('Failed: '+e.message);}
};
res.appendChild(d);
});
return;
}
// NAM / IR tab: Tone3000 search
const q=document.getElementById('dlSearch').value.trim();
const endpoint=dlTab==='nam'?'/api/models/tonedownload/search':'/api/irs/tonedownload/search';
const params='?q='+encodeURIComponent(q)+(dlArch?'&arch='+dlArch:'')+'&page=0';
let params='?q='+encodeURIComponent(q)+'&page='+dlPage;
if(dlArch)params+='&arch='+dlArch;
if(dlTab==='nam'&&dlGear)params+='&gear='+dlGear;
if(dlTab==='nam')params+='&sort='+dlSort;
const data=await API._fetch(endpoint+params);
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'+(q?'':' \u2014 tap Search or type a query')+'</div>';return;}
if(items.length===0){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim);font-size:11px">No results'+(q?'':' — no models found')+'</div>';return;}
items.forEach(item=>{
const d=document.createElement('div');
d.className='preset-card';
@@ -1062,10 +1175,20 @@ function init(){
if(item.size_display)meta.push(item.size_display);
if(item.architecture)meta.push(item.architecture);
if(item.tone_gear&&item.tone_gear!=='nam')meta.push(item.tone_gear);
d.innerHTML='<div class="pc-name">'+(item.name||'Unknown')+'</div><div class="pc-num">'+(item.author||'')+(item.pack_name?' \u00b7 '+item.pack_name:'')+'</div><div style="font-size:8px;color:var(--text-dim);margin-top:2px">'+meta.join(' \u00b7 ')+'</div>';
if(item.downloads>0)meta.push('⬇'+item.downloads);
d.innerHTML='<div class="pc-name">'+(item.name||'Unknown')+'</div><div class="pc-num">'+(item.author||'')+(item.pack_name?' · '+item.pack_name:'')+'</div><div style="font-size:8px;color:var(--text-dim);margin-top:2px">'+meta.join(' · ')+'</div>';
d.onclick=()=>showDlDetail(item);
res.appendChild(d);
});
// Pagination bar
const pbar=document.createElement('div');
pbar.style.cssText='display:flex;align-items:center;justify-content:center;gap:8px;padding:8px 0;margin-top:4px;border-top:1px solid var(--border)';
pbar.innerHTML='<button class="sr-action" id="dlPrevBtn"'+(dlPage<1?' disabled':'')+'>← Prev</button>'+
'<span style="font-size:11px;color:var(--text-dim)">Page '+(dlPage+1)+'</span>'+
'<button class="sr-action" id="dlNextBtn"'+(items.length<20?' disabled':'')+'>Next →</button>';
res.appendChild(pbar);
document.getElementById('dlPrevBtn').onclick=()=>{if(dlPage>0){dlPage--;doDlSearch();}};
document.getElementById('dlNextBtn').onclick=()=>{if(items.length>=20){dlPage++;doDlSearch();}};
}catch(e){res.innerHTML='<div style="text-align:center;padding:16px;color:var(--text-dim)">Error</div>';}
}
function showDlDetail(item){
@@ -1078,6 +1201,8 @@ function init(){
if(item.pack_name)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Pack: '+item.pack_name+'</div>';
if(item.architecture)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Arch: '+item.architecture+'</div>';
if(item.size_display)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Size: '+item.size_display+'</div>';
if(item.downloads>0)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">⬇ Downloads: '+item.downloads+'</div>';
if(item.likes>0)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">❤ Likes: '+item.likes+'</div>';
if(item.tone_gear)html+='<div style="font-size:10px;color:var(--text-dim);margin-bottom:4px">Gear: '+item.tone_gear+'</div>';
if(item.tone_description)html+='<div style="font-size:10px;color:var(--text2);margin-top:6px;padding:6px 8px;background:var(--surface);border-radius:6px;border:1px solid var(--border);line-height:1.4">'+(item.tone_description||'')+'</div>';
html+='</div>';
@@ -1090,7 +1215,7 @@ function init(){
btn.textContent='...';
try{
const endpoint=isIr?'/api/irs/tonedownload/install':'/api/models/tonedownload/install';
await API._fetch(endpoint,{method:'POST',body:JSON.stringify({id:item.id,download_url:item.download_url,name:item.name,filename:item.filename,size:item.size,author:item.author,author_avatar:item.author_avatar,thumbnail:item.thumbnail,tone_description:item.tone_description,pack_name:item.pack_name,architecture:item.architecture,created_at:item.created_at}),timeout:60000});
await API._fetch(endpoint,{method:'POST',body:JSON.stringify({id:item.id,download_url:item.download_url,name:item.name,filename:item.filename,size:item.size,author:item.author,author_avatar:item.author_avatar,thumbnail:item.thumbnail,tone_description:item.tone_description,pack_name:item.pack_name,architecture:item.architecture,created_at:item.created_at,downloads:item.downloads,likes:item.likes}),timeout:60000});
toast('Downloaded!');
btn.textContent='Downloaded';
btn.className='sr-action';