fix(wiki): JobStageBar terminal state — badge sync, pulse stop, line coloring

Three bugs fixed:

1. Badge stays "processing" after job completes: pollJobs() never called
   fetchRawMaterials() when a job reached terminal status, so
   raw.processingStatus stayed processing in the store. Fix: detect
   terminal job status in pollJobs, trigger fetchRawMaterials to sync.

2. "Completed" dot pulses instead of solid: dotClass() treated completed
   the same as in-progress (target === cur → active). Fix: add
   isTerminal computed (includes completed), return done for all
   dots at or before the terminal position — no pulse animation.

3. Stage label stays orange at terminal: same cause — active class
   applied regardless of terminal state. Fix: use done class for
   terminal labels (green instead of orange).

Also: v-if on JobStageBar now shows for terminal status jobs (not just
stage !== queued), so completed/failed stage bars remain visible.
This commit is contained in:
matevip 2026-04-19 21:00:08 +08:00
parent 0301d5628e
commit 55a7eb96dc
2 changed files with 37 additions and 8 deletions

View File

@ -15,7 +15,7 @@
</div>
</div>
<div class="stage-labels">
<span v-for="stage in stages" :key="stage.key" class="stage-label" :class="{ active: stage.key === currentStage }">
<span v-for="stage in stages" :key="stage.key" class="stage-label" :class="{ active: stage.key === currentStage && !isTerminal, done: stage.key === currentStage && isTerminal }">
{{ t(`wiki.jobStage.${stage.key}`) }}
</span>
</div>
@ -96,7 +96,12 @@ const currentStage = computed(() => {
return stageMapping[raw] ?? raw
})
// True when backend status indicates a terminal failure (dots should not pulse)
// True when the job has reached any terminal state (dots should not pulse)
const isTerminal = computed(() =>
props.status === 'completed' || props.status === 'failed' || props.status === 'partial' || props.status === 'cancelled'
)
// True specifically for failure terminals (dots show red instead of done)
const isTerminalFailure = computed(() =>
props.status === 'failed' || props.status === 'partial' || props.status === 'cancelled'
)
@ -117,11 +122,16 @@ function dotClass(key: string) {
const target = stageIndex(key)
if (cur < 0) return 'pending' // unknown stage all pending
if (isTerminalFailure.value) {
// Terminal failure: all dots before the failure point are done, the failure point is failed
// Terminal failure: dots before failure point are done, failure point is red
if (target < cur) return 'done'
if (target === cur) return 'failed'
return 'pending'
}
if (isTerminal.value) {
// Successful terminal (completed): all dots up to and including current are done (no pulse)
if (target <= cur) return 'done'
return 'pending'
}
if (target < cur) return 'done'
if (target === cur) return 'active'
return 'pending'
@ -202,6 +212,7 @@ const elapsed = computed(() => {
flex: 1;
}
.stage-label.active { color: var(--mc-primary); font-weight: 600; }
.stage-label.done { color: var(--mc-success, #5a8a5a); font-weight: 600; }
.stage-info {
display: flex;

View File

@ -91,9 +91,9 @@
</button>
</div>
</div>
<!-- RFC-033: Job stage bar only show when job has progressed past 'queued' -->
<!-- RFC-033: Job stage bar show when job has progressed past 'queued' or reached terminal -->
<JobStageBar
v-if="rawJobs[raw.id] && rawJobs[raw.id].stage !== 'queued'"
v-if="rawJobs[raw.id] && (rawJobs[raw.id].stage !== 'queued' || rawJobs[raw.id].status !== 'queued')"
:stage="rawJobs[raw.id].stage"
:status="rawJobs[raw.id].status"
:current-model="rawJobs[raw.id].currentModelName ?? (rawJobs[raw.id].currentModelId ? `Model #${rawJobs[raw.id].currentModelId}` : undefined)"
@ -288,19 +288,37 @@ onBeforeUnmount(() => {
const rawJobs = reactive<Record<number, WikiProcessingJob>>({})
let jobPoller: ReturnType<typeof setTimeout> | null = null
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'partial', 'cancelled'])
async function pollJobs() {
if (!store.currentKB) return
const kbId = store.currentKB.id
const processingRaws = store.rawMaterials.filter(
r => r.processingStatus === 'processing' || r.processingStatus === 'pending'
)
let anyTerminal = false
for (const raw of processingRaws) {
try {
const res: any = await wikiApi.getWikiJobs(store.currentKB.id, raw.id)
const res: any = await wikiApi.getWikiJobs(kbId, raw.id)
const list = res.data || res || []
if (list.length > 0) rawJobs[raw.id] = list[0]
if (list.length > 0) {
const job = list[0]
rawJobs[raw.id] = job
if (TERMINAL_STATUSES.has(job.status)) {
anyTerminal = true
}
}
} catch { /* ignore */ }
}
if (processingRaws.length > 0) {
// When any job reaches terminal, refresh raw materials to sync status badges
if (anyTerminal) {
await store.fetchRawMaterials(kbId)
}
// Continue polling while there are still processing/pending raws
const stillActive = store.rawMaterials.some(
r => r.processingStatus === 'processing' || r.processingStatus === 'pending'
)
if (stillActive) {
jobPoller = setTimeout(pollJobs, 3000)
}
}