feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
< template >
< private-view title = "Image Previews" >
< main class = "ip" >
< v-notice v-if = "status && !status.enabled" type="warning" >
Preview generation is disabled . Enable it in Configuration below .
< / v-notice >
2026-10-25 14:26:49 +03:00
< v-notice v-if = "status?.run?.error" type="danger" >
Last run error : { { status . run . error } }
< / v-notice >
feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
< section class = "ip__card" >
< header class = "ip__head" >
< h2 > Backfill progress < / h2 >
< div class = "ip__actions" >
< v-button kind = "primary" :loading = "starting" @click ="startRun" > Run now < / v-button >
< v-button v-if = "status?.run?.running" kind="danger" @click="cancelRun" > Cancel < / v-button >
< / div >
< / header >
< p class = "ip__hint" >
“ Run now ” processes the whole library for all enabled presets and ignores the maintenance
window . Progress refreshes every 3 seconds .
< / p >
< v-progress-linear :value = "percent" / >
< div class = "ip__grid" >
< div > < span class = "ip__label" > done < / span > { { status ? . run ? . done ? ? 0 } } / { { status ? . run ? . total ? ? 0 } } < / div >
< div > < span class = "ip__label" > remaining < / span > { { remaining } } < / div >
< div > < span class = "ip__label" > generated < / span > { { status ? . run ? . generated ? ? 0 } } < / div >
< div > < span class = "ip__label" > already cached < / span > { { status ? . run ? . hits ? ? 0 } } < / div >
< div > < span class = "ip__label" > failed < / span > { { status ? . run ? . failed ? ? 0 } } < / div >
< div > < span class = "ip__label" > rate < / span > { { status ? . run ? . rateHuman ? ? '—' } } < / div >
< div > < span class = "ip__label" > eta < / span > { { status ? . run ? . etaHuman ? ? '—' } } < / div >
< div > < span class = "ip__label" > last started < / span > { { status ? . run ? . startedAtHuman ? ? '—' } } < / div >
< div > < span class = "ip__label" > last finished < / span > { { status ? . run ? . finishedAtHuman ? ? '—' } } < / div >
< / div >
< p class = "ip__hint" >
{ { status ? . enabled ? 'enabled' : 'disabled' } } ·
2026-10-25 14:26:49 +03:00
{ { status ? . run ? . paused ? 'paused (window closed)' : status ? . inWindow ? 'inside maintenance window' : 'outside maintenance window' } } ·
feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
next window { { status ? . nextWindowHuman ? ? '—' } }
< / p >
< / section >
< section class = "ip__card" >
< header class = "ip__head" >
< h2 > Storage < / h2 >
< v-button @click ="loadDisk" > Refresh < / v-button >
< / header >
< table class = "ip__table" v-if = "disk" >
< thead >
< tr > < th > < / th > < th > size < / th > < th > files < / th > < / tr >
< / thead >
< tbody >
< tr > < td > Preview assets < / td > < td > { { disk . previews . bytesHuman } } < / td > < td > { { disk . previews . count } } < / td > < / tr >
< tr > < td > Original assets < / td > < td > { { disk . originals . bytesHuman } } < / td > < td > { { disk . originals . count } } < / td > < / tr >
< tr > < td > < strong > Total < / strong > < / td > < td > < strong > { { disk . total . bytesHuman } } < / strong > < / td > < td > < strong > { { disk . total . count } } < / strong > < / td > < / tr >
< / tbody >
< / table >
< p v -else class = "ip__hint" > Loading … < / p >
< p class = "ip__hint" > Preview assets are the generated transforms ( < code > & lt ; id & gt ; _ _ & lt ; hash & gt ; . avif | webp < / code > ) . Originals are the uploaded files . < / p >
< / section >
< section class = "ip__card" >
< header class = "ip__head" > < h2 > Configuration < / h2 > < / header >
< form v-if = "config" class="ip__form" @submit.prevent="saveConfig" >
< v-checkbox v-model = "config.enabled" label="Enabled" / >
< p class = "ip__hint" > Master switch . When off , no previews are generated ( upload hook and backfill are inert ) . < / p >
< v-checkbox v-model = "config.onUpload" label="Warm on upload" / >
< p class = "ip__hint" >
Generate previews immediately after an image is uploaded , so admin thumbnails load
instantly . When off , previews are generated on first view instead .
< / p >
< v-checkbox v-model = "config.debug" label="Debug logging" / >
< p class = "ip__hint" >
Write verbose generation logs to the Directus server log ( container logs /
< code > logs / directus . log < / code > ) .
< / p >
< div class = "ip__row" >
< label class = "ip__field" >
< span > Maintenance window start < / span >
< v-input v-model = "config.windowStart" placeholder="01:00" / >
< / label >
< label class = "ip__field" >
< span > Maintenance window end < / span >
< v-input v-model = "config.windowEnd" placeholder="04:00" / >
< / label >
< / div >
< p class = "ip__hint" >
Scheduled runs only start inside this window ( local time ) . “ Run now ” ignores it .
Default : 01 : 00 – 04 : 00.
< / p >
< label class = "ip__field" >
< span > Concurrency < / span >
< v-input v-model = "config.concurrency" type="number" / >
< / label >
< p class = "ip__hint" > How many previews are generated in parallel . Keep low on CPU - limited hosts . < / p >
< label class = "ip__field" >
< span > Formats for “ auto ” presets < / span >
< v-input v-model = "formatsInput" placeholder="avif,webp" / >
< / label >
< p class = "ip__hint" >
Used only by presets whose format is < code > auto < / code > ( all system presets ) . Fixed - format
custom presets ignore this list and use their own format . One file is generated per
format listed here .
< / p >
< div class = "ip__actions" >
< v-button kind = "primary" @click ="saveConfig" > Save < / v-button >
< span class = "ip__hint" > { { message } } < / span >
< / div >
< / form >
< / section >
< section class = "ip__card" >
< header class = "ip__head" > < h2 > Presets < / h2 > < / header >
< p class = "ip__hint" >
System presets are always generated with their built - in sizes ( 64 / 300 / 800 px ,
cover / contain ) . Custom presets from Settings → File Storage are added automatically ;
“ Run now ” also generates them for existing images .
< / p >
< v-notice v-if = "shadowedKeys.length" type="warning" >
These custom preset keys are shadowed by a system preset and will not be used :
{ { shadowedKeys . join ( ', ' ) } }
< / v-notice >
< table class = "ip__table" >
< thead >
< tr > < th > key < / th > < th > format < / th > < th > generated formats < / th > < th > source < / th > < / tr >
< / thead >
< tbody >
< tr v-for = "preset in presets" :key="preset.key" >
< td class = "ip__mono" > { { preset . key } } < / td >
< td > { { preset . format ? ? 'original' } } < / td >
< td > { { preset . effectiveFormats . map ( ( f ) => f ? ? 'original' ) . join ( ', ' ) } } < / td >
< td > { { preset . system ? 'system' : 'custom' } } < / td >
< / tr >
< / tbody >
< / table >
< / section >
< section class = "ip__card" >
< header class = "ip__head" > < h2 > Diagnostics < / h2 > < / header >
< div class = "ip__row" >
< label class = "ip__field" >
< span > File id < / span >
< v-input v-model = "fileId" placeholder="file uuid" / >
< / label >
< label class = "ip__field" >
< span > Preset < / span >
< v-select v-model = "selectedKey" :items="presetItems" placeholder="Select a preset" / >
< / label >
< / div >
< div class = "ip__actions" >
< v-button : disabled = "!fileId || !selectedKey" @click ="resolvePlan" > Resolve plan < / v-button >
< v-button : disabled = "!fileId || !selectedKey" @click ="generate(true)" > Dry run < / v-button >
< v-button kind = "primary" : disabled = "!fileId || !selectedKey" @click ="generate(false)" > Generate < / v-button >
< / div >
< p class = "ip__hint" >
“ Resolve plan ” predicts the exact cache filename without writing anything — use it to
verify a custom preset maps to the file the browser will request .
< / p >
< pre v-if = "plan" class="ip__pre" > {{ JSON.stringify ( plan , null , 2 ) }} < / pre >
< / section >
< section class = "ip__card" >
< header class = "ip__head" >
< h2 > Recent operations < / h2 >
< v-button @click ="loadStatus" > Refresh < / v-button >
< / header >
< table class = "ip__table" >
< thead >
< tr > < th > time < / th > < th > file < / th > < th > key < / th > < th > format < / th > < th > ok < / th > < th > duration < / th > < th > error < / th > < / tr >
< / thead >
< tbody >
< tr v-for ="(op, index) in ops" :key ="index" >
< td > { { op . tsHuman } } < / td >
< td class = "ip__mono" > { { op . fileId . slice ( 0 , 8 ) } } < / td >
< td > { { op . key } } < / td >
< td > { { op . format ? ? 'original' } } < / td >
< td > { { op . ok ? '✓' : '✗' } } < / td >
< td > { { op . msHuman } } < / td >
< td class = "ip__error" > { { op . error ? ? '' } } < / td >
< / tr >
< / tbody >
< / table >
< / section >
2026-10-30 10:22:29 +03:00
< section class = "ip__card" >
< header class = "ip__head" >
< h2 > Failed previews < / h2 >
< v-button @click ="loadFailures" > Refresh < / v-button >
< / header >
< p class = "ip__hint" >
Persisted failure journal . Retries run automatically inside the maintenance window with no
active backfill : up to 3 attempts , then 5 and 15 minute backoff .
< / p >
< table class = "ip__table" >
< thead >
< tr > < th > file < / th > < th > preset < / th > < th > format < / th > < th > attempts < / th > < th > last error < / th > < th > next retry < / th > < th > status < / th > < / tr >
< / thead >
< tbody >
< tr v-for = "failure in failures" :key="failure.file_id + failure.preset_key + (failure.format ?? '')" >
< td class = "ip__mono" > { { failure . file ? . filename _download ? ? failure . file _id . slice ( 0 , 8 ) } } < / td >
< td > { { failure . preset _key } } < / td >
< td > { { failure . format ? ? 'original' } } < / td >
< td > { { failure . attempts } } < / td >
< td class = "ip__error" > { { failure . error ? ? '' } } < / td >
< td > { { failure . next _retry _at _human ? ? '—' } } < / td >
< td > { { failure . status } } < / td >
< / tr >
< / tbody >
< / table >
< p v-if = "failures.length === 0" class="ip__hint" > No failures recorded. < / p >
< / section >
feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
< section class = "ip__card" >
< header class = "ip__head" > < h2 > API reference < / h2 > < / header >
< p class = "ip__hint" >
Base path < code > / image - previews < / code > . Every endpoint requires an admin session or an
admin token : < code > - H "Authorization: Bearer <token>" < / code > .
< / p >
< details v-for = "endpoint in endpoints" :key="endpoint.method + endpoint.path" class="ip__details" >
< summary > < code > { { endpoint . method } } { { endpoint . path } } < / code > — { { endpoint . summary } } < / summary >
< p class = "ip__hint" > { { endpoint . description } } < / p >
< pre class = "ip__pre" > { { endpoint . example } } < / pre >
< / details >
< / section >
< / main >
< / private-view >
< / template >
< script setup lang = "ts" >
import { computed , onMounted , onUnmounted , ref } from 'vue' ;
interface Bucket {
bytes : number ;
count : number ;
bytesHuman : string ;
}
interface RunState {
running : boolean ;
done : number ;
total : number ;
generated : number ;
hits : number ;
failed : number ;
rateHuman : string ;
etaHuman : string ;
startedAtHuman : string | null ;
finishedAtHuman : string | null ;
2026-10-25 14:26:49 +03:00
paused ? : boolean ;
error ? : string ;
feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
}
interface Status {
enabled : boolean ;
inWindow : boolean ;
nextWindowHuman : string | null ;
run : RunState | null ;
}
interface PresetInfo {
key : string ;
format : string | null ;
effectiveFormats : ( string | null ) [ ] ;
system : boolean ;
enabled : boolean ;
}
2026-10-30 10:22:29 +03:00
interface FailureRecord {
file _id : string ;
preset _key : string ;
format : string | null ;
attempts : number ;
status : string ;
error : string | null ;
next _retry _at _human : string | null ;
file : { filename _download : string | null ; type : string | null ; folder : string | null } | null ;
}
feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
const base = '/image-previews' ;
const status = ref < Status | null > ( null ) ;
const config = ref < Record < string , any > | null > ( null ) ;
const disk = ref < { previews : Bucket ; originals : Bucket ; total : Bucket } | null > ( null ) ;
const ops = ref < any [ ] > ( [ ] ) ;
2026-10-30 10:22:29 +03:00
const failures = ref < FailureRecord [ ] > ( [ ] ) ;
feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
const presets = ref < PresetInfo [ ] > ( [ ] ) ;
const shadowedKeys = ref < string [ ] > ( [ ] ) ;
const fileId = ref ( '' ) ;
const selectedKey = ref ( '' ) ;
const plan = ref < any > ( null ) ;
const message = ref ( '' ) ;
const formatsInput = ref ( '' ) ;
const starting = ref ( false ) ;
let timer : ReturnType < typeof setInterval > | null = null ;
const presetItems = computed ( ( ) => presets . value . map ( ( preset ) => ( { text : preset . key , value : preset . key } ) ) ) ;
const percent = computed ( ( ) => {
const run = status . value ? . run ;
if ( ! run || ! run . total ) return 0 ;
return Math . min ( 100 , Math . round ( ( run . done / run . total ) * 100 ) ) ;
} ) ;
const remaining = computed ( ( ) => {
const run = status . value ? . run ;
if ( ! run ) return 0 ;
return Math . max ( 0 , run . total - run . done ) ;
} ) ;
const endpoints = [
{
method : 'GET' ,
path : '/status' ,
summary : 'current run + window state' ,
description : 'Progress counters, rate, ETA, maintenance-window state, and human-readable dates.' ,
example : 'curl -H "Authorization: Bearer $TOKEN" $BASE/image-previews/status' ,
} ,
{
method : 'GET' ,
path : '/config' ,
summary : 'read settings' ,
description : 'Returns the effective settings (env defaults merged with the persisted config).' ,
example : 'curl -H "Authorization: Bearer $TOKEN" $BASE/image-previews/config' ,
} ,
{
method : 'PUT' ,
path : '/config' ,
summary : 'update settings' ,
description : 'Persists enabled, windowStart/windowEnd, concurrency, presetKeys, formats, onUpload, debug.' ,
example :
'curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \\\n -d \'{"enabled":true,"windowStart":"01:00","windowEnd":"04:00","concurrency":1,"formats":["avif","webp"],"onUpload":true,"debug":false,"presetKeys":null}\' \\\n $BASE/image-previews/config' ,
} ,
{
method : 'GET' ,
path : '/presets' ,
summary : 'list presets' ,
description : 'System + custom presets with the formats each one generates; flags custom keys shadowed by system presets.' ,
example : 'curl -H "Authorization: Bearer $TOKEN" $BASE/image-previews/presets' ,
} ,
{
method : 'POST' ,
path : '/run' ,
summary : 'start a backfill' ,
description : 'Body { "ignoreWindow": true } runs immediately; omit it to respect the maintenance window. Optional { "presets": ["key"] }.' ,
example : 'curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \\\n -d \'{"ignoreWindow":true}\' $BASE/image-previews/run' ,
} ,
{
method : 'POST' ,
path : '/run/cancel' ,
summary : 'cancel a running backfill' ,
description : 'Sets a cancel flag; the backfill stops after the current file.' ,
example : 'curl -X POST -H "Authorization: Bearer $TOKEN" $BASE/image-previews/run/cancel' ,
} ,
{
method : 'GET' ,
path : '/disk-usage' ,
summary : 'preview vs original storage' ,
description : 'Bytes and file counts for preview assets, original assets, and the total (machine + human-readable).' ,
example : 'curl -H "Authorization: Bearer $TOKEN" $BASE/image-previews/disk-usage' ,
} ,
{
method : 'POST' ,
path : '/generate' ,
summary : 'generate one file' ,
description : 'Body { fileId, keys?, formats?, dryRun? }. Returns per preset/format results, filenames, sizes and durations.' ,
example : 'curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \\\n -d \'{"fileId":"<uuid>","keys":["system-medium-cover"],"dryRun":true}\' $BASE/image-previews/generate' ,
} ,
{
method : 'GET' ,
path : '/resolve-plan' ,
summary : 'predict a cache filename' ,
description : 'Returns resolved transforms and the exact cache filename without writing anything.' ,
example : 'curl -H "Authorization: Bearer $TOKEN" "$BASE/image-previews/resolve-plan?fileId=<uuid>&key=system-medium-cover"' ,
} ,
{
method : 'GET' ,
path : '/ops' ,
summary : 'recent operations' ,
description : 'In-memory ring buffer of the last 200 generation attempts with timing and error messages.' ,
example : 'curl -H "Authorization: Bearer $TOKEN" "$BASE/image-previews/ops?limit=50"' ,
} ,
2026-10-30 10:22:29 +03:00
{
method : 'GET' ,
path : '/failures' ,
summary : 'persisted failures' ,
description : 'Failure journal records with file metadata, attempts, timestamps, next retry and status. Filter with status=failed|recovered|retry, limit (max 200) and offset.' ,
example : 'curl -H "Authorization: Bearer $TOKEN" "$BASE/image-previews/failures?status=failed&limit=50&offset=0"' ,
} ,
feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
] ;
async function api ( path : string , init ? : RequestInit ) {
const response = await fetch ( ` ${ base } ${ path } ` , {
credentials : 'include' ,
headers : { 'Content-Type' : 'application/json' , ... ( init ? . headers ? ? { } ) } ,
... init ,
} ) ;
if ( ! response . ok ) throw new Error ( ` ${ response . status } ${ await response . text ( ) } ` ) ;
return response . json ( ) ;
}
async function loadStatus ( ) {
try {
status . value = await api ( '/status' ) ;
ops . value = ( await api ( '/ops?limit=50' ) ) . ops ;
} catch {
/* ignore polling errors */
}
}
async function loadConfig ( ) {
config . value = await api ( '/config' ) ;
formatsInput . value = ( config . value ? . formats ? ? [ ] ) . join ( ',' ) ;
}
async function loadDisk ( ) {
disk . value = await api ( '/disk-usage' ) ;
}
async function loadPresets ( ) {
const result = await api ( '/presets' ) ;
presets . value = result . presets ;
shadowedKeys . value = result . shadowedKeys ? ? [ ] ;
}
2026-10-30 10:22:29 +03:00
async function loadFailures ( ) {
try {
const result = await api ( '/failures?limit=100' ) ;
failures . value = result . records ? ? [ ] ;
} catch {
/* ignore polling errors */
}
}
feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
async function saveConfig ( ) {
if ( ! config . value ) return ;
try {
const payload = {
... config . value ,
concurrency : Number ( config . value . concurrency ) ,
formats : formatsInput . value . split ( ',' ) . map ( ( part ) => part . trim ( ) ) . filter ( Boolean ) ,
} ;
config . value = await api ( '/config' , { method : 'PUT' , body : JSON . stringify ( payload ) } ) ;
formatsInput . value = ( config . value ? . formats ? ? [ ] ) . join ( ',' ) ;
message . value = 'saved' ;
await loadStatus ( ) ;
} catch ( error ) {
message . value = String ( error ) ;
}
}
async function startRun ( ) {
starting . value = true ;
message . value = '' ;
try {
await api ( '/run' , { method : 'POST' , body : JSON . stringify ( { ignoreWindow : true } ) } ) ;
message . value = 'backfill started' ;
await loadStatus ( ) ;
} catch ( error ) {
message . value = String ( error ) ;
} finally {
starting . value = false ;
}
}
async function cancelRun ( ) {
try {
await api ( '/run/cancel' , { method : 'POST' , body : '{}' } ) ;
message . value = 'cancelling after the current file' ;
} catch ( error ) {
message . value = String ( error ) ;
}
}
async function resolvePlan ( ) {
try {
plan . value = await api (
` /resolve-plan?fileId= ${ encodeURIComponent ( fileId . value ) } &key= ${ encodeURIComponent ( selectedKey . value ) } ` ,
) ;
} catch ( error ) {
message . value = String ( error ) ;
}
}
async function generate ( dryRun : boolean ) {
try {
plan . value = await api ( '/generate' , {
method : 'POST' ,
body : JSON . stringify ( { fileId : fileId . value , keys : [ selectedKey . value ] , dryRun } ) ,
} ) ;
await loadStatus ( ) ;
await loadDisk ( ) ;
} catch ( error ) {
message . value = String ( error ) ;
}
}
onMounted ( async ( ) => {
2026-10-30 10:22:29 +03:00
await Promise . all ( [ loadStatus ( ) , loadConfig ( ) , loadDisk ( ) , loadPresets ( ) , loadFailures ( ) ] ) ;
feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms
Directus image transform cache in-process with an explicit acceptFormat,
so the generated file matches the browser's cache key byte-for-byte.
- resumable, window-aware backfill with progress + cancel
- system + custom presets, effective-format reporting, shadow warnings
- storage breakdown (previews vs originals), human-readable API fields
- diagnostics: resolve-plan prediction, per-file generate, ops ring buffer
- isolated dev/staging compose stack + deploy/rollback scripts
Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed,
idempotent backfill, custom fixed-format presets, error surfacing.
2026-10-25 11:19:54 +03:00
timer = setInterval ( loadStatus , 3000 ) ;
} ) ;
onUnmounted ( ( ) => {
if ( timer ) clearInterval ( timer ) ;
} ) ;
< / script >
< style scoped >
. ip {
display : flex ;
flex - direction : column ;
gap : 16 px ;
padding : 24 px ;
max - width : 1100 px ;
}
. ip _ _card {
border : 1 px solid var ( -- border - normal ) ;
border - radius : 8 px ;
padding : 16 px ;
}
. ip _ _head {
display : flex ;
align - items : center ;
justify - content : space - between ;
margin - bottom : 12 px ;
}
. ip _ _head h2 {
font - size : 15 px ;
font - weight : 600 ;
margin : 0 ;
}
. ip _ _actions {
display : flex ;
align - items : center ;
gap : 8 px ;
flex - wrap : wrap ;
}
. ip _ _row {
display : flex ;
gap : 12 px ;
flex - wrap : wrap ;
}
. ip _ _field {
display : flex ;
flex - direction : column ;
gap : 4 px ;
min - width : 180 px ;
}
. ip _ _form {
display : flex ;
flex - direction : column ;
gap : 6 px ;
}
. ip _ _grid {
display : grid ;
grid - template - columns : repeat ( auto - fit , minmax ( 160 px , 1 fr ) ) ;
gap : 8 px ;
margin : 12 px 0 ;
font - variant - numeric : tabular - nums ;
}
. ip _ _label {
color : var ( -- foreground - subdued ) ;
margin - right : 4 px ;
}
. ip _ _hint {
color : var ( -- foreground - subdued ) ;
font - size : 13 px ;
margin : 2 px 0 6 px ;
}
. ip _ _pre {
background : var ( -- background - subdued ) ;
border - radius : 6 px ;
padding : 12 px ;
overflow : auto ;
max - height : 320 px ;
font - size : 12 px ;
white - space : pre - wrap ;
}
. ip _ _table {
width : 100 % ;
border - collapse : collapse ;
font - size : 13 px ;
font - variant - numeric : tabular - nums ;
}
. ip _ _table th ,
. ip _ _table td {
text - align : left ;
padding : 4 px 8 px ;
border - bottom : 1 px solid var ( -- border - subdued ) ;
vertical - align : top ;
}
. ip _ _mono {
font - family : monospace ;
}
. ip _ _error {
color : var ( -- danger ) ;
max - width : 320 px ;
word - break : break - word ;
}
. ip _ _details {
border - bottom : 1 px solid var ( -- border - subdued ) ;
padding : 6 px 0 ;
}
. ip _ _details summary {
cursor : pointer ;
}
< / style >