This commit is contained in:
jeanotx32
2026-08-11 00:26:56 +02:00
commit 6f11b72cbb
43 changed files with 7175 additions and 0 deletions

255
packages/server/src/db.ts Normal file
View File

@@ -0,0 +1,255 @@
import fs from 'node:fs';
import path from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import type { LogEntry, LogLevel, ObsSettings, Platform } from '@stream-control/shared';
import { DEFAULT_OBS_SETTINGS } from '@stream-control/shared';
import { config } from './config.ts';
fs.mkdirSync(path.dirname(config.dbPath), { recursive: true });
export const db = new DatabaseSync(config.dbPath);
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
hostname TEXT,
platform TEXT NOT NULL DEFAULT 'unknown',
agent_version TEXT,
token_hash TEXT NOT NULL,
obs_host TEXT NOT NULL DEFAULT '127.0.0.1',
obs_port INTEGER NOT NULL DEFAULT 4455,
obs_password TEXT NOT NULL DEFAULT '',
auto_connect INTEGER NOT NULL DEFAULT 1,
notes TEXT,
created_at INTEGER NOT NULL,
last_seen_at INTEGER
);
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT,
level TEXT NOT NULL,
message TEXT NOT NULL,
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs (ts DESC);
`);
export interface AgentRow {
id: string;
name: string;
hostname: string | null;
platform: string;
agent_version: string | null;
token_hash: string;
obs_host: string;
obs_port: number;
obs_password: string;
auto_connect: number;
notes: string | null;
created_at: number;
last_seen_at: number | null;
}
export interface AgentRecord {
id: string;
name: string;
hostname: string | null;
platform: Platform;
agentVersion: string | null;
tokenHash: string;
obs: ObsSettings;
autoConnectObs: boolean;
notes: string | null;
createdAt: number;
lastSeenAt: number | null;
}
function toRecord(row: AgentRow): AgentRecord {
return {
id: row.id,
name: row.name,
hostname: row.hostname,
platform: (row.platform as Platform) ?? 'unknown',
agentVersion: row.agent_version,
tokenHash: row.token_hash,
obs: {
host: row.obs_host || DEFAULT_OBS_SETTINGS.host,
port: Number(row.obs_port) || DEFAULT_OBS_SETTINGS.port,
password: row.obs_password ?? '',
},
autoConnectObs: Number(row.auto_connect) === 1,
notes: row.notes,
createdAt: Number(row.created_at),
lastSeenAt: row.last_seen_at === null ? null : Number(row.last_seen_at),
};
}
const stmts = {
listAgents: db.prepare('SELECT * FROM agents ORDER BY name COLLATE NOCASE'),
getAgent: db.prepare('SELECT * FROM agents WHERE id = ?'),
getAgentByTokenHash: db.prepare('SELECT * FROM agents WHERE token_hash = ?'),
insertAgent: db.prepare(`
INSERT INTO agents (id, name, hostname, platform, agent_version, token_hash,
obs_host, obs_port, obs_password, auto_connect, notes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`),
updateIdentity: db.prepare(`
UPDATE agents SET hostname = ?, platform = ?, agent_version = ?, last_seen_at = ?
WHERE id = ?
`),
updateSettings: db.prepare(`
UPDATE agents SET name = ?, obs_host = ?, obs_port = ?, obs_password = ?,
auto_connect = ?, notes = ?
WHERE id = ?
`),
touchAgent: db.prepare('UPDATE agents SET last_seen_at = ? WHERE id = ?'),
rotateToken: db.prepare('UPDATE agents SET token_hash = ? WHERE id = ?'),
deleteAgent: db.prepare('DELETE FROM agents WHERE id = ?'),
insertLog: db.prepare('INSERT INTO logs (agent_id, level, message, ts) VALUES (?, ?, ?, ?)'),
recentLogs: db.prepare(`
SELECT l.id, l.agent_id, l.level, l.message, l.ts, a.name AS agent_name
FROM logs l LEFT JOIN agents a ON a.id = l.agent_id
ORDER BY l.id DESC LIMIT ?
`),
pruneLogs: db.prepare(`
DELETE FROM logs WHERE id NOT IN (SELECT id FROM logs ORDER BY id DESC LIMIT ?)
`),
};
export const agentsRepo = {
list(): AgentRecord[] {
return (stmts.listAgents.all() as unknown as AgentRow[]).map(toRecord);
},
get(id: string): AgentRecord | null {
const row = stmts.getAgent.get(id) as unknown as AgentRow | undefined;
return row ? toRecord(row) : null;
},
findByTokenHash(tokenHash: string): AgentRecord | null {
const row = stmts.getAgentByTokenHash.get(tokenHash) as unknown as AgentRow | undefined;
return row ? toRecord(row) : null;
},
create(input: {
id: string;
name: string;
tokenHash: string;
hostname?: string | null;
platform?: Platform;
agentVersion?: string | null;
obs?: Partial<ObsSettings>;
autoConnectObs?: boolean;
notes?: string | null;
}): AgentRecord {
const obs = { ...DEFAULT_OBS_SETTINGS, ...input.obs };
stmts.insertAgent.run(
input.id,
input.name,
input.hostname ?? null,
input.platform ?? 'unknown',
input.agentVersion ?? null,
input.tokenHash,
obs.host,
obs.port,
obs.password,
input.autoConnectObs === false ? 0 : 1,
input.notes ?? null,
Date.now(),
);
const created = agentsRepo.get(input.id);
if (!created) throw new Error(`Échec de création de l'agent ${input.id}`);
return created;
},
updateIdentity(
id: string,
identity: { hostname: string | null; platform: Platform; agentVersion: string | null },
): void {
stmts.updateIdentity.run(
identity.hostname,
identity.platform,
identity.agentVersion,
Date.now(),
id,
);
},
updateSettings(
id: string,
settings: {
name: string;
obs: ObsSettings;
autoConnectObs: boolean;
notes: string | null;
},
): void {
stmts.updateSettings.run(
settings.name,
settings.obs.host,
settings.obs.port,
settings.obs.password,
settings.autoConnectObs ? 1 : 0,
settings.notes,
id,
);
},
touch(id: string): void {
stmts.touchAgent.run(Date.now(), id);
},
rotateToken(id: string, tokenHash: string): void {
stmts.rotateToken.run(tokenHash, id);
},
remove(id: string): void {
stmts.deleteAgent.run(id);
},
};
interface LogRow {
id: number;
agent_id: string | null;
agent_name: string | null;
level: string;
message: string;
ts: number;
}
export const logsRepo = {
append(agentId: string | null, level: LogLevel, message: string, ts = Date.now()): LogEntry {
const info = stmts.insertLog.run(agentId, level, message, ts);
if (Math.random() < 0.02) stmts.pruneLogs.run(config.logRetention);
const agent = agentId ? agentsRepo.get(agentId) : null;
return {
id: Number(info.lastInsertRowid),
agentId,
agentName: agent?.name ?? null,
level,
message,
ts,
};
},
recent(limit = 200): LogEntry[] {
const rows = stmts.recentLogs.all(limit) as unknown as LogRow[];
return rows
.map((row) => ({
id: Number(row.id),
agentId: row.agent_id,
agentName: row.agent_name,
level: row.level as LogLevel,
message: row.message,
ts: Number(row.ts),
}))
.reverse();
},
};