Add SQLite persistence + official Minecraft item icons
Database (better-sqlite3): - Persist items, furnaces, alerts, recipes, settings to SQLite - Auto-restore last known state when server restarts or bridge disconnects - Item count history tracking (5-min snapshots, 7-day retention) - /api/history/:itemName endpoint for item count history - Docker volume for database file persistence - Graceful shutdown with DB connection cleanup Icons: - Replace mc-heads.net with official Minecraft game textures via CDN - Cascading fallback: item texture -> block texture -> emoji - In-memory URL cache to avoid redundant network requests - Block texture suffix mapping (furnace_front, barrel_top, etc.) - Crisp pixel-art rendering with image-rendering: pixelated
This commit is contained in:
@@ -2,6 +2,11 @@ import express from 'express';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import cors from 'cors';
|
||||
import { createServer } from 'http';
|
||||
import {
|
||||
loadFullState, saveFullState, recordItemHistory,
|
||||
saveItems, saveFurnaces, saveAlerts, saveState,
|
||||
getHistory, closeDb,
|
||||
} from './db.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
@@ -14,29 +19,26 @@ app.use(express.json({ limit: '5mb' }));
|
||||
const webClients = new Set();
|
||||
const bridgeClients = new Set();
|
||||
|
||||
// Latest inventory state from the CC:Tweaked bridge
|
||||
let inventoryState = {
|
||||
itemList: [],
|
||||
grandTotal: 0,
|
||||
chestCount: 0,
|
||||
totalSlots: 0,
|
||||
usedSlots: 0,
|
||||
freeSlots: 0,
|
||||
usedRatio: 0,
|
||||
dropperOk: false,
|
||||
barrelOk: false,
|
||||
furnaceCount: 0,
|
||||
furnaceStatus: {},
|
||||
};
|
||||
// Load persisted state from SQLite on startup
|
||||
console.log('💾 Loading persisted state from database...');
|
||||
const persisted = loadFullState();
|
||||
|
||||
let activityState = {};
|
||||
let alertsState = [];
|
||||
let smeltingPaused = false;
|
||||
let disabledRecipes = {};
|
||||
let smeltableRecipes = {};
|
||||
let craftableRecipes = [];
|
||||
let craftTurtleOk = false;
|
||||
let lastUpdate = 0;
|
||||
let inventoryState = persisted.inventoryState;
|
||||
let activityState = persisted.activityState;
|
||||
let alertsState = persisted.alertsState;
|
||||
let smeltingPaused = persisted.smeltingPaused;
|
||||
let disabledRecipes = persisted.disabledRecipes;
|
||||
let smeltableRecipes = persisted.smeltableRecipes;
|
||||
let craftableRecipes = persisted.craftableRecipes;
|
||||
let craftTurtleOk = persisted.craftTurtleOk;
|
||||
let lastUpdate = persisted.lastUpdate;
|
||||
|
||||
if (lastUpdate > 0) {
|
||||
const ago = Math.round((Date.now() - lastUpdate) / 1000);
|
||||
console.log(`💾 Restored state from ${ago}s ago (${inventoryState.itemList.length} items)`);
|
||||
} else {
|
||||
console.log('💾 No previous state found, starting fresh');
|
||||
}
|
||||
|
||||
// Pending commands for bridge HTTP polling (fallback)
|
||||
let pendingCommands = [];
|
||||
@@ -109,6 +111,13 @@ app.get('/api/alerts', (req, res) => {
|
||||
res.json({ alerts: alertsState });
|
||||
});
|
||||
|
||||
// Get item count history
|
||||
app.get('/api/history/:itemName', (req, res) => {
|
||||
const limit = parseInt(req.query.limit) || 100;
|
||||
const history = getHistory(req.params.itemName, Math.min(limit, 1000));
|
||||
res.json({ item: req.params.itemName, history });
|
||||
});
|
||||
|
||||
// Get recipes (smeltable + craftable)
|
||||
app.get('/api/recipes', (req, res) => {
|
||||
res.json({
|
||||
@@ -368,7 +377,27 @@ function updateStateFromBridge(data) {
|
||||
if (data.craftTurtleOk !== undefined) craftTurtleOk = data.craftTurtleOk;
|
||||
|
||||
lastUpdate = Date.now();
|
||||
|
||||
|
||||
// Persist to SQLite
|
||||
try {
|
||||
saveFullState({
|
||||
inventoryState,
|
||||
activityState,
|
||||
alertsState,
|
||||
smeltingPaused,
|
||||
disabledRecipes,
|
||||
smeltableRecipes,
|
||||
craftableRecipes,
|
||||
craftTurtleOk,
|
||||
});
|
||||
// Record history snapshot (throttled to every 5 min internally)
|
||||
if (inventoryState.itemList?.length) {
|
||||
recordItemHistory(inventoryState.itemList);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ DB save error:', err.message);
|
||||
}
|
||||
|
||||
// Broadcast to all web clients
|
||||
broadcastToClients({
|
||||
type: 'state_update',
|
||||
@@ -490,7 +519,16 @@ server.listen(PORT, HOST, () => {
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
function shutdown() {
|
||||
console.log('\n🛑 Shutting down server...');
|
||||
try {
|
||||
closeDb();
|
||||
console.log('💾 Database closed');
|
||||
} catch (err) {
|
||||
console.error('❌ Error closing database:', err.message);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
|
||||
Reference in New Issue
Block a user