'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: `
`,
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
Empty
Partial
Full
Loose Items
`,
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 ''}
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 };