Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 129 additions & 1 deletion src/lib/server/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
configSets,
hostMetrics,
autoUpdateSettings,
containerStartSchedules,
notificationSettings,
environmentNotifications,
authSettings,
Expand Down Expand Up @@ -55,6 +56,7 @@ import {
type ConfigSet,
type HostMetric,
type AutoUpdateSetting,
type ContainerStartSchedule,
type NotificationSetting,
type EnvironmentNotification,
type AuthSetting,
Expand Down Expand Up @@ -88,6 +90,7 @@ export type {
ConfigSet,
HostMetric,
AutoUpdateSetting as AutoUpdateSettingType,
ContainerStartSchedule as ContainerStartScheduleType,
User,
Session,
Role,
Expand Down Expand Up @@ -624,6 +627,18 @@ export interface AutoUpdateSettingData {
updatedAt: string;
}

export interface ContainerStartScheduleData {
id: number;
environmentId: number | null;
containerName: string;
enabled: boolean;
scheduleType: 'daily' | 'weekly' | 'custom';
cronExpression: string | null;
lastStarted: string | null;
createdAt: string;
updatedAt: string;
}

export async function getAutoUpdateSettings(environmentId?: number): Promise<AutoUpdateSettingData[]> {
if (environmentId) {
return db.select().from(autoUpdateSettings)
Expand Down Expand Up @@ -754,6 +769,119 @@ export async function renameAutoUpdateSchedule(
return true;
}

export async function getContainerStartSchedules(environmentId?: number): Promise<ContainerStartScheduleData[]> {
if (environmentId) {
return db.select().from(containerStartSchedules)
.where(eq(containerStartSchedules.environmentId, environmentId)) as Promise<ContainerStartScheduleData[]>;
}
return db.select().from(containerStartSchedules) as Promise<ContainerStartScheduleData[]>;
}

export async function getContainerStartSchedule(containerName: string, environmentId?: number): Promise<ContainerStartScheduleData | undefined> {
const results = await db.select().from(containerStartSchedules)
.where(and(
eq(containerStartSchedules.containerName, containerName),
environmentId ? eq(containerStartSchedules.environmentId, environmentId) : isNull(containerStartSchedules.environmentId)
));
return results[0] as ContainerStartScheduleData | undefined;
}

export async function getContainerStartScheduleById(id: number): Promise<ContainerStartScheduleData | undefined> {
const results = await db.select().from(containerStartSchedules)
.where(eq(containerStartSchedules.id, id));
return results[0] as ContainerStartScheduleData | undefined;
}

export async function updateContainerStartScheduleById(id: number, data: Partial<ContainerStartScheduleData>): Promise<void> {
await db.update(containerStartSchedules)
.set({
...data,
updatedAt: new Date().toISOString()
})
.where(eq(containerStartSchedules.id, id));
}

export async function getEnabledContainerStartSchedules(): Promise<ContainerStartScheduleData[]> {
return db.select().from(containerStartSchedules)
.where(eq(containerStartSchedules.enabled, true)) as Promise<ContainerStartScheduleData[]>;
}

export async function getAllContainerStartSchedules(): Promise<ContainerStartScheduleData[]> {
return db.select().from(containerStartSchedules)
.orderBy(desc(containerStartSchedules.containerName)) as Promise<ContainerStartScheduleData[]>;
}

export async function upsertContainerStartSchedule(
containerName: string,
settingsData: {
enabled: boolean;
scheduleType: 'daily' | 'weekly' | 'custom';
cronExpression?: string | null;
},
environmentId?: number
): Promise<ContainerStartScheduleData> {
const existing = await getContainerStartSchedule(containerName, environmentId);

if (existing) {
await db.update(containerStartSchedules)
.set({
enabled: settingsData.enabled,
scheduleType: settingsData.scheduleType,
cronExpression: settingsData.cronExpression || null,
updatedAt: new Date().toISOString()
})
.where(eq(containerStartSchedules.id, existing.id));
return getContainerStartSchedule(containerName, environmentId) as Promise<ContainerStartScheduleData>;
}

await db.insert(containerStartSchedules).values({
environmentId: environmentId || null,
containerName,
enabled: settingsData.enabled,
scheduleType: settingsData.scheduleType,
cronExpression: settingsData.cronExpression || null
});

return getContainerStartSchedule(containerName, environmentId) as Promise<ContainerStartScheduleData>;
}

export async function updateContainerStartLastStarted(containerName: string, environmentId?: number): Promise<void> {
await db.update(containerStartSchedules)
.set({
lastStarted: new Date().toISOString(),
updatedAt: new Date().toISOString()
})
.where(and(
eq(containerStartSchedules.containerName, containerName),
environmentId ? eq(containerStartSchedules.environmentId, environmentId) : isNull(containerStartSchedules.environmentId)
));
}

export async function deleteContainerStartSchedule(containerName: string, environmentId?: number): Promise<boolean> {
await db.delete(containerStartSchedules)
.where(and(
eq(containerStartSchedules.containerName, containerName),
environmentId ? eq(containerStartSchedules.environmentId, environmentId) : isNull(containerStartSchedules.environmentId)
));
return true;
}

export async function renameContainerStartSchedule(
oldName: string,
newName: string,
environmentId?: number
): Promise<boolean> {
await db.update(containerStartSchedules)
.set({ containerName: newName })
.where(and(
eq(containerStartSchedules.containerName, oldName),
environmentId
? eq(containerStartSchedules.environmentId, environmentId)
: isNull(containerStartSchedules.environmentId)
));
return true;
}

// =============================================================================
// NOTIFICATION SETTINGS
// =============================================================================
Expand Down Expand Up @@ -3827,7 +3955,7 @@ export async function saveDashboardPreferences(data: {
// SCHEDULE EXECUTION OPERATIONS
// =============================================================================

export type ScheduleType = 'container_update' | 'git_stack_sync' | 'system_cleanup' | 'env_update_check';
export type ScheduleType = 'container_update' | 'container_start' | 'git_stack_sync' | 'system_cleanup' | 'env_update_check' | 'image_prune';
export type ScheduleTrigger = 'cron' | 'webhook' | 'manual' | 'startup';
export type ScheduleStatus = 'queued' | 'running' | 'success' | 'failed' | 'skipped';

Expand Down
3 changes: 3 additions & 0 deletions src/lib/server/db/drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ export const stackEvents = schemaProxy.stackEvents;
export const hostMetrics = schemaProxy.hostMetrics;
export const configSets = schemaProxy.configSets;
export const autoUpdateSettings = schemaProxy.autoUpdateSettings;
export const containerStartSchedules = schemaProxy.containerStartSchedules;
export const notificationSettings = schemaProxy.notificationSettings;
export const environmentNotifications = schemaProxy.environmentNotifications;
export const authSettings = schemaProxy.authSettings;
Expand Down Expand Up @@ -951,6 +952,8 @@ export type {
NewStackEvent,
AutoUpdateSetting,
NewAutoUpdateSetting,
ContainerStartSchedule,
NewContainerStartSchedule,
UserPreference,
NewUserPreference,
ScheduleExecution,
Expand Down
17 changes: 17 additions & 0 deletions src/lib/server/db/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ export const autoUpdateSettings = sqliteTable('auto_update_settings', {
envContainerUnique: unique().on(table.environmentId, table.containerName)
}));

export const containerStartSchedules = sqliteTable('container_start_schedules', {
id: integer('id').primaryKey({ autoIncrement: true }),
environmentId: integer('environment_id').references(() => environments.id),
containerName: text('container_name').notNull(),
enabled: integer('enabled', { mode: 'boolean' }).default(false),
scheduleType: text('schedule_type').default('daily'),
cronExpression: text('cron_expression'),
lastStarted: text('last_started'),
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`)
}, (table) => ({
envContainerUnique: unique().on(table.environmentId, table.containerName)
}));

export const notificationSettings = sqliteTable('notification_settings', {
id: integer('id').primaryKey({ autoIncrement: true }),
type: text('type').notNull(),
Expand Down Expand Up @@ -580,6 +594,9 @@ export type NewStackEvent = typeof stackEvents.$inferInsert;
export type AutoUpdateSetting = typeof autoUpdateSettings.$inferSelect;
export type NewAutoUpdateSetting = typeof autoUpdateSettings.$inferInsert;

export type ContainerStartSchedule = typeof containerStartSchedules.$inferSelect;
export type NewContainerStartSchedule = typeof containerStartSchedules.$inferInsert;

export type UserPreference = typeof userPreferences.$inferSelect;
export type NewUserPreference = typeof userPreferences.$inferInsert;

Expand Down
14 changes: 14 additions & 0 deletions src/lib/server/db/schema/pg-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,20 @@ export const autoUpdateSettings = pgTable('auto_update_settings', {
envContainerUnique: unique().on(table.environmentId, table.containerName)
}));

export const containerStartSchedules = pgTable('container_start_schedules', {
id: serial('id').primaryKey(),
environmentId: integer('environment_id').references(() => environments.id),
containerName: text('container_name').notNull(),
enabled: boolean('enabled').default(false),
scheduleType: text('schedule_type').default('daily'),
cronExpression: text('cron_expression'),
lastStarted: timestamp('last_started', { mode: 'string' }),
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(),
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow()
}, (table) => ({
envContainerUnique: unique().on(table.environmentId, table.containerName)
}));

export const notificationSettings = pgTable('notification_settings', {
id: serial('id').primaryKey(),
type: text('type').notNull(),
Expand Down
74 changes: 71 additions & 3 deletions src/lib/server/scheduler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
import { Cron } from 'croner';
import {
getEnabledAutoUpdateSettings,
getEnabledContainerStartSchedules,
getEnabledAutoUpdateGitStacks,
getAutoUpdateSettingById,
getContainerStartScheduleById,
getGitStack,
getScheduleCleanupCron,
getEventCleanupCron,
Expand All @@ -40,6 +42,7 @@ import {

// Import task execution functions
import { runContainerUpdate } from './tasks/container-update';
import { runContainerStart } from './tasks/container-start';
import { runGitStackSync } from './tasks/git-stack-sync';
import { runEnvUpdateCheckJob } from './tasks/env-update-check';
import { runImagePrune } from './tasks/image-prune';
Expand Down Expand Up @@ -231,6 +234,7 @@ export async function refreshAllSchedules(): Promise<void> {
activeJobs.clear();

let containerCount = 0;
let containerStartCount = 0;
let gitStackCount = 0;

// Register container auto-update schedules
Expand All @@ -251,6 +255,24 @@ export async function refreshAllSchedules(): Promise<void> {
console.error('[Scheduler] Error loading container schedules:', errorMsg);
}

// Register container start schedules
try {
const containerStartSettings = await getEnabledContainerStartSchedules();
for (const setting of containerStartSettings) {
if (setting.cronExpression) {
const registered = await registerSchedule(
setting.id,
'container_start',
setting.environmentId
);
if (registered) containerStartCount++;
}
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error('[Scheduler] Error loading container start schedules:', errorMsg);
}

// Register git stack auto-sync schedules
try {
const gitStacks = await getEnabledAutoUpdateGitStacks();
Expand Down Expand Up @@ -307,7 +329,7 @@ export async function refreshAllSchedules(): Promise<void> {
console.error('[Scheduler] Error loading image prune schedules:', errorMsg);
}

console.log(`[Scheduler] Registered ${containerCount} container schedules, ${gitStackCount} git stack schedules, ${envUpdateCheckCount} env update check schedules, ${imagePruneCount} image prune schedules`);
console.log(`[Scheduler] Registered ${containerCount} container update schedules, ${containerStartCount} container start schedules, ${gitStackCount} git stack schedules, ${envUpdateCheckCount} env update check schedules, ${imagePruneCount} image prune schedules`);
}

/**
Expand All @@ -316,7 +338,7 @@ export async function refreshAllSchedules(): Promise<void> {
*/
export async function registerSchedule(
scheduleId: number,
type: 'container_update' | 'git_stack_sync' | 'env_update_check' | 'image_prune',
type: 'container_update' | 'container_start' | 'git_stack_sync' | 'env_update_check' | 'image_prune',
environmentId: number | null
): Promise<boolean> {
const key = `${type}-${scheduleId}`;
Expand All @@ -336,6 +358,12 @@ export async function registerSchedule(
cronExpression = setting.cronExpression;
entityName = setting.containerName;
enabled = setting.enabled;
} else if (type === 'container_start') {
const setting = await getContainerStartScheduleById(scheduleId);
if (!setting) return false;
cronExpression = setting.cronExpression;
entityName = setting.containerName;
enabled = setting.enabled;
} else if (type === 'git_stack_sync') {
const stack = await getGitStack(scheduleId);
if (!stack) return false;
Expand Down Expand Up @@ -375,6 +403,10 @@ export async function registerSchedule(
const setting = await getAutoUpdateSettingById(scheduleId);
if (!setting || !setting.enabled) return;
await runContainerUpdate(scheduleId, setting.containerName, environmentId, 'cron');
} else if (type === 'container_start') {
const setting = await getContainerStartScheduleById(scheduleId);
if (!setting || !setting.enabled) return;
await runContainerStart(scheduleId, setting.containerName, environmentId, 'cron');
} else if (type === 'git_stack_sync') {
const stack = await getGitStack(scheduleId);
if (!stack || !stack.autoUpdate) return;
Expand Down Expand Up @@ -406,7 +438,7 @@ export async function registerSchedule(
*/
export function unregisterSchedule(
scheduleId: number,
type: 'container_update' | 'git_stack_sync' | 'env_update_check' | 'image_prune'
type: 'container_update' | 'container_start' | 'git_stack_sync' | 'env_update_check' | 'image_prune'
): void {
const key = `${type}-${scheduleId}`;
const job = activeJobs.get(key);
Expand Down Expand Up @@ -445,6 +477,24 @@ export async function refreshSchedulesForEnvironment(environmentId: number): Pro
console.error('[Scheduler] Error refreshing container schedules:', errorMsg);
}

// Re-register container start schedules for this environment
try {
const containerStartSettings = await getEnabledContainerStartSchedules();
for (const setting of containerStartSettings) {
if (setting.environmentId === environmentId && setting.cronExpression) {
const registered = await registerSchedule(
setting.id,
'container_start',
setting.environmentId
);
if (registered) refreshedCount++;
}
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error('[Scheduler] Error refreshing container start schedules:', errorMsg);
}

// Re-register git stack auto-sync schedules for this environment
try {
const gitStacks = await getEnabledAutoUpdateGitStacks();
Expand Down Expand Up @@ -584,6 +634,24 @@ export async function triggerContainerUpdate(settingId: number): Promise<{ succe
}
}

/**
* Manually trigger a container start schedule.
*/
export async function triggerContainerStart(settingId: number): Promise<{ success: boolean; executionId?: number; error?: string }> {
try {
const setting = await getContainerStartScheduleById(settingId);
if (!setting) {
return { success: false, error: 'Container start schedule not found' };
}

runContainerStart(settingId, setting.containerName, setting.environmentId, 'manual');

return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
}

/**
* Manually trigger a git stack sync.
*/
Expand Down
Loading