Files
mc-bot-town/nodejs/controller/storage/web.js
2026-01-31 22:34:20 -05:00

405 lines
11 KiB
JavaScript

'use strict';
const express = require('express');
const cors = require('cors');
const database = require('./database');
class WebServer {
constructor() {
console.log('WebServer: Constructor called');
this.app = null;
this.port = null;
this.host = null;
this.server = null;
}
async start(bot) {
console.log('WebServer: start() called');
const conf = require('../../conf');
this.port = conf.storage?.webPort || 3000;
this.host = conf.storage?.webHost || '0.0.0.0';
console.log(`WebServer: Configuring server on ${this.host}:${this.port}`);
this.app = express();
// Middleware
this.app.use(express.json());
this.app.use(cors());
// Request logging
this.app.use((req, res, next) => {
console.log(`WebServer: ${req.method} ${req.path}`);
next();
});
// Routes
this.setupRoutes();
// Start server
return new Promise((resolve, reject) => {
this.server = this.app.listen(this.port, this.host, () => {
console.log(`WebServer: Running at http://${this.host}:${this.port}`);
console.log(`WebServer: Try http://localhost:${this.port}/health`);
resolve();
});
this.server.on('error', (err) => {
console.error('WebServer: Failed to start:', err);
reject(err);
});
});
}
setupRoutes() {
console.log('WebServer: Setting up routes...');
// Index page - Full web UI
this.app.get('/', async (req, res) => {
res.send(this.getIndexHTML());
});
// Health check
this.app.get('/health', (req, res) => {
res.json({ status: 'ok', server: `${this.host}:${this.port}` });
});
// Inventory - Get all items aggregated
this.app.get('/api/inventory', async (req, res) => {
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 });
}
});
// Get specific item details
this.app.get('/api/inventory/:itemId', async (req, res) => {
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 });
}
});
// Get all chests
this.app.get('/api/chests', async (req, res) => {
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 });
}
});
// Get specific chest with shulker contents
this.app.get('/api/chests/:id', async (req, res) => {
try {
// This would return chest + shulker details
// Implementation would scan the specified chest and shulkers
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);
// Return chest + shulkers
res.json({ chest, shulkers });
} catch (error) {
console.error('API Error /api/chests/:id:', error);
res.status(500).json({ error: error.message });
}
});
// Stats
this.app.get('/api/stats', async (req, res) => {
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 });
}
});
// Trade history
this.app.get('/api/trades', async (req, res) => {
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 });
}
});
// Pending withdrawals (by player)
this.app.get('/api/pending/:playerName', async (req, res) => {
try {
const playerName = req.params.playerName;
const pending = await database.getPendingWithdrawals(playerName);
res.json({ pending });
} catch (error) {
console.error('API Error /api/pending/:playerName:', error);
res.status(500).json({ error: error.message });
}
});
}
async close() {
console.log('WebServer: Closing...');
if (this.server) {
this.server.close();
console.log('WebServer: Closed');
}
}
getIndexHTML() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Storage System</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #eee;
min-height: 100vh;
padding: 20px;
}
.container { max-width: 1200px; margin: 0 auto; }
h1 {
text-align: center;
margin-bottom: 30px;
color: #00d4ff;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255,255,255,0.1);
border-radius: 10px;
padding: 20px;
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #00d4ff;
}
.stat-card .label { color: #aaa; font-size: 0.9em; }
.search-box {
width: 100%;
padding: 15px;
font-size: 1.1em;
border: none;
border-radius: 10px;
background: rgba(255,255,255,0.1);
color: #fff;
margin-bottom: 20px;
}
.search-box::placeholder { color: #888; }
.search-box:focus { outline: 2px solid #00d4ff; }
.section { margin-bottom: 30px; }
.section-title {
font-size: 1.3em;
margin-bottom: 15px;
color: #00d4ff;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding-bottom: 10px;
}
.item-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
}
.item-card {
background: rgba(255,255,255,0.05);
border-radius: 8px;
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid rgba(255,255,255,0.1);
transition: all 0.2s;
}
.item-card:hover {
background: rgba(255,255,255,0.1);
transform: translateY(-2px);
}
.item-name { font-weight: 500; }
.item-count {
background: #00d4ff;
color: #000;
padding: 3px 10px;
border-radius: 15px;
font-weight: bold;
}
.chest-list { list-style: none; }
.chest-item {
background: rgba(255,255,255,0.05);
padding: 10px 15px;
margin-bottom: 5px;
border-radius: 5px;
display: flex;
justify-content: space-between;
}
.loading { text-align: center; padding: 40px; color: #888; }
.error { color: #ff6b6b; padding: 20px; text-align: center; }
.refresh-btn {
background: #00d4ff;
color: #000;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
margin-bottom: 20px;
}
.refresh-btn:hover { background: #00b8e6; }
.category-badge {
font-size: 0.8em;
padding: 2px 8px;
border-radius: 10px;
background: rgba(255,255,255,0.2);
}
</style>
</head>
<body>
<div class="container">
<h1>📦 Storage System</h1>
<button class="refresh-btn" onclick="loadAll()">🔄 Refresh</button>
<div class="stats-grid" id="stats">
<div class="stat-card"><div class="value">-</div><div class="label">Loading...</div></div>
</div>
<input type="text" class="search-box" id="search" placeholder="🔍 Search items..." oninput="filterItems()">
<div class="section">
<div class="section-title">📦 Inventory</div>
<div class="item-grid" id="inventory">
<div class="loading">Loading inventory...</div>
</div>
</div>
<div class="section">
<div class="section-title">🗃️ Chests</div>
<ul class="chest-list" id="chests">
<li class="loading">Loading chests...</li>
</ul>
</div>
</div>
<script>
let allItems = [];
async function loadStats() {
try {
const res = await fetch('/api/stats');
const stats = await res.json();
document.getElementById('stats').innerHTML = \`
<div class="stat-card"><div class="value">\${stats.totalItems || 0}</div><div class="label">Total Items</div></div>
<div class="stat-card"><div class="value">\${stats.totalShulkers || 0}</div><div class="label">Shulkers</div></div>
<div class="stat-card"><div class="value">\${stats.totalChests || 0}</div><div class="label">Chests</div></div>
<div class="stat-card"><div class="value">\${stats.emptyShulkers || 0}</div><div class="label">Empty Shulkers</div></div>
\`;
} catch (e) {
document.getElementById('stats').innerHTML = '<div class="error">Failed to load stats</div>';
}
}
async function loadInventory() {
try {
const res = await fetch('/api/inventory');
const data = await res.json();
allItems = data.items || [];
renderItems(allItems);
} catch (e) {
document.getElementById('inventory').innerHTML = '<div class="error">Failed to load inventory</div>';
}
}
function renderItems(items) {
if (items.length === 0) {
document.getElementById('inventory').innerHTML = '<div class="loading">No items found. Run "scan" command in-game.</div>';
return;
}
document.getElementById('inventory').innerHTML = items.map(item => \`
<div class="item-card">
<span class="item-name">\${formatName(item.item_name)}</span>
<span class="item-count">\${item.total_count}</span>
</div>
\`).join('');
}
function filterItems() {
const query = document.getElementById('search').value.toLowerCase();
const filtered = allItems.filter(i => i.item_name.toLowerCase().includes(query));
renderItems(filtered);
}
async function loadChests() {
try {
const res = await fetch('/api/chests');
const data = await res.json();
const chests = data.chests || [];
if (chests.length === 0) {
document.getElementById('chests').innerHTML = '<li class="loading">No chests found. Run "scan" command in-game.</li>';
return;
}
document.getElementById('chests').innerHTML = chests.map(c => \`
<li class="chest-item">
<span>\${c.chest_type} chest at (\${c.pos_x}, \${c.pos_y}, \${c.pos_z})</span>
<span class="category-badge">\${c.category || 'misc'}</span>
</li>
\`).join('');
} catch (e) {
document.getElementById('chests').innerHTML = '<li class="error">Failed to load chests</li>';
}
}
function formatName(name) {
return name.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase());
}
function loadAll() {
loadStats();
loadInventory();
loadChests();
}
loadAll();
// Auto-refresh every 30 seconds
setInterval(loadAll, 30000);
</script>
</body>
</html>`;
}
}
module.exports = WebServer;