'use strict'; const express = require('express'); const database = require('./database'); function createRouter(getActiveInstance) { const router = express.Router(); function dbAvailable() { return database && database.db; } // ======================================== // Read-only routes (query DB singleton directly) // ======================================== router.get('/api/inventory', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const items = await database.searchItems(req.query.q); res.json({ items }); } catch (error) { console.error('API Error /api/inventory:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/inventory/:itemId', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const itemId = parseInt(req.params.itemId); const item = await database.getItemDetails(itemId); if (!item) return res.status(404).json({ error: 'Item not found' }); res.json({ item }); } catch (error) { console.error('API Error /api/inventory/:itemId:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/chests', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const chests = await database.getChests(); res.json({ chests }); } catch (error) { console.error('API Error /api/chests:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/chests/:id', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const chestId = parseInt(req.params.id); const chest = await database.getChestById(chestId); if (!chest) return res.status(404).json({ error: 'Chest not found' }); const shulkers = await database.getShulkersByChest(chestId); res.json({ chest, shulkers }); } catch (error) { console.error('API Error /api/chests/:id:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/stats', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const stats = await database.getStats(); res.json(stats); } catch (error) { console.error('API Error /api/stats:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/trades', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const limit = parseInt(req.query.limit) || 50; const trades = await database.getRecentTrades(limit); res.json({ trades }); } catch (error) { console.error('API Error /api/trades:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/pending/:playerName', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const pending = await database.getPendingWithdrawals(req.params.playerName); res.json({ pending }); } catch (error) { console.error('API Error /api/pending/:playerName:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/map', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const chests = await database.getChestsWithSummary(); res.json({ chests }); } catch (error) { console.error('API Error /api/map:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/chests/:id/contents', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const chestId = parseInt(req.params.id); const contents = await database.getChestContents(chestId); if (!contents) return res.status(404).json({ error: 'Chest not found' }); res.json(contents); } catch (error) { console.error('API Error /api/chests/:id/contents:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/shulkers/:id', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const shulkerId = parseInt(req.params.id); const shulker = await database.getShulkerWithItems(shulkerId); if (!shulker) return res.status(404).json({ error: 'Shulker not found' }); res.json({ shulker }); } catch (error) { console.error('API Error /api/shulkers/:id:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/players', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const players = await database.getAllPlayers(); res.json({ players }); } catch (error) { console.error('API Error /api/players:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/special-items', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); try { const items = await database.getSpecialItems(); const parsed = items.map(item => { let nbt = null; try { nbt = JSON.parse(item.nbt_data); } catch (e) {} return { ...item, nbt_parsed: nbt }; }); res.json({ items: parsed }); } catch (error) { console.error('API Error /api/special-items:', error); res.status(500).json({ error: error.message }); } }); // ======================================== // Storage status (for command polling) // ======================================== router.get('/api/storage/status', (req, res) => { try { const { plugin } = getActiveInstance(req.query.bot); if (!plugin) { return res.json({ busy: false, command: null }); } res.json({ busy: !!plugin._busy, command: plugin._currentCommand || null, }); } catch (error) { res.json({ busy: false, command: null }); } }); // ======================================== // Action routes (need live plugin instance) // ======================================== router.post('/api/withdraw-special', async (req, res) => { try { const { playerName, shulkerItemId } = req.body; if (!playerName || !shulkerItemId) { return res.status(400).json({ error: 'Missing playerName or shulkerItemId' }); } if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); const hasPermission = await database.checkPermission(playerName, 'team'); if (!hasPermission) { return res.status(403).json({ error: `Player ${playerName} does not have permission` }); } const { plugin, bot } = getActiveInstance(req.query.bot); if (!plugin && !bot) { return res.status(503).json({ error: 'Storage plugin not available' }); } const parsedId = parseInt(shulkerItemId); const connecting = !plugin; if (plugin) { plugin.handleWithdrawSpecialItem(playerName, parsedId) .catch(err => console.error('Web special withdraw error:', err)); } else { bot.ensureConnected(async () => { const p = bot.plunginsLoaded['Storage']; if (!p) throw new Error('Storage plugin not loaded after connect'); await p.handleWithdrawSpecialItem(playerName, parsedId); }).catch(err => console.error('On-demand special withdraw error:', err)); } res.json({ status: 'queued', connecting, message: connecting ? `Bot connecting, special item withdrawal queued for ${playerName}...` : `Special item withdrawal queued for ${playerName}` }); } catch (error) { console.error('API Error /api/withdraw-special:', error); res.status(500).json({ error: error.message }); } }); router.post('/api/withdraw', async (req, res) => { try { const { playerName, itemName, count, mode } = req.body; if (!playerName || !itemName || !count) { return res.status(400).json({ error: 'Missing playerName, itemName, or count' }); } if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); const hasPermission = await database.checkPermission(playerName, 'team'); if (!hasPermission) { return res.status(403).json({ error: `Player ${playerName} does not have permission` }); } const { plugin, bot } = getActiveInstance(req.query.bot); if (!plugin && !bot) { return res.status(503).json({ error: 'Storage plugin not available' }); } const parsedCount = parseInt(count); const connecting = !plugin; const runTask = async (p) => { if (mode === 'shulkers') { await p.handleWithdrawShulkers(playerName, itemName, parsedCount); } else { await p.handleWithdrawRequest(playerName, itemName, parsedCount); } }; if (plugin) { runTask(plugin).catch(err => console.error('Web withdraw error:', err)); } else { bot.ensureConnected(async () => { const p = bot.plunginsLoaded['Storage']; if (!p) throw new Error('Storage plugin not loaded after connect'); await runTask(p); }).catch(err => console.error('On-demand withdraw error:', err)); } const modeLabel = mode === 'shulkers' ? `${parsedCount} shulker(s) of ${itemName}` : `${parsedCount}x ${itemName}`; res.json({ status: 'queued', connecting, message: connecting ? `Bot connecting, withdrawal of ${modeLabel} queued for ${playerName}...` : `Withdrawal of ${modeLabel} queued for ${playerName}` }); } catch (error) { console.error('API Error /api/withdraw:', error); res.status(500).json({ error: error.message }); } }); return router; } const webUI = { tabId: 'storage', tabLabel: 'Storage', tabOrder: 10, sidebarHtml: `
Loading...
`, sidebarJs: ` // Sidebar search autocomplete setupAC('search','ac-search', q=>{ const lower=q.toLowerCase(); return allItems .filter(i=>!lower||i.item_name.includes(lower)) .slice(0,15) .map(i=>({label:i.item_name,value:i.item_name,extra:fmt(i.total_count)})); }, val=>{if(val!==null){filterItems()}else{filterItems()}} ); `, onTabActive: 'onStorageTabActive', html: `
Inventory
Storage Map
Special Items
Withdraw
Item Count
Empty Partial Full Loose Items

Named & Custom Items

Items with custom names, lore, or special properties stored separately from regular items.

Click tab to load...

Request Withdrawal

`, css: ` .stats-row{display:grid;grid-template-columns:repeat(7,1fr);gap:12px;margin-bottom:20px} .stat{background:#1f2937;border:1px solid #374151;border-radius:8px;padding:14px;text-align:center} .stat .val{font-size:1.6em;font-weight:700;color:#60a5fa} .stat .lbl{font-size:.75em;color:#9ca3af;margin-top:2px} .search-wrap{padding:12px;border-bottom:1px solid #374151} .search-wrap input{width:100%;padding:10px 12px;border:1px solid #374151;border-radius:6px;background:#111827;color:#e5e7eb;font-size:.9em} .search-wrap input:focus{outline:none;border-color:#2563eb} .inv-list{flex:1;overflow:auto;padding:4px 0} .inv-item{display:flex;justify-content:space-between;align-items:center;padding:8px 14px;cursor:pointer;border-bottom:1px solid #1f2937} .inv-item:hover{background:#374151} .inv-item .name{font-size:.85em} .inv-item .count{background:#2563eb;color:#fff;padding:2px 8px;border-radius:10px;font-size:.8em;font-weight:600;min-width:40px;text-align:center} .inv-item .count.large{background:#059669} table{width:100%;border-collapse:collapse} th{text-align:left;padding:10px 12px;background:#1f2937;border-bottom:1px solid #374151;color:#9ca3af;font-size:.8em;cursor:pointer;user-select:none;position:sticky;top:0} th:hover{color:#e5e7eb} th .arrow{margin-left:4px;font-size:.7em} td{padding:8px 12px;border-bottom:1px solid #1f2937;font-size:.85em} tr:hover td{background:#1f2937} .map-container{position:relative;background:#0f172a;border:1px solid #374151;border-radius:8px;overflow:auto} .map-level{margin-bottom:16px} .map-level h3{color:#60a5fa;font-size:.9em;margin-bottom:8px;padding:8px 12px;background:#1f2937;border-radius:6px 6px 0 0} .map-grid{position:relative;margin:0 auto} .map-chest{position:absolute;border-radius:3px;cursor:pointer;font-size:7px;display:flex;align-items:center;justify-content:center;color:#000;font-weight:700;transition:transform .1s;border:1px solid rgba(0,0,0,.3)} .map-chest:hover{transform:scale(1.5);z-index:10} .map-chest.empty{background:#6b7280} .map-chest.partial{background:#f59e0b} .map-chest.full{background:#10b981} .map-chest.loose{background:#ef4444} .map-chest.unscanned{background:#8b5cf6} .map-legend{display:flex;gap:16px;padding:12px;font-size:.8em;color:#9ca3af} .map-legend span{display:flex;align-items:center;gap:4px} .map-legend .dot{width:10px;height:10px;border-radius:2px} .panel{background:#1f2937;border:1px solid #374151;border-radius:8px;padding:16px;margin-bottom:16px} .panel h3{color:#60a5fa;font-size:1em;margin-bottom:12px} .shulker-grid{display:grid;grid-template-columns:repeat(9,1fr);gap:2px;background:#374151;padding:2px;border-radius:4px;margin-bottom:8px} .shulker-slot{background:#111827;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65em;color:#9ca3af;border-radius:2px;position:relative} .shulker-slot.filled{background:#1e3a5f;color:#60a5fa} .shulker-slot .slot-count{position:absolute;bottom:1px;right:2px;font-size:.6em;color:#f59e0b} .withdraw-form{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:8px} .withdraw-form input{padding:8px;border:1px solid #374151;border-radius:6px;background:#111827;color:#e5e7eb;font-size:.85em} .withdraw-form button{background:#2563eb;color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer;font-size:.85em} #withdrawStatus{font-size:.85em;min-height:20px} .detail-panel{position:fixed;right:0;top:57px;width:400px;height:calc(100vh - 57px);background:#1f2937;border-left:1px solid #374151;overflow:auto;padding:16px;z-index:50;transform:translateX(100%);transition:transform .2s} .detail-panel.open{transform:translateX(0)} .detail-panel .close{position:absolute;top:12px;right:12px;background:none;border:none;color:#9ca3af;font-size:1.2em;cursor:pointer} .detail-panel .close:hover{color:#e5e7eb} .chest-info{margin-bottom:16px} .chest-info p{font-size:.85em;color:#9ca3af;margin:4px 0} .shulker-card{background:#111827;border:1px solid #374151;border-radius:6px;padding:10px;margin-bottom:8px} .shulker-card h4{font-size:.85em;color:#f59e0b;margin-bottom:6px} .shulker-card .items{font-size:.8em;color:#9ca3af} .shulker-card .items span{display:inline-block;background:#1f2937;padding:2px 6px;border-radius:4px;margin:2px} .ac-wrap{position:relative} .ac-list{position:absolute;top:100%;left:0;right:0;background:#1f2937;border:1px solid #374151;border-top:none;border-radius:0 0 6px 6px;max-height:220px;overflow:auto;z-index:60;display:none} .ac-list.open{display:block} .ac-opt{padding:8px 12px;cursor:pointer;font-size:.85em;display:flex;justify-content:space-between;align-items:center} .ac-opt:hover,.ac-opt.active{background:#374151} .ac-opt .ac-count{color:#9ca3af;font-size:.75em} .ac-opt .ac-match{color:#60a5fa;font-weight:600} .special-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px} .special-card{background:#111827;border:1px solid #374151;border-radius:8px;padding:14px;transition:border-color .2s} .special-card:hover{border-color:#60a5fa} .special-card .sp-name{font-size:1em;font-weight:700;color:#f59e0b;margin-bottom:4px} .special-card .sp-base{font-size:.8em;color:#9ca3af;margin-bottom:8px} .special-card .sp-enchants{font-size:.8em;color:#a78bfa;margin-bottom:4px} .special-card .sp-lore{font-size:.8em;color:#6ee7b7;font-style:italic;margin-bottom:4px} .special-card .sp-count{font-size:.85em;color:#60a5fa;margin-bottom:8px} .special-card .sp-withdraw{display:flex;gap:6px;align-items:center;margin-top:8px} .special-card .sp-withdraw input{padding:6px;border:1px solid #374151;border-radius:4px;background:#1f2937;color:#e5e7eb;font-size:.8em;width:120px} .special-card .sp-withdraw button{background:#2563eb;color:#fff;border:none;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:.8em} .special-card .sp-withdraw button:hover{background:#1d4ed8} .special-card .sp-status{font-size:.8em;margin-top:4px;min-height:16px} .stab-content{display:none} .stab-content.active{display:block} .storage-sub-tabs{display:flex;border-bottom:1px solid #374151} .db-unavailable{padding:20px;color:#f59e0b;text-align:center;font-size:.9em} `, js: ` let allItems=[], mapData=[], sortKey='total_count', sortDir=-1; let specialLoaded=false, storageSubTab='inventory'; function onStorageTabActive() { if (allItems.length === 0) { loadStats(); loadInventory(); loadPlayers(); } } function switchStorageSubTab(name) { storageSubTab = name; document.querySelectorAll('.storage-sub-tabs .tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.stab-content').forEach(t => t.classList.remove('active')); const el = document.getElementById('stab-'+name); if (el) el.classList.add('active'); const subNames=['inventory','map','special','withdraw']; const tabs = document.querySelectorAll('.storage-sub-tabs .tab'); const idx = subNames.indexOf(name); if (idx >= 0 && tabs[idx]) tabs[idx].classList.add('active'); if (name === 'map' && mapData.length === 0) loadMap(); if (name === 'special' && !specialLoaded) loadSpecialItems(); } async function loadStats() { try { const r = await fetch('/api/stats'); if (!r.ok) { document.getElementById('stats').innerHTML=''; return; } const s = await r.json(); document.getElementById('stats').innerHTML = stat(fmt(s.totalItems||0),'Total Items')+ stat(fmt(s.totalShulkers||0),'Shulkers')+ stat(fmt(s.totalChests||0),'Chests')+ stat(fmt(s.emptyShulkers||0),'Empty')+ stat(fmt(s.recentTrades||0),'Trades (24h)')+ stat(fmt(s.looseItemCount||0),'Loose Items')+ stat((s.totalChestSlots ? Math.round((s.totalShulkers||0)/(s.totalChestSlots)*100) : 0)+'%','Storage Full'); } catch(e) { document.getElementById('stats').innerHTML=''; } } function stat(v,l){return '
'+v+'
'+l+'
'} async function loadInventory() { try { const r = await fetch('/api/inventory'); if (!r.ok) { document.getElementById('invList').innerHTML='
Storage database not available
'; document.getElementById('invTable').innerHTML=''; return; } const d = await r.json(); allItems = d.items || []; renderSidebar(allItems); renderTable(allItems); updateTimestamp('ts-storage'); } catch(e) { document.getElementById('invList').innerHTML='
Error loading
'; } } function renderSidebar(items) { if (!items.length) { document.getElementById('invList').innerHTML='
No items found
'; return; } document.getElementById('invList').innerHTML = items.map(i => '
' + ''+fmtName(i.item_name)+'' + ''+fmt(i.total_count)+'
' ).join(''); } function renderTable(items) { const sorted = [...items].sort((a,b) => { const av=a[sortKey], bv=b[sortKey]; if (typeof av==='string') return sortDir*av.localeCompare(bv); return sortDir*(av-bv); }); document.querySelectorAll('th .arrow').forEach(a=>a.textContent=''); const el=document.getElementById('sort-'+sortKey); if(el)el.textContent=sortDir>0?'\\u25B2':'\\u25BC'; document.getElementById('invTable').innerHTML = sorted.map(i => ''+fmtName(i.item_name)+''+fmt(i.total_count)+'' ).join(''); } function sortItems(key) { if (sortKey===key) sortDir*=-1; else { sortKey=key; sortDir=key==='total_count'?-1:1; } const q=document.getElementById('search').value.toLowerCase(); const filtered=q?allItems.filter(i=>i.item_name.includes(q)):allItems; renderTable(filtered); } function filterItems() { const q=document.getElementById('search').value.toLowerCase(); const filtered=q?allItems.filter(i=>i.item_name.includes(q)):allItems; renderSidebar(filtered); renderTable(filtered); } function highlightItem(name) { document.getElementById('search').value=name; filterItems(); } // === MAP === async function loadMap() { try { const r = await fetch('/api/map'); if (!r.ok) { document.getElementById('mapArea').innerHTML='
Storage database not available
'; return; } const d = await r.json(); mapData = d.chests || []; renderMap(mapData); } catch(e) { document.getElementById('mapArea').innerHTML='
Failed to load map
'; } } function renderMap(chests) { if (!chests.length) { document.getElementById('mapArea').innerHTML='
No chests found
'; return; } const levels = {}; let minX=Infinity,maxX=-Infinity,minZ=Infinity,maxZ=-Infinity; for (const c of chests) { if(!levels[c.pos_y]) levels[c.pos_y]=[]; levels[c.pos_y].push(c); minX=Math.min(minX,c.pos_x); maxX=Math.max(maxX,c.pos_x); minZ=Math.min(minZ,c.pos_z); maxZ=Math.max(maxZ,c.pos_z); } const scale=18, pad=20; const w=(maxX-minX+2)*scale+pad*2; const h=(maxZ-minZ+2)*scale+pad*2; const sortedYs = Object.keys(levels).sort((a,b)=>Number(b)-Number(a)); let html=''; for (const y of sortedYs) { html+='

Level Y='+y+' ('+levels[y].length+' chests)

'; html+='
'; for (let x=minX; x<=maxX; x++) { const px=(x-minX)*scale+pad; html+='
'; } for (let z=minZ; z<=maxZ; z++) { const py=(z-minZ)*scale+pad; html+='
'; } for (const c of levels[y]) { const px=(c.pos_x-minX)*scale+pad; const py=(c.pos_z-minZ)*scale+pad; const cw=c.chest_type==='double'?scale*2-2:scale-2; const cls=c.loose_item_count>0?'loose':c.shulker_count===0?'empty':c.total_items===0?'empty':c.shulker_count>20?'full':'partial'; const focuses=(c.item_focuses||'').split(',').filter(Boolean).slice(0,3).map(fmtName).join(', ')||'Empty'; const looseLabel=c.loose_item_count>0?'\\\\n'+c.loose_item_count+' loose item(s)':''; html+='
'+ c.shulker_count+'
'; } html+='
'; } document.getElementById('mapArea').innerHTML=html; } function showTooltip(e, text) { const t=document.getElementById('tooltip'); t.innerHTML=text.replace(/\\\\n/g,'
'); t.style.display='block'; t.style.left=(e.clientX+12)+'px'; t.style.top=(e.clientY+12)+'px'; } function hideTooltip(){document.getElementById('tooltip').style.display='none'} // === CHEST DETAIL === async function showChestDetail(chestId) { const panel=document.getElementById('detailPanel'); const content=document.getElementById('detailContent'); content.innerHTML='

Loading...

'; panel.classList.add('open'); try { const r=await fetch('/api/chests/'+chestId+'/contents'); const d=await r.json(); const c=d.chest; let html='

Chest #'+c.id+'

'; html+='

Position: ('+c.pos_x+', '+c.pos_y+', '+c.pos_z+')

'; html+='

Type: '+c.chest_type+'

'; html+='

Category: '+(c.category||'none')+'

'; const shulkers=d.shulkers||[]; html+='

'+shulkers.length+' Shulkers

'; for (const s of shulkers) { html+='
'; html+='

Slot '+s.slot+' - '+(s.item_focus?fmtName(s.item_focus):'Empty')+' ('+s.slot_count+'/27 slots, '+fmt(s.total_items)+' items)

'; if (s.item_summary) { const items=s.item_summary.split(',').reduce((acc,pair)=>{ const[name,cnt]=pair.split(':'); if(!acc[name])acc[name]=0; acc[name]+=parseInt(cnt)||0; return acc; },{}); html+='
'; for(const[name,cnt] of Object.entries(items).sort((a,b)=>b[1]-a[1])) { html+=''+fmtName(name)+': '+cnt+''; } html+='
'; } else { html+='
Empty
'; } html+='
'; } content.innerHTML=html; } catch(e) { content.innerHTML='

Failed to load chest

'; } } function closeDetail(){document.getElementById('detailPanel').classList.remove('open')} // === WITHDRAW === function updateWithdrawMode(){ const mode=document.getElementById('wMode').value; const countInput=document.getElementById('wCount'); if(mode==='shulkers'){countInput.placeholder='Shulkers';countInput.max=12;if(parseInt(countInput.value)>12)countInput.value=12} else{countInput.placeholder='Count';countInput.removeAttribute('max')} } document.getElementById('withdrawForm').addEventListener('submit', async(e)=>{ e.preventDefault(); const p=document.getElementById('wPlayer').value.trim(); const i=document.getElementById('wItem').value.trim(); const c=parseInt(document.getElementById('wCount').value); const mode=document.getElementById('wMode').value; const st=document.getElementById('withdrawStatus'); if(!p||!i||!c){st.textContent='Fill all fields';st.style.color='#ef4444';return} try{ const r=await fetch('/api/withdraw',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({playerName:p,itemName:i,count:c,mode:mode})}); const d=await r.json(); st.textContent=r.ok?(d.message||'Queued'):(d.error||'Failed'); st.style.color=r.ok?'#60a5fa':'#ef4444'; }catch(e){st.textContent='Network error';st.style.color='#ef4444'} }); // Withdraw player autocomplete setupAC('wPlayer','ac-wPlayer', q=>{ const lower=q.toLowerCase(); return playerNames.filter(p=>!lower||p.toLowerCase().includes(lower)).map(p=>({label:p,value:p})); } ); // Withdraw item autocomplete setupAC('wItem','ac-wItem', q=>{ const lower=q.toLowerCase(); return allItems .filter(i=>!lower||i.item_name.includes(lower)) .slice(0,15) .map(i=>({label:i.item_name,value:i.item_name,extra:fmt(i.total_count)})); }, val=>{ if(val){ const item=allItems.find(i=>i.item_name===val); if(item){document.getElementById('wCount').max=item.total_count} } } ); // Parse Minecraft text: JSON chat components or section-sign formatted strings function parseMcText(raw) { if (!raw) return ''; if (typeof raw !== 'string') return String(raw); // Strip surrounding quotes if present let s = raw; if (s.startsWith('"') && s.endsWith('"')) s = s.slice(1, -1); // Try parsing as JSON chat component try { const obj = JSON.parse(s); if (typeof obj === 'object' && obj !== null) return extractChatText(obj); } catch(e) {} // Try parsing the original (unstripped) as JSON too try { const obj = JSON.parse(raw); if (typeof obj === 'object' && obj !== null) return extractChatText(obj); } catch(e) {} // Fall back to stripping section-sign codes return stripMcCodes(s); } function extractChatText(obj) { if (typeof obj === 'string') return stripMcCodes(obj); let text = ''; if (obj.text) text += obj.text; if (Array.isArray(obj.extra)) text += obj.extra.map(extractChatText).join(''); if (Array.isArray(obj)) text += obj.map(extractChatText).join(''); return stripMcCodes(text); } function stripMcCodes(s) { return s.replace(/\\u00a7[0-9a-fk-or]/gi, '').replace(/ยง[0-9a-fk-or]/gi, ''); } let allSpecialItems = []; function renderSpecialCard(item) { const nbt = item.nbt_parsed || {}; const displayName = parseMcText(nbt.displayName || ''); const enchants = (nbt.enchantments || []).map(e => fmtName(String(e.id).replace('minecraft:','')) + ' ' + toRoman(e.level)).join(', '); const loreLines = (nbt.lore || []).map(l => parseMcText(l)).filter(Boolean); return '
' + (displayName ? '
' + escHtml(displayName) + '
' : '') + '
' + fmtName(item.item_name) + '
' + (enchants ? '
' + escHtml(enchants) + '
' : '') + (loreLines.length ? '
' + loreLines.map(l => escHtml(l)).join('
') + '
' : '') + '
x' + item.count + '
' + '
' + '' + '' + '
' + '
' + '
'; } function renderSpecialItems(items) { const container = document.getElementById('specialItems'); if (items.length === 0) { container.innerHTML='
No special items found
'; return; } container.innerHTML = '
' + items.map(renderSpecialCard).join('') + '
'; } function filterSpecialItems() { const q = (document.getElementById('specialSearch').value || '').toLowerCase(); if (!q) { renderSpecialItems(allSpecialItems); return; } const filtered = allSpecialItems.filter(item => { const nbt = item.nbt_parsed || {}; const name = parseMcText(nbt.displayName || '').toLowerCase(); const base = (item.item_name || '').toLowerCase(); const lore = (nbt.lore || []).map(l => parseMcText(l).toLowerCase()).join(' '); const enchants = (nbt.enchantments || []).map(e => String(e.id).replace('minecraft:','')).join(' ').toLowerCase(); return name.includes(q) || base.includes(q) || lore.includes(q) || enchants.includes(q); }); renderSpecialItems(filtered); } async function loadSpecialItems() { const container = document.getElementById('specialItems'); container.innerHTML='
Loading...
'; try { const r = await fetch('/api/special-items'); if (!r.ok) { container.innerHTML='
Storage database not available
'; return; } const d = await r.json(); allSpecialItems = d.items || []; specialLoaded = true; renderSpecialItems(allSpecialItems); } catch(e) { container.innerHTML='
Failed to load special items
'; } } async function withdrawSpecial(itemId) { const playerInput = document.getElementById('sp-player-' + itemId); const statusEl = document.getElementById('sp-status-' + itemId); const playerName = playerInput.value.trim(); if (!playerName) { statusEl.textContent = 'Enter player name'; statusEl.style.color = '#ef4444'; return; } try { const r = await fetch('/api/withdraw-special', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ playerName, shulkerItemId: itemId }) }); const d = await r.json(); statusEl.textContent = r.ok ? (d.message || 'Queued') : (d.error || 'Failed'); statusEl.style.color = r.ok ? '#60a5fa' : '#ef4444'; if (r.ok) { specialLoaded = false; setTimeout(loadSpecialItems, 3000); } } catch(e) { statusEl.textContent = 'Network error'; statusEl.style.color = '#ef4444'; } } function toRoman(n) { if (!n || n <= 0) return ''; const vals = [10,9,5,4,1]; const syms = ['X','IX','V','IV','I']; let result = ''; for (let i = 0; i < vals.length; i++) { while (n >= vals[i]) { result += syms[i]; n -= vals[i]; } } return result; } let playerNames=[]; async function loadPlayers(){ try{const r=await fetch('/api/players');if(!r.ok)return;const d=await r.json();playerNames=(d.players||[]).map(p=>p.player_name)}catch(e){} } function storageLoadAll(){loadStats();loadInventory();loadPlayers();if(storageSubTab==='map')loadMap();if(storageSubTab==='special')loadSpecialItems()} `, }; module.exports = { createRouter, webUI };