Skip to content

Commit e761568

Browse files
committed
feat: enhance cron task execution with immediate run capabilities, improved error handling, and UI updates for auto-refreshing logs
1 parent 5d71818 commit e761568

File tree

5 files changed

+114
-12
lines changed

5 files changed

+114
-12
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ dev: ## Start development environment
8484
--network dev \
8585
--name $(APP_NAME) \
8686
-e SESAME_SENTRY_DSN=$(SESAME_SENTRY_DSN) \
87+
-e SESAME_CRON_LOG_ROTATE_MAX_SIZE_BYTES=10485760 \
8788
-p $(APP_WEB_PORT):3000 \
8889
-p $(APP_WEB_PORT_SECURE):3443 \
8990
-p $(APP_API_PORT):4000 \

apps/api/src/core/cron/cron-hooks.service.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { createHandlerLogger } from '~/_common/functions/handler-logger'
1212
@Injectable()
1313
export class CronHooksService {
1414
private readonly logger = new Logger(CronHooksService.name)
15+
private manualRunInProgress = false
16+
private manualRunTaskName: string | null = null
1517

1618
private cronHandlerExpression = '0 * * * *' // Toutes les heures
1719

@@ -102,18 +104,26 @@ export class CronHooksService {
102104
await this.handleCron()
103105
}
104106

105-
public async runTaskNow(name: string): Promise<boolean> {
107+
public async runTaskNow(name: string): Promise<'started' | 'not_found' | 'busy'> {
108+
if (this.manualRunInProgress) {
109+
this.logger.warn(`Manual run ignored for <${name}>: another task is already running (<${this.manualRunTaskName}>)`)
110+
return 'busy'
111+
}
112+
106113
let currentTask = this.cronTasks.find((task) => task.name === name)
107114
if (!currentTask) {
108115
await this.refreshCronTasksFileCache()
109116
currentTask = this.cronTasks.find((task) => task.name === name)
110117
}
111118

112119
if (!currentTask) {
113-
return false
120+
return 'not_found'
114121
}
115122

116123
this.logger.warn(`Running cron task manually: ${currentTask.name}`)
124+
this.manualRunInProgress = true
125+
this.manualRunTaskName = currentTask.name
126+
117127
// Fire-and-forget: the HTTP caller should get an immediate response.
118128
void this.executeHandlerCommand(currentTask.name, currentTask.handler, currentTask.options)
119129
.then(() => {
@@ -122,8 +132,12 @@ export class CronHooksService {
122132
.catch((error) => {
123133
this.logger.error(`Manual cron task <${currentTask.name}> failed`, error?.message || error)
124134
})
135+
.finally(() => {
136+
this.manualRunInProgress = false
137+
this.manualRunTaskName = null
138+
})
125139

126-
return true
140+
return 'started'
127141
}
128142

129143
private async handleCron(): Promise<void> {

apps/api/src/core/cron/cron.controller.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
import { Body, Controller, DefaultValuePipe, Get, HttpStatus, NotFoundException, Param, ParseIntPipe, Patch, Post, Query, Res } from '@nestjs/common'
2+
import { Body, ConflictException, Controller, DefaultValuePipe, Get, HttpStatus, NotFoundException, Param, ParseIntPipe, Patch, Post, Query, Res } from '@nestjs/common'
33
import { ApiTags } from '@nestjs/swagger'
44
import { CronService } from './cron.service'
55
import { Response } from 'express'
@@ -140,10 +140,13 @@ export class CronController {
140140
@Param('name') name: string,
141141
@Res() res: Response,
142142
): Promise<Response> {
143-
const launched = await this.cronService.runImmediately(name)
144-
if (!launched) {
143+
const status = await this.cronService.runImmediately(name)
144+
if (status === 'not_found') {
145145
throw new NotFoundException(`Cron task <${name}> not found`)
146146
}
147+
if (status === 'busy') {
148+
throw new ConflictException('Another run-immediately task is already in progress')
149+
}
147150

148151
return res.json({
149152
statusCode: HttpStatus.OK,

apps/api/src/core/cron/cron.service.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,13 @@ export class CronService {
132132
return this.read(name)
133133
}
134134

135-
public async runImmediately(name: string): Promise<boolean> {
135+
public async runImmediately(name: string): Promise<'started' | 'not_found' | 'busy'> {
136136
const task = await this.read(name)
137137
if (!task) {
138-
return false
138+
return 'not_found'
139139
}
140140

141-
await this.cronHooksService.runTaskNow(name)
142-
return true
141+
return this.cronHooksService.runTaskNow(name)
143142
}
144143

145144
public async readLogs(name: string, tail = 500): Promise<{

apps/web/src/pages/settings/cron.vue

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,25 @@
6262
q-dialog(v-model='logsDialog' maximized)
6363
q-card.fit.column.no-wrap(style='overflow: hidden;')
6464
q-toolbar.bg-info.text-white(bordered dense style='height: 28px; line-height: 28px;')
65-
q-toolbar-title Logs de la tâche "{{ selectedCronName }}"
65+
q-toolbar-title
66+
span Logs de la tâche "{{ selectedCronName }}"
67+
span.q-ml-sm.text-caption(v-if='logsDialog && selectedCronName') (Actualisation dans {{ logsAutoRefreshCountdown }}s)
6668
q-space
69+
q-btn(
70+
flat
71+
round
72+
dense
73+
icon='mdi-play-circle-outline'
74+
:loading='logsRunLoading'
75+
:disable='!selectedCronName'
76+
@click='runSelectedCronImmediately'
77+
)
78+
q-tooltip.text-body2(anchor='top middle' self='bottom middle') Exécuter immédiatement
79+
q-separator.q-mx-xs(vertical inset)
6780
q-btn(flat round dense icon='mdi-refresh' :loading='logsLoading' @click='loadCronLogs')
81+
q-tooltip.text-body2(anchor='top middle' self='bottom middle') Actualiser les logs
6882
q-btn(flat round dense icon='mdi-close' v-close-popup)
83+
q-tooltip.text-body2(anchor='top middle' self='bottom middle') Fermer
6984
q-separator
7085
q-card-section.col.q-pa-none(ref='logsContainer' style='min-height: 0; overflow: auto;')
7186
q-inner-loading(:showing='logsLoading')
@@ -156,6 +171,10 @@ export default defineNuxtComponent({
156171
const logsMonacoEditor = ref<any>(null)
157172
const logsScrollDispose = ref<null | (() => void)>(null)
158173
const logsLoadingMore = ref(false)
174+
const logsRunLoading = ref(false)
175+
const logsAutoRefreshMs = ref(60_000)
176+
const logsAutoRefreshTimer = ref<ReturnType<typeof setInterval> | null>(null)
177+
const logsAutoRefreshCountdown = ref(5)
159178
const cronToggleLoading = reactive<Record<string, boolean>>({})
160179
const cronRunLoading = reactive<Record<string, boolean>>({})
161180
const logsMonacoOptions = computed(() => ({
@@ -209,6 +228,10 @@ export default defineNuxtComponent({
209228
logsMonacoEditor,
210229
logsScrollDispose,
211230
logsLoadingMore,
231+
logsRunLoading,
232+
logsAutoRefreshMs,
233+
logsAutoRefreshTimer,
234+
logsAutoRefreshCountdown,
212235
cronToggleLoading,
213236
cronRunLoading,
214237
logsMonacoOptions,
@@ -217,13 +240,16 @@ export default defineNuxtComponent({
217240
watch: {
218241
logsDialog(isOpen: boolean): void {
219242
if (isOpen) {
243+
this.startLogsAutoRefresh()
220244
return
221245
}
222246
247+
this.stopLogsAutoRefresh()
223248
this.resetLogsViewerState()
224249
},
225250
},
226251
beforeUnmount(): void {
252+
this.stopLogsAutoRefresh()
227253
this.resetLogsViewerState()
228254
},
229255
computed: {
@@ -247,6 +273,36 @@ export default defineNuxtComponent({
247273
},
248274
},
249275
methods: {
276+
startLogsAutoRefresh(): void {
277+
if (this.logsAutoRefreshTimer || !this.logsDialog) {
278+
return
279+
}
280+
281+
const refreshSeconds = Math.max(Math.round(this.logsAutoRefreshMs / 1000), 1)
282+
this.logsAutoRefreshCountdown = refreshSeconds
283+
this.logsAutoRefreshTimer = setInterval(() => {
284+
if (!this.logsDialog || !this.selectedCronName || this.logsLoading || this.logsLoadingMore) {
285+
return
286+
}
287+
288+
if (this.logsAutoRefreshCountdown > 1) {
289+
this.logsAutoRefreshCountdown -= 1
290+
return
291+
}
292+
293+
this.logsAutoRefreshCountdown = refreshSeconds
294+
void this.loadCronLogs()
295+
}, 1000)
296+
},
297+
stopLogsAutoRefresh(): void {
298+
if (!this.logsAutoRefreshTimer) {
299+
return
300+
}
301+
302+
clearInterval(this.logsAutoRefreshTimer)
303+
this.logsAutoRefreshTimer = null
304+
this.logsAutoRefreshCountdown = Math.max(Math.round(this.logsAutoRefreshMs / 1000), 1)
305+
},
250306
resetLogsViewerState(): void {
251307
if (this.logsScrollDispose) {
252308
this.logsScrollDispose()
@@ -272,6 +328,7 @@ export default defineNuxtComponent({
272328
this.selectedCronName = cronTask?.name || ''
273329
this.logsDialog = true
274330
await this.loadCronLogs()
331+
this.startLogsAutoRefresh()
275332
},
276333
async toggleCronEnabled(cronTask: any): Promise<void> {
277334
const name = cronTask?.name
@@ -315,7 +372,7 @@ export default defineNuxtComponent({
315372
this.$q.notify({
316373
message: `Exécution immédiate lancée pour "${name}".`,
317374
color: 'positive',
318-
position: 'top-right',
375+
position: 'bottom-center',
319376
icon: 'mdi-play-circle-outline',
320377
})
321378
await this.refresh()
@@ -330,6 +387,31 @@ export default defineNuxtComponent({
330387
this.cronRunLoading[name] = false
331388
}
332389
},
390+
async runSelectedCronImmediately(): Promise<void> {
391+
if (!this.selectedCronName) {
392+
return
393+
}
394+
395+
this.logsRunLoading = true
396+
try {
397+
await this.$http.post(`/core/cron/${encodeURIComponent(this.selectedCronName)}/run-immediately`)
398+
this.$q.notify({
399+
message: `Exécution immédiate lancée pour "${this.selectedCronName}".`,
400+
color: 'positive',
401+
position: 'bottom',
402+
icon: 'mdi-play-circle-outline',
403+
})
404+
} catch (error: any) {
405+
this.$q.notify({
406+
message: error?.response?._data?.message || `Impossible de lancer immédiatement "${this.selectedCronName}".`,
407+
color: 'negative',
408+
position: 'bottom',
409+
icon: 'mdi-alert-circle-outline',
410+
})
411+
} finally {
412+
this.logsRunLoading = false
413+
}
414+
},
333415
async loadCronLogs(): Promise<void> {
334416
if (!this.selectedCronName) {
335417
return
@@ -358,6 +440,9 @@ export default defineNuxtComponent({
358440
})
359441
} finally {
360442
this.logsLoading = false
443+
if (this.logsDialog && this.selectedCronName) {
444+
this.logsAutoRefreshCountdown = Math.max(Math.round(this.logsAutoRefreshMs / 1000), 1)
445+
}
361446
}
362447
},
363448
onLogsEditorLoad(editor: any): void {

0 commit comments

Comments
 (0)