Initial PlayBull release with Trilogy Hub integration.
Includes homelab deploy tooling, image position/zoom manifest persistence, and full trading/casino stack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
208
server/db.js
Normal file
208
server/db.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import path from 'node:path';
|
||||
import { config } from './config.js';
|
||||
|
||||
const db = new DatabaseSync(path.join(config.DATA_DIR, 'playbull.db'));
|
||||
|
||||
db.exec(`
|
||||
PRAGMA journal_mode = WAL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
balance REAL NOT NULL DEFAULT 0,
|
||||
avatar TEXT DEFAULT '🐂',
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS holdings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
quantity REAL NOT NULL DEFAULT 0,
|
||||
avg_price REAL NOT NULL DEFAULT 0,
|
||||
UNIQUE(user_id, symbol)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS trades (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
side TEXT NOT NULL,
|
||||
qty REAL NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
ts INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS games (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
game TEXT NOT NULL,
|
||||
bet REAL NOT NULL,
|
||||
payout REAL NOT NULL,
|
||||
result TEXT,
|
||||
ts INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS feed (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
kind TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
ts INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS uploads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
frame TEXT NOT NULL DEFAULT 'gallery',
|
||||
focus_x REAL DEFAULT 0.5,
|
||||
focus_y REAL DEFAULT 0.5,
|
||||
zoom REAL DEFAULT 1.0,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// Migration: Avatar-Bild + Positions-Metadaten
|
||||
const userCols = db.prepare('PRAGMA table_info(users)').all().map((c) => c.name);
|
||||
if (!userCols.includes('avatar_image')) db.exec('ALTER TABLE users ADD COLUMN avatar_image TEXT');
|
||||
if (!userCols.includes('avatar_focus_x')) db.exec('ALTER TABLE users ADD COLUMN avatar_focus_x REAL DEFAULT 0.5');
|
||||
if (!userCols.includes('avatar_focus_y')) db.exec('ALTER TABLE users ADD COLUMN avatar_focus_y REAL DEFAULT 0.5');
|
||||
if (!userCols.includes('avatar_zoom')) db.exec('ALTER TABLE users ADD COLUMN avatar_zoom REAL DEFAULT 1.0');
|
||||
|
||||
export default db;
|
||||
|
||||
// ---- User Helpers ----
|
||||
export const Users = {
|
||||
create(username, hash, balance) {
|
||||
const info = db.prepare(
|
||||
'INSERT INTO users (username, password_hash, balance, created_at) VALUES (?,?,?,?)'
|
||||
).run(username, hash, balance, Date.now());
|
||||
return this.byId(info.lastInsertRowid);
|
||||
},
|
||||
byId(id) {
|
||||
return db.prepare('SELECT * FROM users WHERE id = ?').get(id);
|
||||
},
|
||||
byName(name) {
|
||||
return db.prepare('SELECT * FROM users WHERE username = ? COLLATE NOCASE').get(name);
|
||||
},
|
||||
all() {
|
||||
return db.prepare(`
|
||||
SELECT id, username, balance, avatar, avatar_image, avatar_focus_x, avatar_focus_y, avatar_zoom, created_at
|
||||
FROM users ORDER BY balance DESC
|
||||
`).all();
|
||||
},
|
||||
setBalance(id, balance) {
|
||||
db.prepare('UPDATE users SET balance = ? WHERE id = ?').run(Math.max(0, balance), id);
|
||||
},
|
||||
addBalance(id, delta) {
|
||||
const u = this.byId(id);
|
||||
if (!u) return null;
|
||||
const nb = Math.max(0, u.balance + delta);
|
||||
db.prepare('UPDATE users SET balance = ? WHERE id = ?').run(nb, id);
|
||||
return nb;
|
||||
},
|
||||
setAvatar(id, avatar) {
|
||||
db.prepare('UPDATE users SET avatar = ? WHERE id = ?').run(avatar, id);
|
||||
},
|
||||
setAvatarImage(id, imagePath, focusX, focusY, zoom) {
|
||||
db.prepare(`
|
||||
UPDATE users SET avatar_image = ?, avatar_focus_x = ?, avatar_focus_y = ?, avatar_zoom = ?
|
||||
WHERE id = ?
|
||||
`).run(imagePath, focusX, focusY, zoom, id);
|
||||
},
|
||||
setAvatarPosition(id, focusX, focusY, zoom) {
|
||||
db.prepare(`
|
||||
UPDATE users SET avatar_focus_x = ?, avatar_focus_y = ?, avatar_zoom = ?
|
||||
WHERE id = ?
|
||||
`).run(focusX, focusY, zoom, id);
|
||||
},
|
||||
clearAvatarImage(id) {
|
||||
db.prepare(`
|
||||
UPDATE users SET avatar_image = NULL, avatar_focus_x = 0.5, avatar_focus_y = 0.5, avatar_zoom = 1.0
|
||||
WHERE id = ?
|
||||
`).run(id);
|
||||
},
|
||||
count() {
|
||||
return db.prepare('SELECT COUNT(*) c FROM users').get().c;
|
||||
},
|
||||
};
|
||||
|
||||
// ---- Holdings Helpers ----
|
||||
export const Holdings = {
|
||||
forUser(userId) {
|
||||
return db.prepare('SELECT * FROM holdings WHERE user_id = ?').all(userId);
|
||||
},
|
||||
get(userId, symbol) {
|
||||
return db.prepare('SELECT * FROM holdings WHERE user_id = ? AND symbol = ?').get(userId, symbol);
|
||||
},
|
||||
upsert(userId, symbol, quantity, avgPrice) {
|
||||
db.prepare(`
|
||||
INSERT INTO holdings (user_id, symbol, quantity, avg_price) VALUES (?,?,?,?)
|
||||
ON CONFLICT(user_id, symbol) DO UPDATE SET quantity = excluded.quantity, avg_price = excluded.avg_price
|
||||
`).run(userId, symbol, quantity, avgPrice);
|
||||
},
|
||||
remove(userId, symbol) {
|
||||
db.prepare('DELETE FROM holdings WHERE user_id = ? AND symbol = ?').run(userId, symbol);
|
||||
},
|
||||
};
|
||||
|
||||
export const Trades = {
|
||||
add(userId, symbol, side, qty, price) {
|
||||
db.prepare('INSERT INTO trades (user_id, symbol, side, qty, price, ts) VALUES (?,?,?,?,?,?)')
|
||||
.run(userId, symbol, side, qty, price, Date.now());
|
||||
},
|
||||
forUser(userId, limit = 50) {
|
||||
return db.prepare('SELECT * FROM trades WHERE user_id = ? ORDER BY ts DESC LIMIT ?').all(userId, limit);
|
||||
},
|
||||
};
|
||||
|
||||
export const Games = {
|
||||
add(userId, game, bet, payout, result) {
|
||||
db.prepare('INSERT INTO games (user_id, game, bet, payout, result, ts) VALUES (?,?,?,?,?,?)')
|
||||
.run(userId, game, bet, payout, JSON.stringify(result || {}), Date.now());
|
||||
},
|
||||
forUser(userId, limit = 50) {
|
||||
return db.prepare('SELECT * FROM games WHERE user_id = ? ORDER BY ts DESC LIMIT ?').all(userId, limit);
|
||||
},
|
||||
};
|
||||
|
||||
export const Feed = {
|
||||
add(kind, text) {
|
||||
db.prepare('INSERT INTO feed (kind, text, ts) VALUES (?,?,?)').run(kind, text, Date.now());
|
||||
// alte Eintraege aufraeumen
|
||||
db.prepare('DELETE FROM feed WHERE id NOT IN (SELECT id FROM feed ORDER BY ts DESC LIMIT 100)').run();
|
||||
},
|
||||
recent(limit = 30) {
|
||||
return db.prepare('SELECT * FROM feed ORDER BY ts DESC LIMIT ?').all(limit);
|
||||
},
|
||||
};
|
||||
|
||||
export const Uploads = {
|
||||
forUser(userId) {
|
||||
return db.prepare('SELECT * FROM uploads WHERE user_id = ? ORDER BY created_at DESC').all(userId);
|
||||
},
|
||||
byId(id) {
|
||||
return db.prepare('SELECT * FROM uploads WHERE id = ?').get(id);
|
||||
},
|
||||
create(userId, frame, focusX, focusY, zoom) {
|
||||
const info = db.prepare(`
|
||||
INSERT INTO uploads (user_id, url, frame, focus_x, focus_y, zoom, created_at)
|
||||
VALUES (?,?,?,?,?,?,?)
|
||||
`).run(userId, 'pending', frame, focusX, focusY, zoom, Date.now());
|
||||
return this.byId(info.lastInsertRowid);
|
||||
},
|
||||
setUrl(id, url) {
|
||||
db.prepare('UPDATE uploads SET url = ? WHERE id = ?').run(url, id);
|
||||
return this.byId(id);
|
||||
},
|
||||
setPosition(id, focusX, focusY, zoom) {
|
||||
db.prepare('UPDATE uploads SET focus_x = ?, focus_y = ?, zoom = ? WHERE id = ?')
|
||||
.run(focusX, focusY, zoom, id);
|
||||
return this.byId(id);
|
||||
},
|
||||
remove(id) {
|
||||
db.prepare('DELETE FROM uploads WHERE id = ?').run(id);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user