Initial commit -- just started to factor and implement python fast API backend

This commit is contained in:
2026-05-11 12:48:35 +10:00
commit b1b621bc4a
23 changed files with 4101 additions and 0 deletions
+314
View File
@@ -0,0 +1,314 @@
// Dashy SQLite layer — uses sql.js (SQLite compiled to WASM)
// Database is initialized once, seeded from SEED_TASKS / SEED_AUDIT / USERS,
// persisted to localStorage on every write, and exposed as window.DashyDB.
window.DashyDB = (function () {
const LS_KEY = 'dashy.db.v1';
let db = null;
let SQL = null;
const listeners = new Set();
let ready = false;
const readyWaiters = [];
// -- schema -----------------------------------------------------------
const SCHEMA = `
CREATE TABLE users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
role TEXT NOT NULL,
hue INTEGER NOT NULL,
initials TEXT NOT NULL,
email TEXT,
phone TEXT,
photo TEXT,
password_hash TEXT,
account_type TEXT NOT NULL DEFAULT 'standard' CHECK (account_type IN ('admin','standard')),
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
assignee_id TEXT NOT NULL REFERENCES users(id),
added_by TEXT NOT NULL,
priority TEXT NOT NULL CHECK (priority IN ('low','med','high')),
source TEXT NOT NULL CHECK (source IN ('manual','imessage','email','automation')),
status TEXT NOT NULL DEFAULT 'open',
added_at TEXT NOT NULL,
due_at TEXT,
reminder_at TEXT
);
CREATE INDEX idx_tasks_assignee ON tasks(assignee_id);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE TABLE task_tags (
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
PRIMARY KEY (task_id, tag)
);
CREATE TABLE task_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
author_id TEXT NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE audit_log (
id TEXT PRIMARY KEY,
at TEXT NOT NULL,
actor TEXT NOT NULL,
action TEXT NOT NULL,
summary TEXT NOT NULL,
target TEXT
);
CREATE INDEX idx_audit_at ON audit_log(at);
CREATE TABLE sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL REFERENCES users(id),
device TEXT NOT NULL,
location TEXT,
last_active TEXT NOT NULL
);
`;
function bumpListeners() { listeners.forEach(fn => { try { fn(); } catch(e){} }); }
function persist() {
try {
const bytes = db.export();
const b64 = btoa(String.fromCharCode(...bytes));
localStorage.setItem(LS_KEY, b64);
} catch(e) { console.warn('DashyDB persist failed', e); }
bumpListeners();
}
function loadFromLS() {
const b64 = localStorage.getItem(LS_KEY);
if (!b64) return null;
const bin = atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
return arr;
}
function seed() {
db.exec('BEGIN');
USERS.forEach(u => {
const isAdmin = u.id === 'rod' || u.id === 'ayron';
db.run(
'INSERT INTO users (id, name, role, hue, initials, email, phone, password_hash, account_type) VALUES (?,?,?,?,?,?,?,?,?)',
[u.id, u.name, u.role, u.hue, u.initials, u.id + '@murchison-auto.co', '+64 27 555 0184',
'pbkdf2$' + Math.random().toString(36).slice(2, 18), isAdmin ? 'admin' : 'standard']
);
});
SEED_TASKS.forEach(t => {
db.run(
'INSERT INTO tasks (id, title, description, assignee_id, added_by, priority, source, status, added_at) VALUES (?,?,?,?,?,?,?,?,?)',
[t.id, t.title, t.description || '', t.assignee, t.addedBy, t.priority, t.source, t.status || 'open', t.addedAt]
);
(t.tags || []).forEach(tag => {
db.run('INSERT INTO task_tags (task_id, tag) VALUES (?,?)', [t.id, tag]);
});
});
SEED_AUDIT.forEach(a => {
db.run(
'INSERT INTO audit_log (id, at, actor, action, summary, target) VALUES (?,?,?,?,?,?)',
[a.id, a.at, a.actor, a.action, a.summary, a.target]
);
});
// sample notes
db.run("INSERT INTO task_notes (task_id, author_id, body, created_at) VALUES ('t11','lani','Customer wants the work done before Mother''s Day weekend.','2026-05-08T09:14:00')");
db.run("INSERT INTO sessions (user_id, device, location, last_active) VALUES ('rod','MacBook · Chrome','Christchurch, NZ','2026-05-08T10:30:00')");
db.run("INSERT INTO sessions (user_id, device, location, last_active) VALUES ('rod','iPhone · Safari','Christchurch, NZ','2026-05-08T08:15:00')");
db.exec('COMMIT');
}
async function init() {
SQL = await window.initSqlJs({
locateFile: f => 'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/' + f
});
const existing = loadFromLS();
if (existing) {
db = new SQL.Database(existing);
// migration: add account_type if missing
try {
const cols = query("PRAGMA table_info(users)").map(c => c.name);
if (!cols.includes('account_type')) {
db.exec("ALTER TABLE users ADD COLUMN account_type TEXT NOT NULL DEFAULT 'standard'");
db.run("UPDATE users SET account_type = 'admin' WHERE id IN ('rod','ayron')");
persist();
}
} catch(e) { console.warn('migration failed', e); }
} else {
db = new SQL.Database();
db.exec(SCHEMA);
seed();
persist();
}
ready = true;
readyWaiters.splice(0).forEach(fn => fn());
bumpListeners();
}
function whenReady() {
if (ready) return Promise.resolve();
return new Promise(r => readyWaiters.push(r));
}
// -- query helpers ----------------------------------------------------
function rowsFrom(stmt) {
const out = [];
while (stmt.step()) out.push(stmt.getAsObject());
stmt.free();
return out;
}
function query(sql, params = []) {
if (!db) return [];
const stmt = db.prepare(sql);
if (params.length) stmt.bind(params);
return rowsFrom(stmt);
}
function exec(sql, params = []) {
if (!db) return;
db.run(sql, params);
persist();
}
// -- domain ops -------------------------------------------------------
function listTasks() {
const rows = query(`
SELECT t.*, GROUP_CONCAT(tt.tag, '|') AS tags_csv
FROM tasks t
LEFT JOIN task_tags tt ON tt.task_id = t.id
GROUP BY t.id
ORDER BY t.added_at DESC
`);
return rows.map(r => ({
id: r.id, title: r.title, description: r.description,
assignee: r.assignee_id, addedBy: r.added_by,
priority: r.priority, source: r.source, status: r.status,
addedAt: r.added_at,
tags: r.tags_csv ? r.tags_csv.split('|') : []
}));
}
function listAudit() {
return query('SELECT id, at, actor, action, summary, target FROM audit_log ORDER BY at DESC');
}
function listUsers() {
return query('SELECT * FROM users ORDER BY rowid ASC');
}
function moveTask(taskId, toUserId, by) {
exec('UPDATE tasks SET assignee_id = ? WHERE id = ?', [toUserId, taskId]);
addAudit({
actor: by, action: 'task_moved',
summary: 'Moved task to ' + (USERS.find(u=>u.id===toUserId)||{name:toUserId}).name,
target: taskId
});
}
function setPriority(taskId, p) {
exec('UPDATE tasks SET priority = ? WHERE id = ?', [p, taskId]);
}
function createTask(t) {
exec(
'INSERT INTO tasks (id, title, description, assignee_id, added_by, priority, source, status, added_at) VALUES (?,?,?,?,?,?,?,?,?)',
[t.id, t.title, t.description || '', t.assignee, t.addedBy, t.priority, t.source || 'manual', t.status || 'open', t.addedAt]
);
(t.tags || []).forEach(tag => {
db.run('INSERT INTO task_tags (task_id, tag) VALUES (?,?)', [t.id, tag]);
});
persist();
}
function addAudit(row) {
const id = 'a_' + Date.now() + '_' + Math.random().toString(36).slice(2,5);
exec(
'INSERT INTO audit_log (id, at, actor, action, summary, target) VALUES (?,?,?,?,?,?)',
[id, row.at || new Date().toISOString(), row.actor, row.action, row.summary, row.target || null]
);
}
function updateUser(userId, edits) {
const fields = []; const vals = [];
if (edits.name !== undefined) { fields.push('name = ?'); vals.push(edits.name); }
if (edits.role !== undefined) { fields.push('role = ?'); vals.push(edits.role); }
if (edits.photo !== undefined) { fields.push('photo = ?'); vals.push(edits.photo); }
if (edits.account_type !== undefined) { fields.push('account_type = ?'); vals.push(edits.account_type); }
if (!fields.length) return;
vals.push(userId);
exec('UPDATE users SET ' + fields.join(', ') + ' WHERE id = ?', vals);
}
function createUser(u) {
const id = u.id || (u.name.toLowerCase().replace(/[^a-z]/g,'').slice(0,8) + '_' + Math.random().toString(36).slice(2,5));
const initials = (u.name.split(' ').map(s=>s[0]).join('') || 'U').slice(0,2).toUpperCase();
const hue = u.hue || Math.floor(Math.random() * 360);
exec(
'INSERT INTO users (id, name, role, hue, initials, email, phone, account_type, password_hash) VALUES (?,?,?,?,?,?,?,?,?)',
[id, u.name, u.role || 'Team member', hue, initials,
u.email || (id + '@murchison-auto.co'), u.phone || '',
u.account_type || 'standard', 'pbkdf2$' + Math.random().toString(36).slice(2,18)]
);
return id;
}
function deleteUser(userId) {
// reassign any tasks to ROD before delete
exec("UPDATE tasks SET assignee_id = 'rod' WHERE assignee_id = ?", [userId]);
exec('DELETE FROM users WHERE id = ?', [userId]);
}
function tableNames() {
return query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name").map(r => r.name);
}
function tableInfo(name) {
return query(`PRAGMA table_info(${name})`);
}
function rowCount(name) {
const r = query(`SELECT COUNT(*) AS c FROM ${name}`);
return r[0] ? r[0].c : 0;
}
function exportFile() {
const bytes = db.export();
return new Blob([bytes], { type: 'application/x-sqlite3' });
}
function reset() {
localStorage.removeItem(LS_KEY);
db = new SQL.Database();
db.exec(SCHEMA);
seed();
persist();
}
function rawExec(sql) {
// returns array of {columns, values} for SELECTs, or empty for writes
const res = db.exec(sql);
persist();
return res;
}
function subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }
return {
init, whenReady, query, exec, rawExec,
listTasks, listAudit, listUsers,
createTask, moveTask, setPriority, updateUser, createUser, deleteUser, addAudit,
tableNames, tableInfo, rowCount, exportFile, reset, subscribe,
get isReady() { return ready; },
};
})();