een-api-toolkit 0.3.20 → 0.3.22
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/docs-accuracy-reviewer.md +146 -0
- package/.claude/agents/een-auth-agent.md +168 -0
- package/.claude/agents/een-devices-agent.md +294 -0
- package/.claude/agents/een-events-agent.md +375 -0
- package/.claude/agents/een-media-agent.md +256 -0
- package/.claude/agents/een-setup-agent.md +126 -0
- package/.claude/agents/een-users-agent.md +239 -0
- package/.claude/agents/test-runner.md +144 -0
- package/CHANGELOG.md +138 -30
- package/docs/AI-CONTEXT.md +169 -1700
- package/docs/ai-reference/AI-AUTH.md +288 -0
- package/docs/ai-reference/AI-DEVICES.md +569 -0
- package/docs/ai-reference/AI-EVENTS.md +1745 -0
- package/docs/ai-reference/AI-MEDIA.md +974 -0
- package/docs/ai-reference/AI-SETUP.md +267 -0
- package/docs/ai-reference/AI-USERS.md +255 -0
- package/examples/vue-event-subscriptions/package-lock.json +8 -1
- package/examples/vue-event-subscriptions/package.json +1 -0
- package/examples/vue-event-subscriptions/src/App.vue +1 -41
- package/examples/vue-event-subscriptions/src/composables/useHlsPlayer.ts +272 -0
- package/examples/vue-event-subscriptions/src/main.ts +3 -3
- package/examples/vue-event-subscriptions/src/stores/connection.ts +101 -0
- package/examples/vue-event-subscriptions/src/stores/mediaSession.ts +79 -0
- package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +349 -88
- package/examples/vue-event-subscriptions/src/views/Logout.vue +6 -0
- package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +0 -13
- package/examples/vue-events/package-lock.json +8 -1
- package/examples/vue-events/package.json +1 -0
- package/examples/vue-events/src/components/EventsModal.vue +269 -47
- package/examples/vue-events/src/composables/useHlsPlayer.ts +272 -0
- package/examples/vue-events/src/stores/mediaSession.ts +79 -0
- package/package.json +10 -2
- package/scripts/setup-agents.ts +116 -0
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, onMounted,
|
|
2
|
+
import { ref, computed, onMounted, watch } from 'vue'
|
|
3
3
|
import { useRoute } from 'vue-router'
|
|
4
4
|
import {
|
|
5
5
|
listEventSubscriptions,
|
|
6
|
-
|
|
6
|
+
getRecordedImage,
|
|
7
7
|
type EventSubscription,
|
|
8
|
-
type
|
|
9
|
-
type SSEConnectionStatus,
|
|
10
|
-
type SSEEvent,
|
|
11
|
-
type EenError
|
|
8
|
+
type SSEEvent
|
|
12
9
|
} from 'een-api-toolkit'
|
|
10
|
+
import { useConnectionStore } from '../stores/connection'
|
|
11
|
+
import { useHlsPlayer } from '../composables/useHlsPlayer'
|
|
13
12
|
|
|
14
13
|
const route = useRoute()
|
|
14
|
+
const connectionStore = useConnectionStore()
|
|
15
|
+
|
|
16
|
+
// Initialize HLS player composable
|
|
17
|
+
const hlsPlayer = useHlsPlayer()
|
|
18
|
+
const { videoUrl, videoError, loadingVideo, loadVideo, resetVideo } = hlsPlayer
|
|
15
19
|
|
|
16
20
|
// Subscriptions state
|
|
17
21
|
const subscriptions = ref<EventSubscription[]>([])
|
|
18
22
|
const selectedSubscriptionId = ref<string | null>(null)
|
|
19
23
|
const loadingSubscriptions = ref(false)
|
|
20
24
|
|
|
21
|
-
// Connection state
|
|
22
|
-
const connection = ref<SSEConnection | null>(null)
|
|
23
|
-
const connectionStatus = ref<SSEConnectionStatus>('disconnected')
|
|
24
|
-
const connectionError = ref<EenError | null>(null)
|
|
25
|
-
|
|
26
|
-
// Events state
|
|
27
|
-
const events = ref<SSEEvent[]>([])
|
|
28
|
-
const maxEvents = 100
|
|
29
|
-
|
|
30
25
|
// Modal state
|
|
31
26
|
const selectedEvent = ref<SSEEvent | null>(null)
|
|
32
27
|
const showModal = ref(false)
|
|
33
28
|
|
|
29
|
+
// Lightbox state
|
|
30
|
+
const showLightbox = ref(false)
|
|
31
|
+
const lightboxImageUrl = ref<string | null>(null)
|
|
32
|
+
const loadingImage = ref(false)
|
|
33
|
+
const imageError = ref<string | null>(null)
|
|
34
|
+
const showVideo = ref(false)
|
|
35
|
+
|
|
34
36
|
const selectedSubscription = computed(() => {
|
|
35
37
|
return subscriptions.value.find(s => s.id === selectedSubscriptionId.value)
|
|
36
38
|
})
|
|
@@ -41,8 +43,13 @@ const canConnect = computed(() => {
|
|
|
41
43
|
return !!selectedSubscription.value.deliveryConfig.sseUrl
|
|
42
44
|
})
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
+
// Use store computed values
|
|
47
|
+
const isConnected = computed(() => connectionStore.isConnected)
|
|
48
|
+
const isConnecting = computed(() => connectionStore.isConnecting)
|
|
49
|
+
const connectionStatus = computed(() => connectionStore.connectionStatus)
|
|
50
|
+
const connectionError = computed(() => connectionStore.connectionError)
|
|
51
|
+
const events = computed(() => connectionStore.events)
|
|
52
|
+
const maxEvents = connectionStore.maxEvents
|
|
46
53
|
|
|
47
54
|
async function loadSubscriptions() {
|
|
48
55
|
loadingSubscriptions.value = true
|
|
@@ -58,51 +65,52 @@ async function loadSubscriptions() {
|
|
|
58
65
|
loadingSubscriptions.value = false
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
function connect() {
|
|
68
|
+
async function connect() {
|
|
62
69
|
if (!canConnect.value || !selectedSubscription.value) return
|
|
63
70
|
|
|
64
|
-
const
|
|
65
|
-
? selectedSubscription.value.deliveryConfig.sseUrl
|
|
66
|
-
: undefined
|
|
67
|
-
|
|
68
|
-
if (!sseUrl) return
|
|
69
|
-
|
|
70
|
-
connectionError.value = null
|
|
71
|
-
events.value = []
|
|
72
|
-
|
|
73
|
-
const result = connectToEventSubscription(sseUrl, {
|
|
74
|
-
onEvent: (event) => {
|
|
75
|
-
// Add new event at the beginning, limit to maxEvents
|
|
76
|
-
// Using unshift + pop is more efficient than spread operator for large arrays
|
|
77
|
-
events.value.unshift(event)
|
|
78
|
-
if (events.value.length > maxEvents) {
|
|
79
|
-
events.value.pop()
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
onError: (error) => {
|
|
83
|
-
connectionError.value = { code: 'NETWORK_ERROR', message: error.message }
|
|
84
|
-
},
|
|
85
|
-
onStatusChange: (status) => {
|
|
86
|
-
connectionStatus.value = status
|
|
87
|
-
}
|
|
88
|
-
})
|
|
71
|
+
const subscriptionId = selectedSubscription.value.id
|
|
89
72
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
// Reload all subscriptions to get fresh SSE URLs
|
|
74
|
+
// SSE URLs become invalid after disconnecting
|
|
75
|
+
await loadSubscriptions()
|
|
76
|
+
|
|
77
|
+
// Find the subscription in the refreshed list
|
|
78
|
+
const freshSubscription = subscriptions.value.find(s => s.id === subscriptionId)
|
|
79
|
+
if (!freshSubscription) {
|
|
80
|
+
connectionStore.setConnectionError({ code: 'NOT_FOUND', message: 'Subscription no longer exists' })
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (freshSubscription.deliveryConfig.type !== 'serverSentEvents.v1') {
|
|
85
|
+
connectionStore.setConnectionError({ code: 'VALIDATION_ERROR', message: 'Subscription is not an SSE type' })
|
|
86
|
+
return
|
|
94
87
|
}
|
|
88
|
+
|
|
89
|
+
const sseUrl = freshSubscription.deliveryConfig.sseUrl
|
|
90
|
+
if (!sseUrl) {
|
|
91
|
+
connectionStore.setConnectionError({ code: 'VALIDATION_ERROR', message: 'No SSE URL available' })
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
connectionStore.connect(subscriptionId, sseUrl)
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function disconnect() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// Get the subscription ID before disconnecting
|
|
100
|
+
const disconnectedId = connectionStore.connectedSubscriptionId
|
|
101
|
+
|
|
102
|
+
connectionStore.disconnect()
|
|
103
|
+
|
|
104
|
+
// Remove the subscription from the list since SSE URLs are single-use
|
|
105
|
+
// and cannot be reconnected
|
|
106
|
+
if (disconnectedId) {
|
|
107
|
+
subscriptions.value = subscriptions.value.filter(s => s.id !== disconnectedId)
|
|
108
|
+
selectedSubscriptionId.value = null
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
function clearEvents() {
|
|
105
|
-
|
|
113
|
+
connectionStore.clearEvents()
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
function formatTimestamp(timestamp: string): string {
|
|
@@ -138,8 +146,12 @@ function closeModal() {
|
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
function handleKeyDown(e: KeyboardEvent) {
|
|
141
|
-
if (e.key === 'Escape'
|
|
142
|
-
|
|
149
|
+
if (e.key === 'Escape') {
|
|
150
|
+
if (showLightbox.value) {
|
|
151
|
+
closeLightbox()
|
|
152
|
+
} else if (showModal.value) {
|
|
153
|
+
closeModal()
|
|
154
|
+
}
|
|
143
155
|
}
|
|
144
156
|
}
|
|
145
157
|
|
|
@@ -149,28 +161,109 @@ function handleModalBackdropClick(e: MouseEvent) {
|
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
|
|
164
|
+
// Extract the image URL from event data
|
|
165
|
+
function getEventImageUrl(event: SSEEvent): string | null {
|
|
166
|
+
if (!event.data) return null
|
|
167
|
+
const fullFrameData = event.data.find(d => d.type === 'een.fullFrameImageUrl.v1')
|
|
168
|
+
if (fullFrameData && typeof fullFrameData.httpsUrl === 'string') {
|
|
169
|
+
return fullFrameData.httpsUrl
|
|
170
|
+
}
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check if event has media URLs
|
|
175
|
+
function hasMediaUrls(event: SSEEvent): boolean {
|
|
176
|
+
return getEventImageUrl(event) !== null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Handle image click (preview or HD)
|
|
180
|
+
// Uses event.actorId and event.startTimestamp directly instead of parsing URL
|
|
181
|
+
async function handleImageClick(quality: 'preview' | 'main' = 'preview') {
|
|
182
|
+
if (!selectedEvent.value) return
|
|
183
|
+
|
|
184
|
+
// Use event properties directly for reliability
|
|
185
|
+
const deviceId = selectedEvent.value.actorId
|
|
186
|
+
const timestamp = selectedEvent.value.startTimestamp
|
|
187
|
+
|
|
188
|
+
if (!deviceId || !timestamp) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
loadingImage.value = true
|
|
193
|
+
imageError.value = null
|
|
194
|
+
lightboxImageUrl.value = null
|
|
195
|
+
showLightbox.value = true
|
|
196
|
+
showVideo.value = false
|
|
197
|
+
|
|
198
|
+
// Use the toolkit's getRecordedImage function
|
|
199
|
+
const result = await getRecordedImage({
|
|
200
|
+
deviceId,
|
|
201
|
+
type: quality,
|
|
202
|
+
timestamp__gte: timestamp
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
if (result.error) {
|
|
206
|
+
imageError.value = result.error.message
|
|
207
|
+
} else if (result.data?.imageData) {
|
|
208
|
+
lightboxImageUrl.value = result.data.imageData
|
|
209
|
+
} else {
|
|
210
|
+
imageError.value = 'No image data returned'
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
loadingImage.value = false
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Handle video click
|
|
217
|
+
// Uses event.actorId and event.startTimestamp directly instead of parsing URL
|
|
218
|
+
async function handleVideoClick() {
|
|
219
|
+
if (!selectedEvent.value) return
|
|
220
|
+
|
|
221
|
+
// Use event properties directly for reliability
|
|
222
|
+
const deviceId = selectedEvent.value.actorId
|
|
223
|
+
const timestamp = selectedEvent.value.startTimestamp
|
|
224
|
+
|
|
225
|
+
if (!deviceId || !timestamp) {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
showVideo.value = true
|
|
230
|
+
showLightbox.value = true
|
|
231
|
+
|
|
232
|
+
// Use the composable to load and play video
|
|
233
|
+
await loadVideo(deviceId, timestamp)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Close lightbox
|
|
237
|
+
function closeLightbox() {
|
|
238
|
+
showLightbox.value = false
|
|
239
|
+
lightboxImageUrl.value = null
|
|
240
|
+
imageError.value = null
|
|
241
|
+
// Cleanup video if it was playing
|
|
242
|
+
resetVideo()
|
|
243
|
+
showVideo.value = false
|
|
244
|
+
}
|
|
245
|
+
|
|
152
246
|
// Load subscriptions on mount
|
|
153
247
|
onMounted(async () => {
|
|
154
248
|
await loadSubscriptions()
|
|
155
249
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
250
|
+
// If already connected, set the selected subscription to match
|
|
251
|
+
if (connectionStore.connectedSubscriptionId) {
|
|
252
|
+
selectedSubscriptionId.value = connectionStore.connectedSubscriptionId
|
|
253
|
+
} else {
|
|
254
|
+
// Check for subscriptionId in query params
|
|
255
|
+
const queryId = route.query.subscriptionId as string | undefined
|
|
256
|
+
if (queryId) {
|
|
257
|
+
selectedSubscriptionId.value = queryId
|
|
258
|
+
}
|
|
160
259
|
}
|
|
161
260
|
})
|
|
162
261
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// Auto-disconnect when changing subscription
|
|
169
|
-
watch(selectedSubscriptionId, () => {
|
|
170
|
-
if (connection.value) {
|
|
262
|
+
// Auto-disconnect when changing subscription (but not if selecting the already connected one)
|
|
263
|
+
watch(selectedSubscriptionId, (newId) => {
|
|
264
|
+
if (newId && newId !== connectionStore.connectedSubscriptionId && connectionStore.isConnected) {
|
|
171
265
|
disconnect()
|
|
172
266
|
}
|
|
173
|
-
events.value = []
|
|
174
267
|
})
|
|
175
268
|
</script>
|
|
176
269
|
|
|
@@ -283,6 +376,12 @@ watch(selectedSubscriptionId, () => {
|
|
|
283
376
|
<li>Maximum {{ maxEvents }} events are displayed (oldest removed first)</li>
|
|
284
377
|
<li>Click on an event card to view detailed information</li>
|
|
285
378
|
</ul>
|
|
379
|
+
<h4>Important</h4>
|
|
380
|
+
<ul class="warning-list">
|
|
381
|
+
<li>SSE URLs are single-use. Once disconnected, the subscription cannot be reconnected.</li>
|
|
382
|
+
<li>To receive events again after disconnecting, create a new subscription.</li>
|
|
383
|
+
<li>Subscriptions have a 15-minute TTL and expire if not connected.</li>
|
|
384
|
+
</ul>
|
|
286
385
|
</div>
|
|
287
386
|
|
|
288
387
|
<!-- Event Details Modal -->
|
|
@@ -297,7 +396,30 @@ watch(selectedSubscriptionId, () => {
|
|
|
297
396
|
<div class="modal-content" role="document">
|
|
298
397
|
<div class="modal-header">
|
|
299
398
|
<h3 id="modal-title">{{ formatEventType(selectedEvent.type) }}</h3>
|
|
300
|
-
<
|
|
399
|
+
<div class="modal-header-buttons">
|
|
400
|
+
<button
|
|
401
|
+
v-if="hasMediaUrls(selectedEvent)"
|
|
402
|
+
class="image-button"
|
|
403
|
+
@click="handleImageClick('preview')"
|
|
404
|
+
>
|
|
405
|
+
Preview
|
|
406
|
+
</button>
|
|
407
|
+
<button
|
|
408
|
+
v-if="hasMediaUrls(selectedEvent)"
|
|
409
|
+
class="image-button image-button-hd"
|
|
410
|
+
@click="handleImageClick('main')"
|
|
411
|
+
>
|
|
412
|
+
HD Image
|
|
413
|
+
</button>
|
|
414
|
+
<button
|
|
415
|
+
v-if="hasMediaUrls(selectedEvent)"
|
|
416
|
+
class="image-button image-button-video"
|
|
417
|
+
@click="handleVideoClick"
|
|
418
|
+
>
|
|
419
|
+
Video
|
|
420
|
+
</button>
|
|
421
|
+
<button class="close-button" @click="closeModal" aria-label="Close modal">×</button>
|
|
422
|
+
</div>
|
|
301
423
|
</div>
|
|
302
424
|
<div class="modal-body">
|
|
303
425
|
<div class="modal-section">
|
|
@@ -339,6 +461,39 @@ watch(selectedSubscriptionId, () => {
|
|
|
339
461
|
</div>
|
|
340
462
|
</div>
|
|
341
463
|
</div>
|
|
464
|
+
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<!-- Image/Video Lightbox -->
|
|
468
|
+
<div v-if="showLightbox" class="lightbox-overlay" @click.self="closeLightbox">
|
|
469
|
+
<div class="lightbox-content">
|
|
470
|
+
<button class="lightbox-close" @click="closeLightbox">×</button>
|
|
471
|
+
<!-- Video mode -->
|
|
472
|
+
<template v-if="showVideo">
|
|
473
|
+
<div v-if="loadingVideo" class="lightbox-loading">Loading video...</div>
|
|
474
|
+
<div v-else-if="videoError" class="lightbox-error">{{ videoError }}</div>
|
|
475
|
+
<video
|
|
476
|
+
v-else-if="videoUrl"
|
|
477
|
+
:ref="(el) => hlsPlayer.videoRef.value = el as HTMLVideoElement | null"
|
|
478
|
+
class="lightbox-video"
|
|
479
|
+
controls
|
|
480
|
+
autoplay
|
|
481
|
+
muted
|
|
482
|
+
playsinline
|
|
483
|
+
/>
|
|
484
|
+
</template>
|
|
485
|
+
<!-- Image mode -->
|
|
486
|
+
<template v-else>
|
|
487
|
+
<div v-if="loadingImage" class="lightbox-loading">Loading image...</div>
|
|
488
|
+
<div v-else-if="imageError" class="lightbox-error">{{ imageError }}</div>
|
|
489
|
+
<img
|
|
490
|
+
v-else-if="lightboxImageUrl"
|
|
491
|
+
:src="lightboxImageUrl"
|
|
492
|
+
alt="Event image"
|
|
493
|
+
class="lightbox-image"
|
|
494
|
+
/>
|
|
495
|
+
</template>
|
|
496
|
+
</div>
|
|
342
497
|
</div>
|
|
343
498
|
</div>
|
|
344
499
|
</template>
|
|
@@ -498,8 +653,8 @@ h2 {
|
|
|
498
653
|
position: fixed;
|
|
499
654
|
top: 0;
|
|
500
655
|
left: 0;
|
|
501
|
-
|
|
502
|
-
|
|
656
|
+
right: 0;
|
|
657
|
+
bottom: 0;
|
|
503
658
|
background: rgba(0, 0, 0, 0.5);
|
|
504
659
|
display: flex;
|
|
505
660
|
align-items: center;
|
|
@@ -509,49 +664,77 @@ h2 {
|
|
|
509
664
|
|
|
510
665
|
.modal-content {
|
|
511
666
|
background: white;
|
|
512
|
-
border-radius:
|
|
513
|
-
width:
|
|
514
|
-
max-width: 600px;
|
|
667
|
+
border-radius: 8px;
|
|
668
|
+
width: 80%;
|
|
515
669
|
max-height: 80vh;
|
|
516
670
|
overflow: hidden;
|
|
517
671
|
display: flex;
|
|
518
672
|
flex-direction: column;
|
|
519
|
-
box-shadow: 0 20px
|
|
673
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
520
674
|
}
|
|
521
675
|
|
|
522
676
|
.modal-header {
|
|
523
677
|
display: flex;
|
|
524
678
|
justify-content: space-between;
|
|
525
679
|
align-items: center;
|
|
526
|
-
padding: 20px;
|
|
527
|
-
border-bottom: 1px solid #
|
|
528
|
-
background: #f8f9fa;
|
|
680
|
+
padding: 16px 20px;
|
|
681
|
+
border-bottom: 1px solid #eee;
|
|
529
682
|
}
|
|
530
683
|
|
|
531
684
|
.modal-header h3 {
|
|
532
685
|
margin: 0;
|
|
533
|
-
|
|
686
|
+
font-size: 1.1rem;
|
|
687
|
+
color: #333;
|
|
534
688
|
}
|
|
535
689
|
|
|
536
|
-
.modal-
|
|
690
|
+
.modal-header-buttons {
|
|
691
|
+
display: flex;
|
|
692
|
+
align-items: center;
|
|
693
|
+
gap: 10px;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.image-button {
|
|
697
|
+
padding: 6px 14px;
|
|
698
|
+
background: #42b883;
|
|
699
|
+
color: white;
|
|
700
|
+
border: none;
|
|
701
|
+
border-radius: 4px;
|
|
702
|
+
cursor: pointer;
|
|
703
|
+
font-size: 0.85rem;
|
|
704
|
+
font-weight: 500;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.image-button:hover {
|
|
708
|
+
background: #3aa876;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.image-button-hd {
|
|
712
|
+
background: #3b82f6;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.image-button-hd:hover {
|
|
716
|
+
background: #2563eb;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.image-button-video {
|
|
720
|
+
background: #9b59b6;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.image-button-video:hover {
|
|
724
|
+
background: #8e44ad;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.close-button {
|
|
537
728
|
background: none;
|
|
538
729
|
border: none;
|
|
539
|
-
font-size:
|
|
730
|
+
font-size: 1.5rem;
|
|
540
731
|
cursor: pointer;
|
|
541
732
|
color: #666;
|
|
542
733
|
padding: 0;
|
|
543
734
|
line-height: 1;
|
|
544
|
-
width: 36px;
|
|
545
|
-
height: 36px;
|
|
546
|
-
display: flex;
|
|
547
|
-
align-items: center;
|
|
548
|
-
justify-content: center;
|
|
549
|
-
border-radius: 50%;
|
|
550
|
-
transition: background-color 0.15s ease;
|
|
551
735
|
}
|
|
552
736
|
|
|
553
|
-
.
|
|
554
|
-
background: #e2e3e5;
|
|
737
|
+
.close-button:hover {
|
|
555
738
|
color: #333;
|
|
556
739
|
}
|
|
557
740
|
|
|
@@ -637,4 +820,82 @@ h2 {
|
|
|
637
820
|
.help-section li {
|
|
638
821
|
margin-bottom: 5px;
|
|
639
822
|
}
|
|
823
|
+
|
|
824
|
+
.help-section .warning-list {
|
|
825
|
+
color: #856404;
|
|
826
|
+
background: #fff3cd;
|
|
827
|
+
padding: 10px 10px 10px 30px;
|
|
828
|
+
border-radius: 4px;
|
|
829
|
+
margin-top: 10px;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.help-section .warning-list li {
|
|
833
|
+
margin-bottom: 4px;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/* Lightbox styles */
|
|
837
|
+
.lightbox-overlay {
|
|
838
|
+
position: fixed;
|
|
839
|
+
top: 0;
|
|
840
|
+
left: 0;
|
|
841
|
+
right: 0;
|
|
842
|
+
bottom: 0;
|
|
843
|
+
background: rgba(0, 0, 0, 0.9);
|
|
844
|
+
display: flex;
|
|
845
|
+
align-items: center;
|
|
846
|
+
justify-content: center;
|
|
847
|
+
z-index: 1100;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.lightbox-content {
|
|
851
|
+
position: relative;
|
|
852
|
+
max-width: 90vw;
|
|
853
|
+
max-height: 90vh;
|
|
854
|
+
display: flex;
|
|
855
|
+
align-items: center;
|
|
856
|
+
justify-content: center;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.lightbox-close {
|
|
860
|
+
position: absolute;
|
|
861
|
+
top: -40px;
|
|
862
|
+
right: 0;
|
|
863
|
+
background: none;
|
|
864
|
+
border: none;
|
|
865
|
+
color: white;
|
|
866
|
+
font-size: 2rem;
|
|
867
|
+
cursor: pointer;
|
|
868
|
+
padding: 5px 10px;
|
|
869
|
+
line-height: 1;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.lightbox-close:hover {
|
|
873
|
+
color: #ccc;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.lightbox-image {
|
|
877
|
+
max-width: 90vw;
|
|
878
|
+
max-height: 90vh;
|
|
879
|
+
object-fit: contain;
|
|
880
|
+
border-radius: 4px;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.lightbox-video {
|
|
884
|
+
max-width: 90vw;
|
|
885
|
+
max-height: 90vh;
|
|
886
|
+
width: 100%;
|
|
887
|
+
background: #000;
|
|
888
|
+
border-radius: 4px;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.lightbox-loading,
|
|
892
|
+
.lightbox-error {
|
|
893
|
+
color: white;
|
|
894
|
+
font-size: 1.1rem;
|
|
895
|
+
padding: 40px;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.lightbox-error {
|
|
899
|
+
color: #ff6b6b;
|
|
900
|
+
}
|
|
640
901
|
</style>
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
import { onMounted, ref } from 'vue'
|
|
3
3
|
import { useRouter } from 'vue-router'
|
|
4
4
|
import { revokeToken } from 'een-api-toolkit'
|
|
5
|
+
import { useConnectionStore } from '../stores/connection'
|
|
5
6
|
|
|
6
7
|
const router = useRouter()
|
|
8
|
+
const connectionStore = useConnectionStore()
|
|
7
9
|
const processing = ref(true)
|
|
8
10
|
const error = ref<string | null>(null)
|
|
9
11
|
|
|
10
12
|
onMounted(async () => {
|
|
13
|
+
// Close any active SSE connection and clear events
|
|
14
|
+
connectionStore.disconnect()
|
|
15
|
+
connectionStore.clearEvents()
|
|
16
|
+
|
|
11
17
|
const result = await revokeToken()
|
|
12
18
|
|
|
13
19
|
if (result.error) {
|
|
@@ -172,13 +172,6 @@ function getDeliveryType(sub: EventSubscription): string {
|
|
|
172
172
|
return sub.deliveryConfig.type
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
function getSseUrl(sub: EventSubscription): string | undefined {
|
|
176
|
-
if (sub.deliveryConfig.type === 'serverSentEvents.v1') {
|
|
177
|
-
return sub.deliveryConfig.sseUrl
|
|
178
|
-
}
|
|
179
|
-
return undefined
|
|
180
|
-
}
|
|
181
|
-
|
|
182
175
|
onMounted(async () => {
|
|
183
176
|
await Promise.all([
|
|
184
177
|
fetchSubscriptions(),
|
|
@@ -267,12 +260,6 @@ onMounted(async () => {
|
|
|
267
260
|
<td>{{ sub.subscriptionConfig?.lifeCycle || '-' }}</td>
|
|
268
261
|
<td>{{ sub.subscriptionConfig?.timeToLiveSeconds ? `${sub.subscriptionConfig.timeToLiveSeconds}s` : '-' }}</td>
|
|
269
262
|
<td class="actions">
|
|
270
|
-
<router-link
|
|
271
|
-
v-if="getSseUrl(sub)"
|
|
272
|
-
:to="{ path: '/live', query: { subscriptionId: sub.id } }"
|
|
273
|
-
>
|
|
274
|
-
<button class="secondary small">Listen</button>
|
|
275
|
-
</router-link>
|
|
276
263
|
<button
|
|
277
264
|
class="danger small"
|
|
278
265
|
@click="handleDelete(sub.id)"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"version": "0.0.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"een-api-toolkit": "file:../..",
|
|
12
|
+
"hls.js": "^1.6.15",
|
|
12
13
|
"pinia": "^3.0.4",
|
|
13
14
|
"vue": "^3.4.0",
|
|
14
15
|
"vue-router": "^4.2.0"
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
}
|
|
24
25
|
},
|
|
25
26
|
"../..": {
|
|
26
|
-
"version": "0.3.
|
|
27
|
+
"version": "0.3.20",
|
|
27
28
|
"license": "MIT",
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@eslint/js": "^9.39.2",
|
|
@@ -1276,6 +1277,12 @@
|
|
|
1276
1277
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
1277
1278
|
}
|
|
1278
1279
|
},
|
|
1280
|
+
"node_modules/hls.js": {
|
|
1281
|
+
"version": "1.6.15",
|
|
1282
|
+
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",
|
|
1283
|
+
"integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==",
|
|
1284
|
+
"license": "Apache-2.0"
|
|
1285
|
+
},
|
|
1279
1286
|
"node_modules/hookable": {
|
|
1280
1287
|
"version": "5.5.3",
|
|
1281
1288
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|