Includes homelab deploy tooling, image position/zoom manifest persistence, and full trading/casino stack. Co-authored-by: Cursor <cursoragent@cursor.com>
209 lines
7.1 KiB
JavaScript
209 lines
7.1 KiB
JavaScript
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);
|
|
},
|
|
};
|