feat: rebuild warm operations workbench

This commit is contained in:
Codex
2026-07-30 14:49:33 +08:00
parent 25c853dea8
commit 570edf6bb2
88 changed files with 13413 additions and 2250 deletions
@@ -71,6 +71,8 @@ describe('database migrations', () => {
'job_items',
'jobs',
'operation_preparations',
'scheduled_runs',
'scheduled_tasks',
'schema_migrations',
'secret_cleanup_tasks',
'secret_references',
@@ -304,6 +304,58 @@ export const MIGRATIONS: readonly Migration[] = [
'CREATE INDEX idx_console_auth_sessions_expires_at ON console_auth_sessions(expires_at)',
],
},
{
id: 8,
name: 'scheduled-automation',
statements: [
`CREATE TABLE scheduled_tasks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
operation_type TEXT NOT NULL CHECK (operation_type IN ('restart-service','reboot-system','send-sms')),
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
version INTEGER NOT NULL DEFAULT 1 CHECK (version > 0),
cron_expression TEXT NOT NULL,
timezone TEXT NOT NULL CHECK (timezone = 'Asia/Shanghai'),
target_selector_json TEXT NOT NULL,
sms_secret_reference TEXT,
sms_recipient_count INTEGER CHECK (sms_recipient_count IS NULL OR sms_recipient_count > 0),
effective_start_at TEXT,
effective_end_at TEXT,
misfire_policy TEXT NOT NULL CHECK (misfire_policy IN ('skip','catch-up-once')),
overlap_policy TEXT NOT NULL CHECK (overlap_policy IN ('skip','queue-once')),
retry_policy_json TEXT NOT NULL,
next_due_at TEXT,
last_evaluated_at TEXT,
created_by TEXT NOT NULL,
updated_by TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
deleted_at TEXT,
CHECK ((operation_type = 'send-sms') = (sms_secret_reference IS NOT NULL)),
CHECK ((sms_secret_reference IS NULL) = (sms_recipient_count IS NULL))
)`,
`CREATE TABLE scheduled_runs (
id TEXT PRIMARY KEY,
scheduled_task_id TEXT NOT NULL REFERENCES scheduled_tasks(id) ON DELETE RESTRICT,
schedule_version INTEGER NOT NULL CHECK (schedule_version > 0),
task_snapshot_json TEXT NOT NULL,
due_at TEXT NOT NULL,
claimed_at TEXT NOT NULL,
started_at TEXT,
finished_at TEXT,
target_snapshot_json TEXT NOT NULL,
outcome TEXT CHECK (outcome IS NULL OR outcome IN ('succeeded','partially-succeeded','failed','skipped','no-targets','needs-attention')),
reason TEXT,
job_ids_json TEXT NOT NULL DEFAULT '[]',
trigger_source TEXT NOT NULL CHECK (trigger_source IN ('scheduled','manual')),
attempt INTEGER NOT NULL DEFAULT 1 CHECK (attempt > 0),
UNIQUE (scheduled_task_id, schedule_version, due_at, trigger_source)
)`,
'CREATE INDEX idx_scheduled_tasks_enabled_next_due ON scheduled_tasks(enabled, next_due_at) WHERE deleted_at IS NULL',
'CREATE INDEX idx_scheduled_runs_task_due ON scheduled_runs(scheduled_task_id, due_at DESC)',
'CREATE INDEX idx_scheduled_runs_outcome_finished ON scheduled_runs(outcome, finished_at DESC)',
],
},
];
const createMigrationsTable = `CREATE TABLE schema_migrations (
@@ -255,3 +255,67 @@ export const secretCleanupTasks = sqliteTable(
},
(table) => [index('idx_secret_cleanup_tasks_queued_at').on(table.queuedAt)],
);
export const scheduledTasks = sqliteTable(
'scheduled_tasks',
{
id: text('id').primaryKey(),
name: text('name').notNull(),
operationType: text('operation_type').notNull(),
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
version: integer('version').notNull().default(1),
cronExpression: text('cron_expression').notNull(),
timezone: text('timezone').notNull(),
targetSelectorJson: text('target_selector_json').notNull(),
smsSecretReference: text('sms_secret_reference'),
smsRecipientCount: integer('sms_recipient_count'),
effectiveStartAt: text('effective_start_at'),
effectiveEndAt: text('effective_end_at'),
misfirePolicy: text('misfire_policy').notNull(),
overlapPolicy: text('overlap_policy').notNull(),
retryPolicyJson: text('retry_policy_json').notNull(),
nextDueAt: text('next_due_at'),
lastEvaluatedAt: text('last_evaluated_at'),
createdBy: text('created_by').notNull(),
updatedBy: text('updated_by').notNull(),
createdAt: text('created_at').notNull(),
updatedAt: text('updated_at').notNull(),
deletedAt: text('deleted_at'),
},
(table) => [
check('scheduled_tasks_enabled_check', sql`${table.enabled} in (0, 1)`),
check('scheduled_tasks_version_check', sql`${table.version} > 0`),
index('idx_scheduled_tasks_enabled_next_due').on(table.enabled, table.nextDueAt),
],
);
export const scheduledRuns = sqliteTable(
'scheduled_runs',
{
id: text('id').primaryKey(),
scheduledTaskId: text('scheduled_task_id')
.notNull()
.references(() => scheduledTasks.id, { onDelete: 'restrict' }),
scheduleVersion: integer('schedule_version').notNull(),
taskSnapshotJson: text('task_snapshot_json').notNull(),
dueAt: text('due_at').notNull(),
claimedAt: text('claimed_at').notNull(),
startedAt: text('started_at'),
finishedAt: text('finished_at'),
targetSnapshotJson: text('target_snapshot_json').notNull(),
outcome: text('outcome'),
reason: text('reason'),
jobIdsJson: text('job_ids_json').notNull().default('[]'),
triggerSource: text('trigger_source').notNull(),
attempt: integer('attempt').notNull().default(1),
},
(table) => [
unique('scheduled_runs_occurrence_unique').on(
table.scheduledTaskId,
table.scheduleVersion,
table.dueAt,
table.triggerSource,
),
index('idx_scheduled_runs_task_due').on(table.scheduledTaskId, desc(table.dueAt)),
],
);