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:
2026-07-02 08:31:36 +02:00
commit 94b6cc3be9
60 changed files with 9206 additions and 0 deletions

208
server/db.js Normal file
View 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);
},
};