159
js/db.js
Normal file
159
js/db.js
Normal file
@@ -0,0 +1,159 @@
|
||||
let db = null;
|
||||
|
||||
// ── Persistence ──────────────────────────────────────────────────────────────
|
||||
|
||||
function saveToStorage() {
|
||||
try {
|
||||
const data = db.export(); // Uint8Array
|
||||
let binary = '';
|
||||
const chunk = 8192;
|
||||
for (let i = 0; i < data.length; i += chunk) {
|
||||
binary += String.fromCharCode(...data.subarray(i, i + chunk));
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, btoa(binary));
|
||||
} catch (e) {
|
||||
console.warn('catalyst: failed to persist database', e);
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromStorage() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (!stored) return null;
|
||||
const binary = atob(stored);
|
||||
const buf = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) buf[i] = binary.charCodeAt(i);
|
||||
return buf;
|
||||
} catch (e) {
|
||||
console.warn('catalyst: failed to load database from storage', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function initDB() {
|
||||
const SQL = await initSqlJs({ locateFile: f => SQL_JS_CDN + f });
|
||||
|
||||
const saved = loadFromStorage();
|
||||
if (saved) {
|
||||
db = new SQL.Database(saved);
|
||||
return;
|
||||
}
|
||||
|
||||
db = new SQL.Database();
|
||||
db.run(`
|
||||
CREATE TABLE instances (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
state TEXT DEFAULT 'deployed',
|
||||
stack TEXT DEFAULT '',
|
||||
vmid INTEGER UNIQUE NOT NULL,
|
||||
atlas INTEGER DEFAULT 0,
|
||||
argus INTEGER DEFAULT 0,
|
||||
semaphore INTEGER DEFAULT 0,
|
||||
patchmon INTEGER DEFAULT 0,
|
||||
tailscale INTEGER DEFAULT 0,
|
||||
andromeda INTEGER DEFAULT 0,
|
||||
tailscale_ip TEXT DEFAULT '',
|
||||
hardware_acceleration INTEGER DEFAULT 0,
|
||||
createdAt TEXT DEFAULT (datetime('now')),
|
||||
updatedAt TEXT DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO instances
|
||||
(name, state, stack, vmid, atlas, argus, semaphore, patchmon, tailscale, andromeda, tailscale_ip, hardware_acceleration)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
SEED.forEach(s => stmt.run([
|
||||
s.name, s.state, s.stack, s.vmid,
|
||||
+s.atlas, +s.argus, +s.semaphore, +s.patchmon,
|
||||
+s.tailscale, +s.andromeda, s.tailscale_ip, +s.hardware_acceleration,
|
||||
]));
|
||||
|
||||
stmt.free();
|
||||
saveToStorage();
|
||||
}
|
||||
|
||||
// ── Queries ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function getInstances(filters = {}) {
|
||||
let sql = 'SELECT * FROM instances WHERE 1=1';
|
||||
const params = [];
|
||||
|
||||
if (filters.search) {
|
||||
sql += ' AND (name LIKE ? OR CAST(vmid AS TEXT) LIKE ? OR stack LIKE ?)';
|
||||
const s = `%${filters.search}%`;
|
||||
params.push(s, s, s);
|
||||
}
|
||||
if (filters.state) { sql += ' AND state = ?'; params.push(filters.state); }
|
||||
if (filters.stack) { sql += ' AND stack = ?'; params.push(filters.stack); }
|
||||
|
||||
sql += ' ORDER BY name ASC';
|
||||
|
||||
const res = db.exec(sql, params);
|
||||
if (!res.length) return [];
|
||||
const cols = res[0].columns;
|
||||
return res[0].values.map(row => Object.fromEntries(cols.map((c, i) => [c, row[i]])));
|
||||
}
|
||||
|
||||
function getInstance(vmid) {
|
||||
const res = db.exec('SELECT * FROM instances WHERE vmid = ?', [vmid]);
|
||||
if (!res.length) return null;
|
||||
const cols = res[0].columns;
|
||||
return Object.fromEntries(cols.map((c, i) => [c, res[0].values[0][i]]));
|
||||
}
|
||||
|
||||
function getDistinctStacks() {
|
||||
const res = db.exec(`SELECT DISTINCT stack FROM instances WHERE stack != '' ORDER BY stack`);
|
||||
if (!res.length) return [];
|
||||
return res[0].values.map(row => row[0]);
|
||||
}
|
||||
|
||||
// ── Mutations ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function createInstance(data) {
|
||||
try {
|
||||
db.run(
|
||||
`INSERT INTO instances
|
||||
(name, state, stack, vmid, atlas, argus, semaphore, patchmon, tailscale, andromeda, tailscale_ip, hardware_acceleration)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[data.name, data.state, data.stack, data.vmid,
|
||||
data.atlas, data.argus, data.semaphore, data.patchmon,
|
||||
data.tailscale, data.andromeda, data.tailscale_ip, data.hardware_acceleration]
|
||||
);
|
||||
saveToStorage();
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
function updateInstance(id, data) {
|
||||
try {
|
||||
db.run(
|
||||
`UPDATE instances SET
|
||||
name=?, state=?, stack=?, vmid=?,
|
||||
atlas=?, argus=?, semaphore=?, patchmon=?,
|
||||
tailscale=?, andromeda=?, tailscale_ip=?, hardware_acceleration=?,
|
||||
updatedAt=datetime('now')
|
||||
WHERE id=?`,
|
||||
[data.name, data.state, data.stack, data.vmid,
|
||||
data.atlas, data.argus, data.semaphore, data.patchmon,
|
||||
data.tailscale, data.andromeda, data.tailscale_ip, data.hardware_acceleration,
|
||||
id]
|
||||
);
|
||||
saveToStorage();
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
function deleteInstance(id) {
|
||||
db.run('DELETE FROM instances WHERE id = ?', [id]);
|
||||
saveToStorage();
|
||||
}
|
||||
Reference in New Issue
Block a user