taskover-mcp 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auth-flow.js +228 -0
- package/auth-gate.js +253 -0
- package/cloud-adapter.js +73 -26
- package/credential-store.js +93 -0
- package/crypto.js +386 -0
- package/data-store.js +9352 -0
- package/data-store.json-backup.js +1264 -0
- package/db.js +2292 -0
- package/image-moderator.js +491 -0
- package/image-processor.js +160 -0
- package/image-upload-service.js +398 -0
- package/index.js +2294 -2068
- package/migrate-json-to-sqlite.js +256 -0
- package/package.json +29 -16
- package/publish/auth-flow.js +275 -0
- package/publish/cloud-adapter.js +246 -0
- package/publish/credential-store.js +93 -0
- package/publish/index.js +1433 -0
- package/publish/package.json +21 -0
- package/publish/tool-map.js +1146 -0
- package/scripts/build-publish.sh +95 -0
- package/scripts/test-auth-failure.js +68 -0
- package/scripts/test-success.js +232 -0
- package/scripts/test-validation.js +105 -0
- package/tool-map.js +58 -0
- /package/{README.md → publish/README.md} +0 -0
package/db.js
ADDED
|
@@ -0,0 +1,2292 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
|
|
4
|
+
const DATA_DIR = process.env.TASKOVER_DATA_DIR || path.join(__dirname, "..", "data");
|
|
5
|
+
const DB_FILE = process.env.TASKOVER_DB_FILE || path.join(DATA_DIR, "taskover.db");
|
|
6
|
+
const BACKUP_DIR = path.join(DATA_DIR, "backups");
|
|
7
|
+
|
|
8
|
+
function ensureDir(dir) {
|
|
9
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ===== ENGINE DETECTION =====
|
|
13
|
+
// better-sqlite3 (native C, ~100x faster) for Docker/production
|
|
14
|
+
// sql.js (WebAssembly) for Electron/local dev where native compilation isn't available
|
|
15
|
+
|
|
16
|
+
let _engine = "sql.js"; // default
|
|
17
|
+
let Database = null;
|
|
18
|
+
let initSqlJs = null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
Database = require("better-sqlite3");
|
|
22
|
+
_engine = "better-sqlite3";
|
|
23
|
+
} catch (_) {
|
|
24
|
+
initSqlJs = require("sql.js");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getEngine() { return _engine; }
|
|
28
|
+
|
|
29
|
+
let _db = null;
|
|
30
|
+
let _SQL = null; // sql.js only
|
|
31
|
+
let _saveTimer = null; // sql.js only
|
|
32
|
+
let _inTransaction = false; // sql.js: defer saves until transaction commits
|
|
33
|
+
|
|
34
|
+
// ===== COMPATIBILITY WRAPPER (sql.js only) =====
|
|
35
|
+
// Wraps sql.js to provide a better-sqlite3-like API so data-store.js works unchanged
|
|
36
|
+
|
|
37
|
+
class CompatDB {
|
|
38
|
+
constructor(sqlDb) {
|
|
39
|
+
this._db = sqlDb;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
prepare(sql) {
|
|
43
|
+
return new CompatStatement(this._db, sql);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
exec(sql) {
|
|
47
|
+
this._db.run(sql);
|
|
48
|
+
_scheduleSave();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
transaction(fn) {
|
|
52
|
+
return (...args) => {
|
|
53
|
+
this._db.run("BEGIN TRANSACTION");
|
|
54
|
+
_inTransaction = true;
|
|
55
|
+
let result;
|
|
56
|
+
try {
|
|
57
|
+
result = fn(...args);
|
|
58
|
+
} catch (fnErr) {
|
|
59
|
+
_inTransaction = false;
|
|
60
|
+
try { this._db.run("ROLLBACK"); } catch (_) {}
|
|
61
|
+
throw fnErr;
|
|
62
|
+
}
|
|
63
|
+
_inTransaction = false;
|
|
64
|
+
this._db.run("COMMIT");
|
|
65
|
+
_scheduleSave();
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pragma(str) {
|
|
71
|
+
try {
|
|
72
|
+
this._db.run("PRAGMA " + str);
|
|
73
|
+
} catch (_) {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
backup(destPath) {
|
|
77
|
+
const data = this._db.export();
|
|
78
|
+
fs.writeFileSync(destPath, Buffer.from(data));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
close() {
|
|
82
|
+
_saveToDiskSqlJs();
|
|
83
|
+
this._db.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class CompatStatement {
|
|
88
|
+
constructor(db, sql) {
|
|
89
|
+
this._db = db;
|
|
90
|
+
this._sql = sql;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
run(...params) {
|
|
94
|
+
const flatParams = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
95
|
+
this._db.run(this._sql, flatParams);
|
|
96
|
+
_scheduleSave();
|
|
97
|
+
return { changes: this._db.getRowsModified() };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get(...params) {
|
|
101
|
+
const flatParams = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
102
|
+
let stmt;
|
|
103
|
+
try {
|
|
104
|
+
stmt = this._db.prepare(this._sql);
|
|
105
|
+
if (flatParams.length > 0) stmt.bind(flatParams);
|
|
106
|
+
if (stmt.step()) {
|
|
107
|
+
const row = stmt.getAsObject();
|
|
108
|
+
stmt.free();
|
|
109
|
+
return row;
|
|
110
|
+
}
|
|
111
|
+
stmt.free();
|
|
112
|
+
return undefined;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (stmt) try { stmt.free(); } catch (_) {}
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
all(...params) {
|
|
120
|
+
const flatParams = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
121
|
+
const results = [];
|
|
122
|
+
let stmt;
|
|
123
|
+
try {
|
|
124
|
+
stmt = this._db.prepare(this._sql);
|
|
125
|
+
if (flatParams.length > 0) stmt.bind(flatParams);
|
|
126
|
+
while (stmt.step()) {
|
|
127
|
+
results.push(stmt.getAsObject());
|
|
128
|
+
}
|
|
129
|
+
stmt.free();
|
|
130
|
+
return results;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
if (stmt) try { stmt.free(); } catch (_) {}
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ===== SAVE MANAGEMENT (sql.js only — better-sqlite3 writes directly) =====
|
|
139
|
+
|
|
140
|
+
let _lastSaveTime = 0;
|
|
141
|
+
|
|
142
|
+
function getLastSaveTime() { return _lastSaveTime; }
|
|
143
|
+
|
|
144
|
+
function _saveToDiskSqlJs() {
|
|
145
|
+
if (!_db || _engine !== "sql.js") return;
|
|
146
|
+
ensureDir(DATA_DIR);
|
|
147
|
+
_lastSaveTime = Date.now();
|
|
148
|
+
const data = _db._db.export();
|
|
149
|
+
fs.writeFileSync(DB_FILE, Buffer.from(data));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _scheduleSave() {
|
|
153
|
+
if (_engine !== "sql.js") return;
|
|
154
|
+
if (_inTransaction) return; // defer save until COMMIT
|
|
155
|
+
_saveToDiskSqlJs();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function saveToDisk() {
|
|
159
|
+
if (_engine === "sql.js") {
|
|
160
|
+
_saveToDiskSqlJs();
|
|
161
|
+
} else {
|
|
162
|
+
_lastSaveTime = Date.now();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ===== INIT =====
|
|
167
|
+
|
|
168
|
+
async function initDb() {
|
|
169
|
+
if (_db) return _db;
|
|
170
|
+
ensureDir(DATA_DIR);
|
|
171
|
+
|
|
172
|
+
if (_engine === "better-sqlite3") {
|
|
173
|
+
_db = new Database(DB_FILE);
|
|
174
|
+
const jm = _db.pragma("journal_mode = WAL");
|
|
175
|
+
_db.pragma("synchronous = NORMAL");
|
|
176
|
+
_db.pragma("foreign_keys = ON");
|
|
177
|
+
console.log(`[DB] PRAGMAs: journal_mode=${jm[0]?.journal_mode ?? jm}, synchronous=NORMAL, foreign_keys=ON`);
|
|
178
|
+
} else {
|
|
179
|
+
_SQL = await initSqlJs();
|
|
180
|
+
if (fs.existsSync(DB_FILE)) {
|
|
181
|
+
const buffer = fs.readFileSync(DB_FILE);
|
|
182
|
+
_db = new CompatDB(new _SQL.Database(buffer));
|
|
183
|
+
} else {
|
|
184
|
+
_db = new CompatDB(new _SQL.Database());
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
runMigrations(_db);
|
|
189
|
+
if (_engine === "sql.js") _saveToDiskSqlJs();
|
|
190
|
+
console.log(`[DB] Initialized with ${_engine} engine`);
|
|
191
|
+
return _db;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getDb() {
|
|
195
|
+
if (!_db) throw new Error("Database not initialized. Call initDb() first.");
|
|
196
|
+
return _db;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function reloadFromDisk() {
|
|
200
|
+
if (_engine === "better-sqlite3") {
|
|
201
|
+
if (_db) { try { _db.close(); } catch (_) {} }
|
|
202
|
+
_db = new Database(DB_FILE);
|
|
203
|
+
_db.pragma("journal_mode = WAL");
|
|
204
|
+
_db.pragma("synchronous = NORMAL");
|
|
205
|
+
_db.pragma("foreign_keys = ON");
|
|
206
|
+
} else {
|
|
207
|
+
if (!_SQL) throw new Error("Database not initialized. Call initDb() first.");
|
|
208
|
+
if (fs.existsSync(DB_FILE)) {
|
|
209
|
+
const buffer = fs.readFileSync(DB_FILE);
|
|
210
|
+
if (_db) _db._db.close();
|
|
211
|
+
_db = new CompatDB(new _SQL.Database(buffer));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function closeDb() {
|
|
217
|
+
if (_saveTimer) { clearTimeout(_saveTimer); _saveTimer = null; }
|
|
218
|
+
if (_db) {
|
|
219
|
+
if (_engine === "sql.js") _db.close();
|
|
220
|
+
else _db.close();
|
|
221
|
+
_db = null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ===== MIGRATION SYSTEM =====
|
|
226
|
+
|
|
227
|
+
const MIGRATIONS = [
|
|
228
|
+
{
|
|
229
|
+
version: 1,
|
|
230
|
+
description: "Initial schema — all 25 collections",
|
|
231
|
+
up(db) {
|
|
232
|
+
db.exec(`
|
|
233
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
234
|
+
id TEXT PRIMARY KEY,
|
|
235
|
+
data TEXT NOT NULL,
|
|
236
|
+
created_at TEXT NOT NULL,
|
|
237
|
+
last_updated TEXT NOT NULL
|
|
238
|
+
);
|
|
239
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
240
|
+
id TEXT PRIMARY KEY,
|
|
241
|
+
project_id TEXT NOT NULL,
|
|
242
|
+
status TEXT NOT NULL DEFAULT 'todo',
|
|
243
|
+
data TEXT NOT NULL,
|
|
244
|
+
created_at TEXT NOT NULL,
|
|
245
|
+
last_updated TEXT NOT NULL
|
|
246
|
+
);
|
|
247
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(project_id, status);
|
|
249
|
+
CREATE TABLE IF NOT EXISTS systems (
|
|
250
|
+
id TEXT PRIMARY KEY,
|
|
251
|
+
project_id TEXT NOT NULL,
|
|
252
|
+
data TEXT NOT NULL,
|
|
253
|
+
created_at TEXT NOT NULL,
|
|
254
|
+
last_updated TEXT NOT NULL
|
|
255
|
+
);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_systems_project ON systems(project_id);
|
|
257
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
258
|
+
id TEXT PRIMARY KEY,
|
|
259
|
+
project_id TEXT NOT NULL,
|
|
260
|
+
date TEXT NOT NULL,
|
|
261
|
+
data TEXT NOT NULL,
|
|
262
|
+
created_at TEXT NOT NULL
|
|
263
|
+
);
|
|
264
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
265
|
+
CREATE TABLE IF NOT EXISTS changelog (
|
|
266
|
+
id TEXT PRIMARY KEY,
|
|
267
|
+
project_id TEXT NOT NULL,
|
|
268
|
+
date TEXT NOT NULL,
|
|
269
|
+
data TEXT NOT NULL,
|
|
270
|
+
created_at TEXT NOT NULL
|
|
271
|
+
);
|
|
272
|
+
CREATE INDEX IF NOT EXISTS idx_changelog_project ON changelog(project_id);
|
|
273
|
+
CREATE TABLE IF NOT EXISTS bugs (
|
|
274
|
+
id TEXT PRIMARY KEY,
|
|
275
|
+
project_id TEXT NOT NULL,
|
|
276
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
277
|
+
data TEXT NOT NULL,
|
|
278
|
+
created_at TEXT NOT NULL
|
|
279
|
+
);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_bugs_project ON bugs(project_id);
|
|
281
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
282
|
+
id TEXT PRIMARY KEY,
|
|
283
|
+
project_id TEXT NOT NULL,
|
|
284
|
+
date TEXT NOT NULL,
|
|
285
|
+
data TEXT NOT NULL,
|
|
286
|
+
created_at TEXT NOT NULL
|
|
287
|
+
);
|
|
288
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_project ON decisions(project_id);
|
|
289
|
+
CREATE TABLE IF NOT EXISTS blueprints (
|
|
290
|
+
id TEXT PRIMARY KEY,
|
|
291
|
+
project_id TEXT NOT NULL,
|
|
292
|
+
data TEXT NOT NULL,
|
|
293
|
+
last_updated TEXT NOT NULL
|
|
294
|
+
);
|
|
295
|
+
CREATE INDEX IF NOT EXISTS idx_blueprints_project ON blueprints(project_id);
|
|
296
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
297
|
+
id TEXT PRIMARY KEY,
|
|
298
|
+
project_id TEXT NOT NULL,
|
|
299
|
+
parent_type TEXT,
|
|
300
|
+
parent_id TEXT,
|
|
301
|
+
data TEXT NOT NULL,
|
|
302
|
+
created_at TEXT NOT NULL
|
|
303
|
+
);
|
|
304
|
+
CREATE INDEX IF NOT EXISTS idx_notes_project ON notes(project_id);
|
|
305
|
+
CREATE TABLE IF NOT EXISTS backups_log (
|
|
306
|
+
id TEXT PRIMARY KEY,
|
|
307
|
+
project_id TEXT NOT NULL,
|
|
308
|
+
date TEXT NOT NULL,
|
|
309
|
+
data TEXT NOT NULL,
|
|
310
|
+
created_at TEXT NOT NULL
|
|
311
|
+
);
|
|
312
|
+
CREATE INDEX IF NOT EXISTS idx_backups_project ON backups_log(project_id);
|
|
313
|
+
CREATE TABLE IF NOT EXISTS levels (
|
|
314
|
+
id TEXT PRIMARY KEY,
|
|
315
|
+
project_id TEXT NOT NULL,
|
|
316
|
+
data TEXT NOT NULL,
|
|
317
|
+
created_at TEXT NOT NULL,
|
|
318
|
+
last_updated TEXT NOT NULL
|
|
319
|
+
);
|
|
320
|
+
CREATE INDEX IF NOT EXISTS idx_levels_project ON levels(project_id);
|
|
321
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
322
|
+
id TEXT PRIMARY KEY,
|
|
323
|
+
project_id TEXT NOT NULL,
|
|
324
|
+
data TEXT NOT NULL,
|
|
325
|
+
created_at TEXT NOT NULL,
|
|
326
|
+
last_updated TEXT NOT NULL
|
|
327
|
+
);
|
|
328
|
+
CREATE INDEX IF NOT EXISTS idx_plugins_project ON plugins(project_id);
|
|
329
|
+
CREATE TABLE IF NOT EXISTS build_errors (
|
|
330
|
+
id TEXT PRIMARY KEY,
|
|
331
|
+
project_id TEXT NOT NULL,
|
|
332
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
333
|
+
data TEXT NOT NULL,
|
|
334
|
+
created_at TEXT NOT NULL
|
|
335
|
+
);
|
|
336
|
+
CREATE INDEX IF NOT EXISTS idx_build_errors_project ON build_errors(project_id);
|
|
337
|
+
CREATE TABLE IF NOT EXISTS optimize_items (
|
|
338
|
+
id TEXT PRIMARY KEY,
|
|
339
|
+
project_id TEXT NOT NULL,
|
|
340
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
341
|
+
data TEXT NOT NULL,
|
|
342
|
+
created_at TEXT NOT NULL
|
|
343
|
+
);
|
|
344
|
+
CREATE INDEX IF NOT EXISTS idx_optimize_project ON optimize_items(project_id);
|
|
345
|
+
CREATE TABLE IF NOT EXISTS perf_budget (
|
|
346
|
+
id TEXT PRIMARY KEY,
|
|
347
|
+
project_id TEXT NOT NULL,
|
|
348
|
+
data TEXT NOT NULL,
|
|
349
|
+
created_at TEXT NOT NULL
|
|
350
|
+
);
|
|
351
|
+
CREATE INDEX IF NOT EXISTS idx_perf_project ON perf_budget(project_id);
|
|
352
|
+
CREATE TABLE IF NOT EXISTS playtests (
|
|
353
|
+
id TEXT PRIMARY KEY,
|
|
354
|
+
project_id TEXT NOT NULL,
|
|
355
|
+
date TEXT NOT NULL,
|
|
356
|
+
data TEXT NOT NULL,
|
|
357
|
+
created_at TEXT NOT NULL
|
|
358
|
+
);
|
|
359
|
+
CREATE INDEX IF NOT EXISTS idx_playtests_project ON playtests(project_id);
|
|
360
|
+
CREATE TABLE IF NOT EXISTS milestones (
|
|
361
|
+
id TEXT PRIMARY KEY,
|
|
362
|
+
project_id TEXT NOT NULL,
|
|
363
|
+
status TEXT NOT NULL DEFAULT 'upcoming',
|
|
364
|
+
data TEXT NOT NULL,
|
|
365
|
+
created_at TEXT NOT NULL
|
|
366
|
+
);
|
|
367
|
+
CREATE INDEX IF NOT EXISTS idx_milestones_project ON milestones(project_id);
|
|
368
|
+
CREATE TABLE IF NOT EXISTS iterations (
|
|
369
|
+
id TEXT PRIMARY KEY,
|
|
370
|
+
project_id TEXT NOT NULL,
|
|
371
|
+
data TEXT NOT NULL,
|
|
372
|
+
created_at TEXT NOT NULL
|
|
373
|
+
);
|
|
374
|
+
CREATE INDEX IF NOT EXISTS idx_iterations_project ON iterations(project_id);
|
|
375
|
+
CREATE TABLE IF NOT EXISTS dialogues (
|
|
376
|
+
id TEXT PRIMARY KEY,
|
|
377
|
+
project_id TEXT NOT NULL,
|
|
378
|
+
data TEXT NOT NULL,
|
|
379
|
+
created_at TEXT NOT NULL
|
|
380
|
+
);
|
|
381
|
+
CREATE INDEX IF NOT EXISTS idx_dialogues_project ON dialogues(project_id);
|
|
382
|
+
CREATE TABLE IF NOT EXISTS sounds (
|
|
383
|
+
id TEXT PRIMARY KEY,
|
|
384
|
+
project_id TEXT NOT NULL,
|
|
385
|
+
data TEXT NOT NULL,
|
|
386
|
+
created_at TEXT NOT NULL
|
|
387
|
+
);
|
|
388
|
+
CREATE INDEX IF NOT EXISTS idx_sounds_project ON sounds(project_id);
|
|
389
|
+
CREATE TABLE IF NOT EXISTS controls (
|
|
390
|
+
id TEXT PRIMARY KEY,
|
|
391
|
+
project_id TEXT NOT NULL,
|
|
392
|
+
data TEXT NOT NULL,
|
|
393
|
+
created_at TEXT NOT NULL
|
|
394
|
+
);
|
|
395
|
+
CREATE INDEX IF NOT EXISTS idx_controls_project ON controls(project_id);
|
|
396
|
+
CREATE TABLE IF NOT EXISTS assets (
|
|
397
|
+
id TEXT PRIMARY KEY,
|
|
398
|
+
project_id TEXT NOT NULL,
|
|
399
|
+
status TEXT NOT NULL DEFAULT 'concept',
|
|
400
|
+
data TEXT NOT NULL,
|
|
401
|
+
created_at TEXT NOT NULL,
|
|
402
|
+
last_updated TEXT NOT NULL
|
|
403
|
+
);
|
|
404
|
+
CREATE INDEX IF NOT EXISTS idx_assets_project ON assets(project_id);
|
|
405
|
+
CREATE TABLE IF NOT EXISTS refs (
|
|
406
|
+
id TEXT PRIMARY KEY,
|
|
407
|
+
project_id TEXT NOT NULL,
|
|
408
|
+
data TEXT NOT NULL,
|
|
409
|
+
created_at TEXT NOT NULL
|
|
410
|
+
);
|
|
411
|
+
CREATE INDEX IF NOT EXISTS idx_refs_project ON refs(project_id);
|
|
412
|
+
CREATE TABLE IF NOT EXISTS marketing_items (
|
|
413
|
+
id TEXT PRIMARY KEY,
|
|
414
|
+
project_id TEXT NOT NULL,
|
|
415
|
+
status TEXT NOT NULL DEFAULT 'todo',
|
|
416
|
+
data TEXT NOT NULL,
|
|
417
|
+
created_at TEXT NOT NULL
|
|
418
|
+
);
|
|
419
|
+
CREATE INDEX IF NOT EXISTS idx_marketing_project ON marketing_items(project_id);
|
|
420
|
+
CREATE TABLE IF NOT EXISTS wiki_pages (
|
|
421
|
+
id TEXT PRIMARY KEY,
|
|
422
|
+
project_id TEXT NOT NULL,
|
|
423
|
+
data TEXT NOT NULL,
|
|
424
|
+
created_at TEXT NOT NULL,
|
|
425
|
+
last_updated TEXT NOT NULL
|
|
426
|
+
);
|
|
427
|
+
CREATE INDEX IF NOT EXISTS idx_wiki_project ON wiki_pages(project_id);
|
|
428
|
+
CREATE TABLE IF NOT EXISTS ship_checked (
|
|
429
|
+
project_id TEXT NOT NULL,
|
|
430
|
+
step_id TEXT NOT NULL,
|
|
431
|
+
PRIMARY KEY (project_id, step_id)
|
|
432
|
+
);
|
|
433
|
+
CREATE TABLE IF NOT EXISTS open_questions (
|
|
434
|
+
id TEXT PRIMARY KEY,
|
|
435
|
+
project_id TEXT NOT NULL,
|
|
436
|
+
resolved INTEGER NOT NULL DEFAULT 0,
|
|
437
|
+
data TEXT NOT NULL,
|
|
438
|
+
created_at TEXT NOT NULL
|
|
439
|
+
);
|
|
440
|
+
CREATE INDEX IF NOT EXISTS idx_questions_project ON open_questions(project_id);
|
|
441
|
+
`);
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
version: 2,
|
|
446
|
+
description: "Add user_settings key-value table",
|
|
447
|
+
up(db) {
|
|
448
|
+
db.exec(`
|
|
449
|
+
CREATE TABLE IF NOT EXISTS user_settings (
|
|
450
|
+
key TEXT PRIMARY KEY,
|
|
451
|
+
value TEXT NOT NULL
|
|
452
|
+
);
|
|
453
|
+
`);
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
version: 3,
|
|
458
|
+
description: "Add attachments table for file attachments on tasks",
|
|
459
|
+
up(db) {
|
|
460
|
+
db.exec(`
|
|
461
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
462
|
+
id TEXT PRIMARY KEY,
|
|
463
|
+
card_id TEXT NOT NULL,
|
|
464
|
+
project_id TEXT NOT NULL,
|
|
465
|
+
name TEXT NOT NULL,
|
|
466
|
+
type TEXT DEFAULT '',
|
|
467
|
+
size INTEGER DEFAULT 0,
|
|
468
|
+
data TEXT,
|
|
469
|
+
time TEXT NOT NULL,
|
|
470
|
+
FOREIGN KEY (card_id) REFERENCES tasks(id) ON DELETE CASCADE
|
|
471
|
+
);
|
|
472
|
+
CREATE INDEX IF NOT EXISTS idx_attachments_card ON attachments(card_id);
|
|
473
|
+
`);
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
version: 4,
|
|
478
|
+
description: "Add sprints table and sprint_id column on tasks",
|
|
479
|
+
up(db) {
|
|
480
|
+
db.exec(`
|
|
481
|
+
CREATE TABLE IF NOT EXISTS sprints (
|
|
482
|
+
id TEXT PRIMARY KEY,
|
|
483
|
+
project_id TEXT NOT NULL,
|
|
484
|
+
name TEXT NOT NULL,
|
|
485
|
+
start_date TEXT NOT NULL,
|
|
486
|
+
end_date TEXT NOT NULL,
|
|
487
|
+
status TEXT NOT NULL DEFAULT 'planned',
|
|
488
|
+
milestone TEXT DEFAULT NULL,
|
|
489
|
+
template TEXT DEFAULT NULL,
|
|
490
|
+
color TEXT DEFAULT '#FDBA74',
|
|
491
|
+
completed_tasks INTEGER DEFAULT 0,
|
|
492
|
+
total_tasks INTEGER DEFAULT 0,
|
|
493
|
+
completed_bugs INTEGER DEFAULT 0,
|
|
494
|
+
created_at TEXT NOT NULL,
|
|
495
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
496
|
+
);
|
|
497
|
+
CREATE INDEX IF NOT EXISTS idx_sprints_project ON sprints(project_id);
|
|
498
|
+
CREATE INDEX IF NOT EXISTS idx_sprints_status ON sprints(status);
|
|
499
|
+
`);
|
|
500
|
+
// Add sprint_id column to tasks — safe to run even if column exists
|
|
501
|
+
try {
|
|
502
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN sprint_id TEXT DEFAULT NULL REFERENCES sprints(id) ON DELETE SET NULL`);
|
|
503
|
+
} catch (_) { /* column already exists */ }
|
|
504
|
+
try {
|
|
505
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_sprint ON tasks(sprint_id)`);
|
|
506
|
+
} catch (_) {}
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
version: 5,
|
|
511
|
+
description: "Add time estimates and time_logs table",
|
|
512
|
+
up(db) {
|
|
513
|
+
try {
|
|
514
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN estimate_hours REAL DEFAULT NULL`);
|
|
515
|
+
} catch (_) { /* column already exists */ }
|
|
516
|
+
try {
|
|
517
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN actual_hours REAL DEFAULT 0`);
|
|
518
|
+
} catch (_) { /* column already exists */ }
|
|
519
|
+
db.exec(`
|
|
520
|
+
CREATE TABLE IF NOT EXISTS time_logs (
|
|
521
|
+
id TEXT PRIMARY KEY,
|
|
522
|
+
card_id TEXT NOT NULL,
|
|
523
|
+
project_id TEXT NOT NULL,
|
|
524
|
+
hours REAL NOT NULL,
|
|
525
|
+
note TEXT DEFAULT '',
|
|
526
|
+
time TEXT NOT NULL,
|
|
527
|
+
FOREIGN KEY (card_id) REFERENCES tasks(id) ON DELETE CASCADE
|
|
528
|
+
);
|
|
529
|
+
CREATE INDEX IF NOT EXISTS idx_time_logs_card ON time_logs(card_id);
|
|
530
|
+
CREATE INDEX IF NOT EXISTS idx_time_logs_project ON time_logs(project_id);
|
|
531
|
+
`);
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
version: 6,
|
|
536
|
+
description: "Add sprint time totals and bug fixed_at timestamp",
|
|
537
|
+
up(db) {
|
|
538
|
+
try { db.exec("ALTER TABLE sprints ADD COLUMN estimate_total REAL DEFAULT 0"); } catch (_) {}
|
|
539
|
+
try { db.exec("ALTER TABLE sprints ADD COLUMN actual_total REAL DEFAULT 0"); } catch (_) {}
|
|
540
|
+
try { db.exec("ALTER TABLE bugs ADD COLUMN fixed_at TEXT DEFAULT NULL"); } catch (_) {}
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
version: 7,
|
|
545
|
+
description: "Add GDD tables — sections, linked tasks, revisions",
|
|
546
|
+
up(db) {
|
|
547
|
+
db.exec(`
|
|
548
|
+
CREATE TABLE IF NOT EXISTS gdd_sections (
|
|
549
|
+
id TEXT PRIMARY KEY,
|
|
550
|
+
project_id TEXT NOT NULL,
|
|
551
|
+
section_key TEXT NOT NULL,
|
|
552
|
+
status TEXT DEFAULT 'draft',
|
|
553
|
+
content TEXT DEFAULT '',
|
|
554
|
+
updated_at TEXT NOT NULL,
|
|
555
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
|
556
|
+
UNIQUE(project_id, section_key)
|
|
557
|
+
);
|
|
558
|
+
CREATE INDEX IF NOT EXISTS idx_gdd_project ON gdd_sections(project_id);
|
|
559
|
+
|
|
560
|
+
CREATE TABLE IF NOT EXISTS gdd_linked_tasks (
|
|
561
|
+
id TEXT PRIMARY KEY,
|
|
562
|
+
gdd_section_id TEXT NOT NULL,
|
|
563
|
+
task_id TEXT NOT NULL,
|
|
564
|
+
task_title TEXT NOT NULL,
|
|
565
|
+
created_at TEXT NOT NULL,
|
|
566
|
+
FOREIGN KEY (gdd_section_id) REFERENCES gdd_sections(id) ON DELETE CASCADE,
|
|
567
|
+
UNIQUE(gdd_section_id, task_id)
|
|
568
|
+
);
|
|
569
|
+
CREATE INDEX IF NOT EXISTS idx_gdd_links_section ON gdd_linked_tasks(gdd_section_id);
|
|
570
|
+
CREATE INDEX IF NOT EXISTS idx_gdd_links_task ON gdd_linked_tasks(task_id);
|
|
571
|
+
|
|
572
|
+
CREATE TABLE IF NOT EXISTS gdd_revisions (
|
|
573
|
+
id TEXT PRIMARY KEY,
|
|
574
|
+
gdd_section_id TEXT NOT NULL,
|
|
575
|
+
content TEXT NOT NULL,
|
|
576
|
+
author TEXT DEFAULT '',
|
|
577
|
+
created_at TEXT NOT NULL,
|
|
578
|
+
FOREIGN KEY (gdd_section_id) REFERENCES gdd_sections(id) ON DELETE CASCADE
|
|
579
|
+
);
|
|
580
|
+
CREATE INDEX IF NOT EXISTS idx_gdd_revisions_section ON gdd_revisions(gdd_section_id);
|
|
581
|
+
`);
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
version: 8,
|
|
586
|
+
description: "Add task assignment + team_members table",
|
|
587
|
+
up(db) {
|
|
588
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN assignee TEXT DEFAULT NULL`);
|
|
589
|
+
db.exec(`
|
|
590
|
+
CREATE TABLE IF NOT EXISTS team_members (
|
|
591
|
+
id TEXT PRIMARY KEY,
|
|
592
|
+
name TEXT NOT NULL,
|
|
593
|
+
avatar TEXT NOT NULL DEFAULT '',
|
|
594
|
+
color TEXT NOT NULL DEFAULT '',
|
|
595
|
+
status TEXT DEFAULT 'offline',
|
|
596
|
+
user_id TEXT,
|
|
597
|
+
created_at TEXT NOT NULL
|
|
598
|
+
);
|
|
599
|
+
`);
|
|
600
|
+
// Team members table created — no seed data for production
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
version: 9,
|
|
605
|
+
description: "Add notifications and notification_settings tables",
|
|
606
|
+
up(db) {
|
|
607
|
+
db.exec(`
|
|
608
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
609
|
+
id TEXT PRIMARY KEY,
|
|
610
|
+
project_id TEXT NOT NULL,
|
|
611
|
+
type TEXT NOT NULL,
|
|
612
|
+
text TEXT NOT NULL,
|
|
613
|
+
from_member TEXT DEFAULT NULL,
|
|
614
|
+
action_tab TEXT DEFAULT 'dashboard',
|
|
615
|
+
action_id TEXT DEFAULT NULL,
|
|
616
|
+
read INTEGER DEFAULT 0,
|
|
617
|
+
created_at TEXT NOT NULL,
|
|
618
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
619
|
+
);
|
|
620
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_project ON notifications(project_id);
|
|
621
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(project_id, read);
|
|
622
|
+
|
|
623
|
+
CREATE TABLE IF NOT EXISTS notification_settings (
|
|
624
|
+
id TEXT PRIMARY KEY,
|
|
625
|
+
user_id TEXT NOT NULL,
|
|
626
|
+
category TEXT NOT NULL,
|
|
627
|
+
enabled INTEGER DEFAULT 1,
|
|
628
|
+
UNIQUE(user_id, category)
|
|
629
|
+
);
|
|
630
|
+
`);
|
|
631
|
+
// Notification settings and notifications created dynamically per user — no seed data
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
version: 10,
|
|
636
|
+
description: "Add UE5 sync tables: sync_channels, sync_log, sync_settings, ue_discovered_bps",
|
|
637
|
+
up(db) {
|
|
638
|
+
db.exec(`
|
|
639
|
+
CREATE TABLE IF NOT EXISTS sync_channels (
|
|
640
|
+
id TEXT PRIMARY KEY,
|
|
641
|
+
project_id TEXT NOT NULL,
|
|
642
|
+
channel TEXT NOT NULL,
|
|
643
|
+
enabled INTEGER DEFAULT 1,
|
|
644
|
+
auto_sync INTEGER DEFAULT 1,
|
|
645
|
+
last_sync TEXT DEFAULT NULL,
|
|
646
|
+
synced_count INTEGER DEFAULT 0,
|
|
647
|
+
status TEXT DEFAULT 'idle',
|
|
648
|
+
UNIQUE(project_id, channel),
|
|
649
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
CREATE TABLE IF NOT EXISTS sync_log (
|
|
653
|
+
id TEXT PRIMARY KEY,
|
|
654
|
+
project_id TEXT NOT NULL,
|
|
655
|
+
channel TEXT NOT NULL,
|
|
656
|
+
action TEXT NOT NULL,
|
|
657
|
+
type TEXT NOT NULL DEFAULT 'info',
|
|
658
|
+
created_at TEXT NOT NULL,
|
|
659
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
660
|
+
);
|
|
661
|
+
CREATE INDEX IF NOT EXISTS idx_sync_log_project ON sync_log(project_id);
|
|
662
|
+
|
|
663
|
+
CREATE TABLE IF NOT EXISTS sync_settings (
|
|
664
|
+
id TEXT PRIMARY KEY,
|
|
665
|
+
project_id TEXT NOT NULL,
|
|
666
|
+
auto_sync_master INTEGER DEFAULT 1,
|
|
667
|
+
sync_interval INTEGER DEFAULT 30,
|
|
668
|
+
UNIQUE(project_id),
|
|
669
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
CREATE TABLE IF NOT EXISTS ue_discovered_bps (
|
|
673
|
+
id TEXT PRIMARY KEY,
|
|
674
|
+
project_id TEXT NOT NULL,
|
|
675
|
+
name TEXT NOT NULL,
|
|
676
|
+
path TEXT NOT NULL,
|
|
677
|
+
parent_class TEXT DEFAULT 'Actor',
|
|
678
|
+
synced INTEGER DEFAULT 0,
|
|
679
|
+
is_new INTEGER DEFAULT 1,
|
|
680
|
+
discovered_at TEXT NOT NULL,
|
|
681
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
682
|
+
);
|
|
683
|
+
`);
|
|
684
|
+
// Seed per-project defaults
|
|
685
|
+
try {
|
|
686
|
+
const row = db.prepare("SELECT id FROM projects LIMIT 1").get();
|
|
687
|
+
if (row) {
|
|
688
|
+
const pid = row.id;
|
|
689
|
+
db.exec(`
|
|
690
|
+
INSERT OR IGNORE INTO sync_channels (id, project_id, channel, enabled, auto_sync, last_sync, synced_count, status) VALUES
|
|
691
|
+
('sc_bp', '${pid}', 'blueprints', 1, 1, NULL, 0, 'idle'),
|
|
692
|
+
('sc_lvl', '${pid}', 'levels', 1, 1, NULL, 0, 'idle'),
|
|
693
|
+
('sc_err', '${pid}', 'buildErrors', 1, 1, NULL, 0, 'idle'),
|
|
694
|
+
('sc_act', '${pid}', 'actors', 0, 0, NULL, 0, 'idle');
|
|
695
|
+
|
|
696
|
+
INSERT OR IGNORE INTO sync_settings (id, project_id, auto_sync_master, sync_interval) VALUES
|
|
697
|
+
('ss_1', '${pid}', 1, 30);
|
|
698
|
+
|
|
699
|
+
INSERT OR IGNORE INTO sync_log (id, project_id, channel, action, type, created_at) VALUES
|
|
700
|
+
('sl1', '${pid}', 'blueprints', 'Synced 5 blueprints from UE5', 'success', datetime('now', '-10 minutes')),
|
|
701
|
+
('sl2', '${pid}', 'levels', 'Synced 3 levels: CursedVillage_Main, Graveyard, ChurchRuins', 'success', datetime('now', '-10 minutes')),
|
|
702
|
+
('sl3', '${pid}', 'buildErrors', 'Captured 2 compile errors from Output Log', 'warning', datetime('now', '-12 minutes')),
|
|
703
|
+
('sl4', '${pid}', 'system', 'Connected to Unreal Engine 5.5.1 — CursedVillage project', 'info', datetime('now', '-14 minutes')),
|
|
704
|
+
('sl5', '${pid}', 'blueprints', 'Detected new BP: BP_HorrorMaiden — auto-registered', 'success', datetime('now', '-27 minutes')),
|
|
705
|
+
('sl6', '${pid}', 'buildErrors', 'Build error resolved: BP_LoopGate unresolved external', 'info', datetime('now', '-42 minutes'));
|
|
706
|
+
|
|
707
|
+
INSERT OR IGNORE INTO ue_discovered_bps (id, project_id, name, path, parent_class, synced, is_new, discovered_at) VALUES
|
|
708
|
+
('ubp1', '${pid}', 'BP_LoopGate', '/Game/CursedVillage/Blueprints/BP_LoopGate', 'Actor', 1, 0, datetime('now', '-2 hours')),
|
|
709
|
+
('ubp2', '${pid}', 'Trigger_TakePhoto', '/Game/HorrorEngine/Blueprints/Assets/Triggers/Trigger_TakePhoto', 'Actor', 1, 0, datetime('now', '-2 hours')),
|
|
710
|
+
('ubp3', '${pid}', 'BP_VillageGameManager', '/Game/CursedVillage/Blueprints/BP_VillageGameManager', 'GameModeBase', 1, 0, datetime('now', '-2 hours')),
|
|
711
|
+
('ubp4', '${pid}', 'BP_AnomalyBase', '/Game/AnomalyFramework/Blueprints/BP_AnomalyBase', 'Actor', 1, 0, datetime('now', '-2 hours')),
|
|
712
|
+
('ubp5', '${pid}', 'BP_HorrorMaiden', '/Game/CursedVillage/Blueprints/BP_HorrorMaiden', 'Actor', 1, 0, datetime('now', '-27 minutes')),
|
|
713
|
+
('ubp6', '${pid}', 'BP_FogManager', '/Game/CursedVillage/Blueprints/BP_FogManager', 'Actor', 0, 1, datetime('now', '-5 minutes')),
|
|
714
|
+
('ubp7', '${pid}', 'BP_AmbientSoundZone', '/Game/CursedVillage/Blueprints/BP_AmbientSoundZone', 'Actor', 0, 1, datetime('now', '-3 minutes'));
|
|
715
|
+
`);
|
|
716
|
+
}
|
|
717
|
+
} catch (_) {}
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
version: 11,
|
|
722
|
+
description: "Add Discord pipeline tables: discord_config, discord_submissions, discord_updates",
|
|
723
|
+
up(db) {
|
|
724
|
+
db.exec(`
|
|
725
|
+
CREATE TABLE IF NOT EXISTS discord_config (
|
|
726
|
+
id TEXT PRIMARY KEY,
|
|
727
|
+
project_id TEXT NOT NULL,
|
|
728
|
+
mode TEXT DEFAULT 'hosted',
|
|
729
|
+
bot_token TEXT DEFAULT NULL,
|
|
730
|
+
server_id TEXT DEFAULT NULL,
|
|
731
|
+
server_name TEXT DEFAULT NULL,
|
|
732
|
+
server_icon TEXT DEFAULT NULL,
|
|
733
|
+
member_count INTEGER DEFAULT 0,
|
|
734
|
+
channel_bugs TEXT DEFAULT '#bug-reports',
|
|
735
|
+
channel_suggestions TEXT DEFAULT '#feature-requests',
|
|
736
|
+
channel_updates TEXT DEFAULT '#dev-updates',
|
|
737
|
+
auto_post_updates INTEGER DEFAULT 1,
|
|
738
|
+
sync_upvotes INTEGER DEFAULT 1,
|
|
739
|
+
connected INTEGER DEFAULT 0,
|
|
740
|
+
connected_at TEXT DEFAULT NULL,
|
|
741
|
+
UNIQUE(project_id),
|
|
742
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
CREATE TABLE IF NOT EXISTS discord_submissions (
|
|
746
|
+
id TEXT PRIMARY KEY,
|
|
747
|
+
project_id TEXT NOT NULL,
|
|
748
|
+
type TEXT NOT NULL,
|
|
749
|
+
title TEXT NOT NULL,
|
|
750
|
+
author TEXT NOT NULL,
|
|
751
|
+
author_avatar TEXT DEFAULT '',
|
|
752
|
+
upvotes INTEGER DEFAULT 0,
|
|
753
|
+
status TEXT DEFAULT 'open',
|
|
754
|
+
imported INTEGER DEFAULT 0,
|
|
755
|
+
discord_message_id TEXT DEFAULT NULL,
|
|
756
|
+
created_at TEXT NOT NULL,
|
|
757
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
758
|
+
);
|
|
759
|
+
CREATE INDEX IF NOT EXISTS idx_discord_subs_project ON discord_submissions(project_id);
|
|
760
|
+
|
|
761
|
+
CREATE TABLE IF NOT EXISTS discord_updates (
|
|
762
|
+
id TEXT PRIMARY KEY,
|
|
763
|
+
project_id TEXT NOT NULL,
|
|
764
|
+
type TEXT NOT NULL,
|
|
765
|
+
title TEXT NOT NULL,
|
|
766
|
+
channel TEXT NOT NULL,
|
|
767
|
+
posted_at TEXT NOT NULL,
|
|
768
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
769
|
+
);
|
|
770
|
+
`);
|
|
771
|
+
// Seed sample data
|
|
772
|
+
try {
|
|
773
|
+
const row = db.prepare("SELECT id FROM projects LIMIT 1").get();
|
|
774
|
+
if (row) {
|
|
775
|
+
const pid = row.id;
|
|
776
|
+
db.exec(`
|
|
777
|
+
INSERT OR IGNORE INTO discord_submissions (id, project_id, type, title, author, author_avatar, upvotes, status, imported, created_at) VALUES
|
|
778
|
+
('ds1', '${pid}', 'bug', 'Graveyard fog clips through walls near gate B', 'ghosthunter42', 'G', 14, 'open', 0, datetime('now', '-6 hours')),
|
|
779
|
+
('ds2', '${pid}', 'suggestion', 'Add a flashlight mechanic for darker areas', 'HorrorFanatic', 'H', 31, 'open', 0, datetime('now', '-1 day')),
|
|
780
|
+
('ds3', '${pid}', 'bug', 'Bell tower audio cuts out after round 5', 'SoundDesignNerd', 'S', 8, 'imported', 1, datetime('now', '-1 day', '-6 hours')),
|
|
781
|
+
('ds4', '${pid}', 'suggestion', 'Let players review photos they''ve taken in a gallery', 'PixelSnapper', 'P', 52, 'open', 0, datetime('now', '-2 days')),
|
|
782
|
+
('ds5', '${pid}', 'bug', 'Anomaly doesn''t despawn when photographed from far away','SpeedrunnerMax', 'S', 22, 'imported', 1, datetime('now', '-2 days', '-6 hours')),
|
|
783
|
+
('ds6', '${pid}', 'suggestion', 'Ambient music between rounds would be great', 'ghosthunter42', 'G', 7, 'dismissed', 0, datetime('now', '-3 days')),
|
|
784
|
+
('ds7', '${pid}', 'bug', 'Player can clip through church ruins wall', 'GlitchFinder99', 'G', 19, 'open', 0, datetime('now', '-10 hours'));
|
|
785
|
+
|
|
786
|
+
INSERT OR IGNORE INTO discord_updates (id, project_id, type, title, channel, posted_at) VALUES
|
|
787
|
+
('du1', '${pid}', 'bugfix', 'Fixed: Camera zoom stuck after taking photo', '#dev-updates', datetime('now', '-8 hours')),
|
|
788
|
+
('du2', '${pid}', 'feature', 'New: Round indicator bell chimes added', '#dev-updates', datetime('now', '-1 day', '-8 hours')),
|
|
789
|
+
('du3', '${pid}', 'bugfix', 'Fixed: Gate teleport infinite loop crash', '#dev-updates', datetime('now', '-2 days'));
|
|
790
|
+
`);
|
|
791
|
+
}
|
|
792
|
+
} catch (_) {}
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
version: 12,
|
|
797
|
+
description: "Add builds table for build pipeline dashboard",
|
|
798
|
+
up(db) {
|
|
799
|
+
db.exec(`
|
|
800
|
+
CREATE TABLE IF NOT EXISTS builds (
|
|
801
|
+
id TEXT PRIMARY KEY,
|
|
802
|
+
project_id TEXT NOT NULL,
|
|
803
|
+
version TEXT NOT NULL,
|
|
804
|
+
platform TEXT DEFAULT 'Win64',
|
|
805
|
+
config TEXT DEFAULT 'Development',
|
|
806
|
+
status TEXT DEFAULT 'success',
|
|
807
|
+
size TEXT DEFAULT '—',
|
|
808
|
+
duration TEXT DEFAULT '—',
|
|
809
|
+
date TEXT NOT NULL,
|
|
810
|
+
playtest_id TEXT DEFAULT NULL,
|
|
811
|
+
errors INTEGER DEFAULT 0,
|
|
812
|
+
notes TEXT DEFAULT '',
|
|
813
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
814
|
+
);
|
|
815
|
+
CREATE INDEX IF NOT EXISTS idx_builds_project ON builds(project_id);
|
|
816
|
+
`);
|
|
817
|
+
// Seed 8 sample builds
|
|
818
|
+
try {
|
|
819
|
+
const row = db.prepare("SELECT id FROM projects LIMIT 1").get();
|
|
820
|
+
if (row) {
|
|
821
|
+
const pid = row.id;
|
|
822
|
+
db.exec(`
|
|
823
|
+
INSERT OR IGNORE INTO builds (id, project_id, version, platform, config, status, size, duration, date, playtest_id, errors, notes) VALUES
|
|
824
|
+
('b1', '${pid}', '0.4.2', 'Win64', 'Development', 'success', '2.8 GB', '8m 42s', datetime('now', '-4 hours'), NULL, 0, 'Added photo mechanic + gate loop system'),
|
|
825
|
+
('b2', '${pid}', '0.4.1', 'Win64', 'Development', 'success', '2.7 GB', '8m 15s', datetime('now', '-2 days'), 'pt3', 0, 'Bell tower audio + anomaly framework integration'),
|
|
826
|
+
('b3', '${pid}', '0.4.0', 'Win64', 'Shipping', 'success', '2.4 GB', '12m 03s', datetime('now', '-4 days'), 'pt2', 0, 'First shipping build — demo candidate'),
|
|
827
|
+
('b4', '${pid}', '0.3.9', 'Win64', 'Development', 'failed', '—', '3m 11s', datetime('now', '-5 days'), NULL, 4, 'Compile error in BP_LoopGate — unresolved reference'),
|
|
828
|
+
('b5', '${pid}', '0.3.8', 'Win64', 'Development', 'success', '2.3 GB', '7m 58s', datetime('now', '-7 days'), 'pt1', 0, 'Initial gate system + fog volumes'),
|
|
829
|
+
('b6', '${pid}', '0.3.7', 'Win64', 'Development', 'success', '2.2 GB', '7m 40s', datetime('now', '-9 days'), NULL, 0, 'World-space interact icons + M_InteractIcon material'),
|
|
830
|
+
('b7', '${pid}', '0.3.6', 'Win64', 'Development', 'failed', '—', '1m 52s', datetime('now', '-11 days'), NULL, 2, 'Missing plugin dependency — Horror Engine SE'),
|
|
831
|
+
('b8', '${pid}', '0.3.5', 'Win64', 'Development', 'success', '2.1 GB', '7m 22s', datetime('now', '-13 days'), NULL, 0, 'Base motel level + player controller');
|
|
832
|
+
`);
|
|
833
|
+
}
|
|
834
|
+
} catch (_) {}
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
version: 13,
|
|
839
|
+
description: "Add devlogs table for devlog generator",
|
|
840
|
+
up(db) {
|
|
841
|
+
db.exec(`
|
|
842
|
+
CREATE TABLE IF NOT EXISTS devlogs (
|
|
843
|
+
id TEXT PRIMARY KEY,
|
|
844
|
+
project_id TEXT NOT NULL,
|
|
845
|
+
title TEXT DEFAULT '',
|
|
846
|
+
template TEXT DEFAULT 'patch',
|
|
847
|
+
format TEXT DEFAULT 'markdown',
|
|
848
|
+
source_entry_ids TEXT DEFAULT '[]',
|
|
849
|
+
output TEXT DEFAULT '',
|
|
850
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
851
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
852
|
+
);
|
|
853
|
+
CREATE INDEX IF NOT EXISTS idx_devlogs_project ON devlogs(project_id);
|
|
854
|
+
`);
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
version: 14,
|
|
859
|
+
description: "Add scope_snapshots and scope_alerts tables for scope creep detector",
|
|
860
|
+
up(db) {
|
|
861
|
+
db.exec(`
|
|
862
|
+
CREATE TABLE IF NOT EXISTS scope_snapshots (
|
|
863
|
+
id TEXT PRIMARY KEY,
|
|
864
|
+
project_id TEXT NOT NULL,
|
|
865
|
+
week_label TEXT NOT NULL,
|
|
866
|
+
week_start TEXT NOT NULL,
|
|
867
|
+
tasks_added INTEGER DEFAULT 0,
|
|
868
|
+
tasks_completed INTEGER DEFAULT 0,
|
|
869
|
+
backlog_total INTEGER DEFAULT 0,
|
|
870
|
+
scope_health INTEGER DEFAULT 100,
|
|
871
|
+
snapshot_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
872
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
873
|
+
);
|
|
874
|
+
CREATE INDEX IF NOT EXISTS idx_scope_snapshots_project ON scope_snapshots(project_id);
|
|
875
|
+
|
|
876
|
+
CREATE TABLE IF NOT EXISTS scope_alerts (
|
|
877
|
+
id TEXT PRIMARY KEY,
|
|
878
|
+
project_id TEXT NOT NULL,
|
|
879
|
+
severity TEXT DEFAULT 'info',
|
|
880
|
+
text TEXT NOT NULL,
|
|
881
|
+
metric TEXT DEFAULT '',
|
|
882
|
+
dismissed INTEGER DEFAULT 0,
|
|
883
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
884
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
885
|
+
);
|
|
886
|
+
CREATE INDEX IF NOT EXISTS idx_scope_alerts_project ON scope_alerts(project_id);
|
|
887
|
+
`);
|
|
888
|
+
// Seed data
|
|
889
|
+
try {
|
|
890
|
+
const row = db.prepare("SELECT id FROM projects LIMIT 1").get();
|
|
891
|
+
if (row) {
|
|
892
|
+
const pid = row.id;
|
|
893
|
+
db.exec(`
|
|
894
|
+
INSERT OR IGNORE INTO scope_snapshots (id, project_id, week_label, week_start, tasks_added, tasks_completed, backlog_total, scope_health, snapshot_at) VALUES
|
|
895
|
+
('ss1', '${pid}', 'Jan 20', '2026-01-20', 5, 4, 12, 88, '2026-01-26T23:59:00Z'),
|
|
896
|
+
('ss2', '${pid}', 'Jan 27', '2026-01-27', 4, 5, 11, 92, '2026-02-02T23:59:00Z'),
|
|
897
|
+
('ss3', '${pid}', 'Feb 3', '2026-02-03', 6, 4, 13, 82, '2026-02-09T23:59:00Z'),
|
|
898
|
+
('ss4', '${pid}', 'Feb 10', '2026-02-10', 7, 3, 17, 68, '2026-02-16T23:59:00Z'),
|
|
899
|
+
('ss5', '${pid}', 'Feb 17', '2026-02-17', 8, 3, 22, 72, '2026-02-18T23:59:00Z');
|
|
900
|
+
|
|
901
|
+
INSERT OR IGNORE INTO scope_alerts (id, project_id, severity, text, metric, created_at) VALUES
|
|
902
|
+
('sa1', '${pid}', 'warning', 'Backlog grew by 8 tasks this week but only 3 were completed', '+5 net new', datetime('now', '-4 hours')),
|
|
903
|
+
('sa2', '${pid}', 'danger', 'Milestone ''Demo Build'' is 4 days away with 6 unfinished tasks', '6 remaining', datetime('now', '-8 hours')),
|
|
904
|
+
('sa3', '${pid}', 'info', 'Feature velocity is steady at ~2.5 tasks/week over the last month', 'On track', datetime('now', '-1 day')),
|
|
905
|
+
('sa4', '${pid}', 'warning', '3 new high-priority tasks added after sprint was planned', 'Scope shift', datetime('now', '-2 days'));
|
|
906
|
+
`);
|
|
907
|
+
}
|
|
908
|
+
} catch (_) {}
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
// ── Migration 15: App Settings (single-row store) ──
|
|
912
|
+
{
|
|
913
|
+
version: 15,
|
|
914
|
+
description: "App settings table",
|
|
915
|
+
up(db) {
|
|
916
|
+
db.exec(`
|
|
917
|
+
CREATE TABLE IF NOT EXISTS app_settings (
|
|
918
|
+
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
919
|
+
minimize_to_tray INTEGER DEFAULT 1,
|
|
920
|
+
close_to_tray INTEGER DEFAULT 0,
|
|
921
|
+
launch_on_startup INTEGER DEFAULT 0,
|
|
922
|
+
start_minimized INTEGER DEFAULT 0,
|
|
923
|
+
global_hotkey TEXT DEFAULT 'Ctrl+Shift+T',
|
|
924
|
+
native_notifications INTEGER DEFAULT 1,
|
|
925
|
+
notif_sound INTEGER DEFAULT 1,
|
|
926
|
+
notif_bug_reports INTEGER DEFAULT 1,
|
|
927
|
+
notif_build_complete INTEGER DEFAULT 1,
|
|
928
|
+
notif_deadlines INTEGER DEFAULT 1,
|
|
929
|
+
notif_discord INTEGER DEFAULT 1,
|
|
930
|
+
deep_links INTEGER DEFAULT 1,
|
|
931
|
+
auto_update INTEGER DEFAULT 1,
|
|
932
|
+
update_channel TEXT DEFAULT 'stable',
|
|
933
|
+
compact_mode INTEGER DEFAULT 0,
|
|
934
|
+
animations_enabled INTEGER DEFAULT 1
|
|
935
|
+
);
|
|
936
|
+
INSERT OR IGNORE INTO app_settings (id) VALUES (1);
|
|
937
|
+
`);
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
// ── Migration 16: Activity log for analytics ──
|
|
941
|
+
{
|
|
942
|
+
version: 16,
|
|
943
|
+
description: "Activity log table for analytics",
|
|
944
|
+
up(db) {
|
|
945
|
+
db.exec(`
|
|
946
|
+
CREATE TABLE IF NOT EXISTS activity_log (
|
|
947
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
948
|
+
project_id TEXT NOT NULL,
|
|
949
|
+
action_type TEXT NOT NULL,
|
|
950
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
951
|
+
metadata TEXT
|
|
952
|
+
);
|
|
953
|
+
CREATE INDEX IF NOT EXISTS idx_activity_date ON activity_log(created_at);
|
|
954
|
+
CREATE INDEX IF NOT EXISTS idx_activity_project ON activity_log(project_id);
|
|
955
|
+
`);
|
|
956
|
+
|
|
957
|
+
// Seed some sample activity data if a project exists
|
|
958
|
+
try {
|
|
959
|
+
const proj = db.prepare("SELECT id FROM projects LIMIT 1").get();
|
|
960
|
+
if (proj) {
|
|
961
|
+
const pid = proj.id;
|
|
962
|
+
const types = ['task_done','task_done','task_done','bug_fixed','task_added','task_added','session','changelog','build','task_done','bug_fixed','task_added','task_done','task_done','bug_added','task_done','session','task_added','task_done','bug_fixed','changelog','task_done','task_added','task_done','task_done','bug_fixed','session','build','task_done','task_added','task_done','bug_added','task_done','changelog','task_done','bug_fixed','task_done','session','task_added','task_done'];
|
|
963
|
+
const stmt = db.prepare("INSERT INTO activity_log (project_id, action_type, created_at) VALUES (?, ?, datetime('now', ? || ' days'))");
|
|
964
|
+
types.forEach((t, i) => {
|
|
965
|
+
const daysAgo = '-' + Math.floor(i * 2.1);
|
|
966
|
+
stmt.run(pid, t, daysAgo);
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
} catch (_) {}
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
// ── Migration 17: Add sync_sound column to app_settings ──
|
|
973
|
+
{
|
|
974
|
+
version: 17,
|
|
975
|
+
description: "Add sync connection sound setting",
|
|
976
|
+
up(db) {
|
|
977
|
+
db.exec(`ALTER TABLE app_settings ADD COLUMN sync_sound INTEGER DEFAULT 1`);
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
// ── Migration 18: Create stories table (narrative bible) ──
|
|
981
|
+
{
|
|
982
|
+
version: 18,
|
|
983
|
+
description: "Create stories table for narrative bible",
|
|
984
|
+
up(db) {
|
|
985
|
+
db.exec(`
|
|
986
|
+
CREATE TABLE IF NOT EXISTS stories (
|
|
987
|
+
id TEXT PRIMARY KEY,
|
|
988
|
+
project_id TEXT NOT NULL,
|
|
989
|
+
data TEXT,
|
|
990
|
+
created_at TEXT NOT NULL,
|
|
991
|
+
last_updated TEXT NOT NULL
|
|
992
|
+
);
|
|
993
|
+
CREATE INDEX IF NOT EXISTS idx_stories_project ON stories(project_id);
|
|
994
|
+
`);
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
// ── Migration 19: Create scenes table ──
|
|
998
|
+
{
|
|
999
|
+
version: 19,
|
|
1000
|
+
description: "Create scenes table for story scenes",
|
|
1001
|
+
up(db) {
|
|
1002
|
+
db.exec(`
|
|
1003
|
+
CREATE TABLE IF NOT EXISTS scenes (
|
|
1004
|
+
id TEXT PRIMARY KEY,
|
|
1005
|
+
project_id TEXT NOT NULL,
|
|
1006
|
+
data TEXT,
|
|
1007
|
+
created_at TEXT NOT NULL,
|
|
1008
|
+
last_updated TEXT NOT NULL
|
|
1009
|
+
);
|
|
1010
|
+
CREATE INDEX IF NOT EXISTS idx_scenes_project ON scenes(project_id);
|
|
1011
|
+
`);
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
// ── Migration 20: Create cloud_sync table for future cloud sync ──
|
|
1015
|
+
{
|
|
1016
|
+
version: 20,
|
|
1017
|
+
description: "Create cloud_sync config table",
|
|
1018
|
+
up(db) {
|
|
1019
|
+
db.exec(`
|
|
1020
|
+
CREATE TABLE IF NOT EXISTS cloud_sync (
|
|
1021
|
+
id TEXT PRIMARY KEY,
|
|
1022
|
+
project_id TEXT NOT NULL,
|
|
1023
|
+
enabled INTEGER DEFAULT 0,
|
|
1024
|
+
mode TEXT DEFAULT 'local',
|
|
1025
|
+
sync_tabs TEXT DEFAULT '{}',
|
|
1026
|
+
encryption_key_hash TEXT,
|
|
1027
|
+
last_synced_at TEXT,
|
|
1028
|
+
data TEXT,
|
|
1029
|
+
created_at TEXT NOT NULL,
|
|
1030
|
+
last_updated TEXT NOT NULL
|
|
1031
|
+
);
|
|
1032
|
+
CREATE INDEX IF NOT EXISTS idx_cloud_sync_project ON cloud_sync(project_id);
|
|
1033
|
+
`);
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
// ── Migration 21: Add accent_color to app_settings ──
|
|
1037
|
+
{
|
|
1038
|
+
version: 21,
|
|
1039
|
+
description: "Add accent_color column to app_settings",
|
|
1040
|
+
up(db) {
|
|
1041
|
+
db.exec(`ALTER TABLE app_settings ADD COLUMN accent_color TEXT DEFAULT '#7F24DD'`);
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
// ── Migration 22: Add users table for authentication ──
|
|
1045
|
+
{
|
|
1046
|
+
version: 22,
|
|
1047
|
+
description: "Add users table for authentication",
|
|
1048
|
+
up(db) {
|
|
1049
|
+
db.exec(`
|
|
1050
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
1051
|
+
id TEXT PRIMARY KEY,
|
|
1052
|
+
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
1053
|
+
password_hash TEXT NOT NULL,
|
|
1054
|
+
salt TEXT NOT NULL,
|
|
1055
|
+
onboarding_complete INTEGER DEFAULT 0,
|
|
1056
|
+
onboarding_data TEXT DEFAULT '{}',
|
|
1057
|
+
created_at TEXT NOT NULL
|
|
1058
|
+
);
|
|
1059
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username COLLATE NOCASE);
|
|
1060
|
+
`);
|
|
1061
|
+
// Migration 22 created the users table — no seed data needed for production
|
|
1062
|
+
},
|
|
1063
|
+
},
|
|
1064
|
+
// ── Migration 23: Per-user data isolation ──
|
|
1065
|
+
{
|
|
1066
|
+
version: 23,
|
|
1067
|
+
description: "Add user_id to projects, user_settings, and team_members for per-user data isolation",
|
|
1068
|
+
up(db) {
|
|
1069
|
+
// 1. Add user_id to projects table
|
|
1070
|
+
db.exec(`
|
|
1071
|
+
ALTER TABLE projects ADD COLUMN user_id TEXT DEFAULT 'klaw';
|
|
1072
|
+
CREATE INDEX idx_projects_user ON projects(user_id);
|
|
1073
|
+
`);
|
|
1074
|
+
|
|
1075
|
+
// 2. Recreate user_settings with composite key (user_id, key)
|
|
1076
|
+
db.exec(`
|
|
1077
|
+
CREATE TABLE user_settings_new (
|
|
1078
|
+
user_id TEXT NOT NULL DEFAULT 'klaw',
|
|
1079
|
+
key TEXT NOT NULL,
|
|
1080
|
+
value TEXT NOT NULL,
|
|
1081
|
+
PRIMARY KEY (user_id, key)
|
|
1082
|
+
);
|
|
1083
|
+
INSERT INTO user_settings_new (user_id, key, value)
|
|
1084
|
+
SELECT 'klaw', key, value FROM user_settings;
|
|
1085
|
+
DROP TABLE user_settings;
|
|
1086
|
+
ALTER TABLE user_settings_new RENAME TO user_settings;
|
|
1087
|
+
`);
|
|
1088
|
+
|
|
1089
|
+
// 3. Add owner_user_id to team_members
|
|
1090
|
+
db.exec(`
|
|
1091
|
+
ALTER TABLE team_members ADD COLUMN owner_user_id TEXT DEFAULT 'klaw';
|
|
1092
|
+
`);
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
// ── Migration 24: Track username change cooldown ──
|
|
1096
|
+
{
|
|
1097
|
+
version: 24,
|
|
1098
|
+
description: "Add username_changed_at to users table for 24h cooldown",
|
|
1099
|
+
up(db) {
|
|
1100
|
+
db.exec(`ALTER TABLE users ADD COLUMN username_changed_at TEXT DEFAULT NULL`);
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1103
|
+
// ── Migration 25: Move machine-level settings to __global__ scope ──
|
|
1104
|
+
{
|
|
1105
|
+
version: 25,
|
|
1106
|
+
description: "Move authUserId and hasBooted from klaw to __global__ user scope",
|
|
1107
|
+
up(db) {
|
|
1108
|
+
const globalKeys = ['authUserId', 'hasBooted'];
|
|
1109
|
+
for (const key of globalKeys) {
|
|
1110
|
+
const row = db.prepare("SELECT value FROM user_settings WHERE key = ? AND user_id = 'klaw'").get(key);
|
|
1111
|
+
if (row) {
|
|
1112
|
+
db.prepare("INSERT OR REPLACE INTO user_settings (user_id, key, value) VALUES ('__global__', ?, ?)").run(key, row.value);
|
|
1113
|
+
db.prepare("DELETE FROM user_settings WHERE user_id = 'klaw' AND key = ?").run(key);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
// ── Migration 26: Messaging system — friends, conversations, messages ──
|
|
1119
|
+
{
|
|
1120
|
+
version: 26,
|
|
1121
|
+
description: "Add friends, conversations, conversation_members, and messages tables",
|
|
1122
|
+
up(db) {
|
|
1123
|
+
db.exec(`
|
|
1124
|
+
CREATE TABLE IF NOT EXISTS friends (
|
|
1125
|
+
id TEXT PRIMARY KEY,
|
|
1126
|
+
user_id TEXT NOT NULL,
|
|
1127
|
+
friend_id TEXT NOT NULL,
|
|
1128
|
+
status TEXT DEFAULT 'pending',
|
|
1129
|
+
created_at TEXT NOT NULL,
|
|
1130
|
+
accepted_at TEXT DEFAULT NULL,
|
|
1131
|
+
UNIQUE(user_id, friend_id)
|
|
1132
|
+
);
|
|
1133
|
+
CREATE INDEX IF NOT EXISTS idx_friends_user ON friends(user_id);
|
|
1134
|
+
CREATE INDEX IF NOT EXISTS idx_friends_friend ON friends(friend_id);
|
|
1135
|
+
|
|
1136
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
1137
|
+
id TEXT PRIMARY KEY,
|
|
1138
|
+
name TEXT DEFAULT NULL,
|
|
1139
|
+
type TEXT DEFAULT 'dm',
|
|
1140
|
+
avatar TEXT DEFAULT NULL,
|
|
1141
|
+
avatar_color TEXT DEFAULT NULL,
|
|
1142
|
+
owner_user_id TEXT NOT NULL,
|
|
1143
|
+
created_at TEXT NOT NULL,
|
|
1144
|
+
last_message_at TEXT DEFAULT NULL
|
|
1145
|
+
);
|
|
1146
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_owner ON conversations(owner_user_id);
|
|
1147
|
+
|
|
1148
|
+
CREATE TABLE IF NOT EXISTS conversation_members (
|
|
1149
|
+
id TEXT PRIMARY KEY,
|
|
1150
|
+
conversation_id TEXT NOT NULL,
|
|
1151
|
+
user_id TEXT NOT NULL,
|
|
1152
|
+
display_name TEXT NOT NULL,
|
|
1153
|
+
avatar TEXT DEFAULT '',
|
|
1154
|
+
color TEXT DEFAULT '',
|
|
1155
|
+
role TEXT DEFAULT 'member',
|
|
1156
|
+
joined_at TEXT NOT NULL,
|
|
1157
|
+
UNIQUE(conversation_id, user_id)
|
|
1158
|
+
);
|
|
1159
|
+
CREATE INDEX IF NOT EXISTS idx_conv_members_conv ON conversation_members(conversation_id);
|
|
1160
|
+
CREATE INDEX IF NOT EXISTS idx_conv_members_user ON conversation_members(user_id);
|
|
1161
|
+
|
|
1162
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
1163
|
+
id TEXT PRIMARY KEY,
|
|
1164
|
+
conversation_id TEXT NOT NULL,
|
|
1165
|
+
sender_id TEXT NOT NULL,
|
|
1166
|
+
sender_name TEXT NOT NULL,
|
|
1167
|
+
text TEXT NOT NULL,
|
|
1168
|
+
created_at TEXT NOT NULL
|
|
1169
|
+
);
|
|
1170
|
+
CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id);
|
|
1171
|
+
CREATE INDEX IF NOT EXISTS idx_messages_time ON messages(conversation_id, created_at);
|
|
1172
|
+
`);
|
|
1173
|
+
|
|
1174
|
+
// Friends, conversations, and messages created dynamically — no seed data
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
version: 27,
|
|
1179
|
+
description: "API keys table for Bridge authentication",
|
|
1180
|
+
up(db) {
|
|
1181
|
+
db.exec(`
|
|
1182
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
1183
|
+
id TEXT PRIMARY KEY,
|
|
1184
|
+
user_id TEXT NOT NULL,
|
|
1185
|
+
key TEXT NOT NULL UNIQUE,
|
|
1186
|
+
name TEXT NOT NULL DEFAULT 'Default',
|
|
1187
|
+
created_at TEXT NOT NULL,
|
|
1188
|
+
last_used_at TEXT DEFAULT NULL
|
|
1189
|
+
);
|
|
1190
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id);
|
|
1191
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key);
|
|
1192
|
+
`);
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
version: 28,
|
|
1197
|
+
description: "MFA support: TOTP secret, enabled flag, backup codes table",
|
|
1198
|
+
up(db) {
|
|
1199
|
+
db.exec(`ALTER TABLE users ADD COLUMN mfa_enabled INTEGER DEFAULT 0`);
|
|
1200
|
+
db.exec(`ALTER TABLE users ADD COLUMN mfa_secret TEXT DEFAULT NULL`);
|
|
1201
|
+
db.exec(`ALTER TABLE users ADD COLUMN mfa_verified_at TEXT DEFAULT NULL`);
|
|
1202
|
+
db.exec(`
|
|
1203
|
+
CREATE TABLE IF NOT EXISTS mfa_backup_codes (
|
|
1204
|
+
id TEXT PRIMARY KEY,
|
|
1205
|
+
user_id TEXT NOT NULL,
|
|
1206
|
+
code_hash TEXT NOT NULL,
|
|
1207
|
+
used INTEGER DEFAULT 0,
|
|
1208
|
+
used_at TEXT DEFAULT NULL,
|
|
1209
|
+
created_at TEXT NOT NULL,
|
|
1210
|
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
1211
|
+
);
|
|
1212
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_backup_user ON mfa_backup_codes(user_id);
|
|
1213
|
+
`);
|
|
1214
|
+
},
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
version: 29,
|
|
1218
|
+
description: "Security events table + auto-lock settings",
|
|
1219
|
+
up(db) {
|
|
1220
|
+
db.exec(`
|
|
1221
|
+
CREATE TABLE IF NOT EXISTS security_events (
|
|
1222
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1223
|
+
user_id TEXT NOT NULL,
|
|
1224
|
+
event_type TEXT NOT NULL,
|
|
1225
|
+
description TEXT NOT NULL,
|
|
1226
|
+
metadata TEXT DEFAULT '{}',
|
|
1227
|
+
created_at TEXT NOT NULL,
|
|
1228
|
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
1229
|
+
);
|
|
1230
|
+
CREATE INDEX IF NOT EXISTS idx_security_events_user ON security_events(user_id);
|
|
1231
|
+
CREATE INDEX IF NOT EXISTS idx_security_events_type ON security_events(event_type);
|
|
1232
|
+
CREATE INDEX IF NOT EXISTS idx_security_events_created ON security_events(created_at);
|
|
1233
|
+
`);
|
|
1234
|
+
db.exec(`ALTER TABLE app_settings ADD COLUMN auto_lock_enabled INTEGER DEFAULT 0`);
|
|
1235
|
+
db.exec(`ALTER TABLE app_settings ADD COLUMN auto_lock_minutes INTEGER DEFAULT 5`);
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
version: 30,
|
|
1240
|
+
description: "Stripe billing tables — stripe_customers and stripe_subscriptions",
|
|
1241
|
+
up(db) {
|
|
1242
|
+
db.exec(`
|
|
1243
|
+
CREATE TABLE IF NOT EXISTS stripe_customers (
|
|
1244
|
+
id TEXT PRIMARY KEY,
|
|
1245
|
+
user_id TEXT NOT NULL UNIQUE,
|
|
1246
|
+
stripe_customer_id TEXT NOT NULL UNIQUE,
|
|
1247
|
+
plan TEXT DEFAULT 'free',
|
|
1248
|
+
created_at TEXT NOT NULL,
|
|
1249
|
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
1250
|
+
);
|
|
1251
|
+
CREATE INDEX IF NOT EXISTS idx_stripe_customers_user ON stripe_customers(user_id);
|
|
1252
|
+
CREATE INDEX IF NOT EXISTS idx_stripe_customers_stripe ON stripe_customers(stripe_customer_id);
|
|
1253
|
+
|
|
1254
|
+
CREATE TABLE IF NOT EXISTS stripe_subscriptions (
|
|
1255
|
+
id TEXT PRIMARY KEY,
|
|
1256
|
+
stripe_customer_id TEXT NOT NULL,
|
|
1257
|
+
stripe_subscription_id TEXT NOT NULL UNIQUE,
|
|
1258
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
1259
|
+
plan TEXT NOT NULL,
|
|
1260
|
+
current_period_end TEXT,
|
|
1261
|
+
created_at TEXT NOT NULL,
|
|
1262
|
+
updated_at TEXT NOT NULL,
|
|
1263
|
+
FOREIGN KEY (stripe_customer_id) REFERENCES stripe_customers(stripe_customer_id)
|
|
1264
|
+
);
|
|
1265
|
+
CREATE INDEX IF NOT EXISTS idx_stripe_subs_customer ON stripe_subscriptions(stripe_customer_id);
|
|
1266
|
+
`);
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
version: 31,
|
|
1271
|
+
description: "Add r2_key column to attachments for cloud file storage",
|
|
1272
|
+
up(db) {
|
|
1273
|
+
db.exec(`ALTER TABLE attachments ADD COLUMN r2_key TEXT DEFAULT NULL`);
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
version: 32,
|
|
1278
|
+
description: "Project collaborators table for shared project access",
|
|
1279
|
+
up(db) {
|
|
1280
|
+
db.exec(`
|
|
1281
|
+
CREATE TABLE IF NOT EXISTS project_collaborators (
|
|
1282
|
+
id TEXT PRIMARY KEY,
|
|
1283
|
+
project_id TEXT NOT NULL,
|
|
1284
|
+
user_id TEXT NOT NULL,
|
|
1285
|
+
role TEXT NOT NULL DEFAULT 'editor',
|
|
1286
|
+
invited_by TEXT NOT NULL,
|
|
1287
|
+
created_at TEXT NOT NULL,
|
|
1288
|
+
UNIQUE(project_id, user_id),
|
|
1289
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
1290
|
+
);
|
|
1291
|
+
CREATE INDEX IF NOT EXISTS idx_proj_collab_project ON project_collaborators(project_id);
|
|
1292
|
+
CREATE INDEX IF NOT EXISTS idx_proj_collab_user ON project_collaborators(user_id);
|
|
1293
|
+
`);
|
|
1294
|
+
},
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
version: 33,
|
|
1298
|
+
description: "Conversation read state for unread message tracking",
|
|
1299
|
+
up(db) {
|
|
1300
|
+
db.exec(`
|
|
1301
|
+
CREATE TABLE IF NOT EXISTS conversation_read_state (
|
|
1302
|
+
user_id TEXT NOT NULL,
|
|
1303
|
+
conversation_id TEXT NOT NULL,
|
|
1304
|
+
last_read_at TEXT NOT NULL,
|
|
1305
|
+
PRIMARY KEY (user_id, conversation_id)
|
|
1306
|
+
);
|
|
1307
|
+
`);
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
version: 34,
|
|
1312
|
+
description: "S6A: Encryption-at-rest schema — DEKs, hash_algorithm, API key HMAC, encryption metadata",
|
|
1313
|
+
up(db) {
|
|
1314
|
+
db.exec(`ALTER TABLE users ADD COLUMN encrypted_dek TEXT DEFAULT NULL`);
|
|
1315
|
+
db.exec(`ALTER TABLE users ADD COLUMN hash_algorithm TEXT DEFAULT 'scrypt'`);
|
|
1316
|
+
db.exec(`ALTER TABLE api_keys ADD COLUMN key_hmac TEXT DEFAULT NULL`);
|
|
1317
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_hmac ON api_keys(key_hmac)`);
|
|
1318
|
+
db.exec(`ALTER TABLE mfa_backup_codes ADD COLUMN salt TEXT DEFAULT NULL`);
|
|
1319
|
+
db.exec(`
|
|
1320
|
+
CREATE TABLE IF NOT EXISTS _encryption_meta (
|
|
1321
|
+
key TEXT PRIMARY KEY,
|
|
1322
|
+
value TEXT NOT NULL
|
|
1323
|
+
);
|
|
1324
|
+
`);
|
|
1325
|
+
// Default migration state
|
|
1326
|
+
try {
|
|
1327
|
+
db.exec(`INSERT INTO _encryption_meta (key, value) VALUES ('migration_status', 'pending')`);
|
|
1328
|
+
} catch (_) {} // Ignore if already exists
|
|
1329
|
+
},
|
|
1330
|
+
},
|
|
1331
|
+
// ── Migration 35: Token revocation support ──
|
|
1332
|
+
{
|
|
1333
|
+
version: 35,
|
|
1334
|
+
description: "Add tokens_revoked_before column for session invalidation on password change",
|
|
1335
|
+
up(db) {
|
|
1336
|
+
db.exec(`ALTER TABLE users ADD COLUMN tokens_revoked_before TEXT DEFAULT NULL`);
|
|
1337
|
+
},
|
|
1338
|
+
},
|
|
1339
|
+
// ── Migration 36: Stripe webhook idempotency ──
|
|
1340
|
+
{
|
|
1341
|
+
version: 36,
|
|
1342
|
+
description: "Track processed Stripe webhook events to prevent duplicate processing",
|
|
1343
|
+
up(db) {
|
|
1344
|
+
db.exec(`
|
|
1345
|
+
CREATE TABLE IF NOT EXISTS stripe_webhook_events (
|
|
1346
|
+
event_id TEXT PRIMARY KEY,
|
|
1347
|
+
event_type TEXT NOT NULL,
|
|
1348
|
+
processed_at TEXT NOT NULL
|
|
1349
|
+
);
|
|
1350
|
+
`);
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
// ── Migration 37: Refresh token rotation ──
|
|
1354
|
+
{
|
|
1355
|
+
version: 37,
|
|
1356
|
+
description: "Refresh token families for rotation and reuse detection",
|
|
1357
|
+
up(db) {
|
|
1358
|
+
db.exec(`
|
|
1359
|
+
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
|
1360
|
+
id TEXT PRIMARY KEY,
|
|
1361
|
+
user_id TEXT NOT NULL,
|
|
1362
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
1363
|
+
family TEXT NOT NULL,
|
|
1364
|
+
used INTEGER DEFAULT 0,
|
|
1365
|
+
expires_at TEXT NOT NULL,
|
|
1366
|
+
created_at TEXT NOT NULL,
|
|
1367
|
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
1368
|
+
);
|
|
1369
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
|
|
1370
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_family ON refresh_tokens(family);
|
|
1371
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash);
|
|
1372
|
+
`);
|
|
1373
|
+
},
|
|
1374
|
+
},
|
|
1375
|
+
// ── Migration 38: Performance indexes for high-volume tables ──
|
|
1376
|
+
{
|
|
1377
|
+
version: 38,
|
|
1378
|
+
description: "Add missing indexes for high-volume query patterns",
|
|
1379
|
+
up(db) {
|
|
1380
|
+
db.exec(`
|
|
1381
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee);
|
|
1382
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project_assignee ON tasks(project_id, assignee);
|
|
1383
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_sprint_status ON tasks(sprint_id, status);
|
|
1384
|
+
CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id);
|
|
1385
|
+
CREATE INDEX IF NOT EXISTS idx_time_logs_project_card ON time_logs(project_id, card_id);
|
|
1386
|
+
CREATE INDEX IF NOT EXISTS idx_attachments_project ON attachments(project_id);
|
|
1387
|
+
CREATE INDEX IF NOT EXISTS idx_activity_action_type ON activity_log(action_type);
|
|
1388
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_used ON refresh_tokens(user_id, used);
|
|
1389
|
+
CREATE INDEX IF NOT EXISTS idx_security_events_user_type ON security_events(user_id, event_type);
|
|
1390
|
+
`);
|
|
1391
|
+
},
|
|
1392
|
+
},
|
|
1393
|
+
// ── Migration 39: Compound indexes for ORDER BY / WHERE filter combos ──
|
|
1394
|
+
{
|
|
1395
|
+
version: 39,
|
|
1396
|
+
description: "Add compound indexes for sorted and filtered queries",
|
|
1397
|
+
up(db) {
|
|
1398
|
+
db.exec(`
|
|
1399
|
+
CREATE INDEX IF NOT EXISTS idx_bugs_project_status ON bugs(project_id, status);
|
|
1400
|
+
CREATE INDEX IF NOT EXISTS idx_notes_project_parent ON notes(project_id, parent_type, parent_id);
|
|
1401
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project_date ON sessions(project_id, date DESC);
|
|
1402
|
+
CREATE INDEX IF NOT EXISTS idx_changelog_project_date ON changelog(project_id, date DESC);
|
|
1403
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_project_date ON decisions(project_id, date DESC);
|
|
1404
|
+
CREATE INDEX IF NOT EXISTS idx_activity_project_created ON activity_log(project_id, created_at);
|
|
1405
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_project_created ON notifications(project_id, created_at DESC);
|
|
1406
|
+
`);
|
|
1407
|
+
},
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
version: 40,
|
|
1411
|
+
description: "Add deleted_usernames table to prevent re-registration of deleted accounts",
|
|
1412
|
+
up(db) {
|
|
1413
|
+
db.exec(`
|
|
1414
|
+
CREATE TABLE IF NOT EXISTS deleted_usernames (
|
|
1415
|
+
username TEXT NOT NULL COLLATE NOCASE,
|
|
1416
|
+
deleted_at TEXT NOT NULL,
|
|
1417
|
+
reason TEXT DEFAULT 'account_deleted'
|
|
1418
|
+
);
|
|
1419
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_deleted_usernames ON deleted_usernames(username COLLATE NOCASE);
|
|
1420
|
+
`);
|
|
1421
|
+
},
|
|
1422
|
+
},
|
|
1423
|
+
// ── Migration 41: Add email column + deleted_emails table ──
|
|
1424
|
+
{
|
|
1425
|
+
version: 41,
|
|
1426
|
+
description: "Add email column to users and deleted_emails table for email-based auth",
|
|
1427
|
+
up(db) {
|
|
1428
|
+
// Add nullable email column
|
|
1429
|
+
db.exec(`ALTER TABLE users ADD COLUMN email TEXT DEFAULT NULL`);
|
|
1430
|
+
// Unique index on email (case-insensitive)
|
|
1431
|
+
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email COLLATE NOCASE)`);
|
|
1432
|
+
// Table to reserve deleted account emails
|
|
1433
|
+
db.exec(`
|
|
1434
|
+
CREATE TABLE IF NOT EXISTS deleted_emails (
|
|
1435
|
+
email TEXT NOT NULL COLLATE NOCASE,
|
|
1436
|
+
deleted_at TEXT NOT NULL,
|
|
1437
|
+
reason TEXT DEFAULT 'account_deleted'
|
|
1438
|
+
);
|
|
1439
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_deleted_emails ON deleted_emails(email COLLATE NOCASE);
|
|
1440
|
+
`);
|
|
1441
|
+
// Migration 32 added email column — no seed data needed
|
|
1442
|
+
},
|
|
1443
|
+
},
|
|
1444
|
+
// ── Migration 42: Make username nullable for email-first registration ──
|
|
1445
|
+
{
|
|
1446
|
+
version: 42,
|
|
1447
|
+
description: "Rebuild users table to make username nullable (email-first auth)",
|
|
1448
|
+
up(db) {
|
|
1449
|
+
db.exec(`
|
|
1450
|
+
CREATE TABLE users_new (
|
|
1451
|
+
id TEXT PRIMARY KEY,
|
|
1452
|
+
username TEXT DEFAULT NULL COLLATE NOCASE,
|
|
1453
|
+
email TEXT DEFAULT NULL COLLATE NOCASE,
|
|
1454
|
+
password_hash TEXT NOT NULL,
|
|
1455
|
+
salt TEXT NOT NULL,
|
|
1456
|
+
onboarding_complete INTEGER DEFAULT 0,
|
|
1457
|
+
onboarding_data TEXT DEFAULT '{}',
|
|
1458
|
+
created_at TEXT NOT NULL,
|
|
1459
|
+
username_changed_at TEXT DEFAULT NULL,
|
|
1460
|
+
mfa_enabled INTEGER DEFAULT 0,
|
|
1461
|
+
mfa_secret TEXT DEFAULT NULL,
|
|
1462
|
+
mfa_verified_at TEXT DEFAULT NULL,
|
|
1463
|
+
encrypted_dek TEXT DEFAULT NULL,
|
|
1464
|
+
hash_algorithm TEXT DEFAULT 'scrypt',
|
|
1465
|
+
tokens_revoked_before TEXT DEFAULT NULL
|
|
1466
|
+
);
|
|
1467
|
+
INSERT INTO users_new SELECT
|
|
1468
|
+
id, username, email, password_hash, salt,
|
|
1469
|
+
onboarding_complete, onboarding_data, created_at,
|
|
1470
|
+
username_changed_at, mfa_enabled, mfa_secret, mfa_verified_at,
|
|
1471
|
+
encrypted_dek, hash_algorithm, tokens_revoked_before
|
|
1472
|
+
FROM users;
|
|
1473
|
+
DROP TABLE users;
|
|
1474
|
+
ALTER TABLE users_new RENAME TO users;
|
|
1475
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username COLLATE NOCASE);
|
|
1476
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email COLLATE NOCASE);
|
|
1477
|
+
`);
|
|
1478
|
+
},
|
|
1479
|
+
},
|
|
1480
|
+
// ── Migration 43: CHECK constraint — at least one identity must be non-NULL ──
|
|
1481
|
+
{
|
|
1482
|
+
version: 43,
|
|
1483
|
+
description: "Add CHECK constraint ensuring username or email is non-NULL",
|
|
1484
|
+
up(db) {
|
|
1485
|
+
db.exec(`
|
|
1486
|
+
CREATE TABLE users_new (
|
|
1487
|
+
id TEXT PRIMARY KEY,
|
|
1488
|
+
username TEXT DEFAULT NULL COLLATE NOCASE,
|
|
1489
|
+
email TEXT DEFAULT NULL COLLATE NOCASE,
|
|
1490
|
+
password_hash TEXT NOT NULL,
|
|
1491
|
+
salt TEXT NOT NULL,
|
|
1492
|
+
onboarding_complete INTEGER DEFAULT 0,
|
|
1493
|
+
onboarding_data TEXT DEFAULT '{}',
|
|
1494
|
+
created_at TEXT NOT NULL,
|
|
1495
|
+
username_changed_at TEXT DEFAULT NULL,
|
|
1496
|
+
mfa_enabled INTEGER DEFAULT 0,
|
|
1497
|
+
mfa_secret TEXT DEFAULT NULL,
|
|
1498
|
+
mfa_verified_at TEXT DEFAULT NULL,
|
|
1499
|
+
encrypted_dek TEXT DEFAULT NULL,
|
|
1500
|
+
hash_algorithm TEXT DEFAULT 'scrypt',
|
|
1501
|
+
tokens_revoked_before TEXT DEFAULT NULL,
|
|
1502
|
+
CHECK (username IS NOT NULL OR email IS NOT NULL)
|
|
1503
|
+
);
|
|
1504
|
+
INSERT INTO users_new SELECT
|
|
1505
|
+
id, username, email, password_hash, salt,
|
|
1506
|
+
onboarding_complete, onboarding_data, created_at,
|
|
1507
|
+
username_changed_at, mfa_enabled, mfa_secret, mfa_verified_at,
|
|
1508
|
+
encrypted_dek, hash_algorithm, tokens_revoked_before
|
|
1509
|
+
FROM users;
|
|
1510
|
+
DROP TABLE users;
|
|
1511
|
+
ALTER TABLE users_new RENAME TO users;
|
|
1512
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username COLLATE NOCASE);
|
|
1513
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email COLLATE NOCASE);
|
|
1514
|
+
`);
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
// ── Migration 44: Email verification + email_tokens table ──
|
|
1518
|
+
{
|
|
1519
|
+
version: 44,
|
|
1520
|
+
description: "Add email_verified columns and email_tokens table",
|
|
1521
|
+
up(db) {
|
|
1522
|
+
db.exec(`ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0`);
|
|
1523
|
+
db.exec(`ALTER TABLE users ADD COLUMN email_verified_at TEXT DEFAULT NULL`);
|
|
1524
|
+
db.exec(`
|
|
1525
|
+
CREATE TABLE IF NOT EXISTS email_tokens (
|
|
1526
|
+
id TEXT PRIMARY KEY,
|
|
1527
|
+
user_id TEXT NOT NULL,
|
|
1528
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
1529
|
+
type TEXT NOT NULL,
|
|
1530
|
+
expires_at TEXT NOT NULL,
|
|
1531
|
+
used INTEGER DEFAULT 0,
|
|
1532
|
+
created_at TEXT NOT NULL,
|
|
1533
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
1534
|
+
);
|
|
1535
|
+
CREATE INDEX IF NOT EXISTS idx_email_tokens_user ON email_tokens(user_id);
|
|
1536
|
+
CREATE INDEX IF NOT EXISTS idx_email_tokens_hash ON email_tokens(token_hash);
|
|
1537
|
+
CREATE INDEX IF NOT EXISTS idx_email_tokens_type ON email_tokens(user_id, type);
|
|
1538
|
+
`);
|
|
1539
|
+
},
|
|
1540
|
+
},
|
|
1541
|
+
|
|
1542
|
+
// ── Migration 45: Phase 2 — FTS4 search index + activity_log event store columns ──
|
|
1543
|
+
{
|
|
1544
|
+
version: 45,
|
|
1545
|
+
description: "FTS4 search index + activity_log user/target columns for Phase 2",
|
|
1546
|
+
up(db) {
|
|
1547
|
+
// 1. Create FTS4 virtual table for full-text search
|
|
1548
|
+
// FTS4 used instead of FTS5 for sql.js (WASM) compatibility — same MATCH semantics
|
|
1549
|
+
db.exec(`
|
|
1550
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts4(
|
|
1551
|
+
title, body,
|
|
1552
|
+
project_id, record_type, record_id,
|
|
1553
|
+
notindexed=project_id, notindexed=record_type, notindexed=record_id,
|
|
1554
|
+
tokenize=unicode61 "remove_diacritics=2"
|
|
1555
|
+
);
|
|
1556
|
+
`);
|
|
1557
|
+
|
|
1558
|
+
// 2. Add event store columns to activity_log (guard against already-existing)
|
|
1559
|
+
const cols = db.prepare("SELECT name FROM pragma_table_info('activity_log')").all().map(r => r.name);
|
|
1560
|
+
if (!cols.includes('user_id')) db.exec("ALTER TABLE activity_log ADD COLUMN user_id TEXT;");
|
|
1561
|
+
if (!cols.includes('target_type')) db.exec("ALTER TABLE activity_log ADD COLUMN target_type TEXT;");
|
|
1562
|
+
if (!cols.includes('target_id')) db.exec("ALTER TABLE activity_log ADD COLUMN target_id TEXT;");
|
|
1563
|
+
if (!cols.includes('detail')) db.exec("ALTER TABLE activity_log ADD COLUMN detail TEXT;");
|
|
1564
|
+
|
|
1565
|
+
db.exec(`
|
|
1566
|
+
CREATE INDEX IF NOT EXISTS idx_activity_user ON activity_log(user_id);
|
|
1567
|
+
CREATE INDEX IF NOT EXISTS idx_activity_target ON activity_log(target_type, target_id);
|
|
1568
|
+
`);
|
|
1569
|
+
|
|
1570
|
+
// 3. Backfill FTS index from existing data (6 collections)
|
|
1571
|
+
const insert = db.prepare(
|
|
1572
|
+
"INSERT INTO search_index(title, body, project_id, record_type, record_id) VALUES (?, ?, ?, ?, ?)"
|
|
1573
|
+
);
|
|
1574
|
+
const tx = db.transaction(() => {
|
|
1575
|
+
for (const r of db.prepare("SELECT id, project_id, data FROM tasks").all()) {
|
|
1576
|
+
try { const d = JSON.parse(r.data || '{}'); insert.run(d.title || '', d.description || '', r.project_id, 'task', r.id); } catch(_){}
|
|
1577
|
+
}
|
|
1578
|
+
for (const r of db.prepare("SELECT id, project_id, data FROM bugs").all()) {
|
|
1579
|
+
try { const d = JSON.parse(r.data || '{}'); insert.run(d.title || '', d.description || '', r.project_id, 'bug', r.id); } catch(_){}
|
|
1580
|
+
}
|
|
1581
|
+
for (const r of db.prepare("SELECT id, project_id, data FROM notes").all()) {
|
|
1582
|
+
try { const d = JSON.parse(r.data || '{}'); insert.run(d.title || '', d.content || '', r.project_id, 'note', r.id); } catch(_){}
|
|
1583
|
+
}
|
|
1584
|
+
for (const r of db.prepare("SELECT id, project_id, data FROM systems").all()) {
|
|
1585
|
+
try { const d = JSON.parse(r.data || '{}'); insert.run(d.name || '', [d.description, d.notes].filter(Boolean).join(' '), r.project_id, 'system', r.id); } catch(_){}
|
|
1586
|
+
}
|
|
1587
|
+
for (const r of db.prepare("SELECT id, project_id, data FROM wiki_pages").all()) {
|
|
1588
|
+
try { const d = JSON.parse(r.data || '{}'); insert.run(d.title || '', d.content || '', r.project_id, 'wiki_page', r.id); } catch(_){}
|
|
1589
|
+
}
|
|
1590
|
+
for (const r of db.prepare("SELECT id, project_id, data FROM sessions").all()) {
|
|
1591
|
+
try { const d = JSON.parse(r.data || '{}'); insert.run(d.title || '', [d.summary, d.whereWeLeftOff].filter(Boolean).join(' '), r.project_id, 'session', r.id); } catch(_){}
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
tx();
|
|
1595
|
+
},
|
|
1596
|
+
},
|
|
1597
|
+
|
|
1598
|
+
// ── Migration 46: Phase 3 — server-side notifications + rules ──
|
|
1599
|
+
{
|
|
1600
|
+
version: 46,
|
|
1601
|
+
description: "Server-side notifications_v2 table for Phase 3 notification engine",
|
|
1602
|
+
up(db) {
|
|
1603
|
+
db.exec(`
|
|
1604
|
+
CREATE TABLE IF NOT EXISTS notifications_v2 (
|
|
1605
|
+
id TEXT PRIMARY KEY,
|
|
1606
|
+
project_id TEXT NOT NULL,
|
|
1607
|
+
user_id TEXT NOT NULL,
|
|
1608
|
+
type TEXT NOT NULL,
|
|
1609
|
+
target_type TEXT,
|
|
1610
|
+
target_id TEXT,
|
|
1611
|
+
detail TEXT,
|
|
1612
|
+
read INTEGER DEFAULT 0,
|
|
1613
|
+
created_at TEXT NOT NULL
|
|
1614
|
+
);
|
|
1615
|
+
CREATE INDEX IF NOT EXISTS idx_notif2_user_project ON notifications_v2(user_id, project_id, created_at);
|
|
1616
|
+
CREATE INDEX IF NOT EXISTS idx_notif2_project ON notifications_v2(project_id);
|
|
1617
|
+
CREATE INDEX IF NOT EXISTS idx_notif2_dedup ON notifications_v2(user_id, type, target_id);
|
|
1618
|
+
`);
|
|
1619
|
+
},
|
|
1620
|
+
},
|
|
1621
|
+
|
|
1622
|
+
// ── Migration 47: Phase 4 — AI usage tracking ──
|
|
1623
|
+
{
|
|
1624
|
+
version: 47,
|
|
1625
|
+
description: "AI usage log for Phase 4 quota tracking",
|
|
1626
|
+
up(db) {
|
|
1627
|
+
db.exec(`
|
|
1628
|
+
CREATE TABLE IF NOT EXISTS ai_usage_log (
|
|
1629
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1630
|
+
user_id TEXT NOT NULL,
|
|
1631
|
+
project_id TEXT NOT NULL,
|
|
1632
|
+
endpoint TEXT NOT NULL,
|
|
1633
|
+
had_enrichment INTEGER DEFAULT 0,
|
|
1634
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1635
|
+
);
|
|
1636
|
+
CREATE INDEX IF NOT EXISTS idx_ai_usage_user ON ai_usage_log(user_id, created_at);
|
|
1637
|
+
CREATE INDEX IF NOT EXISTS idx_ai_usage_user_endpoint ON ai_usage_log(user_id, endpoint, created_at);
|
|
1638
|
+
`);
|
|
1639
|
+
},
|
|
1640
|
+
},
|
|
1641
|
+
|
|
1642
|
+
// ── Migration 48: Admin flag on users ──
|
|
1643
|
+
{
|
|
1644
|
+
version: 48,
|
|
1645
|
+
description: "Add is_admin flag to users table",
|
|
1646
|
+
up(db) {
|
|
1647
|
+
db.exec(`ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0`);
|
|
1648
|
+
// Backfill: first registered user becomes admin (deterministic tiebreak by id)
|
|
1649
|
+
db.exec(`UPDATE users SET is_admin = 1 WHERE id = (SELECT id FROM users ORDER BY created_at ASC, id ASC LIMIT 1)`);
|
|
1650
|
+
},
|
|
1651
|
+
},
|
|
1652
|
+
|
|
1653
|
+
// ── Migration 49: Access codes for invite-only registration ──
|
|
1654
|
+
{
|
|
1655
|
+
version: 49,
|
|
1656
|
+
description: "Access codes table for invite-only registration",
|
|
1657
|
+
up(db) {
|
|
1658
|
+
db.exec(`
|
|
1659
|
+
CREATE TABLE IF NOT EXISTS access_codes (
|
|
1660
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1661
|
+
code_hash TEXT NOT NULL UNIQUE,
|
|
1662
|
+
created_at INTEGER NOT NULL,
|
|
1663
|
+
created_by TEXT NOT NULL REFERENCES users(id),
|
|
1664
|
+
redeemed_at INTEGER DEFAULT NULL,
|
|
1665
|
+
redeemed_by TEXT DEFAULT NULL REFERENCES users(id),
|
|
1666
|
+
expires_at INTEGER DEFAULT NULL,
|
|
1667
|
+
max_uses INTEGER NOT NULL DEFAULT 1 CHECK(max_uses >= 1),
|
|
1668
|
+
uses INTEGER NOT NULL DEFAULT 0 CHECK(uses >= 0),
|
|
1669
|
+
note TEXT DEFAULT NULL,
|
|
1670
|
+
disabled INTEGER NOT NULL DEFAULT 0
|
|
1671
|
+
);
|
|
1672
|
+
CREATE INDEX IF NOT EXISTS idx_access_codes_created_by ON access_codes(created_by);
|
|
1673
|
+
`);
|
|
1674
|
+
},
|
|
1675
|
+
},
|
|
1676
|
+
|
|
1677
|
+
// ── Migration 50: Email verification codes (pre-registration) ──
|
|
1678
|
+
{
|
|
1679
|
+
version: 50,
|
|
1680
|
+
description: "Email verification codes for pre-registration flow",
|
|
1681
|
+
up(db) {
|
|
1682
|
+
db.exec(`
|
|
1683
|
+
CREATE TABLE IF NOT EXISTS email_verifications (
|
|
1684
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1685
|
+
email_hash TEXT NOT NULL,
|
|
1686
|
+
access_code_hash TEXT NOT NULL,
|
|
1687
|
+
code_hash TEXT NOT NULL,
|
|
1688
|
+
expires_at TEXT NOT NULL,
|
|
1689
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
1690
|
+
consumed INTEGER NOT NULL DEFAULT 0,
|
|
1691
|
+
last_sent_at TEXT NOT NULL,
|
|
1692
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1693
|
+
);
|
|
1694
|
+
CREATE INDEX IF NOT EXISTS idx_email_verifications_email ON email_verifications(email_hash, consumed);
|
|
1695
|
+
CREATE INDEX IF NOT EXISTS idx_email_verifications_lookup ON email_verifications(email_hash, access_code_hash, consumed);
|
|
1696
|
+
`);
|
|
1697
|
+
},
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
version: 51,
|
|
1701
|
+
description: "Add disabled column to users for admin revoke",
|
|
1702
|
+
up(db) {
|
|
1703
|
+
db.exec(`ALTER TABLE users ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0`);
|
|
1704
|
+
},
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
version: 52,
|
|
1708
|
+
description: "Create pre_verify_tokens table without FK to users",
|
|
1709
|
+
up(db) {
|
|
1710
|
+
db.exec(`CREATE TABLE IF NOT EXISTS pre_verify_tokens (
|
|
1711
|
+
id TEXT PRIMARY KEY,
|
|
1712
|
+
email_hash TEXT NOT NULL,
|
|
1713
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
1714
|
+
expires_at TEXT NOT NULL,
|
|
1715
|
+
used INTEGER DEFAULT 0,
|
|
1716
|
+
created_at TEXT NOT NULL
|
|
1717
|
+
)`);
|
|
1718
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_pvt_token ON pre_verify_tokens(token_hash)`);
|
|
1719
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_pvt_email ON pre_verify_tokens(email_hash)`);
|
|
1720
|
+
},
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
version: 53,
|
|
1724
|
+
description: "Add Bridge heartbeat columns to users",
|
|
1725
|
+
up(db) {
|
|
1726
|
+
db.exec(`ALTER TABLE users ADD COLUMN last_bridge_heartbeat INTEGER`);
|
|
1727
|
+
db.exec(`ALTER TABLE users ADD COLUMN bridge_mcp_configured INTEGER NOT NULL DEFAULT 0`);
|
|
1728
|
+
db.exec(`ALTER TABLE users ADD COLUMN last_bridge_device_id TEXT`);
|
|
1729
|
+
db.exec(`ALTER TABLE users ADD COLUMN last_bridge_version TEXT`);
|
|
1730
|
+
},
|
|
1731
|
+
},
|
|
1732
|
+
{
|
|
1733
|
+
version: 54,
|
|
1734
|
+
description: "Add engine detection column to users",
|
|
1735
|
+
up(db) {
|
|
1736
|
+
db.exec(`ALTER TABLE users ADD COLUMN bridge_engine_running TEXT`);
|
|
1737
|
+
},
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
version: 55,
|
|
1741
|
+
description: "Write audit log table for abuse detection",
|
|
1742
|
+
up: (db) => {
|
|
1743
|
+
db.exec(`
|
|
1744
|
+
CREATE TABLE IF NOT EXISTS write_audit_log (
|
|
1745
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1746
|
+
user_id TEXT NOT NULL,
|
|
1747
|
+
project_id TEXT,
|
|
1748
|
+
method TEXT NOT NULL,
|
|
1749
|
+
payload_bytes INTEGER DEFAULT 0,
|
|
1750
|
+
rejected INTEGER DEFAULT 0,
|
|
1751
|
+
reject_reason TEXT,
|
|
1752
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1753
|
+
);
|
|
1754
|
+
CREATE INDEX IF NOT EXISTS idx_wal_user ON write_audit_log(user_id, created_at);
|
|
1755
|
+
CREATE INDEX IF NOT EXISTS idx_wal_project ON write_audit_log(project_id, created_at);
|
|
1756
|
+
CREATE INDEX IF NOT EXISTS idx_wal_method ON write_audit_log(method, created_at);
|
|
1757
|
+
CREATE INDEX IF NOT EXISTS idx_wal_rejected ON write_audit_log(rejected, created_at);
|
|
1758
|
+
`);
|
|
1759
|
+
},
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
version: 56,
|
|
1763
|
+
description: "Recovery Center — soft-delete snapshot table",
|
|
1764
|
+
up: (db) => {
|
|
1765
|
+
db.exec(`
|
|
1766
|
+
CREATE TABLE IF NOT EXISTS recovery_items (
|
|
1767
|
+
id TEXT PRIMARY KEY,
|
|
1768
|
+
project_id TEXT NOT NULL,
|
|
1769
|
+
user_id TEXT NOT NULL,
|
|
1770
|
+
item_type TEXT NOT NULL,
|
|
1771
|
+
item_id TEXT NOT NULL,
|
|
1772
|
+
item_title TEXT NOT NULL DEFAULT '',
|
|
1773
|
+
snapshot TEXT NOT NULL,
|
|
1774
|
+
source_page TEXT DEFAULT NULL,
|
|
1775
|
+
deleted_at TEXT NOT NULL,
|
|
1776
|
+
expires_at TEXT NOT NULL,
|
|
1777
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
1778
|
+
);
|
|
1779
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_project ON recovery_items(project_id, deleted_at);
|
|
1780
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_user ON recovery_items(user_id, deleted_at);
|
|
1781
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_expires ON recovery_items(expires_at);
|
|
1782
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_type ON recovery_items(project_id, item_type);
|
|
1783
|
+
`);
|
|
1784
|
+
},
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
version: 57,
|
|
1788
|
+
description: "Data Control — per-project AI/MCP permission policies",
|
|
1789
|
+
up: (db) => {
|
|
1790
|
+
db.exec(`
|
|
1791
|
+
CREATE TABLE IF NOT EXISTS data_control_policies (
|
|
1792
|
+
project_id TEXT PRIMARY KEY,
|
|
1793
|
+
policy_version INTEGER NOT NULL DEFAULT 1,
|
|
1794
|
+
policies TEXT NOT NULL DEFAULT '{}',
|
|
1795
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1796
|
+
updated_by TEXT NOT NULL DEFAULT ''
|
|
1797
|
+
);
|
|
1798
|
+
`);
|
|
1799
|
+
},
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
version: 58,
|
|
1803
|
+
description: "Data Control — blocked action audit log",
|
|
1804
|
+
up: (db) => {
|
|
1805
|
+
db.exec(`
|
|
1806
|
+
CREATE TABLE IF NOT EXISTS dc_audit_log (
|
|
1807
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1808
|
+
user_id TEXT NOT NULL,
|
|
1809
|
+
project_id TEXT,
|
|
1810
|
+
method TEXT NOT NULL,
|
|
1811
|
+
domain TEXT,
|
|
1812
|
+
action TEXT,
|
|
1813
|
+
permission TEXT,
|
|
1814
|
+
denial_code TEXT NOT NULL,
|
|
1815
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1816
|
+
);
|
|
1817
|
+
CREATE INDEX IF NOT EXISTS idx_dc_audit_user ON dc_audit_log(user_id, created_at);
|
|
1818
|
+
CREATE INDEX IF NOT EXISTS idx_dc_audit_project ON dc_audit_log(project_id, created_at);
|
|
1819
|
+
`);
|
|
1820
|
+
},
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
version: 59,
|
|
1824
|
+
description: "Data Control — policy change audit trail + version increment",
|
|
1825
|
+
up: (db) => {
|
|
1826
|
+
db.exec(`
|
|
1827
|
+
CREATE TABLE IF NOT EXISTS dc_policy_changes (
|
|
1828
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1829
|
+
project_id TEXT NOT NULL,
|
|
1830
|
+
user_id TEXT NOT NULL,
|
|
1831
|
+
auth_type TEXT NOT NULL DEFAULT 'jwt',
|
|
1832
|
+
changed_domains TEXT NOT NULL DEFAULT '[]',
|
|
1833
|
+
old_values TEXT NOT NULL DEFAULT '{}',
|
|
1834
|
+
new_values TEXT NOT NULL DEFAULT '{}',
|
|
1835
|
+
version_before INTEGER,
|
|
1836
|
+
version_after INTEGER,
|
|
1837
|
+
request_id TEXT,
|
|
1838
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1839
|
+
);
|
|
1840
|
+
CREATE INDEX IF NOT EXISTS idx_dc_policy_changes_project ON dc_policy_changes(project_id, created_at);
|
|
1841
|
+
`);
|
|
1842
|
+
},
|
|
1843
|
+
},
|
|
1844
|
+
// ── Migration 60: Admin audit log ──
|
|
1845
|
+
{
|
|
1846
|
+
version: 60,
|
|
1847
|
+
description: "Admin audit log for granular admin action tracking",
|
|
1848
|
+
up(db) {
|
|
1849
|
+
db.exec(`
|
|
1850
|
+
CREATE TABLE IF NOT EXISTS admin_audit_log (
|
|
1851
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1852
|
+
admin_user_id TEXT NOT NULL,
|
|
1853
|
+
action TEXT NOT NULL,
|
|
1854
|
+
target_type TEXT,
|
|
1855
|
+
target_id TEXT,
|
|
1856
|
+
detail TEXT DEFAULT '{}',
|
|
1857
|
+
ip TEXT,
|
|
1858
|
+
request_id TEXT,
|
|
1859
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1860
|
+
);
|
|
1861
|
+
CREATE INDEX IF NOT EXISTS idx_admin_audit_user ON admin_audit_log(admin_user_id, created_at);
|
|
1862
|
+
CREATE INDEX IF NOT EXISTS idx_admin_audit_action ON admin_audit_log(action, created_at);
|
|
1863
|
+
CREATE INDEX IF NOT EXISTS idx_admin_audit_created ON admin_audit_log(created_at);
|
|
1864
|
+
`);
|
|
1865
|
+
},
|
|
1866
|
+
},
|
|
1867
|
+
// ── Migration 62: Add bridge_glow to app_settings ──
|
|
1868
|
+
{
|
|
1869
|
+
version: 62,
|
|
1870
|
+
description: "Add bridge_glow column to app_settings for glow bar toggle",
|
|
1871
|
+
up(db) {
|
|
1872
|
+
db.exec(`ALTER TABLE app_settings ADD COLUMN bridge_glow INTEGER DEFAULT 1`);
|
|
1873
|
+
},
|
|
1874
|
+
},
|
|
1875
|
+
{
|
|
1876
|
+
version: 63,
|
|
1877
|
+
description: "User feedback table",
|
|
1878
|
+
up(db) {
|
|
1879
|
+
db.exec(`
|
|
1880
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
1881
|
+
id TEXT PRIMARY KEY,
|
|
1882
|
+
user_id TEXT NOT NULL,
|
|
1883
|
+
type TEXT NOT NULL,
|
|
1884
|
+
mood TEXT,
|
|
1885
|
+
text TEXT NOT NULL,
|
|
1886
|
+
email TEXT,
|
|
1887
|
+
status TEXT NOT NULL DEFAULT 'new',
|
|
1888
|
+
admin_note TEXT,
|
|
1889
|
+
created_at TEXT NOT NULL
|
|
1890
|
+
);
|
|
1891
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_user ON feedback(user_id);
|
|
1892
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_status ON feedback(status);
|
|
1893
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_created ON feedback(created_at);
|
|
1894
|
+
`);
|
|
1895
|
+
},
|
|
1896
|
+
},
|
|
1897
|
+
{
|
|
1898
|
+
version: 64,
|
|
1899
|
+
description: "MCP browser auth: auth codes and refresh tokens",
|
|
1900
|
+
up(db) {
|
|
1901
|
+
db.exec(`CREATE TABLE IF NOT EXISTS mcp_auth_codes (
|
|
1902
|
+
code TEXT PRIMARY KEY,
|
|
1903
|
+
user_id TEXT NOT NULL,
|
|
1904
|
+
code_challenge TEXT NOT NULL,
|
|
1905
|
+
code_challenge_method TEXT NOT NULL DEFAULT 'S256',
|
|
1906
|
+
redirect_uri TEXT NOT NULL,
|
|
1907
|
+
state_hash TEXT NOT NULL,
|
|
1908
|
+
created_at INTEGER NOT NULL,
|
|
1909
|
+
expires_at INTEGER NOT NULL,
|
|
1910
|
+
used INTEGER NOT NULL DEFAULT 0,
|
|
1911
|
+
used_at INTEGER
|
|
1912
|
+
)`);
|
|
1913
|
+
db.exec(`CREATE TABLE IF NOT EXISTS mcp_refresh_tokens (
|
|
1914
|
+
token_hash TEXT PRIMARY KEY,
|
|
1915
|
+
user_id TEXT NOT NULL,
|
|
1916
|
+
family_id TEXT NOT NULL,
|
|
1917
|
+
device_label TEXT,
|
|
1918
|
+
created_at INTEGER NOT NULL,
|
|
1919
|
+
expires_at INTEGER NOT NULL,
|
|
1920
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
1921
|
+
revoked_at INTEGER,
|
|
1922
|
+
rotated_at INTEGER,
|
|
1923
|
+
last_used_at INTEGER,
|
|
1924
|
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
1925
|
+
)`);
|
|
1926
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mcp_rt_user ON mcp_refresh_tokens(user_id)");
|
|
1927
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_mcp_rt_family ON mcp_refresh_tokens(family_id)");
|
|
1928
|
+
}
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
version: 65,
|
|
1932
|
+
description: "Drop orphaned bridge_* columns from users and app_settings",
|
|
1933
|
+
up(db) {
|
|
1934
|
+
// Clean up any stale temp tables from a previous failed attempt
|
|
1935
|
+
db.exec("DROP TABLE IF EXISTS users_new");
|
|
1936
|
+
db.exec("DROP TABLE IF EXISTS app_settings_new");
|
|
1937
|
+
|
|
1938
|
+
// SQLite 3.35+ supports ALTER TABLE DROP COLUMN (better-sqlite3 bundles 3.45+)
|
|
1939
|
+
const dropCols = ["last_bridge_heartbeat", "bridge_mcp_configured", "last_bridge_device_id", "last_bridge_version", "bridge_engine_running"];
|
|
1940
|
+
const userCols = db.pragma("table_info(users)").map(c => c.name);
|
|
1941
|
+
for (const col of dropCols) {
|
|
1942
|
+
if (userCols.includes(col)) db.exec(`ALTER TABLE users DROP COLUMN ${col}`);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
const asCols = db.pragma("table_info(app_settings)").map(c => c.name);
|
|
1946
|
+
if (asCols.includes("bridge_glow")) db.exec("ALTER TABLE app_settings DROP COLUMN bridge_glow");
|
|
1947
|
+
},
|
|
1948
|
+
},
|
|
1949
|
+
// ── Migration 66: Fleet heartbeat tables ──
|
|
1950
|
+
{
|
|
1951
|
+
version: 66,
|
|
1952
|
+
description: "Fleet heartbeat tables for VPS monitoring",
|
|
1953
|
+
up(db) {
|
|
1954
|
+
db.exec(`
|
|
1955
|
+
DROP TABLE IF EXISTS fleet_heartbeats;
|
|
1956
|
+
DROP TABLE IF EXISTS fleet_events;
|
|
1957
|
+
DROP TABLE IF EXISTS fleet_nodes;
|
|
1958
|
+
CREATE TABLE fleet_nodes (
|
|
1959
|
+
id TEXT PRIMARY KEY,
|
|
1960
|
+
name TEXT NOT NULL,
|
|
1961
|
+
roles TEXT NOT NULL DEFAULT '[]',
|
|
1962
|
+
color TEXT,
|
|
1963
|
+
last_heartbeat_at TEXT,
|
|
1964
|
+
status TEXT NOT NULL DEFAULT 'offline',
|
|
1965
|
+
latest_payload TEXT NOT NULL DEFAULT '{}',
|
|
1966
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1967
|
+
);
|
|
1968
|
+
CREATE TABLE fleet_heartbeats (
|
|
1969
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1970
|
+
node_id TEXT NOT NULL,
|
|
1971
|
+
payload TEXT NOT NULL,
|
|
1972
|
+
received_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1973
|
+
FOREIGN KEY (node_id) REFERENCES fleet_nodes(id)
|
|
1974
|
+
);
|
|
1975
|
+
CREATE INDEX IF NOT EXISTS idx_fleet_hb_node ON fleet_heartbeats(node_id, received_at);
|
|
1976
|
+
CREATE TABLE IF NOT EXISTS fleet_events (
|
|
1977
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1978
|
+
node_id TEXT,
|
|
1979
|
+
type TEXT NOT NULL,
|
|
1980
|
+
severity TEXT NOT NULL DEFAULT 'info',
|
|
1981
|
+
message TEXT NOT NULL,
|
|
1982
|
+
detail TEXT DEFAULT '{}',
|
|
1983
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1984
|
+
);
|
|
1985
|
+
CREATE INDEX IF NOT EXISTS idx_fleet_events_created ON fleet_events(created_at);
|
|
1986
|
+
`);
|
|
1987
|
+
// Seed the 3 VPS nodes
|
|
1988
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO fleet_nodes (id, name, roles, color) VALUES (?, ?, ?, ?)");
|
|
1989
|
+
stmt.run("vps1", "VPS-1", JSON.stringify(["API Server", "Proxy"]), "#4ade80");
|
|
1990
|
+
stmt.run("vps2", "VPS-2", JSON.stringify(["Backup", "Logs"]), "#1B76FF");
|
|
1991
|
+
stmt.run("vps3", "VPS-3", JSON.stringify(["Web", "CDN Origin"]), "#a78bfa");
|
|
1992
|
+
},
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
version: 67,
|
|
1996
|
+
description: "Create public_project_pages table for community project pages",
|
|
1997
|
+
up: (db) => {
|
|
1998
|
+
db.exec(`
|
|
1999
|
+
CREATE TABLE IF NOT EXISTS public_project_pages (
|
|
2000
|
+
id TEXT PRIMARY KEY,
|
|
2001
|
+
project_id TEXT NOT NULL,
|
|
2002
|
+
user_id TEXT NOT NULL,
|
|
2003
|
+
slug TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
2004
|
+
is_published INTEGER NOT NULL DEFAULT 0,
|
|
2005
|
+
custom_slug INTEGER NOT NULL DEFAULT 0,
|
|
2006
|
+
public_title TEXT NOT NULL DEFAULT '',
|
|
2007
|
+
public_tagline TEXT NOT NULL DEFAULT '',
|
|
2008
|
+
public_image TEXT NOT NULL DEFAULT '',
|
|
2009
|
+
public_engine TEXT NOT NULL DEFAULT '',
|
|
2010
|
+
public_genre TEXT NOT NULL DEFAULT '',
|
|
2011
|
+
public_status TEXT NOT NULL DEFAULT '',
|
|
2012
|
+
public_team_size TEXT NOT NULL DEFAULT '',
|
|
2013
|
+
public_stage TEXT NOT NULL DEFAULT '',
|
|
2014
|
+
creator_display_name TEXT NOT NULL DEFAULT '',
|
|
2015
|
+
created_at TEXT NOT NULL,
|
|
2016
|
+
updated_at TEXT NOT NULL,
|
|
2017
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
|
2018
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
2019
|
+
);
|
|
2020
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_public_pages_slug ON public_project_pages(slug);
|
|
2021
|
+
CREATE INDEX IF NOT EXISTS idx_public_pages_project ON public_project_pages(project_id);
|
|
2022
|
+
CREATE INDEX IF NOT EXISTS idx_public_pages_user ON public_project_pages(user_id);
|
|
2023
|
+
CREATE INDEX IF NOT EXISTS idx_public_pages_published ON public_project_pages(is_published);
|
|
2024
|
+
`);
|
|
2025
|
+
},
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
version: 68,
|
|
2029
|
+
description: "Create burned_slugs table for permanent slug reservation + add public_code to projects",
|
|
2030
|
+
up: (db) => {
|
|
2031
|
+
// Burned slugs: permanently reserved codes from deleted/archived projects
|
|
2032
|
+
// These can NEVER be reused, even after project deletion
|
|
2033
|
+
db.exec(`
|
|
2034
|
+
CREATE TABLE IF NOT EXISTS burned_slugs (
|
|
2035
|
+
slug TEXT PRIMARY KEY COLLATE NOCASE,
|
|
2036
|
+
original_project_id TEXT,
|
|
2037
|
+
original_user_id TEXT,
|
|
2038
|
+
burned_at TEXT NOT NULL,
|
|
2039
|
+
reason TEXT NOT NULL DEFAULT 'deleted'
|
|
2040
|
+
);
|
|
2041
|
+
CREATE INDEX IF NOT EXISTS idx_burned_slugs_date ON burned_slugs(burned_at);
|
|
2042
|
+
`);
|
|
2043
|
+
// Add public_code column to projects table for automatic permanent code assignment
|
|
2044
|
+
try { db.exec("ALTER TABLE projects ADD COLUMN public_code TEXT"); } catch(_) {}
|
|
2045
|
+
try { db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_public_code ON projects(public_code) WHERE public_code IS NOT NULL"); } catch(_) {}
|
|
2046
|
+
},
|
|
2047
|
+
},
|
|
2048
|
+
// ── Migration 69: Image moderation system ──
|
|
2049
|
+
{
|
|
2050
|
+
version: 69,
|
|
2051
|
+
description: "Image uploads, moderation actions, and moderation reports tables",
|
|
2052
|
+
up(db) {
|
|
2053
|
+
db.exec(`
|
|
2054
|
+
CREATE TABLE IF NOT EXISTS image_uploads (
|
|
2055
|
+
id TEXT PRIMARY KEY,
|
|
2056
|
+
user_id TEXT NOT NULL,
|
|
2057
|
+
project_id TEXT,
|
|
2058
|
+
image_type TEXT NOT NULL,
|
|
2059
|
+
original_mime TEXT,
|
|
2060
|
+
original_size INTEGER,
|
|
2061
|
+
detected_mime TEXT,
|
|
2062
|
+
processed_mime TEXT DEFAULT 'image/webp',
|
|
2063
|
+
processed_size INTEGER,
|
|
2064
|
+
width INTEGER,
|
|
2065
|
+
height INTEGER,
|
|
2066
|
+
r2_key TEXT,
|
|
2067
|
+
moderation_state TEXT NOT NULL DEFAULT 'pending',
|
|
2068
|
+
moderation_reason TEXT,
|
|
2069
|
+
moderation_labels TEXT,
|
|
2070
|
+
moderation_provider TEXT,
|
|
2071
|
+
moderation_score REAL,
|
|
2072
|
+
moderation_decided_at TEXT,
|
|
2073
|
+
is_orphaned INTEGER DEFAULT 0,
|
|
2074
|
+
created_at TEXT NOT NULL,
|
|
2075
|
+
updated_at TEXT NOT NULL
|
|
2076
|
+
);
|
|
2077
|
+
CREATE INDEX IF NOT EXISTS idx_image_uploads_user ON image_uploads(user_id);
|
|
2078
|
+
CREATE INDEX IF NOT EXISTS idx_image_uploads_state ON image_uploads(moderation_state);
|
|
2079
|
+
CREATE INDEX IF NOT EXISTS idx_image_uploads_type ON image_uploads(image_type);
|
|
2080
|
+
|
|
2081
|
+
CREATE TABLE IF NOT EXISTS image_moderation_actions (
|
|
2082
|
+
id TEXT PRIMARY KEY,
|
|
2083
|
+
image_upload_id TEXT NOT NULL,
|
|
2084
|
+
actor_user_id TEXT NOT NULL,
|
|
2085
|
+
action TEXT NOT NULL,
|
|
2086
|
+
previous_state TEXT,
|
|
2087
|
+
new_state TEXT,
|
|
2088
|
+
note TEXT,
|
|
2089
|
+
created_at TEXT NOT NULL,
|
|
2090
|
+
FOREIGN KEY (image_upload_id) REFERENCES image_uploads(id)
|
|
2091
|
+
);
|
|
2092
|
+
CREATE INDEX IF NOT EXISTS idx_mod_actions_image ON image_moderation_actions(image_upload_id);
|
|
2093
|
+
CREATE INDEX IF NOT EXISTS idx_mod_actions_actor ON image_moderation_actions(actor_user_id);
|
|
2094
|
+
|
|
2095
|
+
CREATE TABLE IF NOT EXISTS image_moderation_reports (
|
|
2096
|
+
id TEXT PRIMARY KEY,
|
|
2097
|
+
image_upload_id TEXT NOT NULL,
|
|
2098
|
+
reporter_user_id TEXT NOT NULL,
|
|
2099
|
+
reason TEXT NOT NULL,
|
|
2100
|
+
detail TEXT,
|
|
2101
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
2102
|
+
reviewed_by TEXT,
|
|
2103
|
+
reviewed_at TEXT,
|
|
2104
|
+
created_at TEXT NOT NULL,
|
|
2105
|
+
FOREIGN KEY (image_upload_id) REFERENCES image_uploads(id)
|
|
2106
|
+
);
|
|
2107
|
+
CREATE INDEX IF NOT EXISTS idx_image_reports_status ON image_moderation_reports(status);
|
|
2108
|
+
CREATE INDEX IF NOT EXISTS idx_image_reports_image ON image_moderation_reports(image_upload_id);
|
|
2109
|
+
`);
|
|
2110
|
+
},
|
|
2111
|
+
},
|
|
2112
|
+
// ── Migration 70: Add content_hash to image_uploads for abuse fingerprinting ──
|
|
2113
|
+
{
|
|
2114
|
+
version: 70,
|
|
2115
|
+
description: "Add content_hash column to image_uploads for abuse fingerprinting",
|
|
2116
|
+
up(db) {
|
|
2117
|
+
try { db.exec("ALTER TABLE image_uploads ADD COLUMN content_hash TEXT"); } catch(_) {}
|
|
2118
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_image_uploads_hash ON image_uploads(content_hash)");
|
|
2119
|
+
},
|
|
2120
|
+
},
|
|
2121
|
+
// ── Migration 71: Expand ue_discovered_bps for Blueprint Intelligence sync ──
|
|
2122
|
+
{
|
|
2123
|
+
version: 71,
|
|
2124
|
+
description: "Expand ue_discovered_bps with data blob, content_hash, sync_status for Blueprint Intelligence",
|
|
2125
|
+
up(db) {
|
|
2126
|
+
try { db.exec("ALTER TABLE ue_discovered_bps ADD COLUMN data TEXT DEFAULT '{}'"); } catch(_) {}
|
|
2127
|
+
try { db.exec("ALTER TABLE ue_discovered_bps ADD COLUMN content_hash TEXT"); } catch(_) {}
|
|
2128
|
+
try { db.exec("ALTER TABLE ue_discovered_bps ADD COLUMN updated_at TEXT"); } catch(_) {}
|
|
2129
|
+
try { db.exec("ALTER TABLE ue_discovered_bps ADD COLUMN sync_status TEXT DEFAULT 'complete'"); } catch(_) {}
|
|
2130
|
+
try { db.exec("ALTER TABLE ue_discovered_bps ADD COLUMN sync_warnings TEXT DEFAULT '[]'"); } catch(_) {}
|
|
2131
|
+
},
|
|
2132
|
+
},
|
|
2133
|
+
// ── Migration 72: Blueprint snapshots + change tracking for timeline ──
|
|
2134
|
+
{
|
|
2135
|
+
version: 72,
|
|
2136
|
+
description: "Create blueprint_snapshots and blueprint_changes tables for change timeline",
|
|
2137
|
+
up(db) {
|
|
2138
|
+
db.exec(`
|
|
2139
|
+
CREATE TABLE IF NOT EXISTS blueprint_snapshots (
|
|
2140
|
+
id TEXT PRIMARY KEY,
|
|
2141
|
+
project_id TEXT NOT NULL,
|
|
2142
|
+
blueprint_id TEXT NOT NULL,
|
|
2143
|
+
snapshot_hash TEXT NOT NULL,
|
|
2144
|
+
data TEXT NOT NULL,
|
|
2145
|
+
metrics TEXT DEFAULT '{}',
|
|
2146
|
+
created_at TEXT NOT NULL,
|
|
2147
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
2148
|
+
);
|
|
2149
|
+
CREATE INDEX IF NOT EXISTS idx_bp_snapshots_bp ON blueprint_snapshots(blueprint_id);
|
|
2150
|
+
CREATE INDEX IF NOT EXISTS idx_bp_snapshots_created ON blueprint_snapshots(created_at);
|
|
2151
|
+
|
|
2152
|
+
CREATE TABLE IF NOT EXISTS blueprint_changes (
|
|
2153
|
+
id TEXT PRIMARY KEY,
|
|
2154
|
+
project_id TEXT NOT NULL,
|
|
2155
|
+
blueprint_id TEXT NOT NULL,
|
|
2156
|
+
snapshot_id TEXT NOT NULL,
|
|
2157
|
+
prev_snapshot_id TEXT,
|
|
2158
|
+
diff TEXT NOT NULL,
|
|
2159
|
+
summary TEXT NOT NULL,
|
|
2160
|
+
complexity_delta INTEGER DEFAULT 0,
|
|
2161
|
+
is_layout_only INTEGER DEFAULT 0,
|
|
2162
|
+
created_at TEXT NOT NULL,
|
|
2163
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
2164
|
+
);
|
|
2165
|
+
CREATE INDEX IF NOT EXISTS idx_bp_changes_bp ON blueprint_changes(blueprint_id);
|
|
2166
|
+
`);
|
|
2167
|
+
},
|
|
2168
|
+
},
|
|
2169
|
+
// ── Migration 73: Blueprint standards + violations for review system ──
|
|
2170
|
+
{
|
|
2171
|
+
version: 73,
|
|
2172
|
+
description: "Create blueprint_standards and blueprint_violations tables for AI review",
|
|
2173
|
+
up(db) {
|
|
2174
|
+
db.exec(`
|
|
2175
|
+
CREATE TABLE IF NOT EXISTS blueprint_standards (
|
|
2176
|
+
id TEXT PRIMARY KEY,
|
|
2177
|
+
project_id TEXT NOT NULL,
|
|
2178
|
+
rule_id TEXT NOT NULL,
|
|
2179
|
+
enabled INTEGER DEFAULT 1,
|
|
2180
|
+
severity TEXT DEFAULT 'warning',
|
|
2181
|
+
created_at TEXT NOT NULL,
|
|
2182
|
+
UNIQUE(project_id, rule_id),
|
|
2183
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
2184
|
+
);
|
|
2185
|
+
|
|
2186
|
+
CREATE TABLE IF NOT EXISTS blueprint_violations (
|
|
2187
|
+
id TEXT PRIMARY KEY,
|
|
2188
|
+
project_id TEXT NOT NULL,
|
|
2189
|
+
blueprint_id TEXT NOT NULL,
|
|
2190
|
+
standard_id TEXT NOT NULL,
|
|
2191
|
+
rule_id TEXT NOT NULL,
|
|
2192
|
+
detail TEXT DEFAULT '{}',
|
|
2193
|
+
resolved INTEGER DEFAULT 0,
|
|
2194
|
+
found_at TEXT NOT NULL,
|
|
2195
|
+
resolved_at TEXT,
|
|
2196
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
2197
|
+
);
|
|
2198
|
+
CREATE INDEX IF NOT EXISTS idx_bp_violations_project ON blueprint_violations(project_id);
|
|
2199
|
+
CREATE INDEX IF NOT EXISTS idx_bp_violations_bp ON blueprint_violations(blueprint_id);
|
|
2200
|
+
`);
|
|
2201
|
+
},
|
|
2202
|
+
},
|
|
2203
|
+
// ── Migration 74: Blueprint dependencies for impact analysis ──
|
|
2204
|
+
{
|
|
2205
|
+
version: 74,
|
|
2206
|
+
description: "Create blueprint_dependencies table for impact analysis",
|
|
2207
|
+
up(db) {
|
|
2208
|
+
db.exec(`
|
|
2209
|
+
CREATE TABLE IF NOT EXISTS blueprint_dependencies (
|
|
2210
|
+
id TEXT PRIMARY KEY,
|
|
2211
|
+
project_id TEXT NOT NULL,
|
|
2212
|
+
source_bp_id TEXT NOT NULL,
|
|
2213
|
+
target_bp_id TEXT NOT NULL,
|
|
2214
|
+
dependency_type TEXT NOT NULL,
|
|
2215
|
+
weight INTEGER DEFAULT 1,
|
|
2216
|
+
confidence TEXT DEFAULT 'full',
|
|
2217
|
+
detail TEXT DEFAULT '{}',
|
|
2218
|
+
computed_at TEXT NOT NULL,
|
|
2219
|
+
UNIQUE(source_bp_id, target_bp_id, dependency_type),
|
|
2220
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
2221
|
+
);
|
|
2222
|
+
CREATE INDEX IF NOT EXISTS idx_bp_deps_source ON blueprint_dependencies(source_bp_id);
|
|
2223
|
+
CREATE INDEX IF NOT EXISTS idx_bp_deps_target ON blueprint_dependencies(target_bp_id);
|
|
2224
|
+
`);
|
|
2225
|
+
},
|
|
2226
|
+
},
|
|
2227
|
+
];
|
|
2228
|
+
|
|
2229
|
+
function runMigrations(db) {
|
|
2230
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _migrations (
|
|
2231
|
+
version INTEGER PRIMARY KEY,
|
|
2232
|
+
description TEXT,
|
|
2233
|
+
applied_at TEXT NOT NULL
|
|
2234
|
+
)`);
|
|
2235
|
+
|
|
2236
|
+
const applied = db.prepare("SELECT version FROM _migrations").all().map(r => r.version);
|
|
2237
|
+
|
|
2238
|
+
for (const m of MIGRATIONS) {
|
|
2239
|
+
if (!applied.includes(m.version)) {
|
|
2240
|
+
m.up(db);
|
|
2241
|
+
db.prepare("INSERT INTO _migrations (version, description, applied_at) VALUES (?, ?, ?)")
|
|
2242
|
+
.run(m.version, m.description, new Date().toISOString());
|
|
2243
|
+
console.log(`Migration ${m.version}: ${m.description}`);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// ===== AUTO BACKUP =====
|
|
2249
|
+
|
|
2250
|
+
function createBackup() {
|
|
2251
|
+
ensureDir(BACKUP_DIR);
|
|
2252
|
+
if (!_db) return null;
|
|
2253
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2254
|
+
const backupPath = path.join(BACKUP_DIR, `taskover-${timestamp}.db`);
|
|
2255
|
+
if (_engine === "sql.js") {
|
|
2256
|
+
_db.backup(backupPath);
|
|
2257
|
+
} else {
|
|
2258
|
+
// better-sqlite3: use VACUUM INTO for a synchronous consistent copy
|
|
2259
|
+
try {
|
|
2260
|
+
_db.exec(`VACUUM INTO '${backupPath.replace(/'/g, "''")}'`);
|
|
2261
|
+
} catch (_) {
|
|
2262
|
+
fs.copyFileSync(DB_FILE, backupPath);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// S6A-14: Encrypt backup if crypto is initialized
|
|
2267
|
+
let finalPath = backupPath;
|
|
2268
|
+
try {
|
|
2269
|
+
const cryptoModule = require("./crypto");
|
|
2270
|
+
if (cryptoModule.isCryptoInitialized()) {
|
|
2271
|
+
const encPath = backupPath + ".enc";
|
|
2272
|
+
cryptoModule.encryptBackup(backupPath, encPath);
|
|
2273
|
+
fs.unlinkSync(backupPath); // Remove plaintext copy
|
|
2274
|
+
finalPath = encPath;
|
|
2275
|
+
}
|
|
2276
|
+
} catch (_) {
|
|
2277
|
+
// Crypto not available — keep plaintext backup
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
// Keep only last 10 backups (match both .db and .db.enc)
|
|
2281
|
+
const backups = fs.readdirSync(BACKUP_DIR)
|
|
2282
|
+
.filter(f => f.startsWith("taskover-") && (f.endsWith(".db") || f.endsWith(".db.enc")))
|
|
2283
|
+
.sort();
|
|
2284
|
+
while (backups.length > 10) {
|
|
2285
|
+
const oldest = backups.shift();
|
|
2286
|
+
try { fs.unlinkSync(path.join(BACKUP_DIR, oldest)); } catch (_) {}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
return finalPath;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
module.exports = { initDb, getDb, closeDb, reloadFromDisk, createBackup, saveToDisk, getLastSaveTime, getEngine, DB_FILE, DATA_DIR, BACKUP_DIR };
|