diff --git a/apps/cron/20260201-000000/index.tsx b/apps/cron/20260201-000000/index.tsx index b3fa2cd..f8640b8 100644 --- a/apps/cron/20260201-000000/index.tsx +++ b/apps/cron/20260201-000000/index.tsx @@ -4,8 +4,8 @@ import { baseStyles, ToolScript, theme } from '@because/toes/tools' import { discoverCronJobs } from './lib/discovery' import { scheduleJob, stopJob } from './lib/scheduler' import { executeJob } from './lib/executor' -import { setJobs, getJob, getAllJobs, broadcast } from './lib/state' -import { SCHEDULES, type CronJob } from './lib/schedules' +import { setJobs, setInvalidJobs, getJob, getAllJobs, getInvalidJobs, broadcast } from './lib/state' +import { SCHEDULES, type CronJob, type InvalidJob } from './lib/schedules' import type { Child } from 'hono/jsx' import { join } from 'path' import { mkdir, writeFile } from 'fs/promises' @@ -92,6 +92,24 @@ const EmptyState = define('EmptyState', { color: theme('colors-textMuted'), }) +const InvalidItem = define('InvalidItem', { + padding: '12px 15px', + borderBottom: `1px solid ${theme('colors-border')}`, + display: 'flex', + alignItems: 'center', + gap: '15px', + opacity: 0.7, + states: { + ':last-child': { borderBottom: 'none' }, + }, +}) + +const ErrorText = define('ErrorText', { + fontSize: '12px', + color: theme('colors-error'), + flex: 1, +}) + const ActionRow = define('ActionRow', { display: 'flex', justifyContent: 'flex-end', @@ -226,19 +244,24 @@ app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { app.get('/', async c => { const appFilter = c.req.query('app') let jobs = getAllJobs() + let invalid = getInvalidJobs() if (appFilter) { jobs = jobs.filter(j => j.app === appFilter) + invalid = invalid.filter(j => j.app === appFilter) } jobs.sort((a, b) => a.id.localeCompare(b.id)) + invalid.sort((a, b) => a.id.localeCompare(b.id)) + + const hasAny = jobs.length > 0 || invalid.length > 0 return c.html( New Job - {jobs.length === 0 ? ( + {!hasAny ? ( No cron jobs found.
@@ -260,6 +283,13 @@ app.get('/', async c => { ))} + {invalid.map(job => ( + + + {job.app}/{job.name} + {job.error} + + ))} )}
@@ -336,8 +366,9 @@ export default async function() { console.log(`[cron] Created ${appName}:${name}`) // Trigger rediscovery - const jobs = await discoverCronJobs() + const { jobs, invalid } = await discoverCronJobs() setJobs(jobs) + setInvalidJobs(invalid) for (const job of jobs) { if (job.id === `${appName}:${name}`) { scheduleJob(job, broadcast) @@ -364,21 +395,26 @@ app.post('/run/:app/:name', async c => { // Initialize async function init() { - const jobs = await discoverCronJobs() + const { jobs, invalid } = await discoverCronJobs() setJobs(jobs) - console.log(`[cron] Discovered ${jobs.length} jobs`) + setInvalidJobs(invalid) + console.log(`[cron] Discovered ${jobs.length} jobs, ${invalid.length} invalid`) for (const job of jobs) { scheduleJob(job, broadcast) console.log(`[cron] Scheduled ${job.id}: ${job.schedule} (${job.cronExpr})`) } + + for (const job of invalid) { + console.log(`[cron] Invalid ${job.id}: ${job.error}`) + } } // Watch for cron file changes let debounceTimer: Timer | null = null async function rediscover() { - const jobs = await discoverCronJobs() + const { jobs, invalid } = await discoverCronJobs() const existing = getAllJobs() // Stop removed jobs @@ -407,6 +443,7 @@ async function rediscover() { } setJobs(jobs) + setInvalidJobs(invalid) } watch(APPS_DIR, { recursive: true }, (_event, filename) => { diff --git a/apps/cron/20260201-000000/lib/discovery.ts b/apps/cron/20260201-000000/lib/discovery.ts index 3cb268f..c540e6a 100644 --- a/apps/cron/20260201-000000/lib/discovery.ts +++ b/apps/cron/20260201-000000/lib/discovery.ts @@ -1,7 +1,7 @@ import { readdir, readFile } from 'fs/promises' import { existsSync } from 'fs' import { join } from 'path' -import { isValidSchedule, toCronExpr, type CronJob, type Schedule } from './schedules' +import { isValidSchedule, toCronExpr, type CronJob, type InvalidJob, type Schedule } from './schedules' const APPS_DIR = process.env.APPS_DIR! @@ -22,8 +22,14 @@ export async function getApps(): Promise { return apps.sort() } -export async function discoverCronJobs(): Promise { +export type DiscoveryResult = { + jobs: CronJob[] + invalid: InvalidJob[] +} + +export async function discoverCronJobs(): Promise { const jobs: CronJob[] = [] + const invalid: InvalidJob[] = [] const apps = await readdir(APPS_DIR, { withFileTypes: true }) for (const app of apps) { @@ -38,25 +44,26 @@ export async function discoverCronJobs(): Promise { const filePath = join(cronDir, file) const name = file.replace(/\.ts$/, '') + const id = `${app.name}:${name}` try { const source = await readFile(filePath, 'utf-8') const match = source.match(SCHEDULE_RE) if (!match) { - console.error(`No schedule export found in ${filePath}`) + invalid.push({ id, app: app.name, name, file: filePath, error: 'Missing schedule export' }) continue } const schedule = match[1] as Schedule if (!isValidSchedule(schedule)) { - console.error(`Invalid schedule in ${filePath}: ${schedule}`) + invalid.push({ id, app: app.name, name, file: filePath, error: `Invalid schedule: "${match[1]}"` }) continue } jobs.push({ - id: `${app.name}:${name}`, + id, app: app.name, name, file: filePath, @@ -65,10 +72,11 @@ export async function discoverCronJobs(): Promise { state: 'idle', }) } catch (e) { - console.error(`Failed to load cron file ${filePath}:`, e) + const msg = e instanceof Error ? e.message : String(e) + invalid.push({ id, app: app.name, name, file: filePath, error: msg }) } } } - return jobs + return { jobs, invalid } } diff --git a/apps/cron/20260201-000000/lib/schedules.ts b/apps/cron/20260201-000000/lib/schedules.ts index 20a3456..6d995ca 100644 --- a/apps/cron/20260201-000000/lib/schedules.ts +++ b/apps/cron/20260201-000000/lib/schedules.ts @@ -20,6 +20,14 @@ export type CronJob = { nextRun?: number } +export type InvalidJob = { + id: string + app: string + name: string + file: string + error: string +} + export const SCHEDULES = [ '1 minute', '5 minutes', diff --git a/apps/cron/20260201-000000/lib/state.ts b/apps/cron/20260201-000000/lib/state.ts index 214d502..965aac3 100644 --- a/apps/cron/20260201-000000/lib/state.ts +++ b/apps/cron/20260201-000000/lib/state.ts @@ -1,8 +1,10 @@ -import type { CronJob } from './schedules' +import type { CronJob, InvalidJob } from './schedules' const jobs = new Map() const listeners = new Set<() => void>() +let invalidJobs: InvalidJob[] = [] + export function setJobs(newJobs: CronJob[]) { jobs.clear() for (const job of newJobs) { @@ -11,6 +13,10 @@ export function setJobs(newJobs: CronJob[]) { broadcast() } +export function setInvalidJobs(newInvalid: InvalidJob[]) { + invalidJobs = newInvalid +} + export function getJob(id: string): CronJob | undefined { return jobs.get(id) } @@ -19,6 +25,10 @@ export function getAllJobs(): CronJob[] { return Array.from(jobs.values()) } +export function getInvalidJobs(): InvalidJob[] { + return invalidJobs +} + export function broadcast() { listeners.forEach(cb => cb()) }