diff --git a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.css index 600d1ba..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; @@ -444,3 +391,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..2a84fd2 100644 --- a/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js +++ b/extensions/PickiPediaReleases/resources/ext.pickipediaReleases.releaseDraft.js @@ -1371,6 +1371,258 @@ 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 ) { + // 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 ) { + 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(); + } + + /** + * 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 || ''; + 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( + '
' + ); + } 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( + '' + ); + } + + // 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 = '' + mw.html.escape( String( e.error ) ) + ''; + } + html += '