diff --git a/components/studio2/Bench.vue b/components/studio2/Bench.vue index bc1299a..6360514 100644 --- a/components/studio2/Bench.vue +++ b/components/studio2/Bench.vue @@ -385,6 +385,7 @@
+
{ const live=job.live const text=live?.resultText || job.resultText || '' const state=live?.status || job.status + const finished=['complete','done','error','cancelled','failed'].includes(String(state)) cards.push({ id:job.id, job, @@ -829,7 +831,8 @@ const queueCards=computed(()=>{ state, statusLabel:live?.message || state, 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))){ @@ -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} 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 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(){ const [nextJobs,nextUpscales,queue]=await Promise.all([ $fetch('/api/studio-2/jobs'), @@ -1779,6 +1790,9 @@ const mobileCtx=computed(()=>({ cancel, focusQueueCard, clearFinished, + dismissFinishedCard, + copyCaption, + useCaptionAsPrompt, removeNegative, addNegative, addPhoto, diff --git a/components/studio2/Mobile.vue b/components/studio2/Mobile.vue index 139fe8f..7cece3b 100644 --- a/components/studio2/Mobile.vue +++ b/components/studio2/Mobile.vue @@ -335,6 +335,12 @@
● {{ card.statusLabel }} + +
+

{{ card.resultText }}

+
+ +
{ - const { owner } = assertLibraryOwner(event), queue=listStudioJobs(owner); - let cleared=0; - for (const r of records(owner)) { - 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++; +import { records, saveRecord } from '../../utils/studio2/store' +import { dismissFinishedStudioJobs, dismissStudioJob, listStudioJobs } from '../../utils/studioQueue' + +export default defineEventHandler(async (event) => { + const { owner } = assertLibraryOwner(event) + const body = await readBody<{ id?: string }>(event).catch(() => ({} as { id?: string })) + const queue = listStudioJobs(owner) + let cleared = 0 + + 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. - return {cleared}; -}); + + for (const r of records(owner)) { + 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 } +}) diff --git a/server/api/studio-queue.get.ts b/server/api/studio-queue.get.ts index ff60bde..fc9987a 100644 --- a/server/api/studio-queue.get.ts +++ b/server/api/studio-queue.get.ts @@ -5,12 +5,14 @@ export default defineEventHandler((event) => { const { owner } = assertLibraryOwner(event) const full = String(getQuery(event).full || '') === '1' const active = listStudioJobs(owner) + .filter(job => !job.dismissed) .filter(job => ( job.status === 'waiting' || job.status === 'running' || job.status === 'held' || (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 === 'cancelled' && Date.now() - job.updatedAt < 60 * 60 * 1000) )) const claimed = new Set(active.map(job => job.shotQueueId).filter(Boolean) as string[]) const queues = listShotQueues(owner) diff --git a/server/utils/studioQueue.ts b/server/utils/studioQueue.ts index 90a5817..0ffd3d8 100644 --- a/server/utils/studioQueue.ts +++ b/server/utils/studioQueue.ts @@ -107,6 +107,8 @@ export interface StudioJob { lastError?: string waitReason?: string resultText?: string + /** Hidden from Queue UI; keeps resultText on the stored row. */ + dismissed?: boolean } type StudioQueueStore = { @@ -535,6 +537,40 @@ function canCancelStudioStatus(status: StudioJobStatus) { 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) { const before = readJobs(owner).find(item => item.id === id) if (!before) throw createError({ statusCode: 404, statusMessage: 'Queued job not found' })