een-api-toolkit 0.3.49 → 0.3.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/een-events-agent.md +34 -1
- package/CHANGELOG.md +5 -5
- package/docs/AI-CONTEXT.md +1 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +114 -4
- package/docs/ai-reference/AI-GROUPING.md +1 -1
- package/docs/ai-reference/AI-JOBS.md +1 -1
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-events/src/components/EventsModal.vue +328 -3
- package/package.json +1 -1
|
@@ -86,10 +86,43 @@ interface ListEventsParams {
|
|
|
86
86
|
endTimestamp__lte?: string // Optional: filter by event end time
|
|
87
87
|
pageSize?: number
|
|
88
88
|
pageToken?: string
|
|
89
|
-
include?: string[] //
|
|
89
|
+
include?: string[] // Data schemas to include (see below)
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Include Parameter & Data Schemas
|
|
94
|
+
|
|
95
|
+
The `include` parameter controls which data schemas are populated in the `event.data[]` array.
|
|
96
|
+
Include values are derived from the event's `dataSchemas` array by adding the `data.` prefix.
|
|
97
|
+
|
|
98
|
+
**How it works:**
|
|
99
|
+
1. Each event has a `dataSchemas` array listing available schemas (e.g., `['een.objectDetection.v1', 'een.fullFrameImageUrl.v1']`)
|
|
100
|
+
2. To include that data, prefix with `data.` (e.g., `include: ['data.een.objectDetection.v1']`)
|
|
101
|
+
3. Without includes, the event may return with minimal or empty `data[]`
|
|
102
|
+
|
|
103
|
+
**Common data schemas:**
|
|
104
|
+
| Schema | Include Value | Description |
|
|
105
|
+
|--------|---------------|-------------|
|
|
106
|
+
| `een.objectDetection.v1` | `data.een.objectDetection.v1` | Bounding boxes `[x1, y1, x2, y2]` (normalized 0-1) |
|
|
107
|
+
| `een.objectClassification.v1` | `data.een.objectClassification.v1` | Object labels (person, vehicle, etc.) |
|
|
108
|
+
| `een.fullFrameImageUrl.v1` | `data.een.fullFrameImageUrl.v1` | Full frame image URL |
|
|
109
|
+
| `een.croppedFrameImageUrl.v1` | `data.een.croppedFrameImageUrl.v1` | Cropped/zoomed image URL |
|
|
110
|
+
| `een.fullFrameImageUrlWithOverlay.v1` | `data.een.fullFrameImageUrlWithOverlay.v1` | Image URL with bounding box overlay |
|
|
111
|
+
| `een.eevaAttributes.v1` | `data.een.eevaAttributes.v1` | EEVA analytics attributes |
|
|
112
|
+
| `een.customLabels.v1` | `data.een.customLabels.v1` | Custom detection labels |
|
|
113
|
+
|
|
114
|
+
**Fetching full event details:**
|
|
115
|
+
```typescript
|
|
116
|
+
import { getEvent } from 'een-api-toolkit'
|
|
117
|
+
|
|
118
|
+
// Get event with all available data based on its dataSchemas
|
|
119
|
+
const simpleEvent = events.value.find(e => e.id === eventId)
|
|
120
|
+
const includes = simpleEvent?.dataSchemas.map(schema => `data.${schema}`) || []
|
|
121
|
+
|
|
122
|
+
const { data: fullEvent } = await getEvent(eventId, { include: includes })
|
|
123
|
+
// fullEvent.data[] now contains all available data objects
|
|
124
|
+
```
|
|
125
|
+
|
|
93
126
|
### EventMetric Interface
|
|
94
127
|
```typescript
|
|
95
128
|
interface EventMetric {
|
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [0.3.
|
|
5
|
+
## [0.3.51] - 2026-02-03
|
|
6
6
|
|
|
7
7
|
### Release Summary
|
|
8
8
|
|
|
@@ -11,14 +11,14 @@ No PR descriptions available for this release.
|
|
|
11
11
|
### Detailed Changes
|
|
12
12
|
|
|
13
13
|
#### Features
|
|
14
|
-
- feat: Add
|
|
14
|
+
- feat: Add JSON viewer to vue-events example and document include parameter
|
|
15
15
|
|
|
16
16
|
#### Bug Fixes
|
|
17
|
-
- fix:
|
|
17
|
+
- fix: Address code review security and error handling concerns
|
|
18
18
|
|
|
19
19
|
### Links
|
|
20
20
|
- [npm package](https://www.npmjs.com/package/een-api-toolkit)
|
|
21
|
-
- [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.
|
|
21
|
+
- [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.49...v0.3.51)
|
|
22
22
|
|
|
23
23
|
---
|
|
24
|
-
*Released: 2026-
|
|
24
|
+
*Released: 2026-02-03 06:37:51 CST*
|
package/docs/AI-CONTEXT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Events, Alerts & Real-Time Streaming - EEN API Toolkit
|
|
2
2
|
|
|
3
|
-
> **Version:** 0.3.
|
|
3
|
+
> **Version:** 0.3.51
|
|
4
4
|
>
|
|
5
5
|
> Complete reference for events, alerts, metrics, and SSE subscriptions.
|
|
6
6
|
> Load this document when implementing event-driven features.
|
|
@@ -292,6 +292,7 @@ import {
|
|
|
292
292
|
listEventFieldValues,
|
|
293
293
|
listEventTypes,
|
|
294
294
|
getRecordedImage,
|
|
295
|
+
getEvent,
|
|
295
296
|
type Camera,
|
|
296
297
|
type Event,
|
|
297
298
|
type EventType,
|
|
@@ -374,6 +375,13 @@ const imageLoadingIds = ref<Set<string>>(new Set()) // Track which images are cu
|
|
|
374
375
|
const boundingBoxCache = ref<Map<string, BoundingBox[]>>(new Map()) // Cache bounding boxes per event
|
|
375
376
|
const enlargedEventId = ref<string | null>(null)
|
|
376
377
|
|
|
378
|
+
// JSON viewer state
|
|
379
|
+
const jsonViewerEventId = ref<string | null>(null)
|
|
380
|
+
const jsonViewerFullEvent = ref<Event | null>(null)
|
|
381
|
+
const jsonViewerLoading = ref(false)
|
|
382
|
+
const jsonViewerError = ref<string | null>(null)
|
|
383
|
+
const copySuccess = ref(false)
|
|
384
|
+
|
|
377
385
|
// Lightbox media state
|
|
378
386
|
const showVideo = ref(false)
|
|
379
387
|
const hdImageUrl = ref<string | null>(null)
|
|
@@ -396,6 +404,21 @@ const enlargedBoundingBoxes = computed(() => {
|
|
|
396
404
|
if (!enlargedEvent.value) return []
|
|
397
405
|
return getBoundingBoxes(enlargedEvent.value)
|
|
398
406
|
})
|
|
407
|
+
const jsonViewerEvent = computed(() => {
|
|
408
|
+
if (!jsonViewerEventId.value) return null
|
|
409
|
+
return events.value.find(e => e.id === jsonViewerEventId.value) || null
|
|
410
|
+
})
|
|
411
|
+
const jsonViewerContent = computed(() => {
|
|
412
|
+
// Use full event if loaded, otherwise fall back to list event
|
|
413
|
+
const eventToShow = jsonViewerFullEvent.value || jsonViewerEvent.value
|
|
414
|
+
if (!eventToShow) return ''
|
|
415
|
+
try {
|
|
416
|
+
return JSON.stringify(eventToShow, null, 2)
|
|
417
|
+
} catch (err) {
|
|
418
|
+
// Safely handle any JSON serialization errors
|
|
419
|
+
return `Error serializing event data: ${String(err)}`
|
|
420
|
+
}
|
|
421
|
+
})
|
|
399
422
|
|
|
400
423
|
// Get start timestamp based on time range
|
|
401
424
|
function getStartTimestamp(range: TimeRange): string {
|
|
@@ -698,6 +721,77 @@ function closeEnlargedImage() {
|
|
|
698
721
|
resetVideo()
|
|
699
722
|
}
|
|
700
723
|
|
|
724
|
+
// Open JSON viewer and fetch full event details
|
|
725
|
+
async function openJsonViewer(eventId: string) {
|
|
726
|
+
jsonViewerEventId.value = eventId
|
|
727
|
+
jsonViewerFullEvent.value = null
|
|
728
|
+
jsonViewerError.value = null
|
|
729
|
+
copySuccess.value = false
|
|
730
|
+
|
|
731
|
+
// Find the event in the list to get its dataSchemas
|
|
732
|
+
const listEvent = events.value.find(e => e.id === eventId)
|
|
733
|
+
if (!listEvent) {
|
|
734
|
+
jsonViewerError.value = 'Event not found in current list'
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Build include array from dataSchemas (prefix with "data.")
|
|
739
|
+
const includes = listEvent.dataSchemas?.map(schema => `data.${schema}`) || []
|
|
740
|
+
|
|
741
|
+
if (includes.length === 0) {
|
|
742
|
+
// No additional data to fetch, use the list event as-is
|
|
743
|
+
return
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Fetch full event details with all data schemas
|
|
747
|
+
jsonViewerLoading.value = true
|
|
748
|
+
const { data, error } = await getEvent(eventId, { include: includes })
|
|
749
|
+
jsonViewerLoading.value = false
|
|
750
|
+
|
|
751
|
+
if (error) {
|
|
752
|
+
jsonViewerError.value = error.message
|
|
753
|
+
return
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (data) {
|
|
757
|
+
jsonViewerFullEvent.value = data
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Close JSON viewer
|
|
762
|
+
function closeJsonViewer() {
|
|
763
|
+
jsonViewerEventId.value = null
|
|
764
|
+
jsonViewerFullEvent.value = null
|
|
765
|
+
jsonViewerError.value = null
|
|
766
|
+
copySuccess.value = false
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Copy JSON to clipboard
|
|
770
|
+
async function copyJsonToClipboard() {
|
|
771
|
+
if (!jsonViewerContent.value) return
|
|
772
|
+
try {
|
|
773
|
+
await navigator.clipboard.writeText(jsonViewerContent.value)
|
|
774
|
+
copySuccess.value = true
|
|
775
|
+
setTimeout(() => {
|
|
776
|
+
copySuccess.value = false
|
|
777
|
+
}, 2000)
|
|
778
|
+
} catch {
|
|
779
|
+
// Fallback for older browsers
|
|
780
|
+
const textarea = document.createElement('textarea')
|
|
781
|
+
textarea.value = jsonViewerContent.value
|
|
782
|
+
textarea.style.position = 'fixed'
|
|
783
|
+
textarea.style.opacity = '0'
|
|
784
|
+
document.body.appendChild(textarea)
|
|
785
|
+
textarea.select()
|
|
786
|
+
document.execCommand('copy')
|
|
787
|
+
document.body.removeChild(textarea)
|
|
788
|
+
copySuccess.value = true
|
|
789
|
+
setTimeout(() => {
|
|
790
|
+
copySuccess.value = false
|
|
791
|
+
}, 2000)
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
701
795
|
// Switch to preview mode
|
|
702
796
|
function showPreview() {
|
|
703
797
|
currentMediaType.value = 'preview'
|
|
@@ -747,8 +841,12 @@ async function showVideoPlayer() {
|
|
|
747
841
|
|
|
748
842
|
// Handle keyboard events for accessibility
|
|
749
843
|
function handleKeydown(event: KeyboardEvent) {
|
|
750
|
-
if (event.key === 'Escape'
|
|
751
|
-
|
|
844
|
+
if (event.key === 'Escape') {
|
|
845
|
+
if (jsonViewerEventId.value) {
|
|
846
|
+
closeJsonViewer()
|
|
847
|
+
} else if (enlargedEventId.value) {
|
|
848
|
+
closeEnlargedImage()
|
|
849
|
+
}
|
|
752
850
|
}
|
|
753
851
|
}
|
|
754
852
|
|
|
@@ -759,6 +857,9 @@ onMounted(() => {
|
|
|
759
857
|
|
|
760
858
|
onUnmounted(() => {
|
|
761
859
|
window.removeEventListener('keydown', handleKeydown)
|
|
860
|
+
// Clean up JSON viewer state to prevent memory leaks
|
|
861
|
+
jsonViewerFullEvent.value = null
|
|
862
|
+
jsonViewerEventId.value = null
|
|
762
863
|
})
|
|
763
864
|
|
|
764
865
|
// Watch for modal open/close
|
|
@@ -784,6 +885,7 @@ watch(() => props.isOpen, async (isOpen) => {
|
|
|
784
885
|
boundingBoxCache.value.clear()
|
|
785
886
|
events.value = []
|
|
786
887
|
enlargedEventId.value = null
|
|
888
|
+
jsonViewerEventId.value = null
|
|
787
889
|
}
|
|
788
890
|
}, { immediate: true })
|
|
789
891
|
|
|
@@ -883,7 +985,15 @@ watch([timeRange, selectedEventTypes], () => {
|
|
|
883
985
|
</div>
|
|
884
986
|
</div>
|
|
885
987
|
<div class="event-info">
|
|
886
|
-
<div class="event-type">
|
|
988
|
+
<div class="event-type-row">
|
|
989
|
+
<span class="event-type">{{ getEventTypeName(event.type) }}</span>
|
|
990
|
+
<button
|
|
991
|
+
class="json-button"
|
|
992
|
+
@click="openJsonViewer(event.id)"
|
|
993
|
+
title="View JSON data"
|
|
994
|
+
data-testid="json-button"
|
|
995
|
+
>{ }</button>
|
|
996
|
+
</div>
|
|
887
997
|
<div class="event-time">{{ formatTimestamp(event.startTimestamp) }}</div>
|
|
888
998
|
<div v-if="getBoundingBoxCount(event) > 0" class="event-detections" data-testid="event-detections">
|
|
889
999
|
{{ getBoundingBoxCount(event) }} detection{{ getBoundingBoxCount(event) !== 1 ? 's' : '' }}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
listEventFieldValues,
|
|
6
6
|
listEventTypes,
|
|
7
7
|
getRecordedImage,
|
|
8
|
+
getEvent,
|
|
8
9
|
type Camera,
|
|
9
10
|
type Event,
|
|
10
11
|
type EventType,
|
|
@@ -87,6 +88,13 @@ const imageLoadingIds = ref<Set<string>>(new Set()) // Track which images are cu
|
|
|
87
88
|
const boundingBoxCache = ref<Map<string, BoundingBox[]>>(new Map()) // Cache bounding boxes per event
|
|
88
89
|
const enlargedEventId = ref<string | null>(null)
|
|
89
90
|
|
|
91
|
+
// JSON viewer state
|
|
92
|
+
const jsonViewerEventId = ref<string | null>(null)
|
|
93
|
+
const jsonViewerFullEvent = ref<Event | null>(null)
|
|
94
|
+
const jsonViewerLoading = ref(false)
|
|
95
|
+
const jsonViewerError = ref<string | null>(null)
|
|
96
|
+
const copySuccess = ref(false)
|
|
97
|
+
|
|
90
98
|
// Lightbox media state
|
|
91
99
|
const showVideo = ref(false)
|
|
92
100
|
const hdImageUrl = ref<string | null>(null)
|
|
@@ -109,6 +117,21 @@ const enlargedBoundingBoxes = computed(() => {
|
|
|
109
117
|
if (!enlargedEvent.value) return []
|
|
110
118
|
return getBoundingBoxes(enlargedEvent.value)
|
|
111
119
|
})
|
|
120
|
+
const jsonViewerEvent = computed(() => {
|
|
121
|
+
if (!jsonViewerEventId.value) return null
|
|
122
|
+
return events.value.find(e => e.id === jsonViewerEventId.value) || null
|
|
123
|
+
})
|
|
124
|
+
const jsonViewerContent = computed(() => {
|
|
125
|
+
// Use full event if loaded, otherwise fall back to list event
|
|
126
|
+
const eventToShow = jsonViewerFullEvent.value || jsonViewerEvent.value
|
|
127
|
+
if (!eventToShow) return ''
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(eventToShow, null, 2)
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Safely handle any JSON serialization errors
|
|
132
|
+
return `Error serializing event data: ${String(err)}`
|
|
133
|
+
}
|
|
134
|
+
})
|
|
112
135
|
|
|
113
136
|
// Get start timestamp based on time range
|
|
114
137
|
function getStartTimestamp(range: TimeRange): string {
|
|
@@ -411,6 +434,77 @@ function closeEnlargedImage() {
|
|
|
411
434
|
resetVideo()
|
|
412
435
|
}
|
|
413
436
|
|
|
437
|
+
// Open JSON viewer and fetch full event details
|
|
438
|
+
async function openJsonViewer(eventId: string) {
|
|
439
|
+
jsonViewerEventId.value = eventId
|
|
440
|
+
jsonViewerFullEvent.value = null
|
|
441
|
+
jsonViewerError.value = null
|
|
442
|
+
copySuccess.value = false
|
|
443
|
+
|
|
444
|
+
// Find the event in the list to get its dataSchemas
|
|
445
|
+
const listEvent = events.value.find(e => e.id === eventId)
|
|
446
|
+
if (!listEvent) {
|
|
447
|
+
jsonViewerError.value = 'Event not found in current list'
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Build include array from dataSchemas (prefix with "data.")
|
|
452
|
+
const includes = listEvent.dataSchemas?.map(schema => `data.${schema}`) || []
|
|
453
|
+
|
|
454
|
+
if (includes.length === 0) {
|
|
455
|
+
// No additional data to fetch, use the list event as-is
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Fetch full event details with all data schemas
|
|
460
|
+
jsonViewerLoading.value = true
|
|
461
|
+
const { data, error } = await getEvent(eventId, { include: includes })
|
|
462
|
+
jsonViewerLoading.value = false
|
|
463
|
+
|
|
464
|
+
if (error) {
|
|
465
|
+
jsonViewerError.value = error.message
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (data) {
|
|
470
|
+
jsonViewerFullEvent.value = data
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Close JSON viewer
|
|
475
|
+
function closeJsonViewer() {
|
|
476
|
+
jsonViewerEventId.value = null
|
|
477
|
+
jsonViewerFullEvent.value = null
|
|
478
|
+
jsonViewerError.value = null
|
|
479
|
+
copySuccess.value = false
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Copy JSON to clipboard
|
|
483
|
+
async function copyJsonToClipboard() {
|
|
484
|
+
if (!jsonViewerContent.value) return
|
|
485
|
+
try {
|
|
486
|
+
await navigator.clipboard.writeText(jsonViewerContent.value)
|
|
487
|
+
copySuccess.value = true
|
|
488
|
+
setTimeout(() => {
|
|
489
|
+
copySuccess.value = false
|
|
490
|
+
}, 2000)
|
|
491
|
+
} catch {
|
|
492
|
+
// Fallback for older browsers
|
|
493
|
+
const textarea = document.createElement('textarea')
|
|
494
|
+
textarea.value = jsonViewerContent.value
|
|
495
|
+
textarea.style.position = 'fixed'
|
|
496
|
+
textarea.style.opacity = '0'
|
|
497
|
+
document.body.appendChild(textarea)
|
|
498
|
+
textarea.select()
|
|
499
|
+
document.execCommand('copy')
|
|
500
|
+
document.body.removeChild(textarea)
|
|
501
|
+
copySuccess.value = true
|
|
502
|
+
setTimeout(() => {
|
|
503
|
+
copySuccess.value = false
|
|
504
|
+
}, 2000)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
414
508
|
// Switch to preview mode
|
|
415
509
|
function showPreview() {
|
|
416
510
|
currentMediaType.value = 'preview'
|
|
@@ -460,8 +554,12 @@ async function showVideoPlayer() {
|
|
|
460
554
|
|
|
461
555
|
// Handle keyboard events for accessibility
|
|
462
556
|
function handleKeydown(event: KeyboardEvent) {
|
|
463
|
-
if (event.key === 'Escape'
|
|
464
|
-
|
|
557
|
+
if (event.key === 'Escape') {
|
|
558
|
+
if (jsonViewerEventId.value) {
|
|
559
|
+
closeJsonViewer()
|
|
560
|
+
} else if (enlargedEventId.value) {
|
|
561
|
+
closeEnlargedImage()
|
|
562
|
+
}
|
|
465
563
|
}
|
|
466
564
|
}
|
|
467
565
|
|
|
@@ -472,6 +570,9 @@ onMounted(() => {
|
|
|
472
570
|
|
|
473
571
|
onUnmounted(() => {
|
|
474
572
|
window.removeEventListener('keydown', handleKeydown)
|
|
573
|
+
// Clean up JSON viewer state to prevent memory leaks
|
|
574
|
+
jsonViewerFullEvent.value = null
|
|
575
|
+
jsonViewerEventId.value = null
|
|
475
576
|
})
|
|
476
577
|
|
|
477
578
|
// Watch for modal open/close
|
|
@@ -497,6 +598,7 @@ watch(() => props.isOpen, async (isOpen) => {
|
|
|
497
598
|
boundingBoxCache.value.clear()
|
|
498
599
|
events.value = []
|
|
499
600
|
enlargedEventId.value = null
|
|
601
|
+
jsonViewerEventId.value = null
|
|
500
602
|
}
|
|
501
603
|
}, { immediate: true })
|
|
502
604
|
|
|
@@ -596,7 +698,15 @@ watch([timeRange, selectedEventTypes], () => {
|
|
|
596
698
|
</div>
|
|
597
699
|
</div>
|
|
598
700
|
<div class="event-info">
|
|
599
|
-
<div class="event-type">
|
|
701
|
+
<div class="event-type-row">
|
|
702
|
+
<span class="event-type">{{ getEventTypeName(event.type) }}</span>
|
|
703
|
+
<button
|
|
704
|
+
class="json-button"
|
|
705
|
+
@click="openJsonViewer(event.id)"
|
|
706
|
+
title="View JSON data"
|
|
707
|
+
data-testid="json-button"
|
|
708
|
+
>{ }</button>
|
|
709
|
+
</div>
|
|
600
710
|
<div class="event-time">{{ formatTimestamp(event.startTimestamp) }}</div>
|
|
601
711
|
<div v-if="getBoundingBoxCount(event) > 0" class="event-detections" data-testid="event-detections">
|
|
602
712
|
{{ getBoundingBoxCount(event) }} detection{{ getBoundingBoxCount(event) !== 1 ? 's' : '' }}
|
|
@@ -769,6 +879,50 @@ watch([timeRange, selectedEventTypes], () => {
|
|
|
769
879
|
</div>
|
|
770
880
|
</div>
|
|
771
881
|
</div>
|
|
882
|
+
|
|
883
|
+
<!-- JSON viewer lightbox -->
|
|
884
|
+
<div
|
|
885
|
+
v-if="jsonViewerEventId"
|
|
886
|
+
class="json-viewer-overlay"
|
|
887
|
+
@click.self="closeJsonViewer"
|
|
888
|
+
data-testid="json-viewer-overlay"
|
|
889
|
+
>
|
|
890
|
+
<div class="json-viewer-content">
|
|
891
|
+
<div class="json-viewer-header">
|
|
892
|
+
<h3>Event JSON Data</h3>
|
|
893
|
+
<div class="json-viewer-actions">
|
|
894
|
+
<button
|
|
895
|
+
class="copy-button"
|
|
896
|
+
:class="{ success: copySuccess }"
|
|
897
|
+
@click="copyJsonToClipboard"
|
|
898
|
+
data-testid="copy-json-button"
|
|
899
|
+
>
|
|
900
|
+
{{ copySuccess ? 'Copied!' : 'Copy' }}
|
|
901
|
+
</button>
|
|
902
|
+
<button
|
|
903
|
+
class="json-viewer-close"
|
|
904
|
+
@click="closeJsonViewer"
|
|
905
|
+
aria-label="Close JSON viewer"
|
|
906
|
+
data-testid="json-viewer-close"
|
|
907
|
+
>×</button>
|
|
908
|
+
</div>
|
|
909
|
+
</div>
|
|
910
|
+
<div class="json-viewer-body">
|
|
911
|
+
<div v-if="jsonViewerLoading" class="json-viewer-loading">
|
|
912
|
+
Loading full event details...
|
|
913
|
+
</div>
|
|
914
|
+
<div v-else-if="jsonViewerError" class="json-viewer-error">
|
|
915
|
+
Error: {{ jsonViewerError }}
|
|
916
|
+
</div>
|
|
917
|
+
<pre v-else><code>{{ jsonViewerContent }}</code></pre>
|
|
918
|
+
</div>
|
|
919
|
+
<div v-if="jsonViewerEvent" class="json-viewer-footer">
|
|
920
|
+
<span class="json-event-type">{{ getEventTypeName(jsonViewerEvent.type) }}</span>
|
|
921
|
+
<span class="json-event-time">{{ formatTimestamp(jsonViewerEvent.startTimestamp) }}</span>
|
|
922
|
+
<span v-if="jsonViewerFullEvent" class="json-full-indicator">Full data loaded</span>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
772
926
|
</div>
|
|
773
927
|
</div>
|
|
774
928
|
</template>
|
|
@@ -1227,4 +1381,175 @@ watch([timeRange, selectedEventTypes], () => {
|
|
|
1227
1381
|
opacity: 0.8;
|
|
1228
1382
|
margin-left: 4px;
|
|
1229
1383
|
}
|
|
1384
|
+
|
|
1385
|
+
/* Event type row with JSON button */
|
|
1386
|
+
.event-type-row {
|
|
1387
|
+
display: flex;
|
|
1388
|
+
align-items: center;
|
|
1389
|
+
justify-content: space-between;
|
|
1390
|
+
gap: 8px;
|
|
1391
|
+
margin-bottom: 5px;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.event-type-row .event-type {
|
|
1395
|
+
margin-bottom: 0;
|
|
1396
|
+
flex: 1;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/* JSON button on event cards */
|
|
1400
|
+
.json-button {
|
|
1401
|
+
padding: 2px 5px;
|
|
1402
|
+
background: #f0f0f0;
|
|
1403
|
+
border: 1px solid #ccc;
|
|
1404
|
+
border-radius: 3px;
|
|
1405
|
+
font-size: 0.65rem;
|
|
1406
|
+
font-family: monospace;
|
|
1407
|
+
color: #666;
|
|
1408
|
+
cursor: pointer;
|
|
1409
|
+
transition: background 0.2s, border-color 0.2s;
|
|
1410
|
+
line-height: 1;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
.json-button:hover {
|
|
1414
|
+
background: #e0e0e0;
|
|
1415
|
+
border-color: #999;
|
|
1416
|
+
color: #333;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/* JSON viewer lightbox */
|
|
1420
|
+
.json-viewer-overlay {
|
|
1421
|
+
position: fixed;
|
|
1422
|
+
top: 0;
|
|
1423
|
+
left: 0;
|
|
1424
|
+
right: 0;
|
|
1425
|
+
bottom: 0;
|
|
1426
|
+
background: rgba(0, 0, 0, 0.85);
|
|
1427
|
+
display: flex;
|
|
1428
|
+
align-items: center;
|
|
1429
|
+
justify-content: center;
|
|
1430
|
+
z-index: 2100;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
.json-viewer-content {
|
|
1434
|
+
background: #1e1e1e;
|
|
1435
|
+
border-radius: 8px;
|
|
1436
|
+
width: 90%;
|
|
1437
|
+
max-width: 700px;
|
|
1438
|
+
max-height: 85vh;
|
|
1439
|
+
display: flex;
|
|
1440
|
+
flex-direction: column;
|
|
1441
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
.json-viewer-header {
|
|
1445
|
+
display: flex;
|
|
1446
|
+
justify-content: space-between;
|
|
1447
|
+
align-items: center;
|
|
1448
|
+
padding: 15px 20px;
|
|
1449
|
+
border-bottom: 1px solid #333;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
.json-viewer-header h3 {
|
|
1453
|
+
margin: 0;
|
|
1454
|
+
color: #fff;
|
|
1455
|
+
font-size: 1rem;
|
|
1456
|
+
font-weight: 500;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
.json-viewer-actions {
|
|
1460
|
+
display: flex;
|
|
1461
|
+
gap: 10px;
|
|
1462
|
+
align-items: center;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
.copy-button {
|
|
1466
|
+
padding: 6px 14px;
|
|
1467
|
+
background: #42b883;
|
|
1468
|
+
color: white;
|
|
1469
|
+
border: none;
|
|
1470
|
+
border-radius: 4px;
|
|
1471
|
+
font-size: 0.85rem;
|
|
1472
|
+
font-weight: 500;
|
|
1473
|
+
cursor: pointer;
|
|
1474
|
+
transition: background 0.2s;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.copy-button:hover {
|
|
1478
|
+
background: #3aa876;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.copy-button.success {
|
|
1482
|
+
background: #2d8659;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.json-viewer-close {
|
|
1486
|
+
background: none;
|
|
1487
|
+
border: none;
|
|
1488
|
+
color: #aaa;
|
|
1489
|
+
font-size: 1.5rem;
|
|
1490
|
+
cursor: pointer;
|
|
1491
|
+
padding: 0 5px;
|
|
1492
|
+
line-height: 1;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.json-viewer-close:hover {
|
|
1496
|
+
color: #fff;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.json-viewer-body {
|
|
1500
|
+
flex: 1;
|
|
1501
|
+
overflow: auto;
|
|
1502
|
+
padding: 15px 20px;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
.json-viewer-body pre {
|
|
1506
|
+
margin: 0;
|
|
1507
|
+
white-space: pre-wrap;
|
|
1508
|
+
word-break: break-word;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
.json-viewer-body code {
|
|
1512
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
|
|
1513
|
+
font-size: 0.8rem;
|
|
1514
|
+
line-height: 1.5;
|
|
1515
|
+
color: #d4d4d4;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
.json-viewer-footer {
|
|
1519
|
+
padding: 12px 20px;
|
|
1520
|
+
border-top: 1px solid #333;
|
|
1521
|
+
display: flex;
|
|
1522
|
+
justify-content: space-between;
|
|
1523
|
+
align-items: center;
|
|
1524
|
+
color: #888;
|
|
1525
|
+
font-size: 0.8rem;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.json-event-type {
|
|
1529
|
+
font-weight: 500;
|
|
1530
|
+
color: #aaa;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.json-event-time {
|
|
1534
|
+
color: #666;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.json-full-indicator {
|
|
1538
|
+
color: #42b883;
|
|
1539
|
+
font-size: 0.75rem;
|
|
1540
|
+
margin-left: auto;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
.json-viewer-loading {
|
|
1544
|
+
color: #aaa;
|
|
1545
|
+
font-size: 0.9rem;
|
|
1546
|
+
text-align: center;
|
|
1547
|
+
padding: 40px 20px;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
.json-viewer-error {
|
|
1551
|
+
color: #ff6b6b;
|
|
1552
|
+
font-size: 0.85rem;
|
|
1553
|
+
margin-bottom: 15px;
|
|
1554
|
+
}
|
|
1230
1555
|
</style>
|