mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
refactor(cron): rename scheduler feature to cron
Rename the recurring-jobs feature scheduler->cron to disambiguate from the legacy ScheduledRun system (/api/scheduled), which is left untouched: - ScheduledJob->CronJob, SchedulerService->CronService - /api/scheduler/jobs -> /api/cron/jobs; SSE scheduler:* -> cron:* - state keys cronJobs/cronJobRuns - files moved to src/cron/, cron-routes.ts, cron-port.ts, types/cron.ts - frontend cron-ui.js, #cronModal, menu "Cron" - docs moved to docs/cron-discovery.md + docs/cron-build-brief.md, README guides - new tests: cron-service.test.ts, cron-time.test.ts Green: tsc, lint, frontend-syntax, format, 30 cron + 9 legacy scheduled-runs tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU
This commit is contained in:
@@ -52,17 +52,17 @@ export const SCHEDULED_CLEANUP_INTERVAL = 5 * 60 * 1000;
|
||||
export const SCHEDULED_RUN_MAX_AGE = 60 * 60 * 1000;
|
||||
|
||||
// ============================================================================
|
||||
// Scheduled Jobs (cron-style scheduler)
|
||||
// Cron Jobs
|
||||
// ============================================================================
|
||||
|
||||
/** How often the scheduler loop wakes to check for due jobs (ms). */
|
||||
export const SCHEDULER_TICK_INTERVAL = 30 * 1000;
|
||||
/** How often the cron loop wakes to check for due jobs (ms). */
|
||||
export const CRON_TICK_INTERVAL = 30 * 1000;
|
||||
|
||||
/** Max attempts (× 500ms) to poll a launched session for CLI readiness before sending the prompt. */
|
||||
export const SCHEDULER_READY_MAX_ATTEMPTS = 60;
|
||||
export const CRON_READY_MAX_ATTEMPTS = 60;
|
||||
|
||||
/** Extra settle delay after CLI readiness is detected, before sending the prompt (ms). */
|
||||
export const SCHEDULER_READY_SETTLE_MS = 2000;
|
||||
export const CRON_READY_SETTLE_MS = 2000;
|
||||
|
||||
/** Session limit retry wait before retrying (ms) */
|
||||
export const SESSION_LIMIT_WAIT_MS = 5000;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/**
|
||||
* @fileoverview Input shape for creating/updating a scheduled job. This is the
|
||||
* user-settable subset of `ScheduledJob` (server-maintained bookkeeping fields
|
||||
* @fileoverview Input shape for creating/updating a cron job. This is the
|
||||
* user-settable subset of `CronJob` (server-maintained bookkeeping fields
|
||||
* such as nextRunAt / lastStatus are excluded). Produced by the zod schema.
|
||||
*/
|
||||
|
||||
import type { ConcurrencyPolicy, InputMode, PromptMode, ScheduleType } from '../types/scheduler.js';
|
||||
import type { ConcurrencyPolicy, InputMode, PromptMode, ScheduleType } from '../types/cron.js';
|
||||
import type { SessionMode } from '../types/session.js';
|
||||
|
||||
export type { ScheduledJob, ScheduledJobRun, ScheduledJobRunStatus, TriggerType } from '../types/scheduler.js';
|
||||
export type { CronJob, CronJobRun, CronJobRunStatus, TriggerType } from '../types/cron.js';
|
||||
|
||||
export interface ScheduledJobInput {
|
||||
export interface CronJobInput {
|
||||
name: string;
|
||||
agentType: SessionMode;
|
||||
workingDir: string;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @fileoverview Scheduler service: CRUD for scheduled jobs, manual Run Now,
|
||||
* @fileoverview Cron service: CRUD for cron jobs, manual Run Now,
|
||||
* the background due-job tick, and run-history recording.
|
||||
*
|
||||
* It does NOT own session/tmux logic — it reuses Codeman's existing session
|
||||
@@ -14,19 +14,19 @@ import { Session } from '../session.js';
|
||||
import { SseEvent } from '../web/sse-events.js';
|
||||
import { getErrorMessage } from '../types/api.js';
|
||||
import { MAX_CONCURRENT_SESSIONS } from '../config/map-limits.js';
|
||||
import { SCHEDULER_READY_MAX_ATTEMPTS, SCHEDULER_READY_SETTLE_MS } from '../config/server-timing.js';
|
||||
import { computeNextRunAt, dueKeyFor } from './scheduler-time.js';
|
||||
import { CRON_READY_MAX_ATTEMPTS, CRON_READY_SETTLE_MS } from '../config/server-timing.js';
|
||||
import { computeNextRunAt, dueKeyFor } from './cron-time.js';
|
||||
import type { SessionPort, EventPort, ConfigPort, InfraPort } from '../web/ports/index.js';
|
||||
import type { ScheduledJob, ScheduledJobRun, ScheduledJobRunStatus, TriggerType } from '../types/scheduler.js';
|
||||
import type { ScheduledJobInput } from './scheduler-input.js';
|
||||
import type { CronJob, CronJobRun, CronJobRunStatus, TriggerType } from '../types/cron.js';
|
||||
import type { CronJobInput } from './cron-input.js';
|
||||
|
||||
/** The subset of the route context the scheduler depends on. */
|
||||
export type SchedulerDeps = SessionPort & EventPort & ConfigPort & InfraPort;
|
||||
/** The subset of the route context the cron depends on. */
|
||||
export type CronDeps = SessionPort & EventPort & ConfigPort & InfraPort;
|
||||
|
||||
const delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
export class SchedulerService {
|
||||
constructor(private readonly deps: SchedulerDeps) {}
|
||||
export class CronService {
|
||||
constructor(private readonly deps: CronDeps) {}
|
||||
|
||||
private get store() {
|
||||
return this.deps.store;
|
||||
@@ -34,17 +34,17 @@ export class SchedulerService {
|
||||
|
||||
// ───────────────────────────── Reads ─────────────────────────────
|
||||
|
||||
listJobs(): ScheduledJob[] {
|
||||
return Object.values(this.store.getScheduledJobs());
|
||||
listJobs(): CronJob[] {
|
||||
return Object.values(this.store.getCronJobs());
|
||||
}
|
||||
|
||||
getJob(id: string): ScheduledJob | null {
|
||||
return this.store.getScheduledJob(id);
|
||||
getJob(id: string): CronJob | null {
|
||||
return this.store.getCronJob(id);
|
||||
}
|
||||
|
||||
listRuns(jobId?: string): ScheduledJobRun[] {
|
||||
const all = Object.values(this.store.getScheduledJobRuns());
|
||||
const filtered = jobId ? all.filter((r) => r.scheduledJobId === jobId) : all;
|
||||
listRuns(jobId?: string): CronJobRun[] {
|
||||
const all = Object.values(this.store.getCronJobRuns());
|
||||
const filtered = jobId ? all.filter((r) => r.cronJobId === jobId) : all;
|
||||
return filtered.sort((a, b) => b.startedAt - a.startedAt);
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ export class SchedulerService {
|
||||
|
||||
// ──────────────────────────── Mutations ───────────────────────────
|
||||
|
||||
createJob(input: ScheduledJobInput): ScheduledJob {
|
||||
createJob(input: CronJobInput): CronJob {
|
||||
const now = Date.now();
|
||||
const job: ScheduledJob = {
|
||||
const job: CronJob = {
|
||||
id: uuidv4(),
|
||||
name: input.name,
|
||||
agentType: input.agentType,
|
||||
@@ -86,16 +86,16 @@ export class SchedulerService {
|
||||
lastDueKey: null,
|
||||
};
|
||||
job.nextRunAt = job.enabled ? computeNextRunAt(job, now) : null;
|
||||
this.store.setScheduledJob(job.id, job);
|
||||
this.store.setCronJob(job.id, job);
|
||||
this.broadcastListChanged();
|
||||
return job;
|
||||
}
|
||||
|
||||
updateJob(id: string, patch: Partial<ScheduledJobInput>): ScheduledJob | null {
|
||||
updateJob(id: string, patch: Partial<CronJobInput>): CronJob | null {
|
||||
const existing = this.getJob(id);
|
||||
if (!existing) return null;
|
||||
const now = Date.now();
|
||||
const updated: ScheduledJob = {
|
||||
const updated: CronJob = {
|
||||
...existing,
|
||||
...patch,
|
||||
id: existing.id,
|
||||
@@ -107,28 +107,28 @@ export class SchedulerService {
|
||||
lastDueKey: null,
|
||||
};
|
||||
updated.nextRunAt = updated.enabled ? computeNextRunAt(updated, now) : null;
|
||||
this.store.setScheduledJob(updated.id, updated);
|
||||
this.store.setCronJob(updated.id, updated);
|
||||
this.broadcastListChanged();
|
||||
return updated;
|
||||
}
|
||||
|
||||
setEnabled(id: string, enabled: boolean): ScheduledJob | null {
|
||||
setEnabled(id: string, enabled: boolean): CronJob | null {
|
||||
const existing = this.getJob(id);
|
||||
if (!existing) return null;
|
||||
const now = Date.now();
|
||||
existing.enabled = enabled;
|
||||
existing.updatedAt = now;
|
||||
existing.nextRunAt = enabled ? computeNextRunAt(existing, now) : null;
|
||||
this.store.setScheduledJob(existing.id, existing);
|
||||
this.store.setCronJob(existing.id, existing);
|
||||
this.broadcastListChanged();
|
||||
return existing;
|
||||
}
|
||||
|
||||
deleteJob(id: string): boolean {
|
||||
if (!this.getJob(id)) return false;
|
||||
this.store.removeScheduledJob(id);
|
||||
for (const run of this.listRuns(id)) this.store.removeScheduledJobRun(run.id);
|
||||
this.deps.broadcast(SseEvent.SchedulerJobDeleted, { id });
|
||||
this.store.removeCronJob(id);
|
||||
for (const run of this.listRuns(id)) this.store.removeCronJobRun(run.id);
|
||||
this.deps.broadcast(SseEvent.CronJobDeleted, { id });
|
||||
this.broadcastListChanged();
|
||||
return true;
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export class SchedulerService {
|
||||
// ──────────────────────────── Execution ───────────────────────────
|
||||
|
||||
/** Manual Run Now — always launches regardless of schedule/enabled state. */
|
||||
async runNow(id: string): Promise<ScheduledJobRun | null> {
|
||||
async runNow(id: string): Promise<CronJobRun | null> {
|
||||
const job = this.getJob(id);
|
||||
if (!job) return null;
|
||||
return this.launch(job, 'manual_run_now');
|
||||
@@ -169,7 +169,7 @@ export class SchedulerService {
|
||||
// re-triggered by the next tick.
|
||||
this.advanceAfterFire(job, now);
|
||||
this.launch(job, 'scheduled').catch((err) =>
|
||||
console.error(`[scheduler] launch failed for job ${job.id}:`, getErrorMessage(err))
|
||||
console.error(`[cron] launch failed for job ${job.id}:`, getErrorMessage(err))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -181,14 +181,14 @@ export class SchedulerService {
|
||||
const isDeadOnce = job.scheduleType === 'once' && job.completedOnce;
|
||||
if (job.enabled && job.nextRunAt == null && !isDeadOnce) {
|
||||
job.nextRunAt = computeNextRunAt(job, now);
|
||||
this.store.setScheduledJob(job.id, job);
|
||||
this.store.setCronJob(job.id, job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────── Internals ───────────────────────────
|
||||
|
||||
private advanceAfterFire(job: ScheduledJob, now: number): void {
|
||||
private advanceAfterFire(job: CronJob, now: number): void {
|
||||
if (job.scheduleType === 'once') {
|
||||
job.completedOnce = true;
|
||||
job.enabled = false;
|
||||
@@ -197,14 +197,14 @@ export class SchedulerService {
|
||||
job.nextRunAt = computeNextRunAt(job, now);
|
||||
}
|
||||
job.updatedAt = now;
|
||||
this.store.setScheduledJob(job.id, job);
|
||||
this.store.setCronJob(job.id, job);
|
||||
this.broadcastListChanged();
|
||||
}
|
||||
|
||||
private async launch(job: ScheduledJob, trigger: TriggerType): Promise<ScheduledJobRun> {
|
||||
const run: ScheduledJobRun = {
|
||||
private async launch(job: CronJob, trigger: TriggerType): Promise<CronJobRun> {
|
||||
const run: CronJobRun = {
|
||||
id: uuidv4(),
|
||||
scheduledJobId: job.id,
|
||||
cronJobId: job.id,
|
||||
sessionId: null,
|
||||
sessionName: null,
|
||||
startedAt: Date.now(),
|
||||
@@ -213,8 +213,8 @@ export class SchedulerService {
|
||||
triggerType: trigger,
|
||||
createdSessionUrl: null,
|
||||
};
|
||||
this.store.setScheduledJobRun(run.id, run);
|
||||
this.deps.broadcast(SseEvent.SchedulerRunCreated, run);
|
||||
this.store.setCronJobRun(run.id, run);
|
||||
this.deps.broadcast(SseEvent.CronRunCreated, run);
|
||||
|
||||
// Resolve the prompt.
|
||||
let prompt: string;
|
||||
@@ -276,8 +276,8 @@ export class SchedulerService {
|
||||
run.sessionName = session.name;
|
||||
run.createdSessionUrl = `/?session=${session.id}`;
|
||||
run.status = 'session_started';
|
||||
this.store.setScheduledJobRun(run.id, run);
|
||||
this.deps.broadcast(SseEvent.SchedulerRunUpdated, run);
|
||||
this.store.setCronJobRun(run.id, run);
|
||||
this.deps.broadcast(SseEvent.CronRunUpdated, run);
|
||||
this.updateJobLastStatus(job.id, 'session_started');
|
||||
|
||||
// Send the prompt once the CLI is ready (async; does not block the caller).
|
||||
@@ -285,7 +285,7 @@ export class SchedulerService {
|
||||
return run;
|
||||
}
|
||||
|
||||
private async resolvePrompt(job: ScheduledJob): Promise<string> {
|
||||
private async resolvePrompt(job: CronJob): Promise<string> {
|
||||
if (job.promptMode === 'prompt_file_path') {
|
||||
if (!job.promptFilePath) throw new Error('prompt file path is empty');
|
||||
return readFile(job.promptFilePath, 'utf-8');
|
||||
@@ -293,18 +293,18 @@ export class SchedulerService {
|
||||
return job.promptText ?? '';
|
||||
}
|
||||
|
||||
private sendPromptWhenReady(sessionId: string, prompt: string, job: ScheduledJob, run: ScheduledJobRun): void {
|
||||
private sendPromptWhenReady(sessionId: string, prompt: string, job: CronJob, run: CronJobRun): void {
|
||||
setImmediate(() => {
|
||||
const poll = async (): Promise<void> => {
|
||||
if (job.agentType !== 'shell') {
|
||||
for (let attempt = 0; attempt < SCHEDULER_READY_MAX_ATTEMPTS; attempt++) {
|
||||
for (let attempt = 0; attempt < CRON_READY_MAX_ATTEMPTS; attempt++) {
|
||||
await delay(500);
|
||||
const s = this.deps.sessions.get(sessionId);
|
||||
if (!s) return; // session was removed
|
||||
const buf = s.getTerminalBuffer().slice(-2048);
|
||||
if (buf.includes('❯') || buf.includes('tokens')) break;
|
||||
}
|
||||
await delay(SCHEDULER_READY_SETTLE_MS);
|
||||
await delay(CRON_READY_SETTLE_MS);
|
||||
} else {
|
||||
await delay(1000);
|
||||
}
|
||||
@@ -319,39 +319,39 @@ export class SchedulerService {
|
||||
}
|
||||
run.status = 'prompt_sent';
|
||||
run.finishedAt = Date.now();
|
||||
this.store.setScheduledJobRun(run.id, run);
|
||||
this.deps.broadcast(SseEvent.SchedulerRunUpdated, run);
|
||||
this.store.setCronJobRun(run.id, run);
|
||||
this.deps.broadcast(SseEvent.CronRunUpdated, run);
|
||||
this.updateJobLastStatus(job.id, 'prompt_sent');
|
||||
} catch (err) {
|
||||
this.failRun(job, run, `Failed to send prompt: ${getErrorMessage(err)}`);
|
||||
}
|
||||
};
|
||||
poll().catch((err) => console.error('[scheduler] sendPromptWhenReady error:', getErrorMessage(err)));
|
||||
poll().catch((err) => console.error('[cron] sendPromptWhenReady error:', getErrorMessage(err)));
|
||||
});
|
||||
}
|
||||
|
||||
private failRun(job: ScheduledJob, run: ScheduledJobRun, message: string): ScheduledJobRun {
|
||||
private failRun(job: CronJob, run: CronJobRun, message: string): CronJobRun {
|
||||
run.status = 'failed';
|
||||
run.errorMessage = message;
|
||||
run.finishedAt = Date.now();
|
||||
this.store.setScheduledJobRun(run.id, run);
|
||||
this.deps.broadcast(SseEvent.SchedulerRunUpdated, run);
|
||||
this.store.setCronJobRun(run.id, run);
|
||||
this.deps.broadcast(SseEvent.CronRunUpdated, run);
|
||||
this.updateJobLastStatus(job.id, 'failed');
|
||||
return run;
|
||||
}
|
||||
|
||||
private updateJobLastStatus(jobId: string, status: ScheduledJobRunStatus): void {
|
||||
const fresh = this.store.getScheduledJob(jobId);
|
||||
private updateJobLastStatus(jobId: string, status: CronJobRunStatus): void {
|
||||
const fresh = this.store.getCronJob(jobId);
|
||||
if (!fresh) return;
|
||||
const now = Date.now();
|
||||
fresh.lastStatus = status;
|
||||
fresh.lastRunAt = now;
|
||||
fresh.updatedAt = now;
|
||||
this.store.setScheduledJob(fresh.id, fresh);
|
||||
this.store.setCronJob(fresh.id, fresh);
|
||||
this.broadcastListChanged();
|
||||
}
|
||||
|
||||
private broadcastListChanged(): void {
|
||||
this.deps.broadcast(SseEvent.SchedulerJobsChanged, { jobs: this.listJobs() });
|
||||
this.deps.broadcast(SseEvent.CronJobsChanged, { jobs: this.listJobs() });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @fileoverview Pure next-run-time calculations for the scheduler.
|
||||
* @fileoverview Pure next-run-time calculations for the cron.
|
||||
*
|
||||
* All functions are pure and take an explicit `after` timestamp (epoch ms) so
|
||||
* they are deterministic and unit-testable. Times use the SERVER'S LOCAL
|
||||
@@ -7,7 +7,7 @@
|
||||
* interpreted via the host's local time.
|
||||
*/
|
||||
|
||||
import type { ScheduledJob } from '../types/scheduler.js';
|
||||
import type { CronJob } from '../types/cron.js';
|
||||
|
||||
/** Parse an 'HH:MM' (24-hour) string into hours/minutes, or null if invalid. */
|
||||
export function parseHHMM(value: string | undefined): { hours: number; minutes: number } | null {
|
||||
@@ -38,7 +38,7 @@ function atLocalTime(base: number, hours: number, minutes: number, dayOffset: nu
|
||||
* For `once`, returns the absolute `runAt` (even if already in the past, so a
|
||||
* missed one-time job still fires once) until it has `completedOnce`.
|
||||
*/
|
||||
export function computeNextRunAt(job: ScheduledJob, after: number): number | null {
|
||||
export function computeNextRunAt(job: CronJob, after: number): number | null {
|
||||
switch (job.scheduleType) {
|
||||
case 'once': {
|
||||
if (job.completedOnce) return null;
|
||||
@@ -74,7 +74,7 @@ export function computeNextRunAt(job: ScheduledJob, after: number): number | nul
|
||||
|
||||
/**
|
||||
* Duplicate-launch guard key: identifies a specific due time for a job. The
|
||||
* scheduler records the key it last consumed so an overlapping or restarted
|
||||
* cron records the key it last consumed so an overlapping or restarted
|
||||
* loop will not launch the same due time twice.
|
||||
*/
|
||||
export function dueKeyFor(jobId: string, fireTime: number): string {
|
||||
+23
-23
@@ -272,11 +272,11 @@ export class StateStore {
|
||||
if (this.state.tokenStats) {
|
||||
parts.push(`"tokenStats":${JSON.stringify(this.state.tokenStats)}`);
|
||||
}
|
||||
if (this.state.scheduledJobs) {
|
||||
parts.push(`"scheduledJobs":${JSON.stringify(this.state.scheduledJobs)}`);
|
||||
if (this.state.cronJobs) {
|
||||
parts.push(`"cronJobs":${JSON.stringify(this.state.cronJobs)}`);
|
||||
}
|
||||
if (this.state.scheduledJobRuns) {
|
||||
parts.push(`"scheduledJobRuns":${JSON.stringify(this.state.scheduledJobRuns)}`);
|
||||
if (this.state.cronJobRuns) {
|
||||
parts.push(`"cronJobRuns":${JSON.stringify(this.state.cronJobRuns)}`);
|
||||
}
|
||||
|
||||
return `{${parts.join(',')}}`;
|
||||
@@ -574,48 +574,48 @@ export class StateStore {
|
||||
this.save();
|
||||
}
|
||||
|
||||
// ========== Scheduled Job Methods (cron-style scheduler) ==========
|
||||
// ========== Cron Job Methods ==========
|
||||
|
||||
/** Returns all scheduled jobs keyed by job ID. */
|
||||
getScheduledJobs(): Record<string, import('./types/scheduler.js').ScheduledJob> {
|
||||
if (!this.state.scheduledJobs) this.state.scheduledJobs = {};
|
||||
return this.state.scheduledJobs;
|
||||
getCronJobs(): Record<string, import('./types/cron.js').CronJob> {
|
||||
if (!this.state.cronJobs) this.state.cronJobs = {};
|
||||
return this.state.cronJobs;
|
||||
}
|
||||
|
||||
/** Returns a scheduled job by ID, or null if not found. */
|
||||
getScheduledJob(id: string): import('./types/scheduler.js').ScheduledJob | null {
|
||||
return this.state.scheduledJobs?.[id] ?? null;
|
||||
getCronJob(id: string): import('./types/cron.js').CronJob | null {
|
||||
return this.state.cronJobs?.[id] ?? null;
|
||||
}
|
||||
|
||||
/** Sets a scheduled job and triggers a debounced save. */
|
||||
setScheduledJob(id: string, job: import('./types/scheduler.js').ScheduledJob): void {
|
||||
if (!this.state.scheduledJobs) this.state.scheduledJobs = {};
|
||||
this.state.scheduledJobs[id] = job;
|
||||
setCronJob(id: string, job: import('./types/cron.js').CronJob): void {
|
||||
if (!this.state.cronJobs) this.state.cronJobs = {};
|
||||
this.state.cronJobs[id] = job;
|
||||
this.save();
|
||||
}
|
||||
|
||||
/** Removes a scheduled job and triggers a debounced save. */
|
||||
removeScheduledJob(id: string): void {
|
||||
if (this.state.scheduledJobs) delete this.state.scheduledJobs[id];
|
||||
removeCronJob(id: string): void {
|
||||
if (this.state.cronJobs) delete this.state.cronJobs[id];
|
||||
this.save();
|
||||
}
|
||||
|
||||
/** Returns all scheduled job runs keyed by run ID. */
|
||||
getScheduledJobRuns(): Record<string, import('./types/scheduler.js').ScheduledJobRun> {
|
||||
if (!this.state.scheduledJobRuns) this.state.scheduledJobRuns = {};
|
||||
return this.state.scheduledJobRuns;
|
||||
getCronJobRuns(): Record<string, import('./types/cron.js').CronJobRun> {
|
||||
if (!this.state.cronJobRuns) this.state.cronJobRuns = {};
|
||||
return this.state.cronJobRuns;
|
||||
}
|
||||
|
||||
/** Sets a scheduled job run (history record) and triggers a debounced save. */
|
||||
setScheduledJobRun(id: string, run: import('./types/scheduler.js').ScheduledJobRun): void {
|
||||
if (!this.state.scheduledJobRuns) this.state.scheduledJobRuns = {};
|
||||
this.state.scheduledJobRuns[id] = run;
|
||||
setCronJobRun(id: string, run: import('./types/cron.js').CronJobRun): void {
|
||||
if (!this.state.cronJobRuns) this.state.cronJobRuns = {};
|
||||
this.state.cronJobRuns[id] = run;
|
||||
this.save();
|
||||
}
|
||||
|
||||
/** Removes a scheduled job run and triggers a debounced save. */
|
||||
removeScheduledJobRun(id: string): void {
|
||||
if (this.state.scheduledJobRuns) delete this.state.scheduledJobRuns[id];
|
||||
removeCronJobRun(id: string): void {
|
||||
if (this.state.cronJobRuns) delete this.state.cronJobRuns[id];
|
||||
this.save();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { SessionState } from './session.js';
|
||||
import type { TaskState } from './task.js';
|
||||
import type { RalphLoopState } from './ralph.js';
|
||||
import type { RespawnConfig } from './respawn.js';
|
||||
import type { ScheduledJob, ScheduledJobRun } from './scheduler.js';
|
||||
import type { CronJob, CronJobRun } from './cron.js';
|
||||
|
||||
// ========== Global Stats Types ==========
|
||||
|
||||
@@ -113,9 +113,9 @@ export interface AppState {
|
||||
/** Orchestrator Loop state (phased plan execution) */
|
||||
orchestrator?: import('./orchestrator.js').OrchestratorPersistState;
|
||||
/** Cron-style scheduled jobs, keyed by job ID. */
|
||||
scheduledJobs?: Record<string, ScheduledJob>;
|
||||
cronJobs?: Record<string, CronJob>;
|
||||
/** Scheduled job run history, keyed by run ID. */
|
||||
scheduledJobRuns?: Record<string, ScheduledJobRun>;
|
||||
cronJobRuns?: Record<string, CronJobRun>;
|
||||
}
|
||||
|
||||
// ========== Default Configuration ==========
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* @fileoverview Scheduled Jobs (cron-style scheduler) type definitions.
|
||||
* @fileoverview Cron Jobs type definitions.
|
||||
*
|
||||
* NOTE: This is intentionally distinct from the existing `ScheduledRun` concept
|
||||
* (see src/web/ports/infra-port.ts), which is a run-now, duration-bounded
|
||||
* autonomous loop. A `ScheduledJob` is a SAVED, NAMED job with a recurring
|
||||
* autonomous loop. A `CronJob` is a SAVED, NAMED job with a recurring
|
||||
* schedule (once/interval/daily/weekly), enable/disable, next-run calculation,
|
||||
* and a history of `ScheduledJobRun` records. The two do not interact.
|
||||
* and a history of `CronJobRun` records. The two do not interact.
|
||||
*
|
||||
* Persisted to `~/.codeman/state.json` via StateStore (see AppState).
|
||||
*/
|
||||
@@ -22,7 +22,7 @@ export type PromptMode = 'inline_text' | 'prompt_file_path';
|
||||
export type InputMode = 'paste' | 'typed';
|
||||
|
||||
/** Lifecycle status of a single job execution. */
|
||||
export type ScheduledJobRunStatus = 'created' | 'session_started' | 'prompt_sent' | 'failed';
|
||||
export type CronJobRunStatus = 'created' | 'session_started' | 'prompt_sent' | 'failed';
|
||||
|
||||
/** What triggered a run. */
|
||||
export type TriggerType = 'scheduled' | 'manual_run_now';
|
||||
@@ -31,9 +31,9 @@ export type TriggerType = 'scheduled' | 'manual_run_now';
|
||||
export type ConcurrencyPolicy = 'warn_only' | 'skip_if_same_agent_running';
|
||||
|
||||
/**
|
||||
* A saved, named scheduled job.
|
||||
* A saved, named cron job.
|
||||
*/
|
||||
export interface ScheduledJob {
|
||||
export interface CronJob {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Reuses Codeman's existing session modes; 'shell' covers Terminal/custom. */
|
||||
@@ -69,7 +69,7 @@ export interface ScheduledJob {
|
||||
updatedAt: number;
|
||||
lastRunAt: number | null;
|
||||
nextRunAt: number | null;
|
||||
lastStatus: ScheduledJobRunStatus | null;
|
||||
lastStatus: CronJobRunStatus | null;
|
||||
/** Duplicate-launch guard: identifies the most recent due-time consumed. */
|
||||
lastDueKey: string | null;
|
||||
/** True once a 'once' job has fired (it is also disabled). */
|
||||
@@ -77,16 +77,16 @@ export interface ScheduledJob {
|
||||
}
|
||||
|
||||
/**
|
||||
* A single execution of a scheduled job (history record).
|
||||
* A single execution of a cron job (history record).
|
||||
*/
|
||||
export interface ScheduledJobRun {
|
||||
export interface CronJobRun {
|
||||
id: string;
|
||||
scheduledJobId: string;
|
||||
cronJobId: string;
|
||||
sessionId: string | null;
|
||||
sessionName: string | null;
|
||||
startedAt: number;
|
||||
finishedAt: number | null;
|
||||
status: ScheduledJobRunStatus;
|
||||
status: CronJobRunStatus;
|
||||
errorMessage?: string;
|
||||
triggerType: TriggerType;
|
||||
/** Best-effort deep link to the created session in the web UI. */
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Cron port — exposes the CronService to
|
||||
* route handlers via the shared route context.
|
||||
*/
|
||||
|
||||
import type { CronService } from '../../cron/cron-service.js';
|
||||
|
||||
export interface CronPort {
|
||||
readonly cron: CronService;
|
||||
}
|
||||
@@ -13,4 +13,4 @@ export type { ConfigPort } from './config-port.js';
|
||||
export type { InfraPort, ScheduledRun } from './infra-port.js';
|
||||
export type { AuthPort } from './auth-port.js';
|
||||
export type { OrchestratorPort } from './orchestrator-port.js';
|
||||
export type { SchedulerPort } from './scheduler-port.js';
|
||||
export type { CronPort } from './cron-port.js';
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Scheduler port — exposes the cron-style SchedulerService to
|
||||
* route handlers via the shared route context.
|
||||
*/
|
||||
|
||||
import type { SchedulerService } from '../../scheduler/scheduler-service.js';
|
||||
|
||||
export interface SchedulerPort {
|
||||
readonly scheduler: SchedulerService;
|
||||
}
|
||||
@@ -166,10 +166,10 @@ const _SSE_HANDLER_MAP = [
|
||||
[SSE_EVENTS.SCHEDULED_STOPPED, '_onScheduledStopped'],
|
||||
|
||||
// Scheduled jobs (cron-style scheduler)
|
||||
[SSE_EVENTS.SCHEDULER_JOBS_CHANGED, '_onSchedulerJobsChanged'],
|
||||
[SSE_EVENTS.SCHEDULER_JOB_DELETED, '_onSchedulerJobsChanged'],
|
||||
[SSE_EVENTS.SCHEDULER_RUN_CREATED, '_onSchedulerRunChanged'],
|
||||
[SSE_EVENTS.SCHEDULER_RUN_UPDATED, '_onSchedulerRunChanged'],
|
||||
[SSE_EVENTS.CRON_JOBS_CHANGED, '_onCronJobsChanged'],
|
||||
[SSE_EVENTS.CRON_JOB_DELETED, '_onCronJobsChanged'],
|
||||
[SSE_EVENTS.CRON_RUN_CREATED, '_onCronRunChanged'],
|
||||
[SSE_EVENTS.CRON_RUN_UPDATED, '_onCronRunChanged'],
|
||||
|
||||
// Respawn
|
||||
[SSE_EVENTS.RESPAWN_STARTED, '_onRespawnStarted'],
|
||||
|
||||
@@ -276,11 +276,11 @@ const SSE_EVENTS = {
|
||||
SCHEDULED_LOG: 'scheduled:log',
|
||||
SCHEDULED_DELETED: 'scheduled:deleted',
|
||||
|
||||
// Scheduled jobs (cron-style scheduler)
|
||||
SCHEDULER_JOBS_CHANGED: 'scheduler:jobsChanged',
|
||||
SCHEDULER_JOB_DELETED: 'scheduler:jobDeleted',
|
||||
SCHEDULER_RUN_CREATED: 'scheduler:runCreated',
|
||||
SCHEDULER_RUN_UPDATED: 'scheduler:runUpdated',
|
||||
// Cron jobs
|
||||
CRON_JOBS_CHANGED: 'cron:jobsChanged',
|
||||
CRON_JOB_DELETED: 'cron:jobDeleted',
|
||||
CRON_RUN_CREATED: 'cron:runCreated',
|
||||
CRON_RUN_UPDATED: 'cron:runUpdated',
|
||||
|
||||
// Respawn
|
||||
RESPAWN_STARTED: 'respawn:started',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @fileoverview Scheduled Jobs UI (cron-style scheduler) mixed into
|
||||
* @fileoverview Cron Jobs UI mixed into
|
||||
* CodemanApp.prototype. Renders the job list + create/edit form in the
|
||||
* #schedulerModal, and reacts to scheduler:* SSE events.
|
||||
* #cronModal, and reacts to cron:* SSE events.
|
||||
*
|
||||
* @mixin Extends CodemanApp.prototype via Object.assign
|
||||
* @dependency app.js, api-client.js, constants.js (escapeHtml)
|
||||
@@ -10,54 +10,54 @@
|
||||
Object.assign(CodemanApp.prototype, {
|
||||
// ── SSE handlers ──────────────────────────────────────────────────────────
|
||||
|
||||
_onSchedulerJobsChanged(data) {
|
||||
_onCronJobsChanged(data) {
|
||||
if (data && Array.isArray(data.jobs)) {
|
||||
this._schedulerJobs = data.jobs;
|
||||
if (this._isSchedulerOpen()) this.renderSchedulerJobs();
|
||||
} else if (this._isSchedulerOpen()) {
|
||||
this.refreshScheduler();
|
||||
this._cronJobs = data.jobs;
|
||||
if (this._isCronOpen()) this.renderCronJobs();
|
||||
} else if (this._isCronOpen()) {
|
||||
this.refreshCron();
|
||||
}
|
||||
},
|
||||
|
||||
_onSchedulerRunChanged() {
|
||||
_onCronRunChanged() {
|
||||
// A run's status changed — refresh the list so lastStatus stays current.
|
||||
if (this._isSchedulerOpen()) this.refreshScheduler();
|
||||
if (this._isCronOpen()) this.refreshCron();
|
||||
},
|
||||
|
||||
// ── Modal open/close ──────────────────────────────────────────────────────
|
||||
|
||||
_isSchedulerOpen() {
|
||||
const el = document.getElementById('schedulerModal');
|
||||
_isCronOpen() {
|
||||
const el = document.getElementById('cronModal');
|
||||
return !!el && el.classList.contains('active');
|
||||
},
|
||||
|
||||
openScheduler() {
|
||||
const el = document.getElementById('schedulerModal');
|
||||
openCron() {
|
||||
const el = document.getElementById('cronModal');
|
||||
if (!el) return;
|
||||
el.classList.add('active');
|
||||
this.cancelSchedulerJobForm();
|
||||
this.refreshScheduler();
|
||||
this.cancelCronJobForm();
|
||||
this.refreshCron();
|
||||
},
|
||||
|
||||
closeScheduler() {
|
||||
const el = document.getElementById('schedulerModal');
|
||||
closeCron() {
|
||||
const el = document.getElementById('cronModal');
|
||||
if (el) el.classList.remove('active');
|
||||
},
|
||||
|
||||
async refreshScheduler() {
|
||||
const jobs = await this._apiJson('/api/scheduler/jobs');
|
||||
this._schedulerJobs = Array.isArray(jobs) ? jobs : [];
|
||||
this.renderSchedulerJobs();
|
||||
async refreshCron() {
|
||||
const jobs = await this._apiJson('/api/cron/jobs');
|
||||
this._cronJobs = Array.isArray(jobs) ? jobs : [];
|
||||
this.renderCronJobs();
|
||||
},
|
||||
|
||||
// ── List rendering ────────────────────────────────────────────────────────
|
||||
|
||||
renderSchedulerJobs() {
|
||||
const list = document.getElementById('schedulerJobList');
|
||||
renderCronJobs() {
|
||||
const list = document.getElementById('cronJobList');
|
||||
if (!list) return;
|
||||
const jobs = this._schedulerJobs || [];
|
||||
const jobs = this._cronJobs || [];
|
||||
if (jobs.length === 0) {
|
||||
list.innerHTML = '<div class="form-hint">No scheduled jobs yet. Click “+ New Job”.</div>';
|
||||
list.innerHTML = '<div class="form-hint">No cron jobs yet. Click “+ New Job”.</div>';
|
||||
return;
|
||||
}
|
||||
const rows = jobs.map((j) => {
|
||||
@@ -65,23 +65,23 @@ Object.assign(CodemanApp.prototype, {
|
||||
const last = this._fmtTime(j.lastRunAt);
|
||||
const status = j.lastStatus ? escapeHtml(j.lastStatus) : '—';
|
||||
return `
|
||||
<div class="scheduler-job-row">
|
||||
<div class="scheduler-job-main">
|
||||
<div class="scheduler-job-name">${escapeHtml(j.name || '(unnamed)')}
|
||||
<span class="scheduler-badge">${escapeHtml(j.agentType)}</span>
|
||||
<span class="scheduler-badge">${escapeHtml(this._fmtSchedule(j))}</span>
|
||||
${j.enabled ? '' : '<span class="scheduler-badge scheduler-badge-off">disabled</span>'}
|
||||
<div class="cron-job-row">
|
||||
<div class="cron-job-main">
|
||||
<div class="cron-job-name">${escapeHtml(j.name || '(unnamed)')}
|
||||
<span class="cron-badge">${escapeHtml(j.agentType)}</span>
|
||||
<span class="cron-badge">${escapeHtml(this._fmtSchedule(j))}</span>
|
||||
${j.enabled ? '' : '<span class="cron-badge cron-badge-off">disabled</span>'}
|
||||
</div>
|
||||
<div class="scheduler-job-meta">
|
||||
<div class="cron-job-meta">
|
||||
<span title="${escapeHtml(j.workingDir || '')}">${escapeHtml(j.workingDir || '')}</span>
|
||||
· next: ${escapeHtml(next)} · last: ${escapeHtml(last)} · status: ${status}
|
||||
</div>
|
||||
</div>
|
||||
<div class="scheduler-job-actions">
|
||||
<button class="btn-toolbar btn-sm btn-primary" onclick="app.runSchedulerJob('${j.id}')">Run Now</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.toggleSchedulerJob('${j.id}', ${j.enabled ? 'false' : 'true'})">${j.enabled ? 'Disable' : 'Enable'}</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.editSchedulerJob('${j.id}')">Edit</button>
|
||||
<button class="btn-toolbar btn-sm btn-danger" onclick="app.deleteSchedulerJob('${j.id}')">Delete</button>
|
||||
<div class="cron-job-actions">
|
||||
<button class="btn-toolbar btn-sm btn-primary" onclick="app.runCronJob('${j.id}')">Run Now</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.toggleCronJob('${j.id}', ${j.enabled ? 'false' : 'true'})">${j.enabled ? 'Disable' : 'Enable'}</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.editCronJob('${j.id}')">Edit</button>
|
||||
<button class="btn-toolbar btn-sm btn-danger" onclick="app.deleteCronJob('${j.id}')">Delete</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
@@ -117,11 +117,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
// ── Create / edit form ────────────────────────────────────────────────────
|
||||
|
||||
openSchedulerJobForm(job) {
|
||||
const form = document.getElementById('schedulerJobForm');
|
||||
openCronJobForm(job) {
|
||||
const form = document.getElementById('cronJobForm');
|
||||
if (!form) return;
|
||||
document.getElementById('schedulerFormError').textContent = '';
|
||||
document.getElementById('schedulerFormTitle').textContent = job ? 'Edit Scheduled Job' : 'New Scheduled Job';
|
||||
document.getElementById('cronFormError').textContent = '';
|
||||
document.getElementById('cronFormTitle').textContent = job ? 'Edit Cron Job' : 'New Cron Job';
|
||||
document.getElementById('schJobId').value = job ? job.id : '';
|
||||
document.getElementById('schName').value = job ? job.name || '' : '';
|
||||
document.getElementById('schAgentType').value = job ? job.agentType || 'claude' : 'claude';
|
||||
@@ -143,28 +143,28 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('schEnabled').checked = job ? !!job.enabled : true;
|
||||
document.getElementById('schNotes').value = job ? job.notes || '' : '';
|
||||
|
||||
this.onSchedulerPromptModeChange();
|
||||
this.onSchedulerScheduleTypeChange();
|
||||
this.onCronPromptModeChange();
|
||||
this.onCronScheduleTypeChange();
|
||||
form.classList.remove('hidden');
|
||||
},
|
||||
|
||||
editSchedulerJob(id) {
|
||||
const job = (this._schedulerJobs || []).find((j) => j.id === id);
|
||||
if (job) this.openSchedulerJobForm(job);
|
||||
editCronJob(id) {
|
||||
const job = (this._cronJobs || []).find((j) => j.id === id);
|
||||
if (job) this.openCronJobForm(job);
|
||||
},
|
||||
|
||||
cancelSchedulerJobForm() {
|
||||
const form = document.getElementById('schedulerJobForm');
|
||||
cancelCronJobForm() {
|
||||
const form = document.getElementById('cronJobForm');
|
||||
if (form) form.classList.add('hidden');
|
||||
},
|
||||
|
||||
onSchedulerPromptModeChange() {
|
||||
onCronPromptModeChange() {
|
||||
const mode = document.getElementById('schPromptMode').value;
|
||||
document.getElementById('schPromptTextRow').classList.toggle('hidden', mode !== 'inline_text');
|
||||
document.getElementById('schPromptFileRow').classList.toggle('hidden', mode !== 'prompt_file_path');
|
||||
},
|
||||
|
||||
onSchedulerScheduleTypeChange() {
|
||||
onCronScheduleTypeChange() {
|
||||
const t = document.getElementById('schScheduleType').value;
|
||||
document.getElementById('schRunAtRow').classList.toggle('hidden', t !== 'once');
|
||||
document.getElementById('schIntervalRow').classList.toggle('hidden', t !== 'interval');
|
||||
@@ -180,7 +180,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
},
|
||||
|
||||
_collectSchedulerForm() {
|
||||
_collectCronForm() {
|
||||
const t = document.getElementById('schScheduleType').value;
|
||||
const promptMode = document.getElementById('schPromptMode').value;
|
||||
const body = {
|
||||
@@ -213,10 +213,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
return body;
|
||||
},
|
||||
|
||||
async saveSchedulerJob() {
|
||||
const errEl = document.getElementById('schedulerFormError');
|
||||
async saveCronJob() {
|
||||
const errEl = document.getElementById('cronFormError');
|
||||
errEl.textContent = '';
|
||||
const body = this._collectSchedulerForm();
|
||||
const body = this._collectCronForm();
|
||||
if (!body.name) {
|
||||
errEl.textContent = 'Name is required.';
|
||||
return;
|
||||
@@ -226,9 +226,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
return;
|
||||
}
|
||||
const id = document.getElementById('schJobId').value;
|
||||
const res = id
|
||||
? await this._apiPut(`/api/scheduler/jobs/${id}`, body)
|
||||
: await this._apiPost('/api/scheduler/jobs', body);
|
||||
const res = id ? await this._apiPut(`/api/cron/jobs/${id}`, body) : await this._apiPost('/api/cron/jobs', body);
|
||||
if (!res || !res.ok) {
|
||||
let msg = 'Failed to save job.';
|
||||
try {
|
||||
@@ -240,22 +238,22 @@ Object.assign(CodemanApp.prototype, {
|
||||
errEl.textContent = msg;
|
||||
return;
|
||||
}
|
||||
this.showToast?.(id ? 'Scheduled job updated' : 'Scheduled job created', 'success');
|
||||
this.cancelSchedulerJobForm();
|
||||
this.refreshScheduler();
|
||||
this.showToast?.(id ? 'Cron job updated' : 'Cron job created', 'success');
|
||||
this.cancelCronJobForm();
|
||||
this.refreshCron();
|
||||
},
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────
|
||||
|
||||
async runSchedulerJob(id) {
|
||||
const job = (this._schedulerJobs || []).find((j) => j.id === id);
|
||||
async runCronJob(id) {
|
||||
const job = (this._cronJobs || []).find((j) => j.id === id);
|
||||
if (job) {
|
||||
const active = this._countActiveAgents(job.agentType);
|
||||
if (active > 0 && !confirm(`${active} ${job.agentType} session(s) already active. Run this job anyway?`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const res = await this._apiPost(`/api/scheduler/jobs/${id}/run`, {});
|
||||
const res = await this._apiPost(`/api/cron/jobs/${id}/run`, {});
|
||||
if (res && res.ok) {
|
||||
this.showToast?.('Run started — opening session', 'success');
|
||||
let data = null;
|
||||
@@ -265,17 +263,17 @@ Object.assign(CodemanApp.prototype, {
|
||||
/* ignore */
|
||||
}
|
||||
const run = data && (data.data ? data.data.run : data.run);
|
||||
if (run && run.sessionId) this._focusScheduledSession(run.sessionId);
|
||||
this.refreshScheduler();
|
||||
if (run && run.sessionId) this._focusCronSession(run.sessionId);
|
||||
this.refreshCron();
|
||||
} else {
|
||||
this.showToast?.('Failed to run job', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
_focusScheduledSession(sessionId) {
|
||||
_focusCronSession(sessionId) {
|
||||
// Best-effort: switch to the created session tab if it exists.
|
||||
if (this.sessions && this.sessions.has(sessionId) && typeof this.switchSession === 'function') {
|
||||
this.closeScheduler();
|
||||
this.closeCron();
|
||||
this.switchSession(sessionId);
|
||||
}
|
||||
},
|
||||
@@ -287,18 +285,18 @@ Object.assign(CodemanApp.prototype, {
|
||||
return n;
|
||||
},
|
||||
|
||||
async toggleSchedulerJob(id, enabled) {
|
||||
const res = await this._apiPut(`/api/scheduler/jobs/${id}/enabled`, { enabled });
|
||||
if (res && res.ok) this.refreshScheduler();
|
||||
async toggleCronJob(id, enabled) {
|
||||
const res = await this._apiPut(`/api/cron/jobs/${id}/enabled`, { enabled });
|
||||
if (res && res.ok) this.refreshCron();
|
||||
else this.showToast?.('Failed to update job', 'error');
|
||||
},
|
||||
|
||||
async deleteSchedulerJob(id) {
|
||||
if (!confirm('Delete this scheduled job and its run history?')) return;
|
||||
const res = await this._apiDelete(`/api/scheduler/jobs/${id}`);
|
||||
async deleteCronJob(id) {
|
||||
if (!confirm('Delete this cron job and its run history?')) return;
|
||||
const res = await this._apiDelete(`/api/cron/jobs/${id}`);
|
||||
if (res && res.ok) {
|
||||
this.showToast?.('Scheduled job deleted', 'success');
|
||||
this.refreshScheduler();
|
||||
this.showToast?.('Cron job deleted', 'success');
|
||||
this.refreshCron();
|
||||
} else {
|
||||
this.showToast?.('Failed to delete job', 'error');
|
||||
}
|
||||
+18
-18
@@ -541,7 +541,7 @@
|
||||
<div class="toolbar-right">
|
||||
<!-- Orchestrator button hidden until feature is ready -->
|
||||
<!-- <button class="btn-toolbar btn-sm" onclick="app.toggleOrchestratorPanel()" title="Orchestrator Loop">⚙ Orchestrator</button> -->
|
||||
<button class="btn-toolbar btn-sm" onclick="app.openScheduler()" title="Scheduled Jobs">⏰ Schedules</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.openCron()" title="Cron Jobs">⏰ Cron</button>
|
||||
<span class="version-display" id="versionDisplay" title="Codeman version">v0.0.0</span>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -571,26 +571,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduled Jobs Modal (cron-style scheduler) -->
|
||||
<div class="modal" id="schedulerModal">
|
||||
<div class="modal-backdrop" onclick="app.closeScheduler()"></div>
|
||||
<!-- Cron Jobs Modal -->
|
||||
<div class="modal" id="cronModal">
|
||||
<div class="modal-backdrop" onclick="app.closeCron()"></div>
|
||||
<div class="modal-content modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3>Scheduled Jobs</h3>
|
||||
<button class="modal-close" onclick="app.closeScheduler()" aria-label="Close scheduler">×</button>
|
||||
<h3>Cron Jobs</h3>
|
||||
<button class="modal-close" onclick="app.closeCron()" aria-label="Close cron">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="form-hint" style="margin-bottom:10px;">Times use the server's local timezone.</p>
|
||||
<div style="margin-bottom:10px;">
|
||||
<button class="btn-toolbar btn-sm btn-primary" onclick="app.openSchedulerJobForm()">+ New Job</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.refreshScheduler()">Refresh</button>
|
||||
<button class="btn-toolbar btn-sm btn-primary" onclick="app.openCronJobForm()">+ New Job</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.refreshCron()">Refresh</button>
|
||||
</div>
|
||||
<!-- Job list -->
|
||||
<div id="schedulerJobList" class="scheduler-job-list"></div>
|
||||
<div id="cronJobList" class="cron-job-list"></div>
|
||||
|
||||
<!-- Create/Edit form (hidden until New/Edit) -->
|
||||
<div id="schedulerJobForm" class="scheduler-job-form hidden">
|
||||
<div class="form-section-header" id="schedulerFormTitle">New Scheduled Job</div>
|
||||
<div id="cronJobForm" class="cron-job-form hidden">
|
||||
<div class="form-section-header" id="cronFormTitle">New Cron Job</div>
|
||||
<input type="hidden" id="schJobId">
|
||||
<div class="form-row"><label>Name</label><input type="text" id="schName" placeholder="My nightly job"></div>
|
||||
<div class="form-row"><label>Agent Type</label>
|
||||
@@ -604,7 +604,7 @@
|
||||
</div>
|
||||
<div class="form-row"><label>Working Directory</label><input type="text" id="schWorkingDir" placeholder="/absolute/path"></div>
|
||||
<div class="form-row"><label>Prompt Source</label>
|
||||
<select id="schPromptMode" onchange="app.onSchedulerPromptModeChange()">
|
||||
<select id="schPromptMode" onchange="app.onCronPromptModeChange()">
|
||||
<option value="inline_text">Inline text</option>
|
||||
<option value="prompt_file_path">Prompt file path</option>
|
||||
</select>
|
||||
@@ -618,7 +618,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row"><label>Schedule Type</label>
|
||||
<select id="schScheduleType" onchange="app.onSchedulerScheduleTypeChange()">
|
||||
<select id="schScheduleType" onchange="app.onCronScheduleTypeChange()">
|
||||
<option value="once">Once</option>
|
||||
<option value="interval">Interval</option>
|
||||
<option value="daily">Daily</option>
|
||||
@@ -629,7 +629,7 @@
|
||||
<div class="form-row hidden" id="schIntervalRow"><label>Every (minutes)</label><input type="number" id="schIntervalMinutes" min="1" value="60"></div>
|
||||
<div class="form-row hidden" id="schDailyRow"><label>Daily Time (HH:MM)</label><input type="time" id="schDailyTime"></div>
|
||||
<div class="form-row hidden" id="schWeeklyDaysRow"><label>Weekdays</label>
|
||||
<span id="schWeeklyDays" class="scheduler-weekdays">
|
||||
<span id="schWeeklyDays" class="cron-weekdays">
|
||||
<label><input type="checkbox" value="0">Sun</label>
|
||||
<label><input type="checkbox" value="1">Mon</label>
|
||||
<label><input type="checkbox" value="2">Tue</label>
|
||||
@@ -650,10 +650,10 @@
|
||||
<label class="switch"><input type="checkbox" id="schEnabled" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="form-row"><label>Notes</label><input type="text" id="schNotes" placeholder="Optional"></div>
|
||||
<div id="schedulerFormError" class="form-hint" style="color:var(--danger,#e55);"></div>
|
||||
<div id="cronFormError" class="form-hint" style="color:var(--danger,#e55);"></div>
|
||||
<div style="margin-top:10px;">
|
||||
<button class="btn-toolbar btn-sm btn-primary" onclick="app.saveSchedulerJob()">Save</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.cancelSchedulerJobForm()">Cancel</button>
|
||||
<button class="btn-toolbar btn-sm btn-primary" onclick="app.saveCronJob()">Save</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.cancelCronJobForm()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2146,7 +2146,7 @@
|
||||
<script defer src="respawn-ui.js"></script>
|
||||
<script defer src="ralph-panel.js"></script>
|
||||
<script defer src="orchestrator-panel.js"></script>
|
||||
<script defer src="scheduler-ui.js"></script>
|
||||
<script defer src="cron-ui.js"></script>
|
||||
<script defer src="settings-ui.js"></script>
|
||||
<script defer src="panels-ui.js"></script>
|
||||
<script defer src="ultracode-panel.js"></script>
|
||||
|
||||
+12
-12
@@ -11072,24 +11072,24 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
||||
box-shadow: 0 0 28px -4px rgba(124, 58, 237, 0.5);
|
||||
}
|
||||
|
||||
/* ── Scheduled Jobs (cron-style scheduler) ───────────────────────────────── */
|
||||
.scheduler-job-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.scheduler-job-row {
|
||||
/* ── Cron Jobs ───────────────────────────────── */
|
||||
.cron-job-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.cron-job-row {
|
||||
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
||||
padding: 10px 12px; border: 1px solid var(--border, #333); border-radius: 8px;
|
||||
background: var(--panel-bg, rgba(255,255,255,0.02));
|
||||
}
|
||||
.scheduler-job-main { min-width: 0; flex: 1; }
|
||||
.scheduler-job-name { font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.scheduler-job-meta { font-size: 12px; opacity: 0.7; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.scheduler-job-actions { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.scheduler-badge {
|
||||
.cron-job-main { min-width: 0; flex: 1; }
|
||||
.cron-job-name { font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.cron-job-meta { font-size: 12px; opacity: 0.7; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.cron-job-actions { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.cron-badge {
|
||||
font-size: 11px; padding: 1px 6px; border-radius: 10px;
|
||||
background: var(--accent-bg, rgba(120,160,255,0.15)); opacity: 0.9;
|
||||
}
|
||||
.scheduler-badge-off { background: rgba(200,80,80,0.18); }
|
||||
.scheduler-weekdays { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.scheduler-weekdays label { display: inline-flex; align-items: center; gap: 3px; font-weight: 400; }
|
||||
.scheduler-job-form {
|
||||
.cron-badge-off { background: rgba(200,80,80,0.18); }
|
||||
.cron-weekdays { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.cron-weekdays label { display: inline-flex; align-items: center; gap: 3px; font-weight: 400; }
|
||||
.cron-job-form {
|
||||
margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border, #333);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @fileoverview Cron Jobs routes.
|
||||
*
|
||||
* CRUD + enable/disable + Run Now + run history for `CronJob`s. These are
|
||||
* separate from the legacy `/api/scheduled` (ScheduledRun) endpoints — see
|
||||
* docs/cron-discovery.md §0.
|
||||
*/
|
||||
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { ApiErrorCode, createErrorResponse } from '../../types.js';
|
||||
import { CronJobSchema, CronJobUpdateSchema, CronJobEnabledSchema } from '../schemas.js';
|
||||
import { parseBody } from '../route-helpers.js';
|
||||
import type { CronPort } from '../ports/index.js';
|
||||
|
||||
export function registerCronRoutes(app: FastifyInstance, ctx: CronPort): void {
|
||||
// ── Jobs ────────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/cron/jobs', async () => {
|
||||
return ctx.cron.listJobs();
|
||||
});
|
||||
|
||||
app.post('/api/cron/jobs', async (req) => {
|
||||
const body = parseBody(CronJobSchema, req.body, 'Invalid cron job');
|
||||
return { job: ctx.cron.createJob(body) };
|
||||
});
|
||||
|
||||
app.get('/api/cron/jobs/:id', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const job = ctx.cron.getJob(id);
|
||||
if (!job) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Cron job not found');
|
||||
return job;
|
||||
});
|
||||
|
||||
app.put('/api/cron/jobs/:id', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const body = parseBody(CronJobUpdateSchema, req.body, 'Invalid cron job update');
|
||||
const job = ctx.cron.updateJob(id, body);
|
||||
if (!job) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Cron job not found');
|
||||
return { job };
|
||||
});
|
||||
|
||||
app.delete('/api/cron/jobs/:id', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
if (!ctx.cron.deleteJob(id)) {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Cron job not found');
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
app.put('/api/cron/jobs/:id/enabled', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const { enabled } = parseBody(CronJobEnabledSchema, req.body, 'Invalid request body');
|
||||
const job = ctx.cron.setEnabled(id, enabled);
|
||||
if (!job) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Cron job not found');
|
||||
return { job };
|
||||
});
|
||||
|
||||
// ── Run Now ──────────────────────────────────────────────────────────────
|
||||
|
||||
app.post('/api/cron/jobs/:id/run', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const job = ctx.cron.getJob(id);
|
||||
if (!job) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Cron job not found');
|
||||
const run = await ctx.cron.runNow(id);
|
||||
return { run, activeAgents: ctx.cron.countActiveAgents(job.agentType) };
|
||||
});
|
||||
|
||||
// ── Run history ──────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/cron/jobs/:id/runs', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
return ctx.cron.listRuns(id);
|
||||
});
|
||||
|
||||
app.get('/api/cron/runs', async () => {
|
||||
return ctx.cron.listRuns();
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export { registerTeamRoutes } from './team-routes.js';
|
||||
export { registerMuxRoutes } from './mux-routes.js';
|
||||
export { registerFileRoutes } from './file-routes.js';
|
||||
export { registerScheduledRoutes } from './scheduled-routes.js';
|
||||
export { registerSchedulerRoutes } from './scheduler-routes.js';
|
||||
export { registerCronRoutes } from './cron-routes.js';
|
||||
export { registerSystemRoutes } from './system-routes.js';
|
||||
export { registerHookEventRoutes } from './hook-event-routes.js';
|
||||
export { registerStatusTelemetryRoutes } from './status-telemetry-routes.js';
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Scheduled Jobs routes (cron-style scheduler).
|
||||
*
|
||||
* CRUD + enable/disable + Run Now + run history for `ScheduledJob`s. These are
|
||||
* separate from the legacy `/api/scheduled` (ScheduledRun) endpoints — see
|
||||
* SCHEDULER_DISCOVERY.md §0.
|
||||
*/
|
||||
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { ApiErrorCode, createErrorResponse } from '../../types.js';
|
||||
import { ScheduledJobSchema, ScheduledJobUpdateSchema, ScheduledJobEnabledSchema } from '../schemas.js';
|
||||
import { parseBody } from '../route-helpers.js';
|
||||
import type { SchedulerPort } from '../ports/index.js';
|
||||
|
||||
export function registerSchedulerRoutes(app: FastifyInstance, ctx: SchedulerPort): void {
|
||||
// ── Jobs ────────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/scheduler/jobs', async () => {
|
||||
return ctx.scheduler.listJobs();
|
||||
});
|
||||
|
||||
app.post('/api/scheduler/jobs', async (req) => {
|
||||
const body = parseBody(ScheduledJobSchema, req.body, 'Invalid scheduled job');
|
||||
return { job: ctx.scheduler.createJob(body) };
|
||||
});
|
||||
|
||||
app.get('/api/scheduler/jobs/:id', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const job = ctx.scheduler.getJob(id);
|
||||
if (!job) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Scheduled job not found');
|
||||
return job;
|
||||
});
|
||||
|
||||
app.put('/api/scheduler/jobs/:id', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const body = parseBody(ScheduledJobUpdateSchema, req.body, 'Invalid scheduled job update');
|
||||
const job = ctx.scheduler.updateJob(id, body);
|
||||
if (!job) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Scheduled job not found');
|
||||
return { job };
|
||||
});
|
||||
|
||||
app.delete('/api/scheduler/jobs/:id', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
if (!ctx.scheduler.deleteJob(id)) {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Scheduled job not found');
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
app.put('/api/scheduler/jobs/:id/enabled', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const { enabled } = parseBody(ScheduledJobEnabledSchema, req.body, 'Invalid request body');
|
||||
const job = ctx.scheduler.setEnabled(id, enabled);
|
||||
if (!job) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Scheduled job not found');
|
||||
return { job };
|
||||
});
|
||||
|
||||
// ── Run Now ──────────────────────────────────────────────────────────────
|
||||
|
||||
app.post('/api/scheduler/jobs/:id/run', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const job = ctx.scheduler.getJob(id);
|
||||
if (!job) return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Scheduled job not found');
|
||||
const run = await ctx.scheduler.runNow(id);
|
||||
return { run, activeAgents: ctx.scheduler.countActiveAgents(job.agentType) };
|
||||
});
|
||||
|
||||
// ── Run history ──────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/api/scheduler/jobs/:id/runs', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
return ctx.scheduler.listRuns(id);
|
||||
});
|
||||
|
||||
app.get('/api/scheduler/runs', async () => {
|
||||
return ctx.scheduler.listRuns();
|
||||
});
|
||||
}
|
||||
+9
-9
@@ -574,13 +574,13 @@ export const ScheduledRunSchema = z.object({
|
||||
durationMinutes: z.number().int().min(1).max(14400).optional(),
|
||||
});
|
||||
|
||||
// ========== Scheduled Jobs (cron-style scheduler) ==========
|
||||
// ========== Cron Jobs ==========
|
||||
|
||||
/** 'HH:MM' 24-hour time. */
|
||||
const hhmmSchema = z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Time must be HH:MM (24-hour)');
|
||||
|
||||
/** Shared field shape for creating/updating a scheduled job. */
|
||||
const ScheduledJobBaseSchema = z.object({
|
||||
const CronJobBaseSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
agentType: z.enum(['claude', 'shell', 'opencode', 'codex', 'gemini']),
|
||||
workingDir: safePathSchema,
|
||||
@@ -601,7 +601,7 @@ const ScheduledJobBaseSchema = z.object({
|
||||
});
|
||||
|
||||
/** Cross-field validation: required fields depend on promptMode + scheduleType. */
|
||||
function refineScheduledJob(val: z.infer<typeof ScheduledJobBaseSchema>, ctx: z.RefinementCtx): void {
|
||||
function refineCronJob(val: z.infer<typeof CronJobBaseSchema>, ctx: z.RefinementCtx): void {
|
||||
const add = (message: string, path: string) => ctx.addIssue({ code: 'custom', message, path: [path] });
|
||||
|
||||
if (val.promptMode === 'inline_text' && !val.promptText) {
|
||||
@@ -624,14 +624,14 @@ function refineScheduledJob(val: z.infer<typeof ScheduledJobBaseSchema>, ctx: z.
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /api/scheduler/jobs — full job definition. */
|
||||
export const ScheduledJobSchema = ScheduledJobBaseSchema.superRefine(refineScheduledJob);
|
||||
/** POST /api/cron/jobs — full job definition. */
|
||||
export const CronJobSchema = CronJobBaseSchema.superRefine(refineCronJob);
|
||||
|
||||
/** PUT /api/scheduler/jobs/:id — partial update. */
|
||||
export const ScheduledJobUpdateSchema = ScheduledJobBaseSchema.partial();
|
||||
/** PUT /api/cron/jobs/:id — partial update. */
|
||||
export const CronJobUpdateSchema = CronJobBaseSchema.partial();
|
||||
|
||||
/** PUT /api/scheduler/jobs/:id/enabled */
|
||||
export const ScheduledJobEnabledSchema = z.object({ enabled: z.boolean() });
|
||||
/** PUT /api/cron/jobs/:id/enabled */
|
||||
export const CronJobEnabledSchema = z.object({ enabled: z.boolean() });
|
||||
|
||||
/** POST /api/cases/link */
|
||||
export const LinkCaseSchema = z.object({
|
||||
|
||||
+13
-13
@@ -152,10 +152,10 @@ import {
|
||||
registerClipboardRoutes,
|
||||
registerSearchRoutes,
|
||||
registerOrchestratorRoutes,
|
||||
registerSchedulerRoutes,
|
||||
registerCronRoutes,
|
||||
registerWsRoutes,
|
||||
} from './routes/index.js';
|
||||
import { SchedulerService } from '../scheduler/scheduler-service.js';
|
||||
import { CronService } from '../cron/cron-service.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -177,7 +177,7 @@ import {
|
||||
ITERATION_PAUSE_MS,
|
||||
STATS_COLLECTION_INTERVAL_MS,
|
||||
INACTIVITY_TIMEOUT_MS,
|
||||
SCHEDULER_TICK_INTERVAL,
|
||||
CRON_TICK_INTERVAL,
|
||||
} from '../config/server-timing.js';
|
||||
|
||||
/**
|
||||
@@ -226,8 +226,8 @@ export class WebServer extends EventEmitter {
|
||||
// Store session listener references for explicit cleanup (prevents memory leaks)
|
||||
private sessionListenerRefs: Map<string, SessionListenerRefs> = new Map();
|
||||
private scheduledRuns: Map<string, ScheduledRun> = new Map();
|
||||
/** Cron-style scheduler service (assigned in setupRoutes). */
|
||||
private schedulerService!: SchedulerService;
|
||||
/** Cron service (assigned in setupRoutes). */
|
||||
private cronService!: CronService;
|
||||
private sse: SseStreamManager;
|
||||
private store = getStore();
|
||||
private port: number;
|
||||
@@ -879,11 +879,11 @@ export class WebServer extends EventEmitter {
|
||||
registerSearchRoutes(this.app, ctx);
|
||||
registerOrchestratorRoutes(this.app, ctx);
|
||||
|
||||
// Cron-style scheduler: build the service from the same context, recompute
|
||||
// Cron: build the service from the same context, recompute
|
||||
// due times for any persisted jobs, then expose it to its routes.
|
||||
this.schedulerService = new SchedulerService(ctx);
|
||||
this.schedulerService.init();
|
||||
registerSchedulerRoutes(this.app, { ...ctx, scheduler: this.schedulerService });
|
||||
this.cronService = new CronService(ctx);
|
||||
this.cronService.init();
|
||||
registerCronRoutes(this.app, { ...ctx, cron: this.cronService });
|
||||
|
||||
registerWsRoutes(this.app, ctx, () => this.getHostPolicy());
|
||||
}
|
||||
@@ -1959,14 +1959,14 @@ export class WebServer extends EventEmitter {
|
||||
{ description: 'scheduled runs cleanup' }
|
||||
);
|
||||
|
||||
// Start the cron-style scheduler loop (fires due ScheduledJobs).
|
||||
// Start the cron loop (fires due CronJobs).
|
||||
this.cleanup.setInterval(
|
||||
() => {
|
||||
this.schedulerService.tickDueJobs().catch((err) => {
|
||||
console.error('[scheduler] tick failed:', getErrorMessage(err));
|
||||
this.cronService.tickDueJobs().catch((err) => {
|
||||
console.error('[cron] tick failed:', getErrorMessage(err));
|
||||
});
|
||||
},
|
||||
SCHEDULER_TICK_INTERVAL,
|
||||
CRON_TICK_INTERVAL,
|
||||
{ description: 'scheduled jobs due-checker' }
|
||||
);
|
||||
|
||||
|
||||
+12
-12
@@ -240,16 +240,16 @@ export const ScheduledLog = 'scheduled:log' as const;
|
||||
/** Scheduled run deleted. */
|
||||
export const ScheduledDeleted = 'scheduled:deleted' as const;
|
||||
|
||||
// ─── Scheduled Jobs (cron-style scheduler) ───────────────────────────────────
|
||||
// ─── Cron Jobs ───────────────────────────────────
|
||||
|
||||
/** The scheduled-jobs list changed (created/updated/enabled/run-status). Payload: { jobs }. */
|
||||
export const SchedulerJobsChanged = 'scheduler:jobsChanged' as const;
|
||||
export const CronJobsChanged = 'cron:jobsChanged' as const;
|
||||
/** A scheduled job was deleted. Payload: { id }. */
|
||||
export const SchedulerJobDeleted = 'scheduler:jobDeleted' as const;
|
||||
/** A scheduled-job run (history record) was created. Payload: ScheduledJobRun. */
|
||||
export const SchedulerRunCreated = 'scheduler:runCreated' as const;
|
||||
/** A scheduled-job run (history record) was updated. Payload: ScheduledJobRun. */
|
||||
export const SchedulerRunUpdated = 'scheduler:runUpdated' as const;
|
||||
export const CronJobDeleted = 'cron:jobDeleted' as const;
|
||||
/** A scheduled-job run (history record) was created. Payload: CronJobRun. */
|
||||
export const CronRunCreated = 'cron:runCreated' as const;
|
||||
/** A scheduled-job run (history record) was updated. Payload: CronJobRun. */
|
||||
export const CronRunUpdated = 'cron:runUpdated' as const;
|
||||
|
||||
// ─── Teams ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -480,11 +480,11 @@ export const SseEvent = {
|
||||
ScheduledLog,
|
||||
ScheduledDeleted,
|
||||
|
||||
// Scheduled jobs (cron-style scheduler)
|
||||
SchedulerJobsChanged,
|
||||
SchedulerJobDeleted,
|
||||
SchedulerRunCreated,
|
||||
SchedulerRunUpdated,
|
||||
// Cron jobs
|
||||
CronJobsChanged,
|
||||
CronJobDeleted,
|
||||
CronRunCreated,
|
||||
CronRunUpdated,
|
||||
|
||||
// Teams
|
||||
TeamCreated,
|
||||
|
||||
Reference in New Issue
Block a user