Let finished caption jobs be dismissed from the Queue panel.

This commit is contained in:
Towsty
2026-09-30 23:18:44 -05:00
parent 15ef891b26
commit 14dcd234f3
5 changed files with 98 additions and 15 deletions
+15 -1
View File
@@ -385,6 +385,7 @@
<div class="job-status" :data-state="card.state"> <div class="job-status" :data-state="card.state">
<span class="left"><span class="dot" aria-hidden="true" />{{ card.statusLabel }}</span> <span class="left"><span class="dot" aria-hidden="true" />{{ card.statusLabel }}</span>
<button v-if="card.cancelable" type="button" class="kebab" aria-label="Cancel job" @click.stop="cancel(card.job)">⋮</button> <button v-if="card.cancelable" type="button" class="kebab" aria-label="Cancel job" @click.stop="cancel(card.job)">⋮</button>
<button v-else-if="card.dismissable" type="button" class="kebab" aria-label="Dismiss job" @click.stop="dismissFinishedCard(card)">×</button>
</div> </div>
<div v-if="card.progress" class="job-progress" :data-indeterminate="card.progress.indeterminate ? 'true' : undefined"> <div v-if="card.progress" class="job-progress" :data-indeterminate="card.progress.indeterminate ? 'true' : undefined">
<progress <progress
@@ -820,6 +821,7 @@ const queueCards=computed(()=>{
const live=job.live const live=job.live
const text=live?.resultText || job.resultText || '' const text=live?.resultText || job.resultText || ''
const state=live?.status || job.status const state=live?.status || job.status
const finished=['complete','done','error','cancelled','failed'].includes(String(state))
cards.push({ cards.push({
id:job.id, id:job.id,
job, job,
@@ -829,7 +831,8 @@ const queueCards=computed(()=>{
state, state,
statusLabel:live?.message || state, statusLabel:live?.message || state,
resultText:text, resultText:text,
cancelable:!['complete','done','error','cancelled'].includes(job.status) && !['complete','error','cancelled'].includes(String(state)) cancelable:!finished,
dismissable:finished
}) })
} }
for(const job of upscales.value.filter((j:any)=>j.folderId===form.folderId && ['queued','running'].includes(j.status))){ for(const job of upscales.value.filter((j:any)=>j.folderId===form.folderId && ['queued','running'].includes(j.status))){
@@ -1410,6 +1413,14 @@ function restore(item:any){Object.assign(form,restoreAll(form,item,xaigen,loraMe
function focusJob(job:any){selectOutput(job.outputs?.at(-1)||null);focusedJobId.value=job.id} function focusJob(job:any){selectOutput(job.outputs?.at(-1)||null);focusedJobId.value=job.id}
async function loadLoras(fresh=false){try{loraNames.value=await $fetch('/api/studio-2/loras'+(fresh?'?fresh=1':''))}catch{error.value='Could not refresh LoRAs. The last loaded list is still available.'}} async function loadLoras(fresh=false){try{loraNames.value=await $fetch('/api/studio-2/loras'+(fresh?'?fresh=1':''))}catch{error.value='Could not refresh LoRAs. The last loaded list is still available.'}}
async function clearFinished(){await $fetch('/api/studio-2/clear-finished',{method:'POST'});await refreshJobs()} async function clearFinished(){await $fetch('/api/studio-2/clear-finished',{method:'POST'});await refreshJobs()}
async function dismissFinishedCard(card:any){
const id=card?.job?.id || card?.id
if(!id) return
try{
await $fetch('/api/studio-2/clear-finished',{method:'POST',body:{id}})
await refreshJobs()
}catch(e:any){error.value=e.data?.statusMessage||e.message||'Could not dismiss job'}
}
async function refreshJobs(){ async function refreshJobs(){
const [nextJobs,nextUpscales,queue]=await Promise.all([ const [nextJobs,nextUpscales,queue]=await Promise.all([
$fetch<any[]>('/api/studio-2/jobs'), $fetch<any[]>('/api/studio-2/jobs'),
@@ -1779,6 +1790,9 @@ const mobileCtx=computed(()=>({
cancel, cancel,
focusQueueCard, focusQueueCard,
clearFinished, clearFinished,
dismissFinishedCard,
copyCaption,
useCaptionAsPrompt,
removeNegative, removeNegative,
addNegative, addNegative,
addPhoto, addPhoto,
+6
View File
@@ -335,6 +335,12 @@
<div class="m-job-status"> <div class="m-job-status">
<span>● {{ card.statusLabel }}</span> <span>● {{ card.statusLabel }}</span>
<button v-if="card.cancelable" type="button" @click.stop="ctx.cancel(card.job)">Cancel</button> <button v-if="card.cancelable" type="button" @click.stop="ctx.cancel(card.job)">Cancel</button>
<button v-else-if="card.dismissable" type="button" @click.stop="ctx.dismissFinishedCard(card)">Dismiss</button>
</div>
<p v-if="card.resultText" class="m-muted" style="white-space:pre-wrap">{{ card.resultText }}</p>
<div v-if="card.resultText" class="m-job-status">
<button type="button" @click.stop="ctx.copyCaption(card.resultText)">Copy</button>
<button type="button" @click.stop="ctx.useCaptionAsPrompt(card.resultText)">Use as prompt</button>
</div> </div>
<div v-if="card.progress" class="m-job-progress" :data-indeterminate="card.progress.indeterminate ? 'true' : undefined"> <div v-if="card.progress" class="m-job-progress" :data-indeterminate="card.progress.indeterminate ? 'true' : undefined">
<progress <progress
+39 -14
View File
@@ -1,15 +1,40 @@
import { records, saveRecord } from '../../utils/studio2/store'; import { records, saveRecord } from '../../utils/studio2/store'
import { listStudioJobs } from '../../utils/studioQueue'; import { dismissFinishedStudioJobs, dismissStudioJob, listStudioJobs } from '../../utils/studioQueue'
export default defineEventHandler(event => {
const { owner } = assertLibraryOwner(event), queue=listStudioJobs(owner); export default defineEventHandler(async (event) => {
let cleared=0; const { owner } = assertLibraryOwner(event)
for (const r of records(owner)) { const body = await readBody<{ id?: string }>(event).catch(() => ({} as { id?: string }))
try { assertFolderAccess(event,r.request.folderId); } catch { continue; } const queue = listStudioJobs(owner)
const status=queue.find(q=>q.id===r.queueId)?.status; let cleared = 0
if (['complete','failed','cancelled'].includes(r.state) || ['cancelled','error'].includes(status || '')) {
r.dismissed=true; saveRecord(r); cleared++; if (body?.id) {
const row = queue.find(job => job.id === body.id)
if (!row) throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })
try { assertFolderAccess(event, row.payload.folderId) } catch {
throw createError({ statusCode: 403, statusMessage: 'Folder locked' })
}
await dismissStudioJob(owner, body.id)
return { cleared: 1 }
} }
}
// Retain internal manifests for cleanup retries; remove these records from the visible jobs list. for (const r of records(owner)) {
return {cleared}; try { assertFolderAccess(event, r.request.folderId) } catch { continue }
}); const status = queue.find(q => q.id === r.queueId)?.status
if (['complete', 'failed', 'cancelled'].includes(r.state) || ['cancelled', 'error'].includes(status || '')) {
r.dismissed = true
saveRecord(r)
cleared++
}
}
// Caption (and other finished studio-queue) cards — hide from Queue, keep resultText on the row
const finishedIds = queue
.filter(job => !job.dismissed && ['complete', 'error', 'cancelled'].includes(job.status))
.filter(job => {
try { assertFolderAccess(event, job.payload.folderId); return true } catch { return false }
})
.map(job => job.id)
cleared += await dismissFinishedStudioJobs(owner, finishedIds)
return { cleared }
})
+2
View File
@@ -5,12 +5,14 @@ export default defineEventHandler((event) => {
const { owner } = assertLibraryOwner(event) const { owner } = assertLibraryOwner(event)
const full = String(getQuery(event).full || '') === '1' const full = String(getQuery(event).full || '') === '1'
const active = listStudioJobs(owner) const active = listStudioJobs(owner)
.filter(job => !job.dismissed)
.filter(job => ( .filter(job => (
job.status === 'waiting' job.status === 'waiting'
|| job.status === 'running' || job.status === 'running'
|| job.status === 'held' || job.status === 'held'
|| (job.status === 'error' && Date.now() - job.updatedAt < 60 * 60 * 1000) || (job.status === 'error' && Date.now() - job.updatedAt < 60 * 60 * 1000)
|| (job.kind === 'caption' && job.status === 'complete' && Date.now() - job.updatedAt < 60 * 60 * 1000) || (job.kind === 'caption' && job.status === 'complete' && Date.now() - job.updatedAt < 60 * 60 * 1000)
|| (job.kind === 'caption' && job.status === 'cancelled' && Date.now() - job.updatedAt < 60 * 60 * 1000)
)) ))
const claimed = new Set(active.map(job => job.shotQueueId).filter(Boolean) as string[]) const claimed = new Set(active.map(job => job.shotQueueId).filter(Boolean) as string[])
const queues = listShotQueues(owner) const queues = listShotQueues(owner)
+36
View File
@@ -107,6 +107,8 @@ export interface StudioJob {
lastError?: string lastError?: string
waitReason?: string waitReason?: string
resultText?: string resultText?: string
/** Hidden from Queue UI; keeps resultText on the stored row. */
dismissed?: boolean
} }
type StudioQueueStore = { type StudioQueueStore = {
@@ -535,6 +537,40 @@ function canCancelStudioStatus(status: StudioJobStatus) {
return status === 'waiting' || status === 'held' || status === 'running' || status === 'error' return status === 'waiting' || status === 'held' || status === 'running' || status === 'error'
} }
function isFinishedStudioStatus(status: StudioJobStatus) {
return status === 'complete' || status === 'error' || status === 'cancelled'
}
/** Hide a finished studio-queue row from the Queue panel (keeps resultText). */
export async function dismissStudioJob(owner: string, id: string) {
return mutate(owner, (jobs) => {
const job = jobs.find(item => item.id === id)
if (!job) throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })
if (!isFinishedStudioStatus(job.status)) {
throw createError({ statusCode: 409, statusMessage: 'Only finished jobs can be dismissed' })
}
job.dismissed = true
job.updatedAt = Date.now()
return structuredClone(job)
})
}
/** Dismiss finished studio-queue rows (caption complete cards, errors, etc.). */
export async function dismissFinishedStudioJobs(owner: string, ids?: string[]) {
return mutate(owner, (jobs) => {
let cleared = 0
for (const job of jobs) {
if (ids?.length && !ids.includes(job.id)) continue
if (job.dismissed) continue
if (!isFinishedStudioStatus(job.status)) continue
job.dismissed = true
job.updatedAt = Date.now()
cleared += 1
}
return cleared
})
}
export async function cancelStudioJob(owner: string, id: string) { export async function cancelStudioJob(owner: string, id: string) {
const before = readJobs(owner).find(item => item.id === id) const before = readJobs(owner).find(item => item.id === id)
if (!before) throw createError({ statusCode: 404, statusMessage: 'Queued job not found' }) if (!before) throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })