revision history WIP
This commit is contained in:
403
server.js
403
server.js
@@ -1,141 +1,262 @@
|
||||
// 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);
|
||||
}
|
||||
);
|
||||
|
||||
// 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 square’s 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 });
|
||||
// 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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// start server
|
||||
const PORT = process.env.PORT || 3085;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Listening on http://localhost:${PORT}`);
|
||||
});
|
||||
// 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 square’s 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}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user