Files
cuberoo/server.js
2025-06-18 16:31:39 -07:00

263 lines
9.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const http = require('http');
const { Server } = require('socket.io');
const fs = require('fs');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'public')));
// open (or create) the SQLite database file
const db = new sqlite3.Database('positions.db', err => {
if (err) {
console.error('Could not open DB', err);
process.exit(1);
}
});
// create table if not exists
db.run(
`CREATE TABLE IF NOT EXISTS positions (
square_id TEXT PRIMARY KEY,
fruit TEXT
)`,
(err) => {
if (err) console.error('Could not ensure table', err);
}
);
// Add a table for history (revision log)
db.run(
`CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
action TEXT,
positions TEXT -- JSON string of all positions
)`,
(err) => {
if (err) console.error('Could not ensure history table', err);
}
);
// Helper to save a revision to history
function saveHistory(action, cb) {
db.all('SELECT square_id, fruit FROM positions', (err, rows) => {
if (err) return cb && cb(err);
const mapping = {};
rows.forEach(r => (mapping[r.square_id] = r.fruit));
db.run(
'INSERT INTO history (action, positions) VALUES (?, ?)',
[action, JSON.stringify(mapping)],
cb
);
});
}
// get all saved positions
app.get('/api/positions', (req, res) => {
db.all('SELECT square_id, fruit FROM positions', (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
// convert to an object. should look like this: { "1": "Apple", "2": "Banana", … }
const mapping = {};
rows.forEach(r => (mapping[r.square_id] = r.fruit));
res.json(mapping);
});
});
// save (or update) a single squares item
app.post('/api/positions', (req, res) => {
const { squareId, fruit } = req.body;
if (!squareId || typeof fruit !== 'string') {
return res.status(400).json({ error: 'squareId and fruit required' });
}
db.run(
`INSERT INTO positions (square_id, fruit)
VALUES (?, ?)
ON CONFLICT(square_id) DO UPDATE SET fruit=excluded.fruit`,
[squareId, fruit],
function (err) {
if (err) return res.status(500).json({ error: err.message });
// Save to history
saveHistory(`Moved ${fruit} to '${squareId}'`, () => {});
// broadcast update via Socket.io
io.emit('update', { squareId, fruit });
res.json({ success: true });
}
);
});
// Serve categories and fruits from a JSON file
app.get('/api/categories', (req, res) => {
const categoriesPath = path.join(__dirname, 'categories.json');
fs.readFile(categoriesPath, 'utf8', (err, data) => {
if (err) return res.status(500).json({ error: 'Could not load categories' });
try {
const categories = JSON.parse(data);
res.json(categories);
} catch (e) {
res.status(500).json({ error: 'Invalid categories file' });
}
});
});
// Add a fruit to a category in categories.json
app.post('/api/add-fruit', (req, res) => {
const { category, fruit } = req.body;
if (!category || !fruit) {
return res.status(400).json({ error: 'category and fruit required' });
}
const categoriesPath = path.join(__dirname, 'categories.json');
fs.readFile(categoriesPath, 'utf8', (err, data) => {
if (err) return res.status(500).json({ error: 'Could not load categories' });
let categories;
try {
categories = JSON.parse(data);
} catch (e) {
return res.status(500).json({ error: 'Invalid categories file' });
}
const cat = categories.find(c => c.name === category);
if (!cat) return res.status(404).json({ error: 'Category not found' });
// Prevent duplicates
if (cat.fruits.includes(fruit)) {
return res.status(400).json({ error: 'Fruit already exists in category' });
}
cat.fruits.push(fruit);
fs.writeFile(categoriesPath, JSON.stringify(categories, null, 2), err2 => {
if (err2) return res.status(500).json({ error: 'Could not save categories' });
res.json({ success: true });
});
});
});
// Delete a fruit from a category in categories.json
app.post('/api/delete-fruit', (req, res) => {
const { category, fruit } = req.body;
if (!category || !fruit) {
return res.status(400).json({ error: 'category and fruit required' });
}
const categoriesPath = path.join(__dirname, 'categories.json');
fs.readFile(categoriesPath, 'utf8', (err, data) => {
if (err) return res.status(500).json({ error: 'Could not load categories' });
let categories;
try {
categories = JSON.parse(data);
} catch (e) {
return res.status(500).json({ error: 'Invalid categories file' });
}
const cat = categories.find(c => c.name === category);
if (!cat) return res.status(404).json({ error: 'Category not found' });
const idx = cat.fruits.indexOf(fruit);
if (idx === -1) return res.status(404).json({ error: 'Fruit not found in category' });
cat.fruits.splice(idx, 1);
fs.writeFile(categoriesPath, JSON.stringify(categories, null, 2), err2 => {
if (err2) return res.status(500).json({ error: 'Could not save categories' });
res.json({ success: true });
});
});
});
// API to get history
app.get('/api/history', (req, res) => {
db.all('SELECT id, timestamp, action FROM history ORDER BY id DESC', (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
// API to get a specific revision's positions
app.get('/api/history/:id', (req, res) => {
db.get('SELECT positions FROM history WHERE id = ?', [req.params.id], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Not found' });
res.json(JSON.parse(row.positions));
});
});
// API to revert to a specific revision
app.post('/api/revert', (req, res) => {
const { id } = req.body;
if (!id) {
// No id: revert to empty board
db.run('DELETE FROM positions', [], (err2) => {
if (err2) return res.status(500).json({ error: err2.message });
saveHistory('Reverted to empty board', () => {});
io.emit('update', { revert: true });
return res.json({ success: true });
});
return;
}
db.get('SELECT positions FROM history WHERE id = ?', [id], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Not found' });
const positions = JSON.parse(row.positions);
// Clear all positions
db.run('DELETE FROM positions', [], (err2) => {
if (err2) return res.status(500).json({ error: err2.message });
// Insert all positions from the revision
const entries = Object.entries(positions);
let done = 0;
if (entries.length === 0) {
saveHistory(`Reverted to revision ${id}`, () => {});
io.emit('update', { revert: true });
return res.json({ success: true });
}
entries.forEach(([squareId, fruit]) => {
db.run(
`INSERT INTO positions (square_id, fruit) VALUES (?, ?)`,
[squareId, fruit],
(err3) => {
done++;
if (done === entries.length) {
saveHistory(`Reverted to revision ${id}`, () => {});
io.emit('update', { revert: true });
res.json({ success: true });
}
}
);
});
});
});
});
// Move a fruit from one square to another in a single revision
app.post('/api/move', (req, res) => {
const { fromSquareId, toSquareId, fruit } = req.body;
if (!fromSquareId || !toSquareId || !fruit) {
return res.status(400).json({ error: 'fromSquareId, toSquareId, and fruit required' });
}
db.serialize(() => {
db.run(
`UPDATE positions SET fruit='' WHERE square_id=?`,
[fromSquareId],
(err1) => {
if (err1) return res.status(500).json({ error: err1.message });
db.run(
`INSERT INTO positions (square_id, fruit)
VALUES (?, ?)
ON CONFLICT(square_id) DO UPDATE SET fruit=excluded.fruit`,
[toSquareId, fruit],
(err2) => {
if (err2) return res.status(500).json({ error: err2.message });
saveHistory(`Moved ${fruit} from ${fromSquareId} to ${toSquareId}`, () => {});
io.emit('update', { fromSquareId, toSquareId, fruit });
res.json({ success: true });
}
);
}
);
});
});
// start server
const PORT = process.env.PORT || 3085;
server.listen(PORT, () => {
console.log(`Listening on http://localhost:${PORT}`);
});