From 14c0d7d065a49f6e51de09d126ade034c92fb9e8 Mon Sep 17 00:00:00 2001 From: Skyler Golden Date: Tue, 5 May 2026 20:13:03 +0000 Subject: [PATCH 1/3] ReleaseDraft: surface upload/finalize/preview logs as a diagnostics panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Justin's pinning service already persists upload_log, finalize_log, and preview_log on each content draft and exposes them via /draft-content/{id}. The wiki side wasn't reading them — a failed transcode or pin showed up on the draft page as a single static "Preview transcoding failed" line with no detail. Adds an Upload Diagnostics panel (placeholder rendered server-side, populated by JS on load) that pulls all three logs from delivery-kid: - Red banner with the last error when status is upload_failed, finalize_failed, or preview_status is failed - Info banner while uploading / finalizing / preview transcoding - Collapsible
, auto-expanded on any *_failed state and collapsed otherwise, listing each log entry with timestamp, stage, message, progress, and structured error text Polls /draft-content every 10s while in-flight, then stops once the draft reaches a terminal state. Album drafts use a different endpoint without logs and are skipped. --- .../ext.pickipediaReleases.releaseDraft.css | 128 ++++++++++++ .../ext.pickipediaReleases.releaseDraft.js | 193 ++++++++++++++++++ .../src/ReleaseDraftContentHandler.php | 9 + 3 files changed, 330 insertions(+) diff --git a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css index 600d1ba..7332fbd 100644 --- a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css +++ b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css @@ -444,3 +444,131 @@ overflow-x: auto; font-size: 0.85em; } + +/* Upload diagnostics panel */ +.rd-diagnostics { + margin: 0 0 1.5em; +} + +.rd-diag-banner { + padding: 0.75em 1em; + margin-bottom: 0.5em; + border-radius: 4px; + font-size: 0.95em; +} + +.rd-diag-banner-error { + background: #fee; + border: 1px solid #d33; + color: #b32424; +} + +.rd-diag-banner-info { + background: #fef6e7; + border: 1px solid #ac6600; + color: #ac6600; +} + +.rd-diag-details { + background: #f8f9fa; + border: 1px solid #c8ccd1; + border-radius: 4px; + padding: 0.5em 0.75em; +} + +.rd-diag-details > summary { + cursor: pointer; + font-weight: bold; + color: #54595d; + padding: 0.25em 0; +} + +.rd-diag-details[open] > summary { + margin-bottom: 0.5em; +} + +.rd-diag-meta { + display: grid; + grid-template-columns: max-content 1fr; + column-gap: 0.75em; + row-gap: 0.15em; + margin: 0.25em 0 0.75em; + font-size: 0.9em; +} + +.rd-diag-meta dt { + font-weight: bold; + color: #72777d; +} + +.rd-diag-meta dd { + margin: 0; + font-family: monospace; + color: #54595d; +} + +.rd-diag-details h4 { + margin: 0.75em 0 0.25em; + font-size: 0.9em; + color: #54595d; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.rd-diag-log { + font-family: monospace; + font-size: 0.82em; + line-height: 1.5; + max-height: 14em; + overflow-y: auto; + background: #fff; + border: 1px solid #eaecf0; + border-radius: 3px; + padding: 0.4em 0.6em; +} + +.rd-diag-row { + padding: 0.15em 0; + border-bottom: 1px solid #f3f4f6; + color: #54595d; +} + +.rd-diag-row:last-child { + border-bottom: none; +} + +.rd-diag-row-error { + color: #b32424; + background: #fff5f5; + margin: 0.15em -0.3em; + padding: 0.25em 0.3em; + border-radius: 2px; + border-bottom: none; +} + +.rd-diag-ts { + color: #a2a9b1; + margin-right: 0.4em; +} + +.rd-diag-stage { + color: #36c; + font-weight: bold; + margin-right: 0.3em; +} + +.rd-diag-pct { + color: #14866d; +} + +.rd-diag-err { + margin: 0.25em 0 0; + padding: 0.4em 0.6em; + background: #fee; + border: 1px solid #fcc; + border-radius: 2px; + font-size: 0.95em; + white-space: pre-wrap; + word-break: break-word; + color: #b32424; +} diff --git a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js index 911f06a..687691f 100644 --- a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js +++ b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js @@ -1371,6 +1371,198 @@ updateTrimPreview(); } + // -- Diagnostics panel -- + // + // Pulls draft state from delivery-kid (/draft-content/{id}) and renders + // the persisted upload_log / finalize_log / preview_log. Justin's + // pinning service writes one entry per phase transition, so when an + // upload, transcode, or pin fails the cause is on disk in draft.json + // and survives reloads. Without this panel the wiki page just shows + // "Preview transcoding failed" as a single static line. + // + // Auto-expanded on any *_failed state, collapsed on success. + // Polls every 10s while status/preview_status is in-flight. + + var DIAG_POLL_INTERVAL_MS = 10000; + var diagPollTimer = null; + + function initDiagnostics() { + var container = el( 'rd-diagnostics' ); + var apiUrl = mw.config.get( 'wgDeliveryKidUrl' ); + var token = mw.config.get( 'wgUploadToken' ); + var draftType = ( draftData.type || 'record' ); + var draftId = draftData.draft_id; + + if ( !container || !apiUrl || !draftId || !token ) { + return; + } + + // Album drafts use /draft-album/{id}, which doesn't expose logs. + // Skip — the panel only makes sense for content drafts. + if ( draftType === 'record' || draftType === 'album' ) { + return; + } + + var headers = { + 'X-Upload-Token': token, + 'X-Upload-User': mw.config.get( 'wgUploadUser' ), + 'X-Upload-Timestamp': String( mw.config.get( 'wgUploadTimestamp' ) ) + }; + + function fetchAndRender() { + fetch( apiUrl + '/draft-content/' + encodeURIComponent( draftId ), { + headers: headers + } ).then( function ( resp ) { + return resp.ok ? resp.json() : null; + } ).then( function ( data ) { + if ( !data ) { + return; + } + renderDiagnostics( container, data ); + if ( diagShouldPoll( data ) ) { + if ( !diagPollTimer ) { + diagPollTimer = setInterval( fetchAndRender, DIAG_POLL_INTERVAL_MS ); + } + } else if ( diagPollTimer ) { + clearInterval( diagPollTimer ); + diagPollTimer = null; + } + } ).catch( function () { + // Silent — diagnostics are best-effort. The video preview + // path surfaces hard fetch failures separately. + } ); + } + + fetchAndRender(); + } + + function diagShouldPoll( data ) { + var status = data.status || ''; + var previewStatus = data.preview_status || ''; + return status === 'uploading' || status === 'finalizing' || + previewStatus === 'pending' || previewStatus === 'processing'; + } + + function renderDiagnostics( container, data ) { + var status = data.status || 'unknown'; + var previewStatus = data.preview_status || 'none'; + var uploadLog = data.upload_log || []; + var finalizeLog = data.finalize_log || []; + var previewLog = data.preview_log || []; + + var failed = ( status === 'upload_failed' ) || + ( status === 'finalize_failed' ) || + ( previewStatus === 'failed' ); + var inFlight = ( status === 'uploading' ) || ( status === 'finalizing' ) || + ( previewStatus === 'pending' ) || ( previewStatus === 'processing' ); + var hasAnyLog = uploadLog.length > 0 || finalizeLog.length > 0 || previewLog.length > 0; + + if ( !failed && !inFlight && !hasAnyLog ) { + container.hidden = true; + container.innerHTML = ''; + return; + } + container.hidden = false; + + var parts = []; + + // Banner + if ( failed ) { + var failedStage; + var lastError; + if ( status === 'upload_failed' ) { + failedStage = 'Upload'; + lastError = diagFindLastError( uploadLog ); + } else if ( status === 'finalize_failed' ) { + failedStage = 'Finalize'; + lastError = diagFindLastError( finalizeLog ); + } else { + failedStage = 'Preview transcoding'; + lastError = diagFindLastError( previewLog ); + } + parts.push( + '
' + + '' + mw.html.escape( failedStage ) + ' failed. ' + + mw.html.escape( lastError || 'See log below for details.' ) + + '
' + ); + } else if ( inFlight ) { + var inFlightLabel; + if ( status === 'uploading' ) { + inFlightLabel = 'Upload in progress…'; + } else if ( status === 'finalizing' ) { + inFlightLabel = 'Finalize in progress…'; + } else { + inFlightLabel = 'Preview transcoding in progress…'; + } + parts.push( + '
' + + mw.html.escape( inFlightLabel ) + + '
' + ); + } + + // Collapsible details — open when failed, closed when clean/in-flight + var openAttr = failed ? ' open' : ''; + var details = '
'; + details += 'Upload diagnostics'; + details += '
'; + details += '
Status
' + mw.html.escape( status ) + '
'; + if ( previewStatus !== 'none' ) { + details += '
Preview
' + mw.html.escape( previewStatus ) + '
'; + } + details += '
'; + + if ( uploadLog.length ) { + details += '

Upload log

' + diagRenderLog( uploadLog ); + } + if ( previewLog.length ) { + details += '

Preview transcoding log

' + diagRenderLog( previewLog ); + } + if ( finalizeLog.length ) { + details += '

Finalize log

' + diagRenderLog( finalizeLog ); + } + details += '
'; + parts.push( details ); + + container.innerHTML = parts.join( '' ); + } + + function diagRenderLog( entries ) { + var rows = entries.map( function ( e ) { + var ts = e.ts ? mw.html.escape( String( e.ts ) ) : ''; + var stage = mw.html.escape( e.phase || e.stage || '' ); + var msg = mw.html.escape( e.message || '' ); + var pct = ( e.progress !== null && e.progress !== undefined ) ? + ' (' + mw.html.escape( String( e.progress ) ) + '%)' : ''; + var rowCls = e.error ? 'rd-diag-row rd-diag-row-error' : 'rd-diag-row'; + var html = '
' + + ( ts ? '' + ts + ' ' : '' ) + + ( stage ? '[' + stage + '] ' : '' ) + + '' + msg + pct + ''; + if ( e.error ) { + html += '
' + mw.html.escape( String( e.error ) ) + '
'; + } + html += '
'; + return html; + } ); + return '
' + rows.join( '' ) + '
'; + } + + function diagFindLastError( entries ) { + for ( var i = entries.length - 1; i >= 0; i-- ) { + if ( entries[ i ].error ) { + return String( entries[ i ].error ); + } + } + // No structured error field — fall back to the last message line so + // the banner is never blank when something went wrong. + if ( entries.length ) { + return entries[ entries.length - 1 ].message || null; + } + return null; + } + // -- Init -- function initCreationTimeFallback() { @@ -1592,6 +1784,7 @@ initDownloadLinks(); initAbandonButtons(); initStageLogs(); + initDiagnostics(); } mw.loader.using( [ 'mediawiki.util', 'mediawiki.api' ] ).then( init ); diff --git a/extensions/PickiPediaReleases/src/ReleaseDraftContentHandler.php b/extensions/PickiPediaReleases/src/ReleaseDraftContentHandler.php index 039e615..980df71 100644 --- a/extensions/PickiPediaReleases/src/ReleaseDraftContentHandler.php +++ b/extensions/PickiPediaReleases/src/ReleaseDraftContentHandler.php @@ -122,6 +122,15 @@ protected function fillParserOutput( 'class' => 'rd-stage-log rd-finalize-log', ], '' ); + // Diagnostics panel (populated by JS from delivery-kid /draft-content + // — shows upload_log / finalize_log / preview_log so users can see + // exactly where a transcode or pin failed instead of a one-liner). + $html .= Html::rawElement( 'div', [ + 'id' => 'rd-diagnostics', + 'class' => 'rd-diagnostics', + 'hidden' => true, + ], '' ); + // Type-specific form if ( $type === 'record' || $type === 'album' ) { $html .= $this->renderAlbumForm( $data, $status ); From e7d7e08804e821c324574a3202864d24a8190185 Mon Sep 17 00:00:00 2001 From: Skyler Golden Date: Thu, 7 May 2026 00:27:16 +0000 Subject: [PATCH 2/3] ReleaseDraft: fall back to wiki snapshot when delivery-kid forgets a draft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When initDiagnostics' fetch of /draft-content/{id} returns 404, fall back to reading ReleaseDraft:{id}/diagnostics via the MediaWiki API. That sub-page is written by delivery-kid's pickipedia_client at every terminal-state transition (cryptograss/maybelle-config branch delivery-kid-snapshot-diagnostics) and survives a delivery-kid storage rebuild. The renderer marks snapshot-sourced data with "(snapshot)" in the disclosure summary and surfaces snapshot_at + a "wiki snapshot — delivery-kid no longer has live data" note in the meta block, so users know they're looking at a frozen view rather than live state. --- .../ext.pickipediaReleases.releaseDraft.js | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js index 687691f..43ccbd9 100644 --- a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js +++ b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js @@ -1413,6 +1413,13 @@ fetch( apiUrl + '/draft-content/' + encodeURIComponent( draftId ), { headers: headers } ).then( function ( resp ) { + // 404 means delivery-kid has forgotten the draft (e.g. its + // staging dir was rebuilt). Fall back to the snapshot the + // pinning-service wrote to ReleaseDraft:{id}/diagnostics + // at terminal state — that page outlives delivery-kid storage. + if ( resp.status === 404 ) { + return loadFromWikiSnapshot(); + } return resp.ok ? resp.json() : null; } ).then( function ( data ) { if ( !data ) { @@ -1436,6 +1443,47 @@ fetchAndRender(); } + /** + * Fetch ReleaseDraft:{id}/diagnostics via the MediaWiki API and parse + * its JSON content. Used as the fallback when delivery-kid's live + * /draft-content endpoint returns 404. + * + * Returns a Promise that resolves to the parsed snapshot dict (with + * an _from_snapshot marker for the renderer) or null if the page + * doesn't exist or its content isn't valid JSON. + */ + function loadFromWikiSnapshot() { + var subpage = mw.config.get( 'wgPageName' ) + '/diagnostics'; + return new mw.Api().get( { + action: 'query', + prop: 'revisions', + rvprop: 'content', + rvslots: 'main', + titles: subpage, + formatversion: 2 + } ).then( function ( resp ) { + var pages = ( resp.query || {} ).pages || []; + var page = pages[ 0 ]; + if ( !page || page.missing ) { + return null; + } + var rev = page.revisions && page.revisions[ 0 ]; + var content = rev && rev.slots && rev.slots.main && rev.slots.main.content; + if ( !content ) { + return null; + } + try { + var data = JSON.parse( content ); + data._from_snapshot = true; + return data; + } catch ( e ) { + return null; + } + } ).catch( function () { + return null; + } ); + } + function diagShouldPoll( data ) { var status = data.status || ''; var previewStatus = data.preview_status || ''; @@ -1504,13 +1552,25 @@ // Collapsible details — open when failed, closed when clean/in-flight var openAttr = failed ? ' open' : ''; + var summaryText = data._from_snapshot ? + 'Upload diagnostics (snapshot)' : + 'Upload diagnostics'; var details = '
'; - details += 'Upload diagnostics'; + details += '' + mw.html.escape( summaryText ) + ''; details += '
'; details += '
Status
' + mw.html.escape( status ) + '
'; if ( previewStatus !== 'none' ) { details += '
Preview
' + mw.html.escape( previewStatus ) + '
'; } + // When the data came from the wiki snapshot (delivery-kid forgot + // the draft), surface that fact + when the snapshot was taken so + // users know they're looking at a frozen view. + if ( data._from_snapshot ) { + details += '
Source
wiki snapshot — delivery-kid no longer has live data
'; + if ( data.snapshot_at ) { + details += '
Snapshot taken
' + mw.html.escape( String( data.snapshot_at ) ) + '
'; + } + } details += '
'; if ( uploadLog.length ) { From bc5756067b62b476963df2b2412648758f7c8f9b Mon Sep 17 00:00:00 2001 From: magent Date: Sat, 30 May 2026 18:16:15 +0000 Subject: [PATCH 3/3] ReleaseDraft: remove stage-logs UI superseded by the diagnostics panel The earlier stage-logs work in this area (bd9fb4d, ef4c001, 48acf92) rendered upload_log and finalize_log into a pair of plain #rd-upload-log and #rd-finalize-log divs, polling /draft-content every 5s. The diagnostics panel added in 14c0d7d is a strict superset: - renders upload_log + finalize_log + preview_log (stage-logs missed preview_log entirely) - falls back to the wiki snapshot when delivery-kid 404s (stage-logs just stopped) - has terminal-state error banners with auto-expand on *_failed - same poll target, same headers, same terminal-state stop logic Keeping both meant rendering the same upload_log / finalize_log data twice on every content-draft page. Drop the stage-logs divs from ReleaseDraftContentHandler, drop initStageLogs + renderStageLog from the JS, and drop the .rd-stage-log* CSS block. The diagnostics panel is the single home for these logs going forward. --- .../ext.pickipediaReleases.releaseDraft.css | 53 --------- .../ext.pickipediaReleases.releaseDraft.js | 103 ------------------ .../src/ReleaseDraftContentHandler.php | 13 --- 3 files changed, 169 deletions(-) diff --git a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css index 7332fbd..d8ae588 100644 --- a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css +++ b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css @@ -352,59 +352,6 @@ margin-right: 0.5em; } -/* Upload-stage and finalize-stage logs (mirrored from delivery-kid's - ContentDraftState.upload_log / finalize_log). Same monospace look as - the preview log; shown above the form so they're visible even when - the draft has no video preview yet. */ -.rd-stage-log { - display: none; - margin: 0.5em 0 0.75em 0; - padding: 0.5em 0.75em; - max-height: 14em; - overflow-y: auto; - font-family: ui-monospace, SFMono-Regular, Menlo, monospace; - font-size: 0.8em; - line-height: 1.5; - color: #444; - background: #f8f9fa; - border: 1px solid #e0e0e0; - border-radius: 3px; -} - -.rd-stage-log-title { - font-weight: 600; - color: #222; - margin-bottom: 0.25em; - font-family: inherit; - font-size: 0.95em; -} - -.rd-stage-log-entry { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.rd-stage-log-error { - color: #a00; -} - -.rd-stage-log-ts { - color: #888; - margin-right: 0.5em; -} - -.rd-stage-log-stage { - color: #1a4480; - margin-right: 0.4em; - font-weight: 600; -} - -.rd-stage-log-errtext { - color: #a00; - font-style: italic; -} - /* Preserve original checkbox */ .rd-preserve-label { display: flex; diff --git a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js index 43ccbd9..2a84fd2 100644 --- a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js +++ b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js @@ -1732,108 +1732,6 @@ } } - // Render upload_log and finalize_log entries from a /draft-content - // response into their respective containers. Idempotent — replaces - // contents on each call so repeated polls just refresh the view. - function renderStageLog( containerId, title, entries ) { - var logEl = document.getElementById( containerId ); - if ( !logEl ) { - return; - } - if ( !entries || !entries.length ) { - logEl.style.display = 'none'; - return; - } - var html = '
' + mw.html.escape( title ) + '
'; - entries.forEach( function ( entry ) { - var ts = ( entry.ts || '' ).replace( 'T', ' ' ).replace( /\..*$/, '' ); - var stage = entry.stage || entry.phase || ''; - var msg = entry.message || ''; - var pct = ( entry.progress != null ) ? ' (' + entry.progress + '%)' : ''; - var entryClass = 'rd-stage-log-entry'; - if ( entry.error ) { - entryClass += ' rd-stage-log-error'; - } - html += '
' + - '' + mw.html.escape( ts ) + ' ' + - ( stage ? '' + mw.html.escape( stage ) + ' ' : '' ) + - mw.html.escape( msg ) + - mw.html.escape( pct ); - if ( entry.error && entry.error !== msg ) { - html += ' ' + mw.html.escape( entry.error ) + ''; - } - html += '
'; - } ); - logEl.innerHTML = html; - logEl.style.display = ''; - logEl.scrollTop = logEl.scrollHeight; - } - - // Poll /draft-content for upload_log + finalize_log + status. Runs - // on every ReleaseDraft page that has a draft_id, even when no video - // preview is rendered (e.g. status: awaiting_upload before any bytes). - // Stops polling once we hit a terminal state. - function initStageLogs() { - var draftId = ( draftData && draftData.draft_id ) || ''; - if ( !draftId ) { - return; - } - var deliveryKidUrl = mw.config.get( 'wgDeliveryKidUrl' ); - if ( !deliveryKidUrl ) { - return; - } - var token = mw.config.get( 'wgFinalizeToken' ) || mw.config.get( 'wgUploadToken' ); - if ( !token ) { - return; - } - var headers = { - 'X-Upload-Token': token, - 'X-Upload-User': mw.config.get( 'wgUploadUser' ), - 'X-Upload-Timestamp': String( mw.config.get( 'wgUploadTimestamp' ) ) - }; - - // If the wiki YAML already says finalized, the staging dir is gone - // and there's nothing left to poll for. Skip. - if ( draftData.status === 'finalized' || draftData.final_cid ) { - return; - } - - var poll = function () { - fetch( deliveryKidUrl + '/draft-content/' + draftId, { - headers: headers - } ).then( function ( resp ) { - if ( resp.status === 404 ) { - // Staging cleaned up — nothing more to render. The - // existing wgReleaseDraftData stays as fallback. - return null; - } - if ( !resp.ok ) { - return null; - } - return resp.json(); - } ).then( function ( data ) { - if ( !data ) { - return; - } - renderStageLog( 'rd-upload-log', 'Upload log', data.upload_log ); - renderStageLog( 'rd-finalize-log', 'Finalize log', data.finalize_log ); - // Stop polling on terminal states. - var s = data.status; - if ( s === 'upload_failed' || s === 'finalize_failed' || s === 'finalized' ) { - if ( pollInterval ) { - clearInterval( pollInterval ); - } - } - } ).catch( function () { - // Silently retry on next tick. - } ); - }; - - // Initial render uses whatever data is available, then poll. - poll(); - var pollInterval = setInterval( poll, 5000 ); - } - function init() { initTrackDragReorder(); initSaveButton(); @@ -1843,7 +1741,6 @@ initCreationTimeFallback(); initDownloadLinks(); initAbandonButtons(); - initStageLogs(); initDiagnostics(); } diff --git a/extensions/PickiPediaReleases/src/ReleaseDraftContentHandler.php b/extensions/PickiPediaReleases/src/ReleaseDraftContentHandler.php index 980df71..264512f 100644 --- a/extensions/PickiPediaReleases/src/ReleaseDraftContentHandler.php +++ b/extensions/PickiPediaReleases/src/ReleaseDraftContentHandler.php @@ -109,19 +109,6 @@ protected function fillParserOutput( // Status banner $html .= $this->renderStatusBanner( $status, $data ); - // Upload-stage and finalize-stage log containers. Empty by default; - // JS populates them by polling /draft-content. Keeping them here - // (above the type-specific form) means they're visible even when - // no video file is yet present (e.g. status: awaiting_upload). - $html .= Html::element( 'div', [ - 'id' => 'rd-upload-log', - 'class' => 'rd-stage-log rd-upload-log', - ], '' ); - $html .= Html::element( 'div', [ - 'id' => 'rd-finalize-log', - 'class' => 'rd-stage-log rd-finalize-log', - ], '' ); - // Diagnostics panel (populated by JS from delivery-kid /draft-content // — shows upload_log / finalize_log / preview_log so users can see // exactly where a transcode or pin failed instead of a one-liner).