diff --git a/frontend/src/components/shared/CrashDetail.jsx b/frontend/src/components/shared/CrashDetail.jsx new file mode 100644 index 0000000..85f4741 --- /dev/null +++ b/frontend/src/components/shared/CrashDetail.jsx @@ -0,0 +1,200 @@ +// frontend/src/components/shared/CrashDetail.jsx +// Crash detail for one fault boot event (firmware F-070): headline, task, +// uptime + heap snapshot at crash, PC/backtrace in ESP serial format, a +// ready-to-run addr2line command, the crashing build's ELF id, and — when the +// server has that build's firmware.elf and the xtensa addr2line binary — +// in-console decoding to function + file:line. +// +// Used by the device Health tab's boot history and the fleet Crash Report. +// +// Props: +// event — a boot event from /mqtt/boot-events (crash / pre_crash objects, +// or only the legacy crash_* columns for older firmware) + +import { useState, useEffect } from 'react' +import api from '@/lib/api' +import Button from '@/components/ui/Button' +import StatusBadge from '@/components/ui/StatusBadge' +import Icon from '@/components/ui/Icon' +import { copyText } from '@/lib/clipboard' +import { + crashOf, preCrashOf, crashHeadline, excCauseName, hex32, isHeapExhaustion, + addressLine, addr2lineCommand, decodeAddresses, fmtBytes, fmtUptime, +} from '@/lib/crashReport' + +const labelStyle = { + fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', + textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', +} +const monoBlockStyle = { + flex: 1, minWidth: 0, margin: 0, + padding: 'var(--space-2) var(--space-3)', + background: 'var(--color-bg-abyss)', border: '1px solid var(--color-border)', + borderRadius: 'var(--radius-md)', + fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', + color: 'var(--color-text-primary)', + whiteSpace: 'pre-wrap', wordBreak: 'break-all', +} + +function Field({ label, value, muted, mono = true }) { + return ( +
{text}
+ }
+ onClick={handleCopy}
+ aria-label={`Copy ${label}`}
+ >
+ {copied ? 'Copied' : 'Copy'}
+
+
+ {results.map(r => `${r.address}: ${r.frames.join('\n ')}`).join('\n')}
+
+ )
+ ) : (
+