Files
markbase/src/page.html
Warren 89aa4989da feat: Add file_locations to scan and fix file info API
Problem:
- Files could not be clicked (error: no location)
- get_file_info used hardcoded demo database
- file_locations table was empty

Solution:
1. Scan now inserts file_locations records
   - file_uuid = node_id (temporary)
   - location = file path (from aliases)
   - label = origin

2. Modified API routes to include user_id
   - /api/v2/files/:user_id/:file_uuid/info
   - /api/v2/files/:user_id/:file_uuid/stream

3. Modified showDetail() to use tree_user from localStorage

Result:
- file_locations: 11857 records 
- Files can be clicked 
- API uses correct user database 

Files:
- src/scan.rs (insert file_locations)
- src/server.rs (user_id parameter)
- src/page.html (showDetail with user_id)
2026-05-17 04:29:46 +08:00

1075 lines
57 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Cache-Control" content="no-cache,no-store,must-revalidate">
<title>{__TITLE__}</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,system-ui,sans-serif;background:#0f172a;color:#e2e8f0;padding:24px 24px 80px;line-height:1.6;font-size:16px}
h1,h2,h3,h4{color:#60a5fa;margin:1em 0 .5em}
h1{font-size:1.8em;border-bottom:1px solid #334155;padding-bottom:.3em}
h2{font-size:1.4em} h3{font-size:1.15em}
p{margin:.6em 0} a{color:#60a5fa}
code{background:#1e293b;padding:2px 6px;border-radius:4px;font-size:.9em}
pre{background:#1e293b;padding:16px;border-radius:8px;overflow-x:auto;margin:.8em 0;font-size:.85em}
pre code{background:none;padding:0}
table{border-collapse:collapse;width:100%;margin:1em 0;font-size:.88em}
th,td{border:1px solid #334155;padding:8px 12px;text-align:left;vertical-align:top}
th{background:#1e293b;color:#94a3b8;font-weight:600;white-space:nowrap}
blockquote{border-left:4px solid #60a5fa;padding-left:16px;color:#94a3b8;margin:.8em 0}
ul,ol{padding-left:24px;margin:.6em 0}
img,video{max-width:100%;border-radius:8px} iframe{width:100%;height:98vh;border:none}
#mb-tree-panel{display:none;position:fixed;top:0;left:0;right:0;bottom:52px;background:#0f172a;z-index:9998;overflow-y:auto;padding:16px 24px}
#mb-tree-panel.active{display:block}
.mb-mode-bar{display:flex;gap:0;margin-bottom:16px;border-bottom:2px solid #1e293b;position:sticky;top:0;background:#0f172a;z-index:10}
.mb-mode-btn{background:none;border:none;color:#64748b;padding:10px 20px;cursor:pointer;font-size:14px;border-bottom:3px solid transparent;transition:all .2s;font-family:inherit}
.mb-mode-btn:hover{color:#94a3b8}
.mb-mode-btn.active{color:#60a5fa;border-bottom-color:#60a5fa}
.mb-mode-btn span{font-size:18px;margin-right:6px}
.mb-tree-node{padding:3px 0;cursor:default;border-radius:4px}
.mb-tree-node:hover{background:#1e293b}
.mb-tree-caret{display:inline-block;width:18px;cursor:pointer;color:#64748b;user-select:none}
.mb-tree-label{display:inline-flex;align-items:center;gap:6px}
.mb-tree-meta{color:#64748b;font-size:11px;margin-left:8px}
.mb-tree-file{cursor:pointer}
.mb-tree-file:hover{color:#60a5fa}
.mb-folder-actions{display:none;gap:3px;margin-left:8px}
.mb-tree-node:hover .mb-folder-actions{display:inline-flex}
body.mb-locked .mb-folder-actions{display:none!important}
body.mb-locked .mb-tree-node:hover .mb-folder-actions{display:none!important}
.mb-folder-btn{background:#334155;border:none;color:#94a3b8;padding:1px 6px;border-radius:3px;cursor:pointer;font-size:10px;font-family:inherit}
.mb-folder-btn:hover{background:#475569;color:#e2e8f0}
.mb-folder-btn.danger:hover{background:#7f1d1d;color:#fca5a5}
.mb-toast{position:fixed;bottom:60px;left:50%;transform:translateX(-50%);background:#064e3b;color:#4ade80;padding:6px 20px;border-radius:6px;font-size:13px;z-index:10001;transition:opacity .3s}
.mb-grid{display:grid;gap:10px}
.mb-grid.sm{grid-template-columns:repeat(auto-fill,minmax(120px,1fr))}
.mb-grid.lg{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}
.mb-grid-cell{background:#1e293b;border-radius:10px;padding:12px;text-align:center;cursor:pointer;transition:all .2s;border:1px solid transparent;min-height:100px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px}
.mb-grid-cell:hover{background:#1e3a5f;border-color:#3b82f6;transform:translateY(-2px)}
.mb-grid-cell .mb-grid-icon{font-size:40px}
.mb-grid-cell .mb-grid-label{font-size:12px;line-height:1.3;max-height:2.6em;overflow:hidden;word-break:break-word}
.mb-grid-cell .mb-grid-uuid{font-size:10px;color:#64748b;font-family:monospace}
#mb-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);z-index:9999}
#mb-overlay.active{display:block}
#mb-detail{display:none;position:fixed;top:10%;left:10%;right:10%;bottom:10%;background:#1e293b;border:1px solid #334155;border-radius:12px;z-index:10000;padding:24px;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.5)}
#mb-detail.active{display:block}
#mb-detail-close{position:absolute;top:12px;right:16px;background:none;border:none;color:#64748b;font-size:20px;cursor:pointer}
.mb-loc-tag{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;margin:2px 4px 2px 0;font-family:monospace}
.mb-loc-tag.hot{background:#064e3b;color:#4ade80}
.mb-loc-tag.warm{background:#451a03;color:#fbbf24}
.mb-loc-tag.cold{background:#1e1b4b;color:#818cf8}
.mb-loc-tag.cloud{background:#0c4a6e;color:#38bdf8}
.mb-new-folder-btn{background:#1e3a5f;border:1px solid #3b82f6;color:#60a5fa;padding:2px 10px;border-radius:4px;cursor:pointer;font-size:11px;font-family:inherit;margin-left:8px}
.mb-new-folder-btn:hover{background:#1e40af;color:#93c5fd}
.mb-lock-btn{background:none;border:none;font-size:16px;cursor:pointer;padding:2px 6px;align-self:center}
.mb-rename-input{background:#1e293b;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:1px 6px;font-size:13px;font-family:inherit;width:180px}
.mb-icon-picker{display:grid;grid-template-columns:repeat(8,1fr);gap:4px;max-height:200px;overflow-y:auto;margin:8px 0}
.mb-icon-picker button{background:#0f172a;border:2px solid transparent;border-radius:6px;font-size:22px;padding:6px;cursor:pointer}
.mb-icon-picker button:hover{background:#1e3a5f;border-color:#60a5fa}
#mb-settings-panel{display:none;position:fixed;top:0;left:0;right:0;bottom:52px;background:#0f172a;z-index:9998;overflow-y:auto;padding:16px 24px}
#mb-settings-panel.active{display:block}
.mb-config-section{background:#1e293b;border-radius:8px;padding:16px;margin:8px 0}
.mb-config-header{color:#60a5fa;font-size:14px;font-weight:600;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #334155}
.mb-config-item{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid #334155}
.mb-config-item:last-child{border-bottom:none}
.mb-config-label{color:#94a3b8;font-size:13px}
.mb-config-value{color:#e2e8f0;font-size:13px;font-family:monospace}
.mb-config-edit-btn{background:#334155;border:none;color:#60a5fa;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px}
.mb-config-edit-btn:hover{background:#475569}
.mb-config-input{background:#0f172a;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:2px 8px;font-size:12px;font-family:monospace;width:150px}
.mb-config-save-btn{background:#064e3b;border:1px solid #4ade80;color:#4ade80;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px}
.mb-config-cancel-btn{background:#451a03;border:1px solid #fbbf24;color:#fbbf24;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px}
.mb-password-toggle{background:none;border:none;color:#60a5fa;cursor:pointer;font-size:14px;padding:0 4px}
.mb-password-toggle:hover{color:#3b82f6}
#mb-admin-modal{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
background:#1e293b;border:1px solid #334155;padding:24px;border-radius:8px;z-index:10000;min-width:280px}
#mb-admin-modal.active{display:block}
.mb-admin-title{color:#60a5fa;font-size:16px;font-weight:600;margin-bottom:16px}
.mb-admin-input{background:#0f172a;border:1px solid #60a5fa;border-radius:4px;
color:#e2e8f0;padding:8px 12px;width:100%;margin-bottom:12px;font-size:13px}
.mb-admin-btn{background:#064e3b;border:1px solid #4ade80;color:#4ade80;
padding:8px 16px;border-radius:4px;cursor:pointer;width:100%;font-size:13px}
.mb-admin-btn:hover{background:#4ade80;color:#064e3b}
.mb-admin-error{color:#ef4444;font-size:12px;margin-top:8px}
.mb-admin-close{position:absolute;top:12px;right:12px;background:none;border:none;
color:#64748b;font-size:18px;cursor:pointer}
</style>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>mermaid.initialize({startOnLoad:false,theme:'dark',themeVariables:{primaryColor:'#60a5fa',primaryTextColor:'#e2e8f0',lineColor:'#94a3b8'}})</script>
</head><body>
<div id=mb-content>{__CONTENT__}</div>
<div id=mb-overlay onclick="closeDetail()"></div>
<div id=mb-detail><button id=mb-detail-close onclick="closeDetail()"></button><div id=mb-detail-body></div></div>
<div id=mb-tree-panel><div id=mb-tree-body></div></div>
<div id=mb-settings-panel><div id=mb-settings-body></div></div>
<div id=mb-bar style="position:fixed;bottom:0;left:0;right:0;background:#1e293b;border-top:1px solid #334155;display:flex;justify-content:center;align-items:center;gap:5px;padding:5px 10px;z-index:9999;font-size:12px">
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'restart'})})" title="Restart"></button>
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'prev'})})" title="Prev"></button>
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'next'})})" title="Next"></button>
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'end'})})" title="End"></button>
<input id=mbgi type=number min=1 max=999 placeholder=N style="width:36px;background:#0f172a;border:1px solid #334155;border-radius:4px;color:white;padding:2px 4px;font-size:10px;text-align:center">
<button onclick="var n=document.getElementById('mbgi').value;if(n)fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'goto',val:parseInt(n)})})" style=font-size:10px>GO</button>
<select id=mbstep onchange="if(this.value)fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'goto',val:parseInt(this.value)})})" style="background:#0f172a;color:white;border:1px solid #334155;border-radius:4px;padding:1px 3px;font-size:10px;max-width:160px"><option value="">Step...</option></select>
<span style=color:#475569;font-size:10px>|</span>
<button onclick="toggleTree()" title="File Tree" style="background:none;border:none;color:#60a5fa;cursor:pointer;font-size:16px">🗂</button>
<span style=color:#475569;font-size:10px>|</span>
<button onclick="toggleSettings()" title="Settings" style="background:none;border:none;color:#60a5fa;cursor:pointer;font-size:16px">⚙️</button>
<span style=color:#475569;font-size:10px>|</span>
<button onclick="var t=this.textContent;this.textContent=t===String.fromCodePoint(0x1F50A)?String.fromCodePoint(0x1F507):String.fromCodePoint(0x1F50A)" id=mbvb title=Voice style=font-size:16px>🔊</button>
<select id=mbvl onchange="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'lang',val:this.value})})" style="background:#0f172a;color:white;border:1px solid #334155;border-radius:4px;padding:1px 3px;font-size:10px">
<option value=zh_TW>🇹🇼</option><option value=en_US>🇺🇸</option><option value=ja_JP>🇯🇵</option><option value=ko_KR>🇰🇷</option><option value=fr_FR>🇫🇷</option></select>
<select id=mbol onchange="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'audio_out',val:this.value})})" style="background:#0f172a;color:white;border:1px solid #334155;border-radius:4px;padding:1px 3px;font-size:10px;max-width:120px">{out_devs}</select>
<button onclick="var l=document.getElementById('mbvl').value;var o=document.getElementById('mbol').value;fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'test_voice',val:l,out:o})})" style=font-size:14px title="Test Voice">🔊</button>
<select id=mbil onchange="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'audio_in',val:this.value})})" style="background:#0f172a;color:white;border:1px solid #334155;border-radius:4px;padding:1px 3px;font-size:10px;max-width:120px">{in_devs}</select>
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'vol_down'})})" style=font-size:13px title="Vol-"></button>
<span id=mbvlvl style=color:#4ade80;font-size:11px>--</span>
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'vol_up'})})" style=font-size:13px title="Vol+"></button>
<span id=mbsi style=color:#94a3b8;font-size:10px;margin-left:2px></span>
</div>
<script>
// ═══════════════ SETTINGS PANEL ═══════════════
var _sv=false;
function toggleSettings(){
var token=localStorage.getItem('admin_token');
var lastClose=localStorage.getItem('admin_close_time');
if(token){
// Check if closed more than 10 seconds ago
if(lastClose){
var now=Date.now();
var elapsed=(now-parseInt(lastClose))/1000;
if(elapsed>10){
// Token expired (>10s since close), clear and show login
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_close_time');
showAdminLoginModal();
return;
}
}
// Verify token validity
fetch('/api/v2/admin/verify',{
headers:{'Authorization':'Bearer '+token}
})
.then(function(r){return r.json()})
.then(function(d){
if(d.ok){
// Token valid, open settings
_sv=!_sv;
document.getElementById("mb-settings-panel").classList.toggle("active",_sv);
if(_sv){
loadSettings();
// Clear close time when opening
localStorage.removeItem('admin_close_time');
}else{
// Record close time when closing
localStorage.setItem('admin_close_time',Date.now());
}
}else{
// Token invalid, remove and show login
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_close_time');
showAdminLoginModal();
}
})
.catch(function(e){
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_close_time');
showAdminLoginModal();
});
}else{
// No token, show login
showAdminLoginModal();
}
}
function showAdminLoginModal(){
var m=document.getElementById('mb-admin-modal');
if(!m){
m=document.createElement('div');
m.id='mb-admin-modal';
m.innerHTML='<button class=mb-admin-close onclick=this.parentElement.classList.remove("active")>✕</button>'+
'<div class=mb-admin-title>Admin Authentication Required</div>'+
'<div style="position:relative">'+
'<input class=mb-admin-input type=password id=admin-password placeholder="Enter admin password" onkeypress=handleAdminKeyPress(event)>'+
'<button class=mb-password-toggle style="position:absolute;right:8px;top:50%;transform:translateY(-50%)" onclick=toggleAdminPassword()>👁</button>'+
'</div>'+
'<button class=mb-admin-btn onclick=submitAdminLogin()>Login</button>'+
'<div class=mb-admin-error id=admin-error></div>';
document.body.appendChild(m);
}
document.getElementById('admin-password').value='';
document.getElementById('admin-password').type='password';
document.getElementById('admin-error').textContent='';
m.classList.add('active');
document.getElementById('admin-password').focus();
}
function toggleAdminPassword(){
var pwdInput=document.getElementById('admin-password');
var toggleBtn=pwdInput.parentElement.querySelector('.mb-password-toggle');
if(pwdInput.type==='password'){
pwdInput.type='text';
toggleBtn.textContent='🙈';
}else{
pwdInput.type='password';
toggleBtn.textContent='👁';
}
}
function handleAdminKeyPress(e){
if(e.key==='Enter'||e.keyCode===13){
submitAdminLogin();
}
}
function submitAdminLogin(){
var pwd=document.getElementById('admin-password').value;
if(!pwd){
document.getElementById('admin-error').textContent='Password required';
return;
}
fetch('/api/v2/admin/login',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({username:'admin',password:pwd})
})
.then(function(r){return r.json()})
.then(function(d){
if(d.token){
localStorage.setItem('admin_token',d.token);
localStorage.removeItem('admin_close_time'); // Clear close time on new login
document.getElementById('mb-admin-modal').classList.remove('active');
toast('Admin authenticated ✓');
toggleSettings(); // Re-open settings
}else{
document.getElementById('admin-error').textContent=d.error||'Login failed';
document.getElementById('admin-password').value='';
document.getElementById('admin-password').focus();
}
})
.catch(function(e){
document.getElementById('admin-error').textContent='Connection error: '+e;
});
}
function loadSettings(){
var b=document.getElementById("mb-settings-body");
if(!b)return;
b.innerHTML="<div style=text-align:center;padding:40px;color:#64748b>Loading...</div>";
fetch("/api/v2/config").then(function(r){return r.json()}).then(function(d){
var h="<div class=mb-mode-bar>";
h+="<span style=color:#60a5fa;font-size:16px>⚙️</span>";
h+="<span style=font-size:14px;color:#e2e8f0;margin-left:8px>Settings</span>";
h+="<span style=flex:1></span>";
h+="<button class=mb-new-folder-btn onclick=validateSettings()>Validate</button>";
h+="<button onclick=toggleSettings() style='background:none;border:none;color:#64748b;font-size:18px;cursor:pointer'>✕</button>";
h+="</div>";
var sections=["server","postgresql","authentication","test","logging"];
sections.forEach(function(sec){
var secData=d[sec];
if(!secData)return;
h+="<div class=mb-config-section>";
h+="<div class=mb-config-header>"+sec.toUpperCase()+"</div>";
for(var key in secData){
if(secData.hasOwnProperty(key)){
var val=secData[key];
var ckey=sec+"."+key;
var dispVal=typeof val==="object"?JSON.stringify(val):String(val);
var safeId=ckey.replace(/[^a-zA-Z0-9]/g,"-");
var isPassword=key.toLowerCase().indexOf("password")!==-1;
h+="<div class=mb-config-item>";
h+="<span class=mb-config-label>"+key+"</span>";
h+="<div style=display:flex;gap:6px;align-items:center>";
if(isPassword){
h+="<span class=mb-config-value id=config-value-"+safeId+">••••••••</span>";
h+="<button class=mb-password-toggle onclick=togglePassword(\""+safeId+"\",\""+encodeURIComponent(dispVal)+"\") title=\"Show/Hide\">👁</button>";
}else{
h+="<span class=mb-config-value id=config-value-"+safeId+">"+dispVal+"</span>";
}
h+="<button class=mb-config-edit-btn onclick=editSetting(\""+ckey+"\",\""+encodeURIComponent(dispVal)+"\")>Edit</button>";
h+="</div>";
h+="</div>";
}
}
h+="</div>";
});
b.innerHTML=h;
}).catch(function(e){
b.innerHTML="<div style=padding:20px;color:#ef4444>Failed to load settings: "+e+"</div>";
});
}
function editSetting(key,currentVal){
var safeId=key.replace(/[^a-zA-Z0-9]/g,"-");
var valEl=document.getElementById("config-value-"+safeId);
if(!valEl)return;
var decodedVal=decodeURIComponent(currentVal);
var isPassword=key.toLowerCase().indexOf("password")!==-1;
var inputType=isPassword?"type=password ":"";
valEl.innerHTML="<input class=mb-config-input id=config-input-"+safeId+" "+inputType+"value='"+decodedVal+"'>";
// Remove password toggle button if exists
var toggleBtn=valEl.parentElement.querySelector(".mb-password-toggle");
if(toggleBtn)toggleBtn.remove();
var parent=valEl.parentElement;
var editBtn=parent.querySelector(".mb-config-edit-btn");
editBtn.outerHTML="<button class=mb-config-save-btn onclick=saveSetting(\""+key+"\",\""+safeId+"\",\""+isPassword+"\")>Save</button><button class=mb-config-cancel-btn onclick=cancelEdit(\""+key+"\",\""+currentVal+"\")>Cancel</button>";
}
function saveSetting(key,safeId,isPassword){
var input=document.getElementById("config-input-"+safeId);
if(!input)return;
var newVal=input.value;
fetch("/api/v2/config/edit?key="+encodeURIComponent(key)+"&value="+encodeURIComponent(newVal),{method:"POST"})
.then(function(r){return r.json()}).then(function(d){
if(d.ok){
toast("Saved: "+key);
loadSettings();
}else{
toast("Error: "+(d.error||"unknown"));
}
}).catch(function(e){toast("Save error: "+e)});
}
function cancelEdit(key,currentVal){
loadSettings();
}
function togglePassword(safeId,actualVal){
var valEl=document.getElementById("config-value-"+safeId);
if(!valEl)return;
var toggleBtn=valEl.parentElement.querySelector(".mb-password-toggle");
if(!toggleBtn)return;
if(valEl.textContent==="••••••••"){
valEl.textContent=decodeURIComponent(actualVal);
toggleBtn.textContent="🙈";
}else{
valEl.textContent="••••••••";
toggleBtn.textContent="👁";
}
}
function validateSettings(){
fetch("/api/v2/config/validate").then(function(r){return r.json()}).then(function(d){
if(d.ok)toast("✓ Settings valid");
else toast("✗ Invalid: "+(d.error||"unknown"));
}).catch(function(e){toast("Validate error: "+e)});
}
function toast(msg){
var t=document.createElement("div");
t.className="mb-toast";
t.textContent=msg;
document.body.appendChild(t);
setTimeout(function(){t.style.opacity="0";setTimeout(function(){t.remove()},300)},2000);
}
// Page version polling (skip while tree or detail panel is open)
var _v=-1;
setInterval(function(){
if(_tv)return;
var ov=document.getElementById("mb-overlay");
if(ov&&ov.style.display==="block")return;
fetch("/version").then(function(r){return r.json()}).then(function(d){
if(d.v!=_v){_v=d.v;
var ov2=document.getElementById("mb-overlay");
if(ov2&&ov2.style.display==="block")return;
if(_v>=0)fetch("/body").then(function(r){return r.text()}).then(function(h){
var ov3=document.getElementById("mb-overlay");
if(ov3&&ov3.style.display==="block")return;
var e=document.getElementById("mb-content");
if(e){e.innerHTML=h;mermaid.run()}})}})
},500)
setInterval(function(){
fetch("/status").then(function(r){return r.json()}).then(function(d){
var s="";d.id&&(s+="["+d.id+"] ");d.step&&(s+="Step "+d.step+"/"+d.total);d.label&&(s+=" "+d.label);
var e=document.getElementById("mbsi");if(e)e.textContent=s})
},1000)
fetch("/volume").then(function(r){return r.json()}).then(function(d){
var e=document.getElementById("mbvlvl");if(e){e.textContent=d.level}
})
fetch("/labels").then(function(r){return r.json()}).then(function(d){
var s=document.getElementById("mbstep");
d.forEach(function(x){var o=document.createElement("option");o.value=x.num;o.text=x.num+". "+x.label;s.appendChild(o)})
})
// ═══════════════ FILE TREE PANEL ═══════════════
var _tv=false, _tm="tree", _td=null, _tree_user=null;
function toggleTree(){
var token=localStorage.getItem('tree_token');
var savedUser=localStorage.getItem('tree_user');
if(token && savedUser){
// Verify token validity
fetch('/api/v2/auth/verify',{
headers:{'Authorization':'Bearer '+token}
})
.then(function(r){return r.json()})
.then(function(d){
if(d.ok && d.user_id===savedUser){
// Token valid, open tree
_tree_user=savedUser;
_tv=!_tv;
document.getElementById("mb-tree-panel").classList.toggle("active",_tv);
if(_tv)loadTree();
}else{
// Token invalid or user mismatch, clear and show login
localStorage.removeItem('tree_token');
localStorage.removeItem('tree_user');
showTreeLoginModal();
}
})
.catch(function(e){
localStorage.removeItem('tree_token');
localStorage.removeItem('tree_user');
showTreeLoginModal();
});
}else{
// No token, show login
showTreeLoginModal();
}
}
function showTreeLoginModal(){
var m=document.getElementById('mb-tree-login-modal');
if(!m){
m=document.createElement('div');
m.id='mb-tree-login-modal';
m.style.cssText='display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#1e293b;border:1px solid #334155;padding:24px;border-radius:8px;z-index:10000;min-width:320px';
m.innerHTML='<button onclick="this.parentElement.style.display=\'none\';this.parentElement.classList.remove(\'active\')" style="position:absolute;top:12px;right:12px;background:none;border:none;color:#64748b;font-size:18px;cursor:pointer">✕</button>'+
'<div style="color:#60a5fa;font-size:16px;font-weight:600;margin-bottom:16px">File Tree Authentication</div>'+
'<div style="margin-bottom:12px">'+
'<label style="color:#94a3b8;font-size:13px;display:block;margin-bottom:4px">User ID</label>'+
'<input style="background:#0f172a;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:8px 12px;width:100%;font-size:13px" type=text id=tree-user placeholder="Enter user ID (e.g., demo)">'+
'</div>'+
'<div style="margin-bottom:12px;position:relative">'+
'<label style="color:#94a3b8;font-size:13px;display:block;margin-bottom:4px">Password</label>'+
'<input style="background:#0f172a;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:8px 12px;width:100%;font-size:13px;padding-right:36px" type=password id=tree-password placeholder="Enter password" onkeypress=handleTreeKeyPress(event)>'+
'<button onclick=toggleTreePassword() style="position:absolute;right:8px;top:28px;transform:translateY(-50%);background:none;border:none;color:#60a5fa;cursor:pointer;font-size:14px">👁</button>'+
'</div>'+
'<button onclick=submitTreeLogin() style="background:#064e3b;border:1px solid #4ade80;color:#4ade80;padding:8px 16px;border-radius:4px;cursor:pointer;width:100%;font-size:13px">Login</button>'+
'<div id=tree-error style="color:#ef4444;font-size:12px;margin-top:8px"></div>';
document.body.appendChild(m);
}
document.getElementById('tree-user').value='';
document.getElementById('tree-password').value='';
document.getElementById('tree-password').type='password';
document.getElementById('tree-error').textContent='';
m.classList.add('active');
m.style.display='block';
document.getElementById('tree-user').focus();
}
function handleTreeKeyPress(e){
if(e.key==='Enter'||e.keyCode===13){
submitTreeLogin();
}
}
function logoutTree(){
// Clear tree authentication data
localStorage.removeItem('tree_token');
localStorage.removeItem('tree_user');
localStorage.removeItem('tree_locked');
// Reset tree data
_td=null;
_tree_user=null;
// Close tree panel
document.getElementById("mb-tree-panel").classList.remove("active");
// Show login modal
showTreeLoginModal();
// Show toast
toast('Tree logout ✓');
}
function toggleTreePassword(){
var pwdInput=document.getElementById('tree-password');
var toggleBtn=pwdInput.parentElement.querySelector('button');
if(pwdInput.type==='password'){
pwdInput.type='text';
toggleBtn.textContent='🙈';
}else{
pwdInput.type='password';
toggleBtn.textContent='👁';
}
}
function submitTreeLogin(){
var user=document.getElementById('tree-user').value.trim();
var pwd=document.getElementById('tree-password').value;
if(!user){
document.getElementById('tree-error').textContent='User ID required';
return;
}
if(!pwd){
document.getElementById('tree-error').textContent='Password required';
return;
}
fetch('/api/v2/auth/login',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({username:user,password:pwd})
})
.then(function(r){return r.json()})
.then(function(d){
if(d.token){
localStorage.setItem('tree_token',d.token);
localStorage.setItem('tree_user',user);
_tree_user=user;
document.getElementById('mb-tree-login-modal').style.display='none';
toast('Logged in as '+user+' ✓');
_tv=true;
document.getElementById("mb-tree-panel").classList.add("active");
loadTree();
}else{
document.getElementById('tree-error').textContent=d.error||'Login failed';
document.getElementById('tree-password').value='';
document.getElementById('tree-password').focus();
}
})
.catch(function(e){
document.getElementById('tree-error').textContent='Connection error: '+e;
});
}
function loadTree(){
var b=document.getElementById("mb-tree-body");
if(!b)return;
b.innerHTML="<div style=text-align:center;padding:40px;color:#64748b>Loading...</div>";
var token=localStorage.getItem('tree_token');
var user=_tree_user||localStorage.getItem('tree_user')||'demo';
fetch("/api/v2/tree/"+user+"?mode="+_tm,{
headers:{'Authorization':'Bearer '+token}
}).then(function(r){return r.json()}).then(function(d){
_td=d;
_td=d;
var h="";
// Mode buttons
var modes=[{k:"tree",i:"🌳",l:"Tree"},{k:"list",i:"📋",l:"List"},{k:"grid_sm",i:"🟦",l:"Icons"},{k:"grid_lg",i:"🔲",l:"Large"}];
h+="<div class=mb-mode-bar>";
modes.forEach(function(m){
h+="<button class=mb-mode-btn"+(_tm==m.k?" active":"")+" onclick='changeMode(\""+m.k+"\")'>";
h+="<span>"+m.i+"</span>"+m.l+"</button>";
});
h+="<span style=flex:1></span>";
h+="<button class=mb-lock-btn id=mb-lock-icon onclick=toggleLock() title='Toggle edit lock'>"+(_locked?"🔒":"🔓")+"</button>";
if(!_locked){
h+="<button class=mb-new-folder-btn onclick='document.getElementById(\"mb-file-input\").click()' style='background:#064e3b;border-color:#4ade80;color:#4ade80'>📤 Upload</button>";
h+="<input type=file id=mb-file-input style=display:none onchange=uploadFile(this)>";
}
if(!_locked){
h+="<button class=mb-new-folder-btn onclick=organizeTree() style='background:#0c4a6e;border-color:#38bdf8;color:#38bdf8'>⚡ Agent</button>";
}
h+="<button class=mb-new-folder-btn onclick=newFolder()>+ Folder</button>";
if(!_locked){
h+="<button class=mb-new-folder-btn onclick=restoreTree() style='background:#1e3a5f;border-color:#3b82f6;color:#93c5fd'>↻ Restore</button>";
h+="<button class=mb-new-folder-btn onclick=findDupes() style='background:#451a03;color:#fbbf24;border-color:#b45309'>🔍 Dupes</button>";
h+="<button class=mb-new-folder-btn onclick=deleteAll() style='background:#451a03;color:#fbbf24;border-color:#b45309'>✕ All</button>";
h+="<button class=mb-new-folder-btn onclick=logoutTree() style='background:#7f1d1d;color:#fca5a5;border-color:#dc2626'>🚪 Logout</button>";
}
h+="<span style=color:#64748b;font-size:12px;align-self:center>"+d.nodes.length+" nodes</span></div>";
if(_tm=="tree")h+=renderTree(d);
else if(_tm=="list")h+=renderList(d);
else h+=renderGrid(d,_tm);
b.innerHTML=h;
}).catch(function(e){
b.innerHTML="<div style=padding:20px;color:#ef4444>Failed to load tree: "+e+"</div>";
});
}
function changeMode(m){
_tm=m;localStorage.setItem("display_mode",m);
loadTree();
}
function dname(n){
var a=n.aliases||{};
for(var k in a){if(a.hasOwnProperty(k)&&a[k])return a[k];}
return n.label;
}
function fsize(b){
if(!b&&b!==0)return "-";
var s=b,i=0,u=["B","KB","MB","GB"];
while(s>=1024&&i<3){s/=1024;i++;}
return (i==0?s:s.toFixed(1))+" "+u[i];
}
// TREE MODE
var _clk=0;
function tgl(id){
var el=document.getElementById(id);
if(el){var v=el.style.display=="none";el.style.display=v?"":"none";
var c=document.getElementById("cr"+id);if(c)c.textContent=v?"▼":"▶";}
}
function renderTree(d){
var ch={};
d.nodes.forEach(function(n){var p=n.parent_id||"root";if(!ch[p])ch[p]=[];ch[p].push(n);});
function rc(pid,ind){
var lst=ch[pid];if(!lst||!lst.length)return"";
var h="";
lst.sort(function(a,b){if(a.node_type!=b.node_type)return a.node_type=="folder"?-1:1;return a.label.localeCompare(b.label);});
lst.forEach(function(n){
if(n.node_type=="folder"){
_clk++;var cid="tc"+_clk;var nid=n.node_id;
h+="<div class=mb-tree-node style=padding-left:"+(ind*20)+"px>";
h+="<span class=mb-tree-caret id=cr"+cid+" onclick='tgl(\""+cid+"\")'>"+(ind==0?"▼":"▶")+"</span>";
h+="<span class=mb-tree-label ondblclick='renameNode(\""+nid+"\")'>"+(n.icon||"📁")+" <b id=flb"+nid+">"+dname(n)+"</b></span>";
h+="<span class=mb-folder-actions>";
h+="<button class=mb-folder-btn onclick='pickIcon(\""+nid+"\")'>🎨</button>";
h+="<button class=mb-folder-btn onclick='renameNode(\""+nid+"\")'>✏️</button>";
h+="<button class=mb-folder-btn onclick='moveNode(\""+nid+"\")'>📦</button>";
h+="<button class=mb-folder-btn danger onclick=delNode(\""+nid+"\",\""+dname(n)+"\")>🗑</button></span>";;
if(ch[n.node_id]&&ch[n.node_id].length){
h+="<div id="+cid+" style=display:"+(ind==0?"block":"none")+">";
h+=rc(n.node_id,ind+1)+"</div>";
}
}else{
var nid=n.node_id;
h+="<div class=mb-tree-node style=padding-left:"+(ind*20+18)+"px>";
h+="<span class=mb-tree-label ondblclick='renameNode(\""+nid+"\")'>";
h+="<span class=mb-tree-file onclick='showDetail(\""+(n.file_uuid||"")+"\")'>";
h+=(n.icon||"📄")+" <b id=flb"+nid+">"+dname(n)+"</b></span>";
h+="<span class=mb-tree-meta>"+fsize(n.file_size)+"</span>";
h+="<span class=mb-folder-btn onclick='quickPreview(\""+(n.file_uuid||"")+"\")' title='Preview' style='display:inline-block;margin-left:4px;font-size:11px'>👁</span></span>";
h+="<span class=mb-folder-actions>";
h+="<button class=mb-folder-btn onclick='pickIcon(\""+nid+"\")'>🎨</button>";
h+="<button class=mb-folder-btn onclick='renameNode(\""+nid+"\")'>✏️</button>";
h+="<button class=mb-folder-btn onclick='moveNode(\""+nid+"\")'>📦</button>";
h+="<button class=mb-folder-btn danger onclick=delNode(\""+nid+"\",\""+dname(n)+"\")>🗑</button></span>";;
}
h+="</div>";
});
return h;
}
return rc("root",0);
}
function renderList(d){
var h="<table style=width:100%><thead><tr><th>Name</th><th>file_uuid</th><th>Size</th><th>Type</th></tr></thead><tbody>";
d.nodes.forEach(function(n){
var icon=n.icon||(n.node_type=="folder"?"📁":"📄");
var badge=n.node_type=="folder"?"<span style=color:#fbbf24>folder</span>":"<span style=color:#4ade80>file</span>";
h+="<tr onclick='"+(n.file_uuid?"showDetail(\""+n.file_uuid+"\")":"")+"' style=cursor:"+(n.file_uuid?"pointer":"default")+">";
h+="<td>"+icon+" "+dname(n)+"</td>";
h+="<td><code>"+(n.file_uuid||"-")+"</code></td>";
h+="<td>"+fsize(n.file_size)+"</td><td>"+badge+"</td></tr>";
});
h+="</tbody></table>";return h;
}
function renderGrid(d,mode){
var cls=mode=="grid_sm"?"sm":"lg";
var h="<div class='mb-grid "+cls+"'>";
d.nodes.forEach(function(n){
var icon=n.icon||(n.node_type=="folder"?"📁":"📄");
h+="<div class=mb-grid-cell onclick='"+(n.file_uuid?"showDetail(\""+n.file_uuid+"\")":"")+"'>";
h+="<div class=mb-grid-icon>"+icon+"</div>";
h+="<div class=mb-grid-label>"+dname(n)+"</div>";
if(n.file_uuid)h+="<div class=mb-grid-uuid>"+n.file_uuid+"</div>";
h+="</div>";
});
h+="</div>";return h;
}
// DETAIL PANEL
function showDetail(fuuid){
if(!fuuid)return;
var userId=localStorage.getItem("tree_user")||"demo";
document.getElementById("mb-overlay").style.display="block";
document.getElementById("mb-detail").style.display="block";
var b=document.getElementById("mb-detail-body");
b.innerHTML="<div style=text-align:center;padding:30px;color:#64748b>Loading...</div>";
fetch("/api/v2/files/"+userId+"/"+fuuid+"/info").then(function(r){return r.json()}).then(function(d){
var node=_td&&_td.nodes?_td.nodes.find(function(n){return n.file_uuid==fuuid}):null;
var label=node?dname(node):fuuid;
var sz=node?fsize(node.file_size):"-";
var sha=node?(node.sha256||"-").substring(0,16)+"...":"-";
var reg=node?(node.registered_at||"-").substring(0,10):"-";
var h="<h2 style=border:none;margin-bottom:16px>📄 "+label+"</h2>";
h+="<table style=width:100%><tr><th>file_uuid</th><td><code>"+d.file_uuid+"</code></td></tr>";
h+="<tr><th>SHA256</th><td><code>"+sha+"</code></td></tr>";
h+="<tr><th>Size</th><td>"+sz+"</td></tr>";
h+="<tr><th>Registered</th><td>"+reg+"</td></tr></table>";
h+="<h3>🌳 Virtual Paths</h3>";
if(d.virtual_paths&&d.virtual_paths.length) d.virtual_paths.forEach(function(vp){h+="<div>📁 "+vp+"</div>"});
else h+="<div style=color:#64748b>—</div>";
h+="<h3>💾 Real Locations ("+d.location_count+")</h3>";
if(d.real_locations&&d.real_locations.length){
d.real_locations.forEach(function(loc){
var lbl=loc.label||"",pth=loc.path||"";
var tier="hot";
if(lbl.indexOf("warm")>=0||lbl.indexOf("nas")>=0) tier="warm";
if(lbl.indexOf("cold")>=0||lbl.indexOf("lto")>=0||lbl.indexOf("glacier")>=0) tier="cold";
if(lbl.indexOf("s3")>=0||lbl.indexOf("cdn")>=0||lbl.indexOf("ipfs")>=0) tier="cloud";
h+="<div><span class=mb-loc-tag "+tier+">"+lbl+"</span><code style=font-size:11px>"+pth.substring(0,80)+"</code></div>";
});
}else h+="<div style=color:#64748b>—</div>";
h+="<h3>🔍 Probe Data <span id=mb-probe-status style=font-size:11px;color:#64748b>loading...</span></h3><div id=mb-probe-data style=color:#64748b>Loading...</div>";
h+="<h3>🖼️ Preview <span style=font-size:12px;color:#94a3b8>"+label+"</span> <span id=mb-preview-res style=font-size:12px;color:#64748b></span></h3><div style=margin-top:8px;position:relative;display:flex;align-items:center;gap:8px>";
var src="/api/v2/files/"+fuuid+"/stream";
var ext=(label||"").split(".").pop().toLowerCase();
var isVideo=(ext=="mp4"||ext=="mov"||ext=="avi"||ext=="webm"||ext=="mkv");
var isTxt=(ext=="txt"||ext=="log"||ext=="csv"||ext=="json"||ext=="xml");
var isMd=(ext=="md"||ext=="markdown");
var isDocText=(ext=="docx"||ext=="doc"||ext=="rtf");
var isDocImg=(ext=="pages"||ext=="key"||ext=="numbers");
var isDocPdf=(ext=="pdf"||ext=="pptx"||ext=="xlsx"||ext=="odt"||ext=="xls"||ext=="ppt"||ext=="epub"||ext=="html");
var isDoc=isDocText||isDocImg||isDocPdf;
if(isVideo){
h+="<video controls style='max-width:100%;max-height:360px;border-radius:8px;background:#0f172a'><source src='"+src+"'></video>";
}else if(isTxt||isDocText){
h+="<pre id=mb-detail-txt style='max-height:300px;overflow:auto;background:#0f172a;color:#e2e8f0;padding:12px;border-radius:8px;font-size:13px;white-space:pre-wrap;width:100%'>Loading...</pre>";
}else if(isMd){
h+="<div id=mb-md-render style='max-height:400px;overflow:auto;background:#0f172a;padding:12px;border-radius:8px'>Loading...</div>";
}else if(isDocImg){
h+="<img id=mb-preview-img src='"+src+"' style='max-width:100%;max-height:400px;border-radius:8px' onerror=\"this.onerror=null;this.alt='No preview'\">";
}else if(isDocPdf){
h+="<iframe sandbox='allow-same-origin' src='"+src+"' style='width:100%;height:400px;border:none;border-radius:8px;background:#fff'></iframe>";
}else{
h+="<button id=mb-prev-btn onclick=navigatePhoto('prev') style='background:#1e293b;border:1px solid #334155;color:#94a3b8;font-size:24px;padding:8px 14px;border-radius:6px;cursor:pointer'>◀</button>";
h+="<img id=mb-preview-img src='"+src+"' style='max-width:100%;max-height:400px;min-height:100px;min-width:100px;border-radius:8px;background:#0f172a' onerror=\"this.onerror=null;this.alt='No preview'\">";
h+="<button id=mb-next-btn onclick=navigatePhoto('next') style='background:#1e293b;border:1px solid #334155;color:#94a3b8;font-size:24px;padding:8px 14px;border-radius:6px;cursor:pointer'>▶</button>";
h+="<span id=mb-photo-pos style='position:absolute;bottom:8px;right:50px;background:rgba(0,0,0,.7);color:#94a3b8;padding:2px 8px;border-radius:4px;font-size:11px'>1/1</span>";
}
h+="</div>";
b.innerHTML=h;
if(!isVideo&&!isTxt&&!isMd&&!isDocText&&!isDocImg&&!isDocPdf){_photoUuid=fuuid;setupPhotoNav(fuuid)}
if(isTxt||isDocText) fetch(src).then(function(r){return r.text()}).then(function(t){
var el=document.getElementById("mb-detail-txt");if(el)el.textContent=t||"(empty)";
});
if(isMd) fetch("/api/v2/render/"+fuuid+"/body").then(function(r){return r.text()}).then(function(h){
var el=document.getElementById("mb-md-render");if(el){el.innerHTML=h;setTimeout(function(){
var nodes=el.querySelectorAll(".mermaid");if(nodes.length)mermaid.run({nodes:Array.from(nodes)})
},100)}
});
fetch("/api/v2/files/"+fuuid+"/probe").then(function(r){return r.json()}).then(function(p){
var pd=document.getElementById("mb-probe-data");
var ps=document.getElementById("mb-probe-status");
var pr=document.getElementById("mb-preview-res");
if(p.width&&p.height)pr.textContent="("+p.width+"×"+p.height+")";
else pr.textContent="";
if(p.probe){
ps.textContent="";
var ph="<table style=width:100%>";
if(p.duration)ph+="<tr><th>Duration</th><td>"+(p.duration).toFixed(1)+"s ("+Math.floor(p.duration/60)+"min "+(p.duration%60).toFixed(0)+"s)</td></tr>";
if(p.width&&p.height)ph+="<tr><th>Resolution</th><td>"+p.width+"×"+p.height+"</td></tr>";
if(p.fps)ph+="<tr><th>FPS</th><td>"+p.fps+"</td></tr>";
if(p.file_type)ph+="<tr><th>Codec</th><td>"+p.file_type+"</td></tr>";
if(p.total_frames)ph+="<tr><th>Total Frames</th><td>"+p.total_frames+"</td></tr>";
if(p.probe.format){
var fmt=p.probe.format;
if(fmt.format_name)ph+="<tr><th>Format</th><td>"+fmt.format_name+"</td></tr>";
if(fmt.size)ph+="<tr><th>Probe Size</th><td>"+fmt.size+"</td></tr>";
if(fmt.bit_rate)ph+="<tr><th>Bitrate</th><td>"+(fmt.bit_rate/1000000).toFixed(1)+" Mbps</td></tr>";
}
if(p.probe.streams){
ph+="<tr><th>Streams</th><td>";
p.probe.streams.forEach(function(s){ph+=" • "+s.codec_type+": "+s.codec_name+"<br>"});
ph+="</td></tr>";
}
ph+="</table>";pd.innerHTML=ph;
}else{
ps.textContent="(not available)";
pd.innerHTML="<div style=color:#64748b>No probe data for this file</div>";
}
}).catch(function(){document.getElementById("mb-probe-status").textContent="(error)"});
}).catch(function(e){b.innerHTML="<div style=color:#ef4444>Error: "+e+"</div>"});
}
function closeDetail(){
document.getElementById("mb-overlay").style.display="none";
document.getElementById("mb-detail").style.display="none";
}
// PHOTO NAVIGATION
var _photoUuid=null,_photoList=[];
function setupPhotoNav(fuuid){
if(!_td) return;
var imgs=["jpg","jpeg","png","gif","bmp","webp","tiff","svg"];
_photoList=_td.nodes.filter(function(n){
if(!n.file_uuid||n.node_type!="file")return false;
var e=(n.label||"").split(".").pop().toLowerCase();
return imgs.indexOf(e)>=0;
}).map(function(n){return n.file_uuid});
updatePhotoPos(fuuid);
}
function updatePhotoPos(fuuid){
_photoUuid=fuuid;
var idx=_photoList.indexOf(fuuid);
var pos=document.getElementById("mb-photo-pos");
if(pos)pos.textContent=(idx+1)+"/"+_photoList.length;
var prev=document.getElementById("mb-prev-btn");
var next=document.getElementById("mb-next-btn");
if(prev)prev.style.visibility=idx>0?"visible":"hidden";
if(next)next.style.visibility=idx<_photoList.length-1?"visible":"hidden";
}
function navigatePhoto(dir){
var idx=_photoList.indexOf(_photoUuid);
if(dir=="prev"&&idx>0)idx--;else if(dir=="next"&&idx<_photoList.length-1)idx++;
if(idx>=0&&idx<_photoList.length){
_photoUuid=_photoList[idx];
var img=document.getElementById("mb-preview-img");
if(img)img.src="/api/v2/files/"+_photoUuid+"/stream?_="+Date.now();
updatePhotoPos(_photoUuid);
}
}
// TOAST
function toast(msg){
var t=document.createElement("div");t.className="mb-toast";t.textContent=msg;
document.body.appendChild(t);
setTimeout(function(){t.style.opacity="0";setTimeout(function(){t.remove()},300)},1500);
}
// ICON PICKER
var ICONS=["📁","📂","📄","🎬","🎵","🖼️","📊","📝","📦","📸","🎨","📹","🎧","📚","🔧","⚙️","🌐","💾","📀","💿","🏠","🏢","🌟","⭐","💡","🔥","❤️","💚","💙","💛","🧡","💜","✅","❌","⚠️","🔒","🔓","🔑","📡","🔗"];
var _icoPicked=null,_icoNode=null;
function pickIcon(nid){
_icoPicked=null;_icoNode=nid;
var o=document.createElement("div");
o.style.cssText="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:10002;display:flex;align-items:center;justify-content:center";
var bx=document.createElement("div");
bx.style.cssText="background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;min-width:380px;max-width:440px";
var h="<h3>🎨 Pick Icon</h3><div class=mb-icon-picker id=mb-ipg>";
ICONS.forEach(function(ico,idx){h+="<button id=ipi"+idx+" onclick='selIcon("+idx+",\""+ico+"\")'>"+ico+"</button>"});
h+="</div>Custom: <input id=mbci placeholder='emoji or SVG url' style='width:100%;background:#0f172a;border:1px solid #334155;border-radius:6px;color:#e2e8f0;padding:6px 10px;font-size:18px;margin-top:8px' oninput='_icoPicked=document.getElementById(\"mbci\").value'>";
h+="<div style=margin-top:10px;display:flex;gap:6px;justify-content:flex-end>";
h+="<button id=idef style='background:#451a03;border:none;color:#fbbf24;padding:4px 14px;border-radius:6px;cursor:pointer'>Default</button>";
h+="<button id=icxl style='background:#334155;border:none;color:#94a3b8;padding:4px 14px;border-radius:6px;cursor:pointer'>Cancel</button>";
h+="<button id=iok style='background:#064e3b;border:none;color:#4ade80;padding:4px 14px;border-radius:6px;cursor:pointer'>OK</button>";
h+="</div>";
bx.innerHTML=h;o.appendChild(bx);document.body.appendChild(o);
document.getElementById("iok").onclick=function(){if(_icoPicked)applyIcon(_icoNode,_icoPicked);o.remove()};
document.getElementById("idef").onclick=function(){applyIcon(_icoNode,"");o.remove()};
document.getElementById("icxl").onclick=function(){o.remove()};
o.onclick=function(e){if(e.target==o)o.remove()};
}
function selIcon(idx,ico){
_icoPicked=ico;
ICONS.forEach(function(_,i){var el=document.getElementById("ipi"+i);if(el)el.style.border=i==idx?"2px solid #4ade80":"2px solid transparent"});
}
function applyIcon(nid,ico){
var token=localStorage.getItem('tree_token');
var user=_tree_user||localStorage.getItem('tree_user')||'demo';
fetch("/api/v2/tree/"+user+"/node/"+nid,{method:"PUT",headers:{"Content-Type":"application/json","Authorization":"Bearer "+token},
body:JSON.stringify({icon:ico})})
.then(function(r){return r.json()}).then(function(){
loadTree();toast(ico?"Icon → "+ico:"Icon reset to default");
});
}
// AGENT ORGANIZE
function organizeTree(){
if(!_td){toast("Load tree first");return}
var folders={};
_td.nodes.forEach(function(n){if(n.node_type=="folder")folders[n.label]=n.node_id});
var cats=["Movies","Marketing","Cartoons","Other"];
var targets={};
cats.forEach(function(c){if(folders[c])targets[c]=folders[c]});
if(Object.keys(targets).length<2){toast("Need at least 2 category folders");return}
var files=_td.nodes.filter(function(n){
if(n.node_type!="file"||!n.parent_id)return false;
var inCat=false;
for(var k in targets){if(n.parent_id==targets[k]){inCat=true;break}}
return !inCat;
});
if(!files.length){toast("All files already organized ✓");return}
var token=localStorage.getItem('tree_token');
var user=_tree_user||localStorage.getItem('tree_user')||'demo';
var mover=function(idx){
if(idx>=files.length){loadTree();toast("Agent: "+files.length+" files organized");return}
var f=files[idx];
var nl=(f.label||"").toLowerCase();
var t="Other";
if(/charade|film|clip|movie|comedy|filmriot/.test(nl))t="Movies";
else if(/exasan|gamma|thunderbolt|nab|koba|webinar|top colorist|accusys|a12t3/.test(nl))t="Marketing";
else if(/cartoon|alice|felix|disney|steamboat|animal/.test(nl))t="Cartoons";
var pid=targets[t];
if(!pid){mover(idx+1);return}
fetch("/api/v2/tree/"+user+"/node/"+f.node_id+"/move",{method:"PUT",headers:{"Content-Type":"application/json","Authorization":"Bearer "+token},
body:JSON.stringify({parent_id:pid})})
.then(function(r){return r.json()}).then(function(){mover(idx+1);})
.catch(function(){mover(idx+1);});
};
mover(0);
}
// DUPLICATE FINDER
function findDupes(){
var o=document.createElement("div");
o.style.cssText="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:10002;display:flex;align-items:center;justify-content:center";
var bx=document.createElement("div");
bx.style.cssText="background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;min-width:500px;max-width:700px;max-height:80vh;overflow-y:auto";
bx.innerHTML="<h3>🔍 Scanning for duplicates...</h3>";
o.appendChild(bx);document.body.appendChild(o);
o.onclick=function(e){if(e.target==o)o.remove()};
fetch("/api/v2/dupes/demo").then(function(r){return r.json()}).then(function(d){
if(!d.dupes||!d.dupes.length){
bx.innerHTML="<h3>✅ No Duplicates Found</h3><button onclick=this.parentElement.parentElement.remove() style='margin-top:12px;background:#334155;border:none;color:#94a3b8;padding:6px 16px;border-radius:6px;cursor:pointer'>Close</button>";
return;
}
var h="<h3>🔍 Duplicates ("+d.dup_groups+" groups)</h3>";
d.dupes.forEach(function(g,i){
h+="<div style='margin:10px 0;padding:10px;background:#0f172a;border-radius:8px'>";
h+="<b>"+(i+1)+". "+g.file_name+"</b> ("+g.count+"x)<br>";
g.uuids.forEach(function(uuid,j){
var st=g.statuses[j]||"?";
var badge=st=="completed"?"background:#064e3b;color:#4ade80":st=="pending"?"background:#451a03;color:#fbbf24":"background:#1e293b;color:#64748b";
h+="<div style='display:flex;align-items:center;gap:8px;margin:4px 0;padding:4px 8px;background:#1e293b;border-radius:4px'>";
h+="<code style='flex:1;font-size:11px'>"+uuid+"</code>";
h+="<span style='padding:1px 6px;border-radius:3px;font-size:10px;"+badge+"'>"+st+"</span>";
h+="<button onclick=unregDup('"+uuid+"') style='background:#451a03;border:none;color:#fbbf24;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px'>✕</button>";
h+="</div>";
});
h+="</div>";
});
h+="<div style=margin-top:12px;display:flex;gap:6px;justify-content:flex-end><button onclick=this.parentElement.parentElement.parentElement.remove() style='background:#334155;border:none;color:#94a3b8;padding:4px 14px;border-radius:6px;cursor:pointer'>Close</button><button onclick='afterUnreg()' style='background:#1e3a5f;border:none;color:#93c5fd;padding:4px 14px;border-radius:6px;cursor:pointer'>Restore Tree</button></div>";
bx.innerHTML=h;
});
}
function unregDup(uuid){
if(!confirm("Unregister "+uuid+" ?"))return;
fetch("/api/v2/unregister/"+uuid,{method:"POST"})
.then(function(r){return r.json()}).then(function(d){
toast("Unregistered: "+uuid);
findDupes(); // re-scan
});
}
function afterUnreg(){
document.querySelectorAll("div[style*='z-index:10002']").forEach(function(el){el.remove()});
restoreTree();
}
function uploadFile(input){
var file=input.files[0];
if(!file)return;
var fd=new FormData();
fd.append("file",file);
toast("Uploading "+file.name+"...");
fetch("/api/v2/upload/demo",{method:"POST",body:fd})
.then(function(r){return r.json()}).then(function(d){
if(d.ok){toast("Uploaded: "+d.filename+" ("+fsize(d.size)+")");loadTree()}
else toast("Upload failed: "+(d.error||"unknown"));
}).catch(function(e){toast("Upload error: "+e)});
input.value="";
}
// QUICK PREVIEW
function quickPreview(fuuid){
if(!fuuid)return;
var node=(_td&&_td.nodes)?_td.nodes.find(function(n){return n.file_uuid==fuuid}):null;
var label=node?node.label:"";
var ext=(label||"").split(".").pop().toLowerCase();
var isVideo=ext=="mp4"||ext=="mov"||ext=="avi"||ext=="webm"||ext=="mkv";
var isTxt=ext=="txt"||ext=="log"||ext=="csv"||ext=="json"||ext=="xml";
var isMd=(ext=="md"||ext=="markdown");
var isDocText=(ext=="docx"||ext=="doc"||ext=="rtf");
var isDocImg=(ext=="pages"||ext=="key"||ext=="numbers");
var isDocPdf=(ext=="pdf"||ext=="pptx"||ext=="xlsx"||ext=="odt"||ext=="xls"||ext=="ppt"||ext=="epub"||ext=="html");
var src="/api/v2/files/"+fuuid+"/stream";
var o=document.createElement("div");
o.style.cssText="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:10002;display:flex;align-items:center;justify-content:center";
var inner="";
if(isVideo){
inner="<video controls autoplay style='max-width:90vw;max-height:85vh;border-radius:8px'><source src='"+src+"'></video>";
}else if(isTxt||isDocText){
inner="<pre id=mb-txt-preview style='max-width:90vw;max-height:85vh;overflow:auto;background:#0f172a;color:#e2e8f0;padding:24px;border-radius:8px;font-size:14px;white-space:pre-wrap'>Loading...</pre>";
}else if(isMd){
inner="<div id=mb-qmd-render style='max-width:90vw;max-height:85vh;overflow:auto;background:#0f172a;padding:24px;border-radius:8px'>Loading...</div>";
}else if(isDocImg){
inner="<img src='"+src+"' style='max-width:90vw;max-height:85vh;border-radius:8px' onerror=\"this.onerror=null;this.alt='No preview'\">";
}else if(isDocPdf){
inner="<iframe sandbox='allow-same-origin' src='"+src+"' style='width:90vw;height:85vh;border:none;border-radius:8px;background:#fff'></iframe>";
}else{
inner="<img src='"+src+"' style='max-width:90vw;max-height:85vh;min-height:100px;min-width:100px;border-radius:8px' onerror=\"this.onerror=null;this.alt='No preview'\">";
}
inner+="<div style='position:absolute;top:16px;right:16px'><button onclick=this.parentElement.parentElement.remove() style='background:rgba(0,0,0,.6);border:none;color:#fff;font-size:24px;cursor:pointer;padding:4px 12px;border-radius:6px'>✕</button></div>";
o.innerHTML=inner;
document.body.appendChild(o);
if(isTxt||isDocText) fetch(src).then(function(r){return r.text()}).then(function(t){
var el=document.getElementById("mb-txt-preview");
if(el)el.textContent=t||"(empty)";
});
if(isMd) fetch("/api/v2/render/"+fuuid+"/body").then(function(r){return r.text()}).then(function(h){
var el=document.getElementById("mb-qmd-render");if(el){el.innerHTML=h;setTimeout(function(){
var nodes=el.querySelectorAll(".mermaid");if(nodes.length)mermaid.run({nodes:Array.from(nodes)})
},100)}
});
o.onclick=function(e){if(e.target==o)o.remove()};
}
// LOCK TOGGLE
var _locked=true;
function toggleLock(){
_locked=!_locked;
localStorage.setItem("tree_locked",_locked?"1":"0");
document.body.classList.toggle("mb-locked",_locked);
var icon=document.getElementById("mb-lock-icon");
if(icon)icon.textContent=_locked?"🔒":"🔓";
loadTree();
}
// Init
(function(){
var s=localStorage.getItem("display_mode");
if(s&&["tree","list","grid_sm","grid_lg"].indexOf(s)>=0)_tm=s;
_locked=localStorage.getItem("tree_locked")!=="0";
document.body.classList.toggle("mb-locked",_locked);
})();
</script>
</body></html>