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:
@@ -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: 'you’re 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)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user