Skip to content

Commit 8943495

Browse files
tanviet12claude
andcommitted
fix: job polling uses server status instead of local state
- isJobRunning computed from jobRuns[0].status (server truth) - Removed testRunning/triggerRunning local refs - Auto-start polling on mount if job already running (F5 resume) - No hardcoded poll timeout - polls until server says done - Buttons disabled when job running, spinner from isJobRunning - Cleanup poll timer on unmount Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0576890 commit 8943495

1 file changed

Lines changed: 41 additions & 30 deletions

File tree

frontend/src/views/Jobs/JobDetail.vue

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@
1414

1515
<v-tooltip v-if="!mdAndUp" text="Chạy thử" location="bottom">
1616
<template #activator="{ props }">
17-
<v-btn v-bind="props" variant="outlined" color="primary" icon="mdi-test-tube" size="small" :loading="testRunning" class="ml-1" @click="testRun" />
17+
<v-btn v-bind="props" variant="outlined" color="primary" icon="mdi-test-tube" size="small" :loading="isJobRunning" :disabled="isJobRunning" class="ml-1" @click="testRun" />
1818
</template>
1919
</v-tooltip>
20-
<v-btn v-else variant="outlined" color="primary" prepend-icon="mdi-test-tube" size="small" :loading="testRunning" class="ml-2" @click="testRun">Chạy thử (3 hội thoại)</v-btn>
20+
<v-btn v-else variant="outlined" color="primary" prepend-icon="mdi-test-tube" size="small" :loading="isJobRunning" :disabled="isJobRunning" class="ml-2" @click="testRun">Chạy thử (3 hội thoại)</v-btn>
2121

2222
<v-tooltip v-if="!mdAndUp" text="Chạy ngay" location="bottom">
2323
<template #activator="{ props }">
24-
<v-btn v-bind="props" color="primary" icon="mdi-play" size="small" :loading="triggerRunning" class="ml-1" @click="openRunDialog" />
24+
<v-btn v-bind="props" color="primary" icon="mdi-play" size="small" :disabled="isJobRunning" class="ml-1" @click="openRunDialog" />
2525
</template>
2626
</v-tooltip>
27-
<v-btn v-else color="primary" prepend-icon="mdi-play" size="small" :loading="triggerRunning" class="ml-2" @click="openRunDialog">{{ $t('run_now') }}</v-btn>
27+
<v-btn v-else color="primary" prepend-icon="mdi-play" size="small" :disabled="isJobRunning" class="ml-2" @click="openRunDialog">{{ $t('run_now') }}</v-btn>
2828

