From 52164a7fecaf0ee78b07693195a4866cdcd5fd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 4 Sep 2026 18:29:28 +0100 Subject: [PATCH] say when a media block points at something not converted yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The site serves nothing but derived files, so a block pointing at an original the pipeline has not been through renders an empty box — on the page, in the preview and in the editor. That is a normal thing to hit: drop a file in, choose it in the same minute, and you are ahead of a conversion that only starts twenty seconds after the folder goes quiet. The action bar already said "Sync 3". What was missing was the link from THIS blank block to that number, which is the half that costs an afternoon: an empty box looks like a block saved wrong, so it gets looked for in the wrong place. So the failed thumbnail is caught and named, using the state the bar already polls: converting now, in a run that started before this file, outstanding for the next one, or nothing running — with Sync now in place for the last case. Co-Authored-By: Claude Opus 5 (1M context) --- web/sections/MediaBlockSummary.tsx | 111 +++++++++++++++++++++++++++++ web/sections/PageBuilderPanel.tsx | 41 ++--------- 2 files changed, 116 insertions(+), 36 deletions(-) create mode 100644 web/sections/MediaBlockSummary.tsx diff --git a/web/sections/MediaBlockSummary.tsx b/web/sections/MediaBlockSummary.tsx new file mode 100644 index 0000000..ab6892b --- /dev/null +++ b/web/sections/MediaBlockSummary.tsx @@ -0,0 +1,111 @@ +import { useEffect, useState } from 'react'; +import { Loader2, RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { posterUrl, thumbUrl } from '../thumbnail'; +import { useMediaSync } from '../useMediaSync'; + +const IS_VIDEO = /\.(mp4|mov|webm|m4v)$/i; + +/** + * Which file a media block points at, and whether anything has been made from it yet. + * + * ── Why "not converted" has to be SAID ── + * + * Nothing on the site renders an ORIGINAL. Every picture and every video is served from the derived + * ladder, so a block pointing at a file the pipeline has not been through renders an empty box — on + * the page, in the preview, and here. + * + * That is a normal thing to hit rather than a mistake: drop a file in, choose it in the same minute, + * and you are ahead of a conversion that only starts twenty seconds after the folder goes quiet. An + * empty box is indistinguishable from a block saved wrong, so it gets looked for in the wrong place. + * + * ── Why it reads the sync rather than guessing ── + * + * "Not converted" and "converting right now" want different things from somebody: the second wants + * waiting and the first wants a button. `useMediaSync` already holds both — `current` is what the run + * has open this second, and the ledger's `missing`/`stale` are what a run would pick up — so this + * says which of the two it is instead of offering one answer for both. + * + * Reading is free here: the state is a `useGlobal` slot kept current by SyncButton in the action bar, + * which is mounted whenever this panel is. Nothing extra is polled by drawing this. + * + * ── The path is shown whole ── + * + * From `originals/`, and not truncated. Half these files are called the same thing — every collection + * has a hero, every folder a `_1` — so a bare filename identifies one only to somebody who already + * knows which it is, and `truncate` cuts the end, which is the wrong half of a path. + */ +export const MediaBlockSummary = ({ url, onChoose }: { url: string | null; onChoose: () => void }) => { + const [missing, setMissing] = useState(false); + const { running, state, ledger, busy, start } = useMediaSync(); + + // Cleared when the block is pointed somewhere else, or the old file's verdict sticks to the new one. + useEffect(() => setMissing(false), [url]); + + const reconciliation = ledger?.reconciliation ?? null; + // `current` is what the run has open this second — see SyncState. + const converting = Boolean(url) && (state?.current ?? []).includes(url!); + const queued = + Boolean(url) && + (reconciliation?.missing.includes(url!) === true || reconciliation?.stale.includes(url!) === true); + + return ( + <> +
+ + + {url || 'Nothing chosen'} + +
+ + {url && !missing && ( + // A video shows its POSTER rather than playing: the question is which file this is. + setMissing(true)} + /> + )} + + {url && missing && ( +
+ {converting ? ( +

+ + + Converting now. This file is in the + run that is going. It appears here when it finishes. + +

+ ) : ( + <> +

+ Not converted yet. The site serves + converted files only, so this block renders nothing until the pipeline has been through + it.{' '} + {running + ? 'A run is going, but this file is not in it — it was picked up after the run started.' + : queued + ? 'It is outstanding and the next run will take it.' + : 'A run starts on its own about twenty seconds after a folder goes quiet. A file renamed or replaced while one was going needs another pass.'} +

+ + + )} +
+ )} + + ); +}; diff --git a/web/sections/PageBuilderPanel.tsx b/web/sections/PageBuilderPanel.tsx index 37a19ed..7a56453 100644 --- a/web/sections/PageBuilderPanel.tsx +++ b/web/sections/PageBuilderPanel.tsx @@ -56,6 +56,7 @@ import { BandSettings } from './BandSettings'; import { BlockSettings } from './BlockSettings'; import { CarouselItems } from './CarouselItems'; import { PartnersEditor } from './PartnersEditor'; +import { MediaBlockSummary } from './MediaBlockSummary'; import { BlockMedia } from './BlockMedia'; import { PageStructure } from './PageStructure'; import { BandBlocks } from './BandBlocks'; @@ -870,42 +871,10 @@ export const PageBuilderPanel = () => { } /> ) : ( - <> -
- - {/* - The whole path from `originals/`, not the filename. - - Half these files are called the same thing — every collection has a hero, every - folder a `_1` — so a bare name says which picture only when you already know. - `truncate` cuts the END, which is exactly backwards for a path, so it is not used - here: the row wraps and the folder is what you read first. - */} - - {block.content?.url ?? 'Nothing chosen'} - -
- - {/* A video shows its POSTER rather than playing: the question is which file this is. */} - {block.content?.url && ( - - )} - + handlersRef.current.editComponent(block)} + /> )} )}