diff --git a/web/src/views/TrainJobsView.test.ts b/web/src/views/TrainJobsView.test.ts index 9d8f3f0..0dadede 100644 --- a/web/src/views/TrainJobsView.test.ts +++ b/web/src/views/TrainJobsView.test.ts @@ -124,4 +124,38 @@ describe('TrainJobsView', () => { await flushPromises() expect(w.find('button.view-log-btn').exists()).toBe(true) }) + it('shows error when config JSON is invalid', async () => { + const w = mount(TrainJobsView) + await flushPromises() + await w.find('input.model-key-input').setValue('my-model') + await w.find('textarea.config-textarea').setValue('{ not valid json }') + await w.find('button.submit-job-btn').trigger('click') + await flushPromises() + expect(w.find('.error-notice').exists()).toBe(true) + expect(w.find('.error-notice').text()).toContain('not valid') + }) + + it('shows error notice when jobs load fails', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: false, + status: 500, + json: async () => ({}), + text: async () => '', + })) + const w = mount(TrainJobsView) + await flushPromises() + expect(w.find('.error-notice').exists()).toBe(true) + expect(w.find('table.jobs-table').exists()).toBe(false) + }) + + it('cancel button optimistically updates job status to cancelled', async () => { + const w = mount(TrainJobsView) + await flushPromises() + await w.find('button.cancel-btn').trigger('click') + await flushPromises() + // After cancel, job should show status-cancelled pill (not status-queued) + expect(w.find('.status-cancelled').exists()).toBe(true) + expect(w.find('.status-queued').exists()).toBe(false) + }) + }) diff --git a/web/src/views/TrainJobsView.vue b/web/src/views/TrainJobsView.vue index 5ecb1ad..3fcdbc5 100644 --- a/web/src/views/TrainJobsView.vue +++ b/web/src/views/TrainJobsView.vue @@ -113,6 +113,7 @@ + @@ -151,6 +152,7 @@ const loadError = ref(null) const submitError = ref(null) const submitting = ref(false) const cancellingId = ref(null) +const cancelError = ref(null) const form = ref({ type: 'classifier' as 'classifier' | 'llm-sft', @@ -225,15 +227,19 @@ async function submitJob() { async function cancelJob(id: string) { cancellingId.value = id + cancelError.value = null try { const res = await fetch(`/api/train/jobs/${encodeURIComponent(id)}/cancel`, { method: 'DELETE' }) if (res.ok) { jobs.value = jobs.value.map(j => j.id === id ? { ...j, status: 'cancelled' as const } : j ) + } else { + cancelError.value = `Failed to cancel job (HTTP ${res.status}).` } - } catch { /* non-fatal */ } - finally { + } catch { + cancelError.value = 'Network error cancelling job.' + } finally { cancellingId.value = null } } @@ -258,6 +264,11 @@ function openLog(id: string) { } if (data.type === 'error') { logLines.value = [...logLines.value, '--- stream ended with error ---'] + nextTick(() => { + if (logPanelEl.value) { + logPanelEl.value.scrollTop = logPanelEl.value.scrollHeight + } + }) } }, () => {