feat(diagnostics): persist full stderr detail behind a "Show details" toggle

describeDownloadError condenses a postprocessing failure to one line for
the queue/notification, discarding the rest of yt-dlp's stderr. The
persisted error log now also keeps the fuller non-noise transcript
(stderrDetail) as an optional `detail` field, and the Diagnostics card
gets a collapsible "Show details" expander per row so a failure like
"Postprocessing: Conversion failed!" can be inspected -- and copied
into a bug report -- without re-running the download from a terminal.
This commit is contained in:
2026-07-05 14:56:09 -04:00
parent 6cb9c5ca47
commit 27cae28e12
9 changed files with 155 additions and 17 deletions
+17
View File
@@ -13,6 +13,23 @@ const seed: ErrorLogEntry[] = PREVIEW
kind: 'video',
error: '[youtube] err0r: Private video. Sign in if you have access.',
occurredAt: Date.now() - 1000 * 60 * 12
},
{
id: 'e2',
title: 'youre about to get hacked!! (7 reasons why)',
url: 'https://youtube.com/watch?v=q2err0r',
kind: 'video',
error:
'Postprocessing: Conversion failed! — [ModifyChapters] Re-encoding "video.mp4" with appropriate keyframes',
detail: [
'[info] q2err0r: Downloading 1 format(s): 137+251',
'[SponsorBlock] Found 1 segments in the SponsorBlock database',
'[Merger] Merging formats into "video.mp4"',
'[ModifyChapters] Re-encoding "video.mp4" with appropriate keyframes',
'[ModifyChapters] Removing chapters from video.mp4',
'ERROR: Postprocessing: Conversion failed!'
].join('\n'),
occurredAt: Date.now() - 1000 * 60 * 5
}
]
: []
@@ -1,6 +1,12 @@
import { useState } from 'react'
import { Button, Card, Subtitle2, Caption1, Text } from '@fluentui/react-components'
import { BugRegular, CopyRegular, DeleteRegular } from '@fluentui/react-icons'
import {
BugRegular,
ChevronDownRegular,
ChevronRightRegular,
CopyRegular,
DeleteRegular
} from '@fluentui/react-icons'
import { useErrorLog } from '../../store/errorlog'
import { useErrorTextStyles } from '../../components/ui/errorText'
import { EmptyState } from '../../components/ui/EmptyState'
@@ -13,13 +19,25 @@ export function DiagnosticsCard(): React.JSX.Element {
const clearErrorLog = useErrorLog((s) => s.clear)
const [confirmClearLog, setConfirmClearLog] = useState(false)
// Which rows have their "Show details" transcript expanded; keyed by entry id.
const [expanded, setExpanded] = useState<Set<string>>(new Set())
function toggleExpanded(id: string): void {
setExpanded((s) => {
const next = new Set(s)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
function copyErrorReport(): void {
// The button is disabled when there are no entries, so `report` is always
// non-empty here -- no need for an unreachable empty-report fallback (L159).
const report = errorEntries
.map((e) =>
[new Date(e.occurredAt).toLocaleString(), e.title ?? e.url, e.url, e.error].join('\n')
[new Date(e.occurredAt).toLocaleString(), e.title ?? e.url, e.url, e.error, e.detail]
.filter(Boolean)
.join('\n')
)
.join('\n\n---\n\n')
navigator.clipboard.writeText(report).catch(() => {})
@@ -78,17 +96,33 @@ export function DiagnosticsCard(): React.JSX.Element {
<EmptyState compact message="No errors yet." />
) : (
<div className={styles.errorList}>
{errorEntries.slice(0, 20).map((e) => (
<div key={e.id} className={styles.errorRow}>
<div className={styles.errorRowHeader}>
<Text className={styles.errorRowTitle}>{e.title ?? e.url}</Text>
<Caption1 className={styles.hint}>
{new Date(e.occurredAt).toLocaleString()}
</Caption1>
{errorEntries.slice(0, 20).map((e) => {
const isExpanded = expanded.has(e.id)
return (
<div key={e.id} className={styles.errorRow}>
<div className={styles.errorRowHeader}>
<Text className={styles.errorRowTitle}>{e.title ?? e.url}</Text>
<Caption1 className={styles.hint}>
{new Date(e.occurredAt).toLocaleString()}
</Caption1>
</div>
<Caption1 className={errText.errorPre}>{e.error}</Caption1>
{e.detail && (
<>
<Button
appearance="transparent"
size="small"
icon={isExpanded ? <ChevronDownRegular /> : <ChevronRightRegular />}
onClick={() => toggleExpanded(e.id)}
>
{isExpanded ? 'Hide details' : 'Show details'}
</Button>
{isExpanded && <pre className={styles.errorDetail}>{e.detail}</pre>}
</>
)}
</div>
<Caption1 className={errText.errorPre}>{e.error}</Caption1>
</div>
))}
)
})}
{errorEntries.length > 20 && (
<Caption1 className={styles.hint}>Showing 20 of {errorEntries.length} errors.</Caption1>
)}
@@ -113,5 +113,21 @@ export const useSettingsStyles = makeStyles({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
},
// The Diagnostics card's "Show details" expander -- the fuller non-noise
// stderr transcript behind a failure's one-line headline. Scrollable and
// capped so a long SponsorBlock/ffmpeg transcript can't blow out the card.
errorDetail: {
fontFamily: tokens.fontFamilyMonospace,
fontSize: tokens.fontSizeBase200,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: tokens.colorNeutralForeground3,
backgroundColor: tokens.colorNeutralBackground1,
marginTop: '6px',
padding: '8px',
maxHeight: '200px',
overflowY: 'auto',
...shorthands.borderRadius(tokens.borderRadiusMedium)
}
})