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:
MayaTheShy
2026-03-21 18:10:44 -04:00
parent bbc44c3d97
commit 9f322003db
8 changed files with 654 additions and 48 deletions

View File

@@ -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);