merge: combine all features from both codebases - mobile dashboard + layer panel + notifications + shares + all fixes

This commit is contained in:
Leopoldadmin
2026-07-04 16:43:55 +02:00
parent 0606dbb501
commit be62470b53
79 changed files with 9562 additions and 3132 deletions
+150 -8
View File
@@ -10,6 +10,8 @@ import type {
DatabaseInterface, DBProject, DBDrawing, DBLayer,
DBElement, DBBlock, DBSetting, DBUser, DBSession,
DBNotification, DBProjectShare,
DBGlobalBlockFolder, DBGlobalBlock,
DBProjectFolder,
} from './DatabaseInterface.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -27,6 +29,11 @@ export class SqliteAdapter implements DatabaseInterface {
const schemaPath = join(__dirname, 'schema.sql');
const schema = readFileSync(schemaPath, 'utf-8');
this.db.exec(schema);
// Add folder_id column to projects table for existing databases
const columns = this.db.prepare("PRAGMA table_info('projects')").all() as { name: string }[];
if (!columns.some(col => col.name === 'folder_id')) {
this.db.exec('ALTER TABLE projects ADD COLUMN folder_id TEXT REFERENCES project_folders(id) ON DELETE SET NULL');
}
}
close(): void {
@@ -73,7 +80,22 @@ export class SqliteAdapter implements DatabaseInterface {
}
// ─── Projects ──────────────────────────────────────
listProjects(): DBProject[] {
listProjects(ownerId?: string, folderId?: string | null): DBProject[] {
if (ownerId && folderId !== undefined) {
if (folderId === null) {
return this.db.prepare('SELECT * FROM projects WHERE owner_id = ? AND folder_id IS NULL ORDER BY updated_at DESC').all(ownerId) as DBProject[];
}
return this.db.prepare('SELECT * FROM projects WHERE owner_id = ? AND folder_id = ? ORDER BY updated_at DESC').all(ownerId, folderId) as DBProject[];
}
if (ownerId) {
return this.db.prepare('SELECT * FROM projects WHERE owner_id = ? ORDER BY updated_at DESC').all(ownerId) as DBProject[];
}
if (folderId !== undefined) {
if (folderId === null) {
return this.db.prepare('SELECT * FROM projects WHERE folder_id IS NULL ORDER BY updated_at DESC').all() as DBProject[];
}
return this.db.prepare('SELECT * FROM projects WHERE folder_id = ? ORDER BY updated_at DESC').all(folderId) as DBProject[];
}
return this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all() as DBProject[];
}
@@ -84,16 +106,17 @@ export class SqliteAdapter implements DatabaseInterface {
createProject(data: Partial<DBProject>): DBProject {
const id = data.id ?? `proj-${Date.now()}`;
this.db.prepare(
'INSERT INTO projects (id, name, description, owner_id) VALUES (?, ?, ?, ?)',
).run(id, data.name ?? 'Unbenannt', data.description ?? null, data.owner_id ?? 'user-default');
'INSERT INTO projects (id, name, description, owner_id, folder_id) VALUES (?, ?, ?, ?, ?)',
).run(id, data.name ?? 'Unbenannt', data.description ?? null, data.owner_id ?? 'user-default', data.folder_id ?? null);
return this.getProject(id)!;
}
updateProject(id: string, data: Partial<DBProject>): DBProject | null {
const sets: string[] = [];
const vals: any[] = [];
const sets: string[] = [];
const vals: unknown[] = [];
if (data.name !== undefined) { sets.push('name = ?'); vals.push(data.name); }
if (data.description !== undefined) { sets.push('description = ?'); vals.push(data.description); }
if (data.folder_id !== undefined) { sets.push('folder_id = ?'); vals.push(data.folder_id); }
sets.push("updated_at = datetime('now')");
if (sets.length > 1) {
this.db.prepare(`UPDATE projects SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id);
@@ -105,6 +128,45 @@ export class SqliteAdapter implements DatabaseInterface {
return this.db.prepare('DELETE FROM projects WHERE id = ?').run(id).changes > 0;
}
moveProjectToFolder(projectId: string, folderId: string | null): DBProject | null {
this.db.prepare('UPDATE projects SET folder_id = ?, updated_at = datetime(\'now\') WHERE id = ?').run(folderId, projectId);
return this.getProject(projectId);
}
// ─── Project Folders ─────────────────────────────────
listProjectFolders(ownerId: string): DBProjectFolder[] {
return this.db.prepare('SELECT * FROM project_folders WHERE owner_id = ? ORDER BY name').all(ownerId) as DBProjectFolder[];
}
getProjectFolder(id: string): DBProjectFolder | null {
return (this.db.prepare('SELECT * FROM project_folders WHERE id = ?').get(id) as DBProjectFolder) ?? null;
}
createProjectFolder(data: Partial<DBProjectFolder>): DBProjectFolder {
const id = data.id ?? `pfolder-${Date.now()}`;
this.db.prepare(
'INSERT INTO project_folders (id, name, parent_id, owner_id) VALUES (?, ?, ?, ?)',
).run(id, data.name ?? 'Neuer Ordner', data.parent_id ?? null, data.owner_id ?? 'user-default');
return this.getProjectFolder(id)!;
}
updateProjectFolder(id: string, data: Partial<DBProjectFolder>): DBProjectFolder | null {
const sets: string[] = [];
const vals: unknown[] = [];
if (data.name !== undefined) { sets.push('name = ?'); vals.push(data.name); }
if (data.parent_id !== undefined) { sets.push('parent_id = ?'); vals.push(data.parent_id); }
if (sets.length > 0) {
this.db.prepare(`UPDATE project_folders SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id);
}
return this.getProjectFolder(id);
}
deleteProjectFolder(id: string): boolean {
// Move projects inside this folder to root (folder_id = NULL) before deleting
this.db.prepare('UPDATE projects SET folder_id = NULL WHERE folder_id = ?').run(id);
return this.db.prepare('DELETE FROM project_folders WHERE id = ?').run(id).changes > 0;
}
// ─── Drawings ──────────────────────────────────────
listDrawings(projectId: string): DBDrawing[] {
return this.db.prepare('SELECT * FROM drawings WHERE project_id = ? ORDER BY updated_at DESC').all(projectId) as DBDrawing[];
@@ -190,7 +252,7 @@ export class SqliteAdapter implements DatabaseInterface {
const vals: any[] = [];
for (const [k, v] of Object.entries(data)) {
if (['layer_id','type','x','y','width','height','properties_json'].includes(k)) {
sets.push(`${k} = ?`); vals.push(k === 'visible' || k === 'locked' ? (v ? 1 : 0) : v);
sets.push(`${k} = ?`); vals.push(v);
}
}
if (sets.length > 0) {
@@ -222,7 +284,7 @@ export class SqliteAdapter implements DatabaseInterface {
const vals: any[] = [];
for (const [k, v] of Object.entries(data)) {
if (['name','description','category','elements_json','thumbnail'].includes(k)) {
sets.push(`${k} = ?`); vals.push(k === 'visible' || k === 'locked' ? (v ? 1 : 0) : v);
sets.push(`${k} = ?`); vals.push(v);
}
}
if (sets.length > 0) {
@@ -264,7 +326,10 @@ export class SqliteAdapter implements DatabaseInterface {
}
deleteExpiredSessions(): void {
this.db.prepare('DELETE FROM sessions WHERE expires_at < ?').run(Date.now());
// Use consistent epoch-millisecond integer comparison.
// <= ensures sessions expiring at exactly the current time are also deleted.
const now = Math.floor(Date.now());
this.db.prepare('DELETE FROM sessions WHERE expires_at <= ?').run(now);
}
// ─── Notifications ──────────────────────────────────
@@ -313,4 +378,81 @@ export class SqliteAdapter implements DatabaseInterface {
deleteProjectShare(id: string): boolean {
return this.db.prepare('DELETE FROM project_shares WHERE id = ?').run(id).changes > 0;
}
// ─── Global Block Folders ──────────────────────────────
listGlobalFolders(parentId?: string | null): DBGlobalBlockFolder[] {
if (parentId === undefined) {
return this.db.prepare('SELECT * FROM global_block_folders ORDER BY name').all() as DBGlobalBlockFolder[];
}
if (parentId === null) {
return this.db.prepare('SELECT * FROM global_block_folders WHERE parent_id IS NULL ORDER BY name').all() as DBGlobalBlockFolder[];
}
return this.db.prepare('SELECT * FROM global_block_folders WHERE parent_id = ? ORDER BY name').all(parentId) as DBGlobalBlockFolder[];
}
getGlobalFolder(id: string): DBGlobalBlockFolder | null {
return (this.db.prepare('SELECT * FROM global_block_folders WHERE id = ?').get(id) as DBGlobalBlockFolder) ?? null;
}
createGlobalFolder(data: Partial<DBGlobalBlockFolder>): DBGlobalBlockFolder {
const id = data.id ?? `gfolder-${Date.now()}`;
this.db.prepare(
'INSERT INTO global_block_folders (id, name, parent_id) VALUES (?, ?, ?)',
).run(id, data.name ?? 'Neuer Ordner', data.parent_id ?? null);
return this.getGlobalFolder(id)!;
}
updateGlobalFolder(id: string, data: Partial<DBGlobalBlockFolder>): DBGlobalBlockFolder | null {
const sets: string[] = [];
const vals: unknown[] = [];
if (data.name !== undefined) { sets.push('name = ?'); vals.push(data.name); }
if (data.parent_id !== undefined) { sets.push('parent_id = ?'); vals.push(data.parent_id); }
if (sets.length > 0) {
this.db.prepare(`UPDATE global_block_folders SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id);
}
return this.getGlobalFolder(id);
}
deleteGlobalFolder(id: string): boolean {
return this.db.prepare('DELETE FROM global_block_folders WHERE id = ?').run(id).changes > 0;
}
// ─── Global Blocks ─────────────────────────────────────
listGlobalBlocks(folderId?: string | null): DBGlobalBlock[] {
if (folderId === undefined) {
return this.db.prepare('SELECT * FROM global_blocks ORDER BY updated_at DESC').all() as DBGlobalBlock[];
}
if (folderId === null) {
return this.db.prepare('SELECT * FROM global_blocks WHERE folder_id IS NULL ORDER BY updated_at DESC').all() as DBGlobalBlock[];
}
return this.db.prepare('SELECT * FROM global_blocks WHERE folder_id = ? ORDER BY updated_at DESC').all(folderId) as DBGlobalBlock[];
}
getGlobalBlock(id: string): DBGlobalBlock | null {
return (this.db.prepare('SELECT * FROM global_blocks WHERE id = ?').get(id) as DBGlobalBlock) ?? null;
}
createGlobalBlock(data: Partial<DBGlobalBlock>): DBGlobalBlock {
const id = data.id ?? `gblock-${Date.now()}`;
this.db.prepare(
'INSERT INTO global_blocks (id, folder_id, name, block_data, svg_data) VALUES (?, ?, ?, ?, ?)',
).run(id, data.folder_id ?? null, data.name ?? 'Global Block', data.block_data ?? '{}', data.svg_data ?? null);
return this.getGlobalBlock(id)!;
}
updateGlobalBlock(id: string, data: Partial<DBGlobalBlock>): DBGlobalBlock | null {
const sets: string[] = [];
const vals: unknown[] = [];
if (data.name !== undefined) { sets.push('name = ?'); vals.push(data.name); }
if (data.folder_id !== undefined) { sets.push('folder_id = ?'); vals.push(data.folder_id); }
if (data.block_data !== undefined) { sets.push('block_data = ?'); vals.push(data.block_data); }
if (data.svg_data !== undefined) { sets.push('svg_data = ?'); vals.push(data.svg_data); }
sets.push("updated_at = datetime('now')");
this.db.prepare(`UPDATE global_blocks SET ${sets.join(', ')} WHERE id = ?`).run(...vals, id);
return this.getGlobalBlock(id);
}
deleteGlobalBlock(id: string): boolean {
return this.db.prepare('DELETE FROM global_blocks WHERE id = ?').run(id).changes > 0;
}
}