2929
<v-btn v-if="isJobRunning" color="error" variant="outlined" prepend-icon="mdi-stop" size="small" class="ml-2" :loading="cancelling" @click="cancelJob">{{ mdAndUp ? 'Dừng' : '' }}</v-btn>
3030
</template>
@@ -754,9 +754,9 @@ function tagColor(tag: string): string {
754754
return idx >= 0 ? TAG_COLORS[idx % TAG_COLORS.length] : TAG_COLORS[0]
755755
}
756756
const selectedRunId = ref<string | null>(null)
757-
const testRunning = ref(false)
758757
const cancelling = ref(false)
759-
const isJobRunning = computed(() => testRunning.value || triggerRunning.value || jobStore.jobRuns?.[0]?.status === 'running')
758+
const isJobRunning = computed(() => jobStore.jobRuns?.[0]?.status === 'running')
759+
let pollTimer: ReturnType<typeof setTimeout> | null = null
760760
const expandedMap = ref<Record<string, boolean>>({})
761761
const runDialog = ref(false)
762762
const clearResultsDialog = ref(false)
@@ -855,7 +855,10 @@ function loadImagesForMessages(msgs: any[]) {
855855
}
856856
}
857857
watch(chatMessages, (val) => { for (const msgs of Object.values(val)) { if (msgs?.length) loadImagesForMessages(msgs) } }, { deep: true })
858-
onUnmounted(() => { for (const url of Object.values(authImageCache.value)) { if (url?.startsWith('blob:')) URL.revokeObjectURL(url) } })
858+
onUnmounted(() => {
859+
stopPolling()
860+
for (const url of Object.values(authImageCache.value)) { if (url?.startsWith('blob:')) URL.revokeObjectURL(url) }
861+
})
859862
860863
// Parsed job fields
861864
const parsedOutputs = computed(() => {
@@ -1088,20 +1091,36 @@ onMounted(async () => {
10881091
tenantAIProvider.value = data?.settings?.ai_provider || 'claude'
10891092
tenantAIModel.value = data?.settings?.ai_model || ''
10901093
} catch { /* fallback empty */ }
1094+
// Auto-start polling if job is currently running (e.g. after F5)
1095+
if (isJobRunning.value) {
1096+
startPolling()
1097+
}
10911098
})
10921099
1093-
async function pollUntilComplete() {
1094-
let pollAttempts = 0
1095-
const maxPollAttempts = 120 // 6 minutes max for AI jobs
1096-
while (pollAttempts < maxPollAttempts) {
1097-
await new Promise(r => setTimeout(r, 3000))
1098-
await jobStore.fetchJobRuns(tenantId.value, jobId.value)
1099-
const latestRun = jobStore.jobRuns[0]
1100-
if (!latestRun || latestRun.status !== 'running') break
1101-
pollAttempts++
1100+
function startPolling() {
1101+
stopPolling()
1102+
async function tick() {
1103+
try {
1104+
await jobStore.fetchJobRuns(tenantId.value, jobId.value)
1105+
if (!isJobRunning.value) {
1106+
// Job finished — fetch final results
1107+
await jobStore.fetchAllJobResults(tenantId.value, jobId.value)
1108+
job.value = await jobStore.fetchJob(tenantId.value, jobId.value)
1109+
stopPolling()
1110+
return
1111+
}
1112+
} catch { /* ignore network errors, retry next tick */ }
1113+
pollTimer = setTimeout(tick, 3000)
1114+
}
1115+
// Small delay before first poll to let backend create the run record
1116+
pollTimer = setTimeout(tick, 2000)
1117+
}
1118+
1119+
function stopPolling() {
1120+
if (pollTimer) {
1121+
clearTimeout(pollTimer)
1122+
pollTimer = null
11021123
}
1103-
await jobStore.fetchAllJobResults(tenantId.value, jobId.value)
1104-
job.value = await jobStore.fetchJob(tenantId.value, jobId.value)
11051124
}
11061125
11071126
const currentRunProgress = computed(() => {
@@ -1143,23 +1162,17 @@ async function openRunDialog() {
11431162
11441163
async function testRun() {
11451164
if (!(await checkAIConfigured())) return
1146-
testRunning.value = true
11471165
try {
11481166
await jobStore.testRunJob(tenantId.value, jobId.value)
1149-
await pollUntilComplete()
1167+
startPolling()
11501168
} catch {
11511169
await jobStore.fetchJobRuns(tenantId.value, jobId.value)
1152-
} finally {
1153-
testRunning.value = false
11541170
}
11551171
}
11561172
1157-
const triggerRunning = ref(false)
1158-
11591173
async function confirmRun() {
11601174
if (runConditionalError.value) return
11611175
runDialog.value = false
1162-
triggerRunning.value = true
11631176
try {
11641177
const params: Record<string, string> = {}
11651178
if (runMode.value === 'conditional') {
@@ -1168,21 +1181,19 @@ async function confirmRun() {
11681181
}
11691182
if (runLimit.value && runLimit.value > 0) params.limit = String(runLimit.value)
11701183
await jobStore.triggerJob(tenantId.value, jobId.value, runMode.value, params)
1171-
await pollUntilComplete()
1184+
startPolling()
11721185
} catch {
11731186
await jobStore.fetchJobRuns(tenantId.value, jobId.value)
1174-
} finally {
1175-
triggerRunning.value = false
11761187
}
11771188
}
11781189
11791190
async function cancelJob() {
11801191
cancelling.value = true
11811192
try {
11821193
await api.post(`/tenants/${tenantId.value}/jobs/${jobId.value}/cancel`)
1183-
testRunning.value = false
1184-
triggerRunning.value = false
1194+
stopPolling()
11851195
await jobStore.fetchJobRuns(tenantId.value, jobId.value)
1196+
await jobStore.fetchAllJobResults(tenantId.value, jobId.value)
11861197
} catch { /* ignore */ }
11871198
finally { cancelling.value = false }
11881199
}

0 commit comments

Comments
 (0)