-
Notifications
You must be signed in to change notification settings - Fork 0
fix: harden Data tab against sibling transform crashes and entity races #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,14 +59,21 @@ export function unwrapItems<T>(response: unknown): T[] { | |
| * Raw fault item shape returned by the gateway faults endpoints. | ||
| */ | ||
| export interface RawFaultItem { | ||
| fault_code: string; | ||
| description: string; | ||
| severity: number; | ||
| severity_label: string; | ||
| status: string; | ||
| /** Canonical SOVD identifier. `code` accepted as a fallback for backends | ||
| * that have not yet aligned on the SOVD field name. */ | ||
| fault_code?: string; | ||
| code?: string; | ||
| /** Human-readable description. `fault_name` accepted as a fallback. */ | ||
| description?: string; | ||
| fault_name?: string; | ||
| severity?: number; | ||
| severity_label?: string; | ||
| /** Canonical value is a string. An object form with `aggregatedStatus` | ||
| * is also accepted to keep the UI from crashing on payload drift. */ | ||
| status?: string | { aggregatedStatus?: string; [key: string]: unknown } | null; | ||
| /** Accepted as unix seconds (number), ISO 8601 string, or missing/invalid; | ||
| * `transformFault` normalises all of these to an ISO timestamp. */ | ||
| first_occurred: number | string | null | undefined; | ||
| first_occurred?: number | string | null; | ||
| last_occurred?: number | string | null; | ||
| occurrence_count?: number; | ||
| reporting_sources?: string[]; | ||
|
|
@@ -91,21 +98,29 @@ export function transformFault(apiFault: RawFaultItem): Fault { | |
| // Map severity number/label to FaultSeverity. | ||
| // Label check takes priority over numeric value; critical is checked first. | ||
| let severity: FaultSeverity = 'info'; | ||
| const severityNum = typeof apiFault.severity === 'number' ? apiFault.severity : 0; | ||
| const label = apiFault.severity_label?.toLowerCase() || ''; | ||
|
|
||
| if (label === 'critical' || apiFault.severity >= 3) { | ||
| if (label === 'critical' || severityNum >= 3) { | ||
| severity = 'critical'; | ||
| } else if (label === 'error' || apiFault.severity === 2) { | ||
| } else if (label === 'error' || severityNum === 2) { | ||
| severity = 'error'; | ||
| } else if (label === 'warn' || label === 'warning' || apiFault.severity === 1) { | ||
| } else if (label === 'warn' || label === 'warning' || severityNum === 1) { | ||
| severity = 'warning'; | ||
| } | ||
|
|
||
| // Map API status string to FaultStatusValue. | ||
| // Map API status to FaultStatusValue. Accept either a string (canonical SOVD) | ||
| // or an object with an `aggregatedStatus` field (UDS DTC-style backends). | ||
| let apiStatus = ''; | ||
| if (typeof apiFault.status === 'string') { | ||
| apiStatus = apiFault.status.toLowerCase(); | ||
| } else if (apiFault.status && typeof apiFault.status === 'object') { | ||
| const agg = (apiFault.status as { aggregatedStatus?: unknown }).aggregatedStatus; | ||
| if (typeof agg === 'string') apiStatus = agg.toLowerCase(); | ||
| } | ||
| let status: FaultStatusValue = 'active'; | ||
| const apiStatus = apiFault.status?.toLowerCase() || ''; | ||
| if (apiStatus === 'confirmed' || apiStatus === 'active') { | ||
| status = 'active'; | ||
| } else if (apiStatus === 'pending' || apiStatus === 'prefailed') { | ||
| } else if (apiStatus === 'pending' || apiStatus === 'prefailed' || apiStatus === 'passive') { | ||
| status = 'pending'; | ||
| } else if (apiStatus === 'cleared' || apiStatus === 'resolved') { | ||
| status = 'cleared'; | ||
|
|
@@ -125,8 +140,8 @@ export function transformFault(apiFault: RawFaultItem): Fault { | |
| const entity_type = apiFault.entity_type || 'app'; | ||
|
|
||
| return { | ||
| code: apiFault.fault_code, | ||
| message: apiFault.description, | ||
| code: apiFault.fault_code ?? apiFault.code ?? 'unknown', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major - When both
Fix options:
Option (a) is a 1-line change and preserves existing dedup semantics. Option (b) is more invasive but fixes the root (single-field identity is inherently fragile). Add regression test: |
||
| message: apiFault.description ?? apiFault.fault_name ?? '', | ||
| severity, | ||
| status, | ||
| timestamp: (() => { | ||
|
|
@@ -328,6 +343,9 @@ export function transformDataResponse(rawData: unknown): ComponentTopic[] { | |
| // `direction` above. | ||
| const typeLabel = xm?.ros2?.type ?? xm?.type; | ||
| const hasValue = item.value !== undefined; | ||
| const rawAccess = typeof xm?.access === 'string' ? xm.access.toLowerCase() : undefined; | ||
| const access: 'read' | 'write' | 'readwrite' | undefined = | ||
| rawAccess === 'read' || rawAccess === 'write' || rawAccess === 'readwrite' ? rawAccess : undefined; | ||
| return { | ||
| topic: topicName, | ||
| timestamp: Date.now(), | ||
|
|
@@ -344,6 +362,7 @@ export function transformDataResponse(rawData: unknown): ComponentTopic[] { | |
| isPublisher: direction === 'publish' || direction === 'both' || direction === 'output', | ||
| isSubscriber: direction === 'subscribe' || direction === 'both' || direction === 'input', | ||
| uniqueKey: direction ? `${topicName}:${direction}` : topicName, | ||
| access, | ||
| }; | ||
| }); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Major -
canWritefalsy-zero bug (round-2 traced end-to-end).!!(topic.type || topic.type_info || topic.data)evaluatesfalsewhentopic.data === 0AND bothtype/type_infoare absent:This is reachable:
transforms.ts:345doeshasValue = item.value !== undefined,:352doesdata: hasValue ? item.value : null. So literal0from the gateway is preserved astopic.data: 0(correct, since 0 is a legitimate reading). If that response also omitsx-medkit.ros2.type/x-medkit.type/x-medkit.type_info(primitive scalars often do), the write form is hidden even whenaccess === 'write'is explicitly set.Concrete scenario: UDS odometer reading at exactly 0, RPM at idle cutoff, counter reset — anything that momentarily reads 0 and has a minimal payload.
Fix (split presence from truthiness):
Or, since
accessis now the authoritative signal when present, trust it:Add a test:
topic = { access: 'write', data: 0 }(no type) →canWrite === true.