@@ -6,15 +6,15 @@ import CourseInfoBanner from './CourseInfoBanner';
66import { ShotData } from './ShotTrajectoryOverlay' ;
77import { useActivitySessionState } from '../hooks/useActivitySessionState' ;
88import { 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
1919interface 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-
8926const 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 }
0 commit comments