Skip to content

Commit 2a8753a

Browse files
committed
Refactors webhook utils and measurement handling
Extracts event/session parsing, color mapping, and measurement logic into dedicated utilities and shared types to reduce duplication and simplify the inspector component. Standardizes measurement event detection to TPS.Simulator events and centralizes payload extraction via EventModel. Merges ShotFinish Actual values with the most recent OnStrokeCompleted measurement (scoped by device) and includes trajectory positions. Updates the component to consume the new APIs and pass playerId from the payload. Improves maintainability and prepares for broader reuse across the app.
1 parent b2d1c82 commit 2a8753a

File tree

5 files changed

+379
-325
lines changed

5 files changed

+379
-325
lines changed

src/components/WebhookInspector.tsx

Lines changed: 14 additions & 325 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import CourseInfoBanner from './CourseInfoBanner';
66
import { ShotData } from './ShotTrajectoryOverlay';
77
import { useActivitySessionState } from '../hooks/useActivitySessionState';
88
import { getEventDisplayName, getEventDescription, hasEventMetadata } from '../utils/eventMetadata';
9-
10-
type EventItem = {
11-
id?: string;
12-
eventType: string;
13-
timestamp: string;
14-
data?: any;
15-
raw?: any;
16-
expanded?: boolean;
17-
};
9+
import { EventItem } from '../types/webhookTypes';
10+
import { getColorForId } from '../utils/sessionColorUtils';
11+
import { getSessionIds, getDeviceIdFromEvent, getEventModelPayload } from '../utils/webhookEventUtils';
12+
import {
13+
isMeasurementEvent,
14+
getMeasurementData,
15+
findRecentChangePlayerData,
16+
findAllShotsForHole
17+
} from '../utils/measurementDataUtils';
1818

1919
interface Props {
2020
userPath: string;
@@ -23,69 +23,6 @@ interface Props {
2323
clearSignal?: number;
2424
}
2525

26-
// Color palette for session/activity indicators
27-
const SESSION_COLORS = [
28-
'#3b82f6', // blue
29-
'#10b981', // green
30-
'#f59e0b', // amber
31-
'#ef4444', // red
32-
'#8b5cf6', // violet
33-
'#ec4899', // pink
34-
'#06b6d4', // cyan
35-
'#f97316', // orange
36-
'#84cc16', // lime
37-
'#6366f1', // indigo
38-
];
39-
40-
const getSessionIds = (e: EventItem): { customerSessionId?: string; activitySessionId?: string } => {
41-
try {
42-
const raw = e.raw as any;
43-
const data = raw?.data || raw;
44-
45-
// Extract CustomerSession.Id
46-
const customerSessionId =
47-
data?.CustomerSession?.Id ||
48-
data?.common?.CustomerSession?.Id ||
49-
raw?.common?.CustomerSession?.Id;
50-
51-
// Extract ActivitySession.Id
52-
const activitySessionId =
53-
data?.ActivitySession?.Id ||
54-
data?.common?.ActivitySession?.Id ||
55-
raw?.common?.ActivitySession?.Id;
56-
57-
return { customerSessionId, activitySessionId };
58-
} catch (err) {
59-
return {};
60-
}
61-
};
62-
63-
const getColorForId = (id: string | undefined, colorMap: Map<string, string>): string | null => {
64-
if (!id) return null;
65-
66-
if (!colorMap.has(id)) {
67-
// Assign a color based on the current size of the map
68-
const colorIndex = colorMap.size % SESSION_COLORS.length;
69-
colorMap.set(id, SESSION_COLORS[colorIndex]);
70-
}
71-
72-
return colorMap.get(id) || null;
73-
};
74-
75-
const getDeviceIdFromEvent = (e: EventItem) => {
76-
try {
77-
const raw = e.raw as any;
78-
const data = e.data as any;
79-
// Check for Device.Id in various locations
80-
if (raw && raw.data && raw.data.Device && raw.data.Device.Id) return raw.data.Device.Id;
81-
if (raw && raw.Device && raw.Device.Id) return raw.Device.Id;
82-
if (data && data.Device && data.Device.Id) return data.Device.Id;
83-
return null;
84-
} catch (err) {
85-
return null;
86-
}
87-
};
88-
8926
const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null, selectedBayId = null, clearSignal }) => {
9027
const [allEvents, setAllEvents] = React.useState<EventItem[]>([]);
9128
const [connected, setConnected] = React.useState(false);
@@ -349,255 +286,6 @@ const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null,
349286

350287
const selectedEvent = selectedIndex === null ? null : visibleEvents[selectedIndex];
351288

352-
const getEventModelPayload = (e: EventItem) => {
353-
try {
354-
// Common places where the EventModel might appear
355-
const maybe = (e.data ?? e.raw) as any;
356-
if (!maybe) return e.data ?? e.raw ?? {};
357-
// If envelope where data contains EventModel
358-
if (maybe.EventModel) return maybe.EventModel;
359-
// Some payloads might have data: { EventModel: {...} }
360-
if (maybe.data && maybe.data.EventModel) return maybe.data.EventModel;
361-
// Some normalized records put typed payload under 'data' already
362-
if (e.data && (e.data.EventModel || e.data.eventModel)) return e.data.EventModel ?? e.data.eventModel;
363-
// Fallback to raw.data.EventModel
364-
if (e.raw && e.raw.data && (e.raw.data.EventModel || e.raw.data.eventModel)) return e.raw.data.EventModel ?? e.raw.data.eventModel;
365-
// Last resort: return the whole data/raw object
366-
return maybe;
367-
} catch (err) {
368-
return e.data ?? e.raw ?? {};
369-
}
370-
};
371-
372-
// Check if event should show measurement tiles
373-
const isMeasurementEvent = (e: EventItem) => {
374-
return e.eventType === 'TPS.Live.OnStrokeCompletedEvent' ||
375-
e.eventType === 'TPS.Simulator.ShotStarting' ||
376-
e.eventType === 'TPS.Simulator.ShotFinish' ||
377-
e.eventType.includes('StrokeCompleted') ||
378-
e.eventType.includes('ShotStarting') ||
379-
e.eventType.includes('ShotFinish');
380-
};
381-
382-
// Extract measurement data from various event types
383-
const getMeasurementData = (e: EventItem, eventsList: EventItem[]) => {
384-
try {
385-
const payload = getEventModelPayload(e);
386-
387-
// TPS.Live.OnStrokeCompletedEvent - has full Measurement object
388-
if (e.eventType === 'TPS.Live.OnStrokeCompletedEvent' || e.eventType.includes('StrokeCompleted')) {
389-
if (payload && payload.Measurement) {
390-
return {
391-
measurement: payload.Measurement,
392-
playerId: payload.PlayerId
393-
};
394-
}
395-
}
396-
397-
// TPS.Simulator.ShotStarting - has limited fields
398-
if (e.eventType === 'TPS.Simulator.ShotStarting' || e.eventType.includes('ShotStarting')) {
399-
// Build a measurement object from available fields
400-
const measurement: any = {};
401-
if (payload.BallSpeed !== undefined) measurement.BallSpeed = payload.BallSpeed;
402-
if (payload.LaunchAngle !== undefined) measurement.LaunchAngle = payload.LaunchAngle;
403-
if (payload.LaunchDirection !== undefined) measurement.LaunchDirection = payload.LaunchDirection;
404-
405-
return {
406-
measurement,
407-
playerId: payload.PlayerId
408-
};
409-
}
410-
411-
// TPS.Simulator.ShotFinish - merge with previous OnStrokeCompletedEvent
412-
if (e.eventType === 'TPS.Simulator.ShotFinish' || e.eventType.includes('ShotFinish')) {
413-
// Find the most recent OnStrokeCompletedEvent before this event
414-
// NOTE: Newest events are at index 0, so we search FORWARD (increasing indices) to find older events
415-
const currentIndex = eventsList.findIndex(evt => evt === e);
416-
let strokeCompletedMeasurement: any = {};
417-
418-
console.log('[ShotFinish] Current event index:', currentIndex);
419-
console.log('[ShotFinish] Events list length:', eventsList.length);
420-
421-
// Search forward from current event (toward older events at higher indices)
422-
for (let i = currentIndex + 1; i < eventsList.length; i++) {
423-
const prevEvent = eventsList[i];
424-
console.log(`[ShotFinish] Checking event at index ${i}:`, prevEvent.eventType);
425-
426-
if (prevEvent.eventType === 'TPS.Live.OnStrokeCompletedEvent' ||
427-
prevEvent.eventType.includes('StrokeCompleted')) {
428-
console.log('[ShotFinish] Found matching OnStrokeCompletedEvent at index:', i);
429-
const prevPayload = getEventModelPayload(prevEvent);
430-
console.log('[ShotFinish] Previous payload:', prevPayload);
431-
432-
if (prevPayload && prevPayload.Measurement) {
433-
console.log('[ShotFinish] Found measurement with keys:', Object.keys(prevPayload.Measurement));
434-
strokeCompletedMeasurement = { ...prevPayload.Measurement };
435-
break;
436-
} else {
437-
console.log('[ShotFinish] No Measurement found in payload');
438-
}
439-
}
440-
}
441-
442-
console.log('[ShotFinish] Final measurement before adding Actuals:', Object.keys(strokeCompletedMeasurement));
443-
444-
// Add the "Actual" fields from ShotFinish
445-
if (payload.Carry !== undefined && payload.Carry !== null) {
446-
strokeCompletedMeasurement.CarryActual = payload.Carry;
447-
}
448-
if (payload.Total !== undefined && payload.Total !== null) {
449-
strokeCompletedMeasurement.TotalActual = payload.Total;
450-
}
451-
if (payload.Curve !== undefined && payload.Curve !== null) {
452-
strokeCompletedMeasurement.CurveActual = payload.Curve;
453-
}
454-
if (payload.Side !== undefined && payload.Side !== null) {
455-
strokeCompletedMeasurement.SideActual = payload.Side;
456-
}
457-
if (payload.SideTotal !== undefined && payload.SideTotal !== null) {
458-
strokeCompletedMeasurement.SideTotalActual = payload.SideTotal;
459-
}
460-
461-
// Add position fields for trajectory visualization
462-
if (payload.StartingPosition) {
463-
strokeCompletedMeasurement.StartingPosition = payload.StartingPosition;
464-
}
465-
if (payload.FinishingPosition) {
466-
strokeCompletedMeasurement.FinishingPosition = payload.FinishingPosition;
467-
}
468-
469-
console.log('[ShotFinish] Final merged measurement keys:', Object.keys(strokeCompletedMeasurement));
470-
471-
return {
472-
measurement: strokeCompletedMeasurement,
473-
playerId: payload.PlayerId
474-
};
475-
}
476-
477-
return null;
478-
} catch (err) {
479-
return null;
480-
}
481-
};
482-
483-
/**
484-
* Find the most recent ChangePlayer event before the given event in the same ActivitySession.
485-
* This allows us to display hole/shot info for all events between ChangePlayer events.
486-
*/
487-
const findRecentChangePlayerData = (event: EventItem, eventsList: EventItem[]) => {
488-
try {
489-
const { activitySessionId } = getSessionIds(event);
490-
if (!activitySessionId) return null;
491-
492-
// First check if this event itself has the data
493-
const payload = getEventModelPayload(event);
494-
if (payload?.ActiveHole !== undefined && payload?.ShotNumber !== undefined) {
495-
return {
496-
hole: payload.ActiveHole,
497-
shot: payload.ShotNumber + 1, // Convert to 1-indexed
498-
playerName: payload.Name
499-
};
500-
}
501-
502-
// Find current event index
503-
const currentIdx = eventsList.findIndex(e => e.id === event.id);
504-
if (currentIdx === -1) return null;
505-
506-
// Search forward (to older events) for the most recent ChangePlayer in the same session
507-
for (let i = currentIdx + 1; i < eventsList.length; i++) {
508-
const prevEvent = eventsList[i];
509-
const { activitySessionId: prevSessionId } = getSessionIds(prevEvent);
510-
511-
// Only look at events in the same ActivitySession
512-
if (prevSessionId !== activitySessionId) continue;
513-
514-
if (prevEvent.eventType === 'TPS.Simulator.ChangePlayer') {
515-
const prevPayload = getEventModelPayload(prevEvent);
516-
if (prevPayload?.ActiveHole !== undefined && prevPayload?.ShotNumber !== undefined) {
517-
return {
518-
hole: prevPayload.ActiveHole,
519-
shot: prevPayload.ShotNumber + 1, // Convert to 1-indexed
520-
playerName: prevPayload.Name
521-
};
522-
}
523-
}
524-
}
525-
526-
return null;
527-
} catch (err) {
528-
return null;
529-
}
530-
};
531-
532-
/**
533-
* Find all ShotFinish events for the given hole in the same ActivitySession,
534-
* up to and including the current event (for progressive display).
535-
* Returns an array of shots with start/finish positions and shot numbers.
536-
*/
537-
const findAllShotsForHole = (event: EventItem, eventsList: EventItem[], holeNumber: number) => {
538-
try {
539-
const { activitySessionId } = getSessionIds(event);
540-
if (!activitySessionId) return [];
541-
542-
const shots: Array<{ startPosition: any; finishPosition: any; shotNumber?: number }> = [];
543-
544-
// Find the index of the current event
545-
const currentIdx = eventsList.findIndex(e => e.id === event.id);
546-
if (currentIdx === -1) return [];
547-
548-
// Search from the current event forward (toward older events at higher indices)
549-
// This way we only show shots that happened at or before the current event
550-
for (let i = currentIdx; i < eventsList.length; i++) {
551-
const evt = eventsList[i];
552-
const { activitySessionId: evtSessionId } = getSessionIds(evt);
553-
554-
// Only look at events in the same ActivitySession
555-
if (evtSessionId !== activitySessionId) continue;
556-
557-
// Check if this is a ShotFinish event
558-
if (evt.eventType === 'TPS.Simulator.ShotFinish') {
559-
const payload = getEventModelPayload(evt);
560-
561-
// Check if it's for the correct hole
562-
const eventHole = payload?.ActiveHole;
563-
if (eventHole === holeNumber) {
564-
const startPos = payload?.StartingPosition;
565-
const finishPos = payload?.FinishingPosition;
566-
567-
// Only include if we have both positions
568-
if (startPos && finishPos) {
569-
// Try to find the shot number from nearby ChangePlayer events
570-
const changePlayerData = findRecentChangePlayerData(evt, eventsList);
571-
shots.push({
572-
startPosition: startPos,
573-
finishPosition: finishPos,
574-
shotNumber: changePlayerData?.shot
575-
});
576-
}
577-
}
578-
}
579-
}
580-
581-
// Sort by shot number ascending (if available)
582-
// Shots without numbers go to the end
583-
shots.sort((a, b) => {
584-
if (a.shotNumber !== undefined && b.shotNumber !== undefined) {
585-
return a.shotNumber - b.shotNumber;
586-
}
587-
if (a.shotNumber !== undefined) return -1;
588-
if (b.shotNumber !== undefined) return 1;
589-
return 0;
590-
});
591-
592-
console.log(`[findAllShotsForHole] Found ${shots.length} shots for hole ${holeNumber}`);
593-
594-
return shots;
595-
} catch (err) {
596-
console.error('[WebhookInspector] Error finding shots for hole:', err);
597-
return [];
598-
}
599-
};
600-
601289
return (
602290
<div className="webhook-inspector">
603291
<div ref={listContainerRef} className="webhook-inspector-list" tabIndex={0} onKeyDown={onListKeyDown}>
@@ -757,12 +445,13 @@ const WebhookInspector: React.FC<Props> = ({ userPath, selectedDeviceId = null,
757445
{/* Check if this is a measurement event - show tiles view instead of JSON */}
758446
{(() => {
759447
if (isMeasurementEvent(selectedEvent)) {
760-
const measurementData = getMeasurementData(selectedEvent, allEvents);
761-
if (measurementData && measurementData.measurement) {
448+
const measurement = getMeasurementData(selectedEvent, allEvents);
449+
if (measurement) {
450+
const payload = getEventModelPayload(selectedEvent);
762451
return (
763452
<MeasurementTilesView
764-
measurement={measurementData.measurement}
765-
playerId={measurementData.playerId}
453+
measurement={measurement}
454+
playerId={payload?.PlayerId}
766455
/>
767456
);
768457
}

src/types/webhookTypes.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export type EventItem = {
2+
id?: string;
3+
eventType: string;
4+
timestamp: string;
5+
data?: any;
6+
raw?: any;
7+
expanded?: boolean;
8+
};
9+
10+
export interface WebhookInspectorProps {
11+
userPath: string;
12+
selectedDeviceId?: string | null;
13+
selectedBayId?: string | null;
14+
clearSignal?: number;
15+
}
16+
17+
export interface SessionIds {
18+
customerSessionId?: string;
19+
activitySessionId?: string;
20+
deviceId?: string;
21+
}

0 commit comments

Comments
 (0)