This commit is contained in:
jarek committed 2026-07-25 08:41:01 +02:00
1 parent 9f09d60a83
commit 64b6dcd744
177 files changed
+21182 -1042

No files matched your search

+12 -1
View File
@@ -655,6 +655,11 @@ async function initializeDatabase() {
if (verbose) logStep('Opening SQLite database...');
rawClient = new Database(dbPath);
// Enforce foreign keys — better-sqlite3 defaults this OFF, which made every
// ON DELETE CASCADE in the schema (backup_configs → environments/destinations,
// etc.) dead code, leaving dangling rows + ghost schedules after a delete
// (audit #12). Must be set per-connection, before any statements run.
rawClient.pragma('foreign_keys = ON');
// Enable WAL mode for better performance and concurrency
rawClient.pragma('journal_mode = WAL');
// Synchronous NORMAL is a good balance between safety and speed
@@ -924,6 +929,8 @@ export const scheduleExecutions = schemaProxy.scheduleExecutions;
export const stackEnvironmentVariables = schemaProxy.stackEnvironmentVariables;
export const pendingContainerUpdates = schemaProxy.pendingContainerUpdates;
export const apiTokens = schemaProxy.apiTokens;
export const backupDestinations = schemaProxy.backupDestinations;
export const backupConfigs = schemaProxy.backupConfigs;
// Re-export types from SQLite schema (they're compatible with PostgreSQL)
export type {
@@ -984,7 +991,11 @@ export type {
PendingContainerUpdate,
NewPendingContainerUpdate,
ApiToken,
NewApiToken
NewApiToken,
BackupDestination,
NewBackupDestination,
BackupConfig,
NewBackupConfig
} from './schema/index.js';
export { eq, and, or, desc, asc, like, sql, inArray, isNull, isNotNull } from 'drizzle-orm';
+51 -1
View File
@@ -444,7 +444,7 @@ export const scheduleExecutions = sqliteTable('schedule_executions', {
completedAt: text('completed_at'),
duration: integer('duration'), // milliseconds
// Result
status: text('status').notNull(), // 'queued' | 'running' | 'success' | 'failed' | 'skipped'
status: text('status').notNull(), // 'queued' | 'running' | 'success' | 'warning' | 'failed' | 'skipped'
errorMessage: text('error_message'),
// Details
details: text('details'), // JSON with execution details
@@ -470,6 +470,50 @@ export const pendingContainerUpdates = sqliteTable('pending_container_updates',
envContainerUnique: unique().on(table.environmentId, table.containerId)
}));
// =============================================================================
// BACKUP DESTINATIONS TABLE
// =============================================================================
export const backupDestinations = sqliteTable('backup_destinations', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull().unique(),
repository: text('repository').notNull(), // restic repo URL/path
password: text('password').notNull(), // AES-256-GCM encrypted
envVars: text('env_vars'), // JSON: { key: value } — values encrypted
flags: text('flags'), // extra restic CLI flags
hostPath: text('host_path'), // bind-mount source for local repos
policies: text('policies'), // JSON: { pruneSchedule, checkSchedule, autoUnlock, maxUnused }
lastTestAt: text('last_test_at'),
lastTestStatus: text('last_test_status'), // 'success' | 'failed'
lastTestError: text('last_test_error'),
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`)
});
// =============================================================================
// BACKUP CONFIGS TABLE
// =============================================================================
export const backupConfigs = sqliteTable('backup_configs', {
id: integer('id').primaryKey({ autoIncrement: true }),
type: text('type').notNull().default('container'), // 'container' | 'stack'
targetName: text('target_name').notNull(),
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
destinationId: integer('destination_id').notNull().references(() => backupDestinations.id, { onDelete: 'cascade' }),
enabled: integer('enabled', { mode: 'boolean' }).default(true),
allVolumes: integer('all_volumes', { mode: 'boolean' }).default(true),
selectedVolumes: text('selected_volumes'), // JSON array of volume names
stopBeforeBackup: integer('stop_before_backup', { mode: 'boolean' }).default(false),
schedule: text('schedule'), // cron expression
retention: text('retention'), // JSON: { keepLast?, keepDaily?, keepWeekly?, keepMonthly?, keepYearly? }
options: text('options'), // JSON: { compression?, limitUpload?, limitDownload?, excludePatterns?, webhookSuccess?, webhookFailure? }
tags: text('tags'), // JSON array of strings
lastBackupAt: text('last_backup_at'),
lastBackupStatus: text('last_backup_status'), // 'success' | 'failed'
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`)
});
// =============================================================================
// API TOKENS TABLE
// =============================================================================
@@ -608,3 +652,9 @@ export type NewPendingContainerUpdate = typeof pendingContainerUpdates.$inferIns
export type ApiToken = typeof apiTokens.$inferSelect;
export type NewApiToken = typeof apiTokens.$inferInsert;
export type BackupDestination = typeof backupDestinations.$inferSelect;
export type NewBackupDestination = typeof backupDestinations.$inferInsert;
export type BackupConfig = typeof backupConfigs.$inferSelect;
export type NewBackupConfig = typeof backupConfigs.$inferInsert;
+45 -1
View File
@@ -447,7 +447,7 @@ export const scheduleExecutions = pgTable('schedule_executions', {
completedAt: timestamp('completed_at', { mode: 'string' }),
duration: integer('duration'), // milliseconds
// Result
status: text('status').notNull(), // 'queued' | 'running' | 'success' | 'failed' | 'skipped'
status: text('status').notNull(), // 'queued' | 'running' | 'success' | 'warning' | 'failed' | 'skipped'
errorMessage: text('error_message'),
// Details
details: text('details'), // JSON with execution details
@@ -473,6 +473,50 @@ export const pendingContainerUpdates = pgTable('pending_container_updates', {
envContainerUnique: unique().on(table.environmentId, table.containerId)
}));
// =============================================================================
// BACKUP DESTINATIONS TABLE
// =============================================================================
export const backupDestinations = pgTable('backup_destinations', {
id: serial('id').primaryKey(),
name: text('name').notNull().unique(),
repository: text('repository').notNull(),
password: text('password').notNull(),
envVars: text('env_vars'),
flags: text('flags'),
hostPath: text('host_path'),
policies: text('policies'),
lastTestAt: timestamp('last_test_at', { mode: 'string' }),
lastTestStatus: text('last_test_status'),
lastTestError: text('last_test_error'),
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
});
// =============================================================================
// BACKUP CONFIGS TABLE
// =============================================================================
export const backupConfigs = pgTable('backup_configs', {
id: serial('id').primaryKey(),
type: text('type').notNull().default('container'),
targetName: text('target_name').notNull(),
environmentId: integer('environment_id').references(() => environments.id, { onDelete: 'cascade' }),
destinationId: integer('destination_id').notNull().references(() => backupDestinations.id, { onDelete: 'cascade' }),
enabled: boolean('enabled').default(true),
allVolumes: boolean('all_volumes').default(true),
selectedVolumes: text('selected_volumes'),
stopBeforeBackup: boolean('stop_before_backup').default(false),
schedule: text('schedule'),
retention: text('retention'),
options: text('options'),
tags: text('tags'),
lastBackupAt: timestamp('last_backup_at', { mode: 'string' }),
lastBackupStatus: text('last_backup_status'),
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
});
// =============================================================================
// API TOKENS TABLE
// =============================================================================