Let finished caption jobs be dismissed from the Queue panel.
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
Reference in New Issue
Block a user