Skip to content

Commit a847295

Browse files
committed
update styling for schedules to show visual indication that schedule is active, fix cron job to execute scheduled workflows not running (#147)
1 parent 44963d8 commit a847295

File tree

13 files changed

+1188
-145
lines changed

13 files changed

+1188
-145
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import crypto from 'crypto'
3+
import { and, eq } from 'drizzle-orm'
4+
import { getSession } from '@/lib/auth'
5+
import { createLogger } from '@/lib/logs/console-logger'
6+
import { db } from '@/db'
7+
import { workflow, workflowSchedule } from '@/db/schema'
8+
9+
const logger = createLogger('ScheduleDeleteAPI')
10+
11+
export const dynamic = 'force-dynamic'
12+
13+
/**
14+
* Delete a schedule
15+
*/
16+
export async function DELETE(
17+
request: NextRequest,
18+
{ params }: { params: Promise<{ id: string }> }
19+
) {
20+
const requestId = crypto.randomUUID().slice(0, 8)
21+
22+
try {
23+
const { id } = await params
24+
logger.debug(`[${requestId}] Deleting schedule with ID: ${id}`)
25+
26+
const session = await getSession()
27+
if (!session?.user?.id) {
28+
logger.warn(`[${requestId}] Unauthorized schedule deletion attempt`)
29+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
30+
}
31+
32+
// Find the schedule and check ownership
33+
const schedules = await db
34+
.select({
35+
schedule: workflowSchedule,
36+
workflow: {
37+
id: workflow.id,
38+
userId: workflow.userId,
39+
},
40+
})
41+
.from(workflowSchedule)
42+
.innerJoin(workflow, eq(workflowSchedule.workflowId, workflow.id))
43+
.where(eq(workflowSchedule.id, id))
44+
.limit(1)
45+
46+
if (schedules.length === 0) {
47+
logger.warn(`[${requestId}] Schedule not found: ${id}`)
48+
return NextResponse.json({ error: 'Schedule not found' }, { status: 404 })
49+
}
50+
51+
if (schedules[0].workflow.userId !== session.user.id) {
52+
logger.warn(`[${requestId}] Unauthorized schedule deletion attempt for schedule: ${id}`)
53+
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
54+
}
55+
56+
// Delete the schedule
57+
await db.delete(workflowSchedule).where(eq(workflowSchedule.id, id))
58+
59+
logger.info(`[${requestId}] Successfully deleted schedule: ${id}`)
60+
return NextResponse.json({ success: true }, { status: 200 })
61+
} catch (error) {
62+
logger.error(`[${requestId}] Error deleting schedule`, error)
63+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
64+
}
65+
}

sim/app/api/scheduled/execute/route.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { environment, workflow, workflowSchedule } from '@/db/schema'
1313
import { Executor } from '@/executor'
1414
import { Serializer } from '@/serializer'
1515

16+
// Add dynamic export to prevent caching
17+
export const dynamic = 'force-dynamic'
18+
1619
const logger = createLogger('ScheduledExecuteAPI')
1720

1821
interface SubBlockValue {
@@ -144,16 +147,12 @@ function calculateNextRunTime(
144147
// Define the schema for environment variables
145148
const EnvVarsSchema = z.record(z.string())
146149

147-
export const config = {
148-
runtime: 'nodejs',
149-
schedule: '*/1 * * * *',
150-
}
151-
152150
// Keep track of running executions to prevent overlap
153151
const runningExecutions = new Set<string>()
154152

155153
// Add GET handler for cron job
156154
export async function GET(req: NextRequest) {
155+
logger.info(`Scheduled execution triggered at ${new Date().toISOString()}`)
157156
const requestId = crypto.randomUUID().slice(0, 8)
158157
const now = new Date()
159158

sim/app/api/scheduled/route.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import crypto from 'crypto'
3+
import { eq } from 'drizzle-orm'
4+
import { getSession } from '@/lib/auth'
5+
import { createLogger } from '@/lib/logs/console-logger'
6+
import { db } from '@/db'
7+
import { workflowSchedule } from '@/db/schema'
8+
9+
const logger = createLogger('Scheduled API')
10+
11+
/**
12+
* Get schedule information for a workflow
13+
*/
14+
export async function GET(req: NextRequest) {
15+
const requestId = crypto.randomUUID().slice(0, 8)
16+
const url = new URL(req.url)
17+
const workflowId = url.searchParams.get('workflowId')
18+
19+
try {
20+
const session = await getSession()
21+
if (!session?.user?.id) {
22+
logger.warn(`[${requestId}] Unauthorized schedule query attempt`)
23+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
24+
}
25+
26+
if (!workflowId) {
27+
return NextResponse.json({ error: 'Missing workflowId parameter' }, { status: 400 })
28+
}
29+
30+
logger.info(`[${requestId}] Getting schedule for workflow ${workflowId}`)
31+
32+
// Find the schedule for this workflow
33+
const schedule = await db
34+
.select()
35+
.from(workflowSchedule)
36+
.where(eq(workflowSchedule.workflowId, workflowId))
37+
.limit(1)
38+
39+
if (schedule.length === 0) {
40+
return NextResponse.json({ schedule: null })
41+
}
42+
43+
return NextResponse.json({ schedule: schedule[0] })
44+
} catch (error) {
45+
logger.error(`[${requestId}] Error retrieving workflow schedule`, error)
46+
return NextResponse.json({ error: 'Failed to retrieve workflow schedule' }, { status: 500 })
47+
}
48+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useEffect, useState } from 'react'
2+
import { useParams } from 'next/navigation'
3+
import { Calendar } from 'lucide-react'
4+
import { Badge } from '@/components/ui/badge'
5+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
6+
import { createLogger } from '@/lib/logs/console-logger'
7+
import { formatDateTime } from '@/lib/utils'
8+
9+
const logger = createLogger('ScheduleStatus')
10+
11+
interface ScheduleStatusProps {
12+
blockId: string
13+
}
14+
15+
export function ScheduleStatus({ blockId }: ScheduleStatusProps) {
16+
const [scheduleId, setScheduleId] = useState<string | null>(null)
17+
const [nextRunAt, setNextRunAt] = useState<string | null>(null)
18+
const [isLoading, setIsLoading] = useState(true)
19+
const params = useParams()
20+
const workflowId = params.id as string
21+
22+
// Check if schedule exists in the database
23+
useEffect(() => {
24+
const checkSchedule = async () => {
25+
setIsLoading(true)
26+
try {
27+
// Check if there's a schedule for this workflow
28+
const response = await fetch(`/api/scheduled?workflowId=${workflowId}`)
29+
if (response.ok) {
30+
const data = await response.json()
31+
if (data.schedule) {
32+
setScheduleId(data.schedule.id)
33+
setNextRunAt(data.schedule.nextRunAt)
34+
} else {
35+
setScheduleId(null)
36+
setNextRunAt(null)
37+
}
38+
}
39+
} catch (error) {
40+
logger.error('Error checking schedule:', { error })
41+
} finally {
42+
setIsLoading(false)
43+
}
44+
}
45+
46+
checkSchedule()
47+
}, [workflowId])
48+
49+
if (isLoading || !scheduleId || !nextRunAt) {
50+
return null
51+
}
52+
53+
return (
54+
<Tooltip>
55+
<TooltipTrigger asChild>
56+
<Badge
57+
variant="outline"
58+
className="ml-auto bg-green-100 text-green-700 border-green-200
59+
dark:bg-green-900 dark:text-green-300 dark:border-green-800
60+
px-2 py-0.5 flex items-center"
61+
>
62+
<Calendar className="h-3.5 w-3.5 mr-1" />
63+
<span className="text-xs">Active</span>
64+
</Badge>
65+
</TooltipTrigger>
66+
<TooltipContent side="right">
67+
<div className="text-xs">
68+
<div>
69+
<strong>Schedule Active</strong>
70+
</div>
71+
<div>Next run: {formatDateTime(new Date(nextRunAt))}</div>
72+
</div>
73+
</TooltipContent>
74+
</Tooltip>
75+
)
76+
}

0 commit comments

Comments
 (0)