Files
markbase/markbase-core/src/page.html
Warren 1300a4e223
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
MarkBase架构升级:Multi-Volume Virtual Tree + Dual-View Management + Git Remote修正
核心功能:
-  Categories/Series双视图管理(category_view.rs + import_markdown.rs)
-  FUSE Multi-Volume支持(tree_type参数)
-  SSH/SFTP/SCP/rsync协议完整实现(4042行)
-  NFS/SMB Module Phase 1-3完成
-  Archive Module Phase 1-4完成(2916行)
-  Download Center API完整实现
-  S3兼容API实现(560行)

Git配置修正:
-  删除错误origin(gitea.momentry.ddns.net)
-  删除m5max128(指向机器名)
-  设置origin = m5max128gitea.momentry.ddns.net/admin/markbase
-  设置m4minigitea = m4minigitea.momentry.ddns.net/warren/markbase

数据清理:
-  删除38个临时SQLite(保留accusys.sqlite、demo.sqlite)
-  删除.bak、test_*.bin、调试脚本等临时文件
-  删除临时目录(build/、download files/、raid_test/等)
-  更新.gitignore排除临时文件

架构优化:
- 52个文件修改,2434行新增,4739行删除
- Workspace成员整合(16个crate)
- 数据库状态:accusys.sqlite保留(主demo测试)

远程同步:
-  准备推送到m5max128gitea(远程Gitea)
-  准备推送到m4minigitea(本地Gitea)
2026-06-12 12:59:54 +08:00

1420 lines
75 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-s3-panel style="display:none;position:fixed;top:0;left:0;right:0;bottom:52px;background:#0f172a;z-index:9998;overflow-y:auto;padding:16px 24px"><div id=mb-s3-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="toggleS3()" title="S3 Service" 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 savedMode=localStorage.getItem('display_mode')||'categories';
if(savedMode==='categories' || savedMode==='series'){
_tm=savedMode;
_tv=!_tv;
if(!_tv){
localStorage.setItem('display_mode','categories');
}
document.getElementById("mb-tree-panel").classList.toggle("active",_tv);
if(_tv)loadTree();
}else{
var token=localStorage.getItem('tree_token');
var savedUser=localStorage.getItem('tree_user');
if(token && savedUser){
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){
_tree_user=savedUser;
_tm=savedMode;
_tv=!_tv;
if(!_tv){
localStorage.setItem('display_mode','categories');
}
document.getElementById("mb-tree-panel").classList.toggle("active",_tv);
if(_tv)loadTree();
}else{
localStorage.removeItem('tree_token');
localStorage.removeItem('tree_user');
localStorage.setItem('display_mode','categories');
showTreeLoginModal();
}
})
.catch(function(e){
localStorage.removeItem('tree_token');
localStorage.removeItem('tree_user');
localStorage.setItem('display_mode','categories');
showTreeLoginModal();
});
}else{
localStorage.setItem('display_mode','categories');
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(searchQuery){
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';
var url;
if(_tm==="categories"){
if(searchQuery && searchQuery.trim()){
url="/api/v2/files/search?q="+encodeURIComponent(searchQuery.trim())+"&view=category";
}else{
url="/api/v2/categories";
}
}else if(_tm==="series"){
if(searchQuery && searchQuery.trim()){
url="/api/v2/files/search?q="+encodeURIComponent(searchQuery.trim())+"&view=series";
}else{
url="/api/v2/series";
}
}else{
url="/api/v2/tree/"+user+"?mode="+_tm;
if(searchQuery && searchQuery.trim()){
url="/api/v2/tree/"+user+"/search?q="+encodeURIComponent(searchQuery.trim())+"&mode="+_tm;
}
}
fetch(url,{
headers:{'Authorization':'Bearer '+token}
}).then(function(r){return r.json()}).then(function(d){
_td=d;
var h="";
// Search box
h+="<div style='margin-bottom:12px;display:flex;gap:8px;align-items:center'>";
h+="<input type=text id=mb-search-input placeholder='Search files...' value='"+(searchQuery||"")+"' style='flex:1;padding:8px 12px;border:1px solid #334155;background:#1e293b;color:#e2e8f0;border-radius:6px;font-size:14px' onkeypress='if(event.key==\"Enter\")searchTree()'>";
h+="<button onclick='searchTree()' style='padding:8px 16px;background:#3b82f6;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px'>🔍 Search</button>";
h+="<button onclick='clearSearch()' style='padding:8px 16px;background:#64748b;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px'>✕ Clear</button>";
h+="</div>";
// Mode buttons
var modes=[{k:"tree",i:"🌳",l:"All Files"},{k:"categories",i:"📁",l:"Categories"},{k:"series",i:"📦",l:"Series"},{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 && _tm==="tree"){
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 && _tm==="tree"){
h+="<button class=mb-new-folder-btn onclick=organizeTree() style='background:#0c4a6e;border-color:#38bdf8;color:#38bdf8'>⚡ Agent</button>";
}
if(_tm==="tree"){
h+="<button class=mb-new-folder-btn onclick=newFolder()>+ Folder</button>";
}
if(!_locked && _tm==="tree"){
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>";
}
var nodeCount=0;
if(_tm==="categories" && d.categories){
nodeCount=d.total_categories||d.categories.length||0;
}else if(_tm==="series" && d.series){
nodeCount=d.total_series||d.series.length||0;
}else if(d.nodes){
nodeCount=d.nodes.length||0;
}
h+="<span style=color:#64748b;font-size:12px;align-self:center>"+nodeCount+(_tm==="categories"||_tm==="series"?" items":" nodes")+"</span></div>";
if(_tm==="categories")h+=renderCategories(d);
else if(_tm==="series")h+=renderSeries(d);
else 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: "+e+"</div>";
});
}
function searchTree(){
var q=document.getElementById('mb-search-input').value;
if(q && q.trim()){
_tm="list";
}
loadTree(q);
}
function clearSearch(){
document.getElementById('mb-search-input').value='';
loadTree();
}
function changeMode(m){
_tm=m;localStorage.setItem("display_mode",m);
if(m==="categories" || m==="series"){
loadTree();
}else{
var searchInput=document.getElementById('mb-search-input');
var q=searchInput?searchInput.value:'';
loadTree(q);
}
}
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;
}
function renderCategories(d){
var h="<div style='padding:20px'>";
h+="<h2 style='border:none;margin-bottom:20px;color:#60a5fa'>📁 Categories ("+(d.total_categories||0)+") - "+(d.total_files||0)+" files</h2>";
h+="<div style='display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px'>";
if(d.categories && d.categories.length){
d.categories.forEach(function(cat){
h+="<div onclick='loadCategoryDetail(\""+cat.name+"\")' style='background:#1e293b;border:1px solid #334155;border-radius:8px;padding:16px;cursor:pointer;transition:all 0.2s' onmouseover='this.style.borderColor=\"#3b82f6\";this.style.background=\"#1e3a5f\"' onmouseout='this.style.borderColor=\"#334155\";this.style.background=\"#1e293b\"'>";
h+="<div style='font-size:18px;font-weight:600;color:#e2e8f0;margin-bottom:8px'>📁 "+(cat.display_name||cat.name)+"</div>";
h+="<div style='font-size:13px;color:#94a3b8'>"+(cat.file_count||0)+" files</div>";
if(cat.description){
h+="<div style='font-size:12px;color:#64748b;margin-top:8px'>"+cat.description+"</div>";
}
h+="</div>";
});
}else{
h+="<div style='color:#64748b'>No categories found</div>";
}
h+="</div></div>";
return h;
}
function renderSeries(d){
var h="<div style='padding:20px'>";
h+="<h2 style='border:none;margin-bottom:20px;color:#60a5fa'>📦 Product Series ("+(d.total_series||0)+") - "+(d.total_files||0)+" files</h2>";
h+="<div style='display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px'>";
if(d.series && d.series.length){
d.series.forEach(function(ser){
h+="<div onclick='loadSeriesDetail(\""+ser.name+"\")' style='background:#1e293b;border:1px solid #334155;border-radius:8px;padding:16px;cursor:pointer;transition:all 0.2s' onmouseover='this.style.borderColor=\"#3b82f6\";this.style.background=\"#1e3a5f\"' onmouseout='this.style.borderColor=\"#334155\";this.style.background=\"#1e293b\"'>";
h+="<div style='font-size:18px;font-weight:600;color:#e2e8f0;margin-bottom:8px'>📦 "+(ser.display_name||ser.name)+"</div>";
h+="<div style='font-size:13px;color:#94a3b8'>"+(ser.file_count||0)+" files</div>";
if(ser.description){
h+="<div style='font-size:12px;color:#64748b;margin-top:8px'>"+ser.description+"</div>";
}
h+="</div>";
});
}else{
h+="<div style='color:#64748b'>No series found</div>";
}
h+="</div></div>";
return h;
}
function loadCategoryDetail(name){
var b=document.getElementById("mb-tree-body");
if(!b)return;
b.innerHTML="<div style=text-align:center;padding:40px;color:#64748b>Loading category...</div>";
fetch("/api/v2/categories/"+encodeURIComponent(name)).then(function(r){return r.json()}).then(function(d){
var h="<div style='padding:20px'>";
h+="<button onclick='loadTree()' style='background:#1e293b;border:1px solid #334155;color:#94a3b8;padding:8px 16px;border-radius:6px;cursor:pointer;margin-bottom:16px'>← Back to Categories</button>";
h+="<h2 style='border:none;margin:20px 0;color:#60a5fa'>📁 "+(d.category.display_name||d.category.name)+" - "+d.category.file_count+" files</h2>";
if(d.series_groups && d.series_groups.length){
d.series_groups.forEach(function(group){
h+="<h3 style='color:#94a3b8;margin-top:24px;margin-bottom:12px'>📦 "+group.series_name+"</h3>";
h+="<table style='width:100%;border-collapse:collapse'>";
h+="<thead><tr style='border-bottom:2px solid #334155'><th style='text-align:left;padding:8px;color:#94a3b8'>File</th><th style='text-align:right;padding:8px;color:#94a3b8;width:100px'>Size</th><th style='text-align:center;padding:8px;color:#94a3b8;width:80px'>Download</th></tr></thead>";
h+="<tbody>";
group.files.forEach(function(file){
h+="<tr style='border-bottom:1px solid #1e293b'>";
h+="<td style='padding:8px;color:#e2e8f0'>"+file.filename+"</td>";
h+="<td style='padding:8px;color:#94a3b8;text-align:right'>"+file.size+"</td>";
h+="<td style='padding:8px;text-align:center'><a href='"+file.download_url+"' target='_blank' style='color:#3b82f6;text-decoration:none'>⬇️</a></td>";
h+="</tr>";
});
h+="</tbody></table>";
});
}
h+="</div>";
b.innerHTML=h;
}).catch(function(e){
b.innerHTML="<div style=padding:20px;color:#ef4444>Failed to load category: "+e+"</div>";
});
}
function loadSeriesDetail(name){
var b=document.getElementById("mb-tree-body");
if(!b)return;
b.innerHTML="<div style=text-align:center;padding:40px;color:#64748b>Loading series...</div>";
fetch("/api/v2/series/"+encodeURIComponent(name)).then(function(r){return r.json()}).then(function(d){
var h="<div style='padding:20px'>";
h+="<button onclick='loadTree()' style='background:#1e293b;border:1px solid #334155;color:#94a3b8;padding:8px 16px;border-radius:6px;cursor:pointer;margin-bottom:16px'>← Back to Series</button>";
h+="<h2 style='border:none;margin:20px 0;color:#60a5fa'>📦 "+(d.series.display_name||d.series.name)+" - "+d.series.file_count+" files</h2>";
if(d.categories && d.categories.length){
d.categories.forEach(function(cat){
h+="<h3 style='color:#94a3b8;margin-top:24px;margin-bottom:12px'>📁 "+cat.category_name+"</h3>";
h+="<table style='width:100%;border-collapse:collapse'>";
h+="<thead><tr style='border-bottom:2px solid #334155'><th style='text-align:left;padding:8px;color:#94a3b8'>File</th><th style='text-align:right;padding:8px;color:#94a3b8;width:100px'>Size</th><th style='text-align:center;padding:8px;color:#94a3b8;width:80px'>Download</th></tr></thead>";
h+="<tbody>";
cat.files.forEach(function(file){
h+="<tr style='border-bottom:1px solid #1e293b'>";
h+="<td style='padding:8px;color:#e2e8f0'>"+file.filename+"</td>";
h+="<td style='padding:8px;color:#94a3b8;text-align:right'>"+file.size+"</td>";
h+="<td style='padding:8px;text-align:center'><a href='"+file.download_url+"' target='_blank' style='color:#3b82f6;text-decoration:none'>⬇️</a></td>";
h+="</tr>";
});
h+="</tbody></table>";
});
}
h+="</div>";
b.innerHTML=h;
}).catch(function(e){
b.innerHTML="<div style=padding:20px;color:#ef4444>Failed to load series: "+e+"</div>";
});
}
// 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 userId=localStorage.getItem("tree_user")||"demo";
var src="/api/v2/files/"+userId+"/"+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+="<div style='display:flex;flex-direction:column;width:100%'>";
h+="<div style='margin-bottom:8px'>";
h+="<button onclick=quickPreview('"+fuuid+"') style='background:#3b82f6;color:#fff;border:none;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:14px'>Fullscreen 📺</button>";
h+="</div>";
h+="<iframe src='"+src+"' style='width:100%;height:600px;border:none;border-radius:8px;background:#fff'></iframe>";
h+="</div>";
}else{
h+="<div style='display:flex;flex-direction:column;align-items:center;width:100%'>";
h+="<div style='display:flex;gap:8px;margin-bottom:8px'>";
h+="<button onclick='zoomPhoto(-0.2)' style='background:#1e293b;border:1px solid #334155;color:#94a3b8;font-size:18px;padding:6px 12px;border-radius:6px;cursor:pointer'>🔍−</button>";
h+="<button onclick='zoomPhoto(0)' style='background:#1e293b;border:1px solid #334155;color:#94a3b8;font-size:18px;padding:6px 12px;border-radius:6px;cursor:pointer'>1:1</button>";
h+="<button onclick='zoomPhoto(0.2)' style='background:#1e293b;border:1px solid #334155;color:#94a3b8;font-size:18px;padding:6px 12px;border-radius:6px;cursor:pointer'>🔍+</button>";
h+="<span id=mb-zoom-level style='color:#94a3b8;font-size:14px;padding:4px 8px'>100%</span>";
h+="</div>";
h+="<div style='position:relative;display:flex;align-items:center;gap:8px'>";
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+="<div style='overflow:auto;max-width:100%;max-height:400px;border-radius:8px;background:#0f172a'>";
h+="<img id=mb-preview-img src='"+src+"' style='min-height:100px;min-width:100px;border-radius:8px;background:#0f172a;transform-origin:center center' onerror=\"this.onerror=null;this.alt='No preview'\">";
h+="</div>";
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>";
h+="</div>";
}
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/"+userId+"/"+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=[],_photoZoom=1;
function zoomPhoto(delta){
var img=document.getElementById("mb-preview-img");
if(!img)return;
if(delta==0){
_photoZoom=1;
}else{
_photoZoom+=delta;
if(_photoZoom<0.2)_photoZoom=0.2;
if(_photoZoom>5)_photoZoom=5;
}
img.style.transform="scale("+_photoZoom+")";
img.style.maxWidth=_photoZoom>=1?"none":"100%";
img.style.maxHeight=_photoZoom>=1?"none":"400px";
var level=document.getElementById("mb-zoom-level");
if(level)level.textContent=Math.round(_photoZoom*100)+"%";
}
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 userId=localStorage.getItem("tree_user")||"demo";
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];
_photoZoom=1;
var img=document.getElementById("mb-preview-img");
if(img){
img.src="/api/v2/files/"+userId+"/"+_photoUuid+"/stream?_="+Date.now();
img.style.transform="scale(1)";
img.style.maxWidth="100%";
img.style.maxHeight="400px";
}
var level=document.getElementById("mb-zoom-level");
if(level)level.textContent="100%";
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 userId=localStorage.getItem("tree_user")||"demo";
var src="/api/v2/files/"+userId+"/"+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 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();
}
// ═══════════════ S3 PANEL ═══════════════
var _s3v=false;
function toggleS3(){
_s3v=!_s3v;
document.getElementById("mb-s3-panel").classList.toggle("active",_s3v);
if(_s3v)loadS3Panel();
}
function loadS3Panel(){
var b=document.getElementById("mb-s3-body");
if(!b)return;
b.innerHTML="<div style=text-align:center;padding:40px;color:#64748b>Loading...</div>";
fetch("/api/v2/s3/status").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>S3 Service</span>";
h+="<span style=flex:1></span>";
h+="<button onclick=toggleS3() style='background:none;border:none;color:#64748b;font-size:18px;cursor:pointer'>✕</button>";
h+="</div>";
// S3 Status Section
h+="<div class=mb-config-section>";
h+="<div class=mb-config-header>S3 STATUS</div>";
h+="<div class=mb-config-item>";
h+="<span class=mb-config-label>Status</span>";
h+="<span class=mb-config-value>"+(d.enabled?"✅ Running":"❌ Disabled")+"</span>";
h+="</div>";
h+="<div class=mb-config-item>";
h+="<span class=mb-config-label>Endpoint</span>";
h+="<span class=mb-config-value>"+d.endpoint+"</span>";
h+="</div>";
h+="<div class=mb-config-item>";
h+="<span class=mb-config-label>Region</span>";
h+="<span class=mb-config-value>"+d.region+"</span>";
h+="</div>";
h+="<div class=mb-config-item>";
h+="<span class=mb-config-label>Buckets</span>";
h+="<span class=mb-config-value>"+d.buckets_count+"</span>";
h+="</div>";
h+="<div class=mb-config-item>";
h+="<span class=mb-config-label>Access Keys</span>";
h+="<span class=mb-config-value>"+d.keys_count+"</span>";
h+="</div>";
h+="</div>";
// Access Keys Section
h+="<div class=mb-config-section>";
h+="<div class=mb-config-header>S3 ACCESS KEYS</div>";
h+="<div style=padding:8px;color:#94a3b8;font-size:12px>";
h+="AWS Signature V4 authentication required";
h+="</div>";
h+="<div class=mb-config-item>";
h+="<span class=mb-config-label>markbase_access_key_001</span>";
h+="<span class=mb-config-value>Access: warren</span>";
h+="<button class=mb-config-edit-btn onclick=copyS3Key('markbase_access_key_001')>Copy</button>";
h+="</div>";
h+="<div class=mb-config-item>";
h+="<span class=mb-config-label>markbase_access_key_002</span>";
h+="<span class=mb-config-value>Access: demo</span>";
h+="<button class=mb-config-edit-btn onclick=copyS3Key('markbase_access_key_002')>Copy</button>";
h+="</div>";
h+="<button class=mb-new-folder-btn onclick=generateS3Key() style=margin-top:8px>Generate New Key</button>";
h+="</div>";
// Client Usage Section
h+="<div class=mb-config-section>";
h+="<div class=mb-config-header>CLIENT USAGE (Python boto3)</div>";
h+="<pre style='background:#0f172a;padding:12px;border-radius:8px;font-size:12px;color:#4ade80;white-space:pre-wrap'>";
h+="import boto3\n\n";
h+="s3 = boto3.client(\n";
h+=" 's3',\n";
h+=" endpoint_url='"+d.endpoint+"',\n";
h+=" aws_access_key_id='markbase_access_key_001',\n";
h+=" aws_secret_access_key='markbase_secret_key_xyz123'\n";
h+=")\n\n";
h+="# List buckets\n";
h+="buckets = s3.list_buckets()\n";
h+="for b in buckets['Buckets']:\n";
h+=" print(b['Name'])\n\n";
h+="# List objects\n";
h+="objects = s3.list_objects_v2(Bucket='warren')\n";
h+="for obj in objects['Contents']:\n";
h+=" print(obj['Key'])\n";
h+="</pre>";
h+="</div>";
b.innerHTML=h;
}).catch(function(e){
b.innerHTML="<div style=padding:20px;color:#ef4444>Failed to load S3 status: "+e+"</div>";
});
}
function copyS3Key(accessKey){
navigator.clipboard.writeText(accessKey);
toast("Copied: "+accessKey);
}
function generateS3Key(){
fetch("/api/v2/s3/generate-key",{method:"POST"})
.then(function(r){return r.json()})
.then(function(d){
if(d.access_key){
toast("Generated: "+d.access_key);
loadS3Panel();
}else{
toast("Error: "+(d.error||"unknown"));
}
});
}
// 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>