jettypod 4.4.81 → 4.4.82
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.
|
@@ -203,6 +203,8 @@ export function DragProvider({ children, renderDragOverlay }: DragProviderProps)
|
|
|
203
203
|
pointerEvents: 'none',
|
|
204
204
|
transform: 'scale(1.02)',
|
|
205
205
|
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.2)',
|
|
206
|
+
borderRadius: 8,
|
|
207
|
+
overflow: 'hidden',
|
|
206
208
|
}}
|
|
207
209
|
>
|
|
208
210
|
{renderDragOverlay(draggedItem)}
|
package/lib/database.js
CHANGED
|
@@ -390,27 +390,151 @@ async function waitForMigrations() {
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Attempt to recover work.db from JSON snapshots
|
|
395
|
+
* Used when database is missing or corrupted
|
|
396
|
+
* @returns {Promise<{recovered: boolean, itemCount: number}>} Recovery result
|
|
397
|
+
*/
|
|
398
|
+
async function recoverFromSnapshots() {
|
|
399
|
+
const snapshotsDir = path.join(getJettypodDir(), 'snapshots');
|
|
400
|
+
const jsonPath = path.join(snapshotsDir, 'work.json');
|
|
401
|
+
|
|
402
|
+
// Check if snapshots exist
|
|
403
|
+
if (!fs.existsSync(jsonPath)) {
|
|
404
|
+
return { recovered: false, itemCount: 0, reason: 'No snapshot file found' };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Read and parse snapshot
|
|
408
|
+
let data;
|
|
409
|
+
try {
|
|
410
|
+
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
411
|
+
data = JSON.parse(jsonContent);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
return { recovered: false, itemCount: 0, reason: `Failed to read snapshot: ${err.message}` };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Close existing connection and delete corrupted file
|
|
417
|
+
await closeDb();
|
|
418
|
+
const dbFilePath = getDbPath();
|
|
419
|
+
|
|
420
|
+
// Remove corrupted database files (including WAL files)
|
|
421
|
+
const filesToRemove = [dbFilePath, `${dbFilePath}-wal`, `${dbFilePath}-shm`];
|
|
422
|
+
for (const file of filesToRemove) {
|
|
423
|
+
if (fs.existsSync(file)) {
|
|
424
|
+
fs.unlinkSync(file);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Reset singleton to force fresh connection
|
|
429
|
+
resetDb();
|
|
430
|
+
|
|
431
|
+
// Create fresh database (this will create schema)
|
|
432
|
+
const database = getDb();
|
|
433
|
+
await waitForMigrations();
|
|
434
|
+
|
|
435
|
+
// Import data from snapshot
|
|
436
|
+
const tableNames = Object.keys(data);
|
|
437
|
+
let totalItems = 0;
|
|
438
|
+
|
|
439
|
+
for (const tableName of tableNames) {
|
|
440
|
+
const rows = data[tableName];
|
|
441
|
+
if (!rows || rows.length === 0) continue;
|
|
442
|
+
|
|
443
|
+
// Get column names from first row
|
|
444
|
+
const columns = Object.keys(rows[0]);
|
|
445
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
446
|
+
const columnNames = columns.join(', ');
|
|
447
|
+
const insertSql = `INSERT INTO ${tableName} (${columnNames}) VALUES (${placeholders})`;
|
|
448
|
+
|
|
449
|
+
for (const row of rows) {
|
|
450
|
+
const values = columns.map(col => row[col]);
|
|
451
|
+
await new Promise((resolve, reject) => {
|
|
452
|
+
database.run(insertSql, values, (err) => {
|
|
453
|
+
if (err) reject(err);
|
|
454
|
+
else resolve();
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
totalItems++;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return { recovered: true, itemCount: totalItems };
|
|
462
|
+
}
|
|
463
|
+
|
|
393
464
|
/**
|
|
394
465
|
* Run startup validation checks on the database
|
|
395
466
|
* Call this early in application startup to detect corruption before operations fail
|
|
396
|
-
*
|
|
397
|
-
* @
|
|
467
|
+
* Automatically attempts recovery from snapshots if corruption is detected
|
|
468
|
+
* @returns {Promise<void>} Resolves if database is healthy (or recovered), rejects if unrecoverable
|
|
469
|
+
* @throws {Error} If database is corrupted and recovery fails
|
|
398
470
|
*/
|
|
399
471
|
async function validateOnStartup() {
|
|
400
|
-
|
|
401
|
-
|
|
472
|
+
// Check if database file is missing
|
|
473
|
+
const dbFilePath = getDbPath();
|
|
474
|
+
const dbMissing = !fs.existsSync(dbFilePath);
|
|
475
|
+
|
|
476
|
+
if (dbMissing) {
|
|
477
|
+
// Try to recover from snapshots
|
|
478
|
+
console.log('⚠️ Database file missing, attempting recovery from snapshots...');
|
|
479
|
+
const result = await recoverFromSnapshots();
|
|
480
|
+
if (result.recovered) {
|
|
481
|
+
console.log(`✅ Recovered ${result.itemCount} items from snapshots`);
|
|
482
|
+
return;
|
|
483
|
+
} else {
|
|
484
|
+
// No snapshots - just create fresh DB (getDb will do this)
|
|
485
|
+
console.log('ℹ️ No snapshots found, creating fresh database');
|
|
486
|
+
getDb();
|
|
487
|
+
await waitForMigrations();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Database exists - check integrity
|
|
493
|
+
let database;
|
|
494
|
+
try {
|
|
495
|
+
database = getDb();
|
|
496
|
+
await waitForMigrations();
|
|
497
|
+
} catch (err) {
|
|
498
|
+
// Failed to even open the database - try recovery
|
|
499
|
+
console.log('⚠️ Database failed to open, attempting recovery from snapshots...');
|
|
500
|
+
const result = await recoverFromSnapshots();
|
|
501
|
+
if (result.recovered) {
|
|
502
|
+
console.log(`✅ Recovered ${result.itemCount} items from snapshots`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Database failed to open and recovery failed.\n` +
|
|
507
|
+
`Original error: ${err.message}\n` +
|
|
508
|
+
`Recovery error: ${result.reason}\n\n` +
|
|
509
|
+
`Manual recovery options:\n` +
|
|
510
|
+
` 1. Restore from backup: jettypod work restore-backup latest\n` +
|
|
511
|
+
` 2. Check ~/.jettypod-backups/ for global backups`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
402
514
|
|
|
403
|
-
// Check file integrity
|
|
515
|
+
// Check file integrity
|
|
404
516
|
const integrity = await checkIntegrity(database);
|
|
405
517
|
if (!integrity.ok) {
|
|
518
|
+
console.log('⚠️ Database corruption detected, attempting recovery from snapshots...');
|
|
519
|
+
const result = await recoverFromSnapshots();
|
|
520
|
+
if (result.recovered) {
|
|
521
|
+
console.log(`✅ Recovered ${result.itemCount} items from snapshots`);
|
|
522
|
+
// Verify the recovered database
|
|
523
|
+
const newDb = getDb();
|
|
524
|
+
const newIntegrity = await checkIntegrity(newDb);
|
|
525
|
+
if (newIntegrity.ok) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Recovery failed or still corrupted
|
|
406
531
|
const errorList = integrity.errors.join('\n - ');
|
|
407
532
|
throw new Error(
|
|
408
|
-
`Database integrity check failed
|
|
533
|
+
`Database integrity check failed and automatic recovery was unsuccessful.\n` +
|
|
409
534
|
`Errors found:\n - ${errorList}\n\n` +
|
|
410
|
-
`
|
|
535
|
+
`Manual recovery options:\n` +
|
|
411
536
|
` 1. Restore from backup: jettypod work restore-backup latest\n` +
|
|
412
|
-
` 2.
|
|
413
|
-
` 3. Check ~/.jettypod-backups/ for global backups`
|
|
537
|
+
` 2. Check ~/.jettypod-backups/ for global backups`
|
|
414
538
|
);
|
|
415
539
|
}
|
|
416
540
|
|
|
@@ -428,6 +552,7 @@ module.exports = {
|
|
|
428
552
|
validateSchema,
|
|
429
553
|
checkIntegrity,
|
|
430
554
|
validateOnStartup,
|
|
555
|
+
recoverFromSnapshots,
|
|
431
556
|
dbPath, // Deprecated: use getDbPath() for dynamic path
|
|
432
557
|
jettypodDir // Deprecated: use getJettypodDir() for dynamic path
|
|
433
558
|
};
|