een-api-toolkit 0.3.10 → 0.3.11

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/CHANGELOG.md CHANGED
@@ -2,24 +2,144 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [0.3.10] - 2026-01-09
5
+ ## [0.3.11] - 2026-01-12
6
6
 
7
7
  ### Release Summary
8
8
 
9
- No PR descriptions available for this release.
9
+ #### PR #52: Release v0.3.10: Vue-media improvements and agent definitions
10
+ ## Summary
11
+
12
+ This release includes improvements to the vue-media example and adds Claude Code agent definitions.
13
+
14
+ ### Changes
15
+
16
+ - **feat(vue-media)**: Add seconds precision and Now button to recorded images
17
+ - Add seconds precision to datetime picker (`step="1"`)
18
+ - Add "Now" button to reset datetime picker to current time
19
+ - Update page titles to indicate "(Preview)" image quality
20
+ - Update README documentation with current API examples
21
+
22
+ - **chore**: Add Claude Code agent definitions
23
+ - `docs-accuracy-reviewer`: Verifies documentation accuracy
24
+ - `test-runner`: Runs unit and E2E test suites
25
+
26
+ ### Commits
27
+
28
+ - 172ea1c chore: Add Claude Code agent definitions
29
+ - 142999e feat(vue-media): Add seconds precision and Now button to recorded images
30
+
31
+ ## Test Results
32
+
33
+ - **Lint**: ✅ Passed (1 warning)
34
+ - **Unit Tests**: ✅ 188 passed
35
+ - **Build**: ✅ Successful
36
+
37
+ ## Version
38
+
39
+ `0.3.10`
40
+
41
+ #### PR #53: feat(vue-media): Add HLS video streaming and improve media pages
42
+ ## Summary
43
+
44
+ This PR adds HLS video streaming capability to the vue-media example and improves the existing media pages with better UX features.
45
+
46
+ ### New Features
47
+ - **HLS Video page** - Stream recorded video using HLS protocol with adaptive bitrate support
48
+ - **Shared datetime state** - Datetime selection persists across Recorded Image and HLS Video pages
49
+ - **Clip Time button** - Quickly set datetime picker to the clip's start time
50
+ - **Time position indicator** - Shows whether selected time is before, inside, or after the segment
51
+ - **Image resolution display** - Shows dimensions for preview and main images on Recorded Image page
52
+
53
+ ### Changes
54
+ - Add HLS.vue page for HLS video streaming
55
+ - Add useSelectedDateTime composable for shared datetime state
56
+ - Display segment start/end times with duration on HLS page
57
+ - Use YYYY-MM-DD date format and AM/PM time notation
58
+ - Remove MP4 playback page (simplified to HLS only)
59
+ - Update README with comprehensive documentation of all pages and APIs
60
+ - Add E2E tests for HLS video page
61
+
62
+ ### Commits
63
+ - e374bd6 feat(vue-media): Add HLS video streaming and improve media pages
64
+ - 14b8d71 fix(vue-media): Now button triggers image fetch like Go button
65
+ - f3bb8bf Merge pull request #52 from klaushofrichter/develop
66
+ - 6368e35 fix: Address code review feedback and test timeout issue
67
+ - 172ea1c chore: Add Claude Code agent definitions
68
+ - 142999e feat(vue-media): Add seconds precision and Now button to recorded images
69
+
70
+ ## Test Results
71
+ - **Lint**: Passed (1 warning)
72
+ - **Unit Tests**: 188 passed
73
+ - **Build**: Success
74
+
75
+ ## Version
76
+ `0.3.10`
77
+
78
+ ---
79
+ 🤖 Generated with [Claude Code](https://claude.ai/code)
80
+
81
+ #### PR #54: chore: Audit and upgrade dependencies
82
+ ## Summary
83
+
84
+ - Adds `@eslint/js` package for ESLint v9 flat config compatibility
85
+ - Merges latest develop changes (conflict resolution)
86
+ - Removes accidentally committed non-code files
87
+
88
+ ## Changes
89
+
90
+ This branch was originally created to audit dependencies but diverged from develop. The main contribution is:
91
+
92
+ - **ESLint v9 compatibility**: Added `@eslint/js` package required for the flat config format
93
+
94
+ ## Conflict Resolution
95
+
96
+ Resolved merge conflicts with develop:
97
+ - `eslint.config.js`: Adopted develop's `commonRules` pattern
98
+ - `package.json`: Merged dependencies
99
+ - `vite.config.ts`: Used develop's test exclude pattern
100
+ - Removed obsolete `examples/vue-basic/` (renamed to `vue-users` in develop)
101
+
102
+ ## Test Results
103
+
104
+ - Linting: Passed (1 warning - existing console statement)
105
+ - Unit tests: 188/188 passed
106
+ - Build: Successful
107
+
108
+ ## Version
109
+
110
+ `0.3.11`
111
+
112
+ ---
113
+ Generated with [Claude Code](https://claude.ai/code)
114
+
10
115
 
11
116
  ### Detailed Changes
12
117
 
13
118
  #### Features
14
- - feat: Add STORAGE_STRATEGY_DESCRIPTIONS constant for dynamic UI display
15
- - feat: Add debug logging for storage fallback and display storage strategy on login pages
119
+ - feat(vue-media): Add 60 second loading timeout for HLS video streams
120
+ - feat(vue-media): Add HLS video streaming and improve media pages
121
+ - feat(vue-media): Add seconds precision and Now button to recorded images
122
+
123
+ #### Bug Fixes
124
+ - fix(vue-media): Improve UI labels and layout
125
+ - fix(vue-media): Address remaining code review issues
126
+ - fix(vue-media): Address code review high/medium priority issues
127
+ - fix(vue-media): Now button triggers image fetch like Go button
128
+ - fix: Address code review feedback and test timeout issue
16
129
 
17
130
  #### Other Changes
18
- - refactor: Move storage strategy display from Login to Home pages
131
+ - chore: Address code review recommendations
132
+ - chore: Consolidate .gitignore presentation patterns
133
+ - chore: Add presentation artifacts to .gitignore
134
+ - chore: Remove accidentally committed non-code files
135
+ - chore: Update example dependencies and fix test configuration
136
+ - refactor(vue-media): Extract timestamp utilities and fix code review issues
137
+ - chore: Add Claude Code agent definitions
138
+ - chore: Upgrade all optional dependencies to latest major versions
19
139
 
20
140
  ### Links
21
141
  - [npm package](https://www.npmjs.com/package/een-api-toolkit)
22
- - [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.8...v0.3.10)
142
+ - [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.10...v0.3.11)
23
143
 
24
144
  ---
25
- *Released: 2026-01-09 20:49:25 CST*
145
+ *Released: 2026-01-12 17:56:08 CST*
@@ -1,6 +1,6 @@
1
1
  # EEN API Toolkit - AI Reference
2
2
 
3
- > **Version:** 0.3.10
3
+ > **Version:** 0.3.11
4
4
  >
5
5
  > This file is optimized for AI assistants. It contains all API signatures,
6
6
  > types, and usage patterns in a single, parseable document.
@@ -1,6 +1,6 @@
1
1
  # EEN API Toolkit - Vue Media Example
2
2
 
3
- A Vue 3 example demonstrating how to fetch live and recorded images from EEN cameras using the een-api-toolkit.
3
+ A Vue 3 example demonstrating how to fetch live images, recorded images, and stream HLS video from EEN cameras using the een-api-toolkit.
4
4
 
5
5
  ![Media Screenshot](media-screenshot.png)
6
6
 
@@ -19,21 +19,78 @@ This is a good balance between security (limiting XSS blast radius) and user exp
19
19
 
20
20
  - OAuth authentication flow (login, callback, logout)
21
21
  - Protected routes with navigation guards
22
- - `getCameras()` function for listing cameras
23
- - `getLiveImage()` function for fetching live preview images
24
- - `getRecordedImage()` function for fetching recorded images with navigation
25
22
  - Camera selection with persistence across pages
26
- - Auto-refresh functionality for live images
27
- - Time-based navigation for recorded images (prev/next)
28
- - Image timestamp display
29
-
30
- ## APIs Used
31
-
32
- - `getCameras()` - List available cameras
33
- - `getLiveImage()` - Fetch live preview image as base64
34
- - `getRecordedImage()` - Fetch recorded image at specific timestamp
35
- - `useAuthStore()` - Authentication state management
36
- - `initEenToolkit()` - Toolkit initialization
23
+ - Live image viewing with auto-refresh
24
+ - Recorded image viewing with prev/next navigation
25
+ - HLS video streaming with adaptive bitrate
26
+ - Datetime picker with seconds precision
27
+ - Shared datetime state across pages
28
+
29
+ ## Pages Overview
30
+
31
+ ### Home Page
32
+ The landing page that displays a welcome message and login prompt when not authenticated. Shows the available toolkit functions and their descriptions.
33
+
34
+ ### Live Camera Image
35
+ Displays live preview images from the selected camera with automatic refresh every 5 seconds.
36
+
37
+ **APIs Used:**
38
+ - `getCameras()` - Lists available cameras for selection
39
+ - `getLiveImage()` - Fetches the current live preview image as base64
40
+
41
+ **Features:**
42
+ - Camera selector dropdown
43
+ - Auto-refresh with error recovery (stops after 3 consecutive failures)
44
+ - Displays image timestamp
45
+
46
+ ### Recorded Image
47
+ Displays recorded images at a specific point in time, showing both preview and main quality images side by side.
48
+
49
+ **APIs Used:**
50
+ - `getCameras()` - Lists available cameras for selection
51
+ - `getRecordedImage()` - Fetches recorded images with pagination tokens
52
+
53
+ **Features:**
54
+ - Camera selector dropdown
55
+ - Datetime picker with seconds precision
56
+ - Previous/Next navigation using pagination tokens
57
+ - "Now" button to reset to current time
58
+ - Displays both preview and main quality images
59
+ - Shows image resolution (e.g., 640x360 for preview, 1920x1080 for main)
60
+ - Displays UTC timestamp in EEN API format
61
+
62
+ ### HLS Video
63
+ Streams recorded video using HLS (HTTP Live Streaming) protocol with adaptive bitrate support.
64
+
65
+ **APIs Used:**
66
+ - `getCameras()` - Lists available cameras for selection
67
+ - `initMediaSession()` - Initializes the media session (required for video URLs)
68
+ - `listMedia()` - Retrieves media intervals with HLS URLs
69
+ - `useAuthStore()` - Explicitly used to get the token for HLS.js requests (see note below)
70
+
71
+ **Features:**
72
+ - Camera selector dropdown
73
+ - Datetime picker with seconds precision
74
+ - "Now" button to reset to current time
75
+ - "Clip Time" button to set picker to the clip's start time
76
+ - Displays segment start/end times with duration
77
+ - Shows whether selected time is before, inside, or after the segment
78
+ - Uses HLS.js library for cross-browser HLS support
79
+ - Adds Authorization header to HLS segment requests
80
+
81
+ ## APIs Used Summary
82
+
83
+ | API Function | Pages | Purpose |
84
+ |--------------|-------|---------|
85
+ | `getCameras()` | All media pages | List available cameras |
86
+ | `getLiveImage()` | Live Camera Image | Fetch live preview image |
87
+ | `getRecordedImage()` | Recorded Image | Fetch recorded images with navigation |
88
+ | `initMediaSession()` | HLS Video | Initialize media session for video URLs |
89
+ | `listMedia()` | HLS Video | Get media intervals with streaming URLs |
90
+ | `useAuthStore()` | All pages | Authentication state management |
91
+ | `initEenToolkit()` | App initialization | Configure toolkit settings |
92
+
93
+ **Note on `useAuthStore()`:** All toolkit functions (`getCameras`, `getLiveImage`, `getRecordedImage`, `listMedia`) use `useAuthStore()` internally to get the authentication token. The HLS Video page is the only one that explicitly calls `useAuthStore()` in its code because HLS.js is a third-party library that makes its own HTTP requests - the token must be manually passed to HLS.js via the `xhrSetup` callback.
37
94
 
38
95
  ## Setup
39
96
 
@@ -65,7 +122,7 @@ All commands below should be run from this example directory (`examples/vue-medi
65
122
  3. Edit `.env` with your EEN credentials:
66
123
  ```env
67
124
  VITE_EEN_CLIENT_ID=your-client-id
68
- VITE_PROXY_URL=http://localhost:8787
125
+ VITE_PROXY_URL=http://127.0.0.1:8787
69
126
  # DO NOT change the redirect URI - EEN IDP only permits this URL
70
127
  VITE_REDIRECT_URI=http://127.0.0.1:3333
71
128
  ```
@@ -87,17 +144,21 @@ All commands below should be run from this example directory (`examples/vue-medi
87
144
 
88
145
  ```
89
146
  src/
90
- ├── main.ts # App entry, toolkit initialization
91
- ├── App.vue # Root component with navigation
147
+ ├── main.ts # App entry, toolkit initialization
148
+ ├── App.vue # Root component with navigation
92
149
  ├── router/
93
- │ └── index.ts # Vue Router with auth guards
150
+ │ └── index.ts # Vue Router with auth guards
151
+ ├── composables/
152
+ │ ├── useSelectedCamera.ts # Shared camera selection state
153
+ │ └── useSelectedDateTime.ts # Shared datetime state
94
154
  └── views/
95
- ├── Home.vue # Home page with login prompt
96
- ├── Login.vue # OAuth login redirect
97
- ├── Callback.vue # OAuth callback handler
98
- ├── LiveCamera.vue # Live image viewer with auto-refresh
99
- ├── RecordedImage.vue # Recorded image viewer with navigation
100
- └── Logout.vue # Logout handler
155
+ ├── Home.vue # Home page with login prompt
156
+ ├── Login.vue # OAuth login redirect
157
+ ├── Callback.vue # OAuth callback handler
158
+ ├── LiveCamera.vue # Live image viewer with auto-refresh
159
+ ├── RecordedImage.vue # Recorded image viewer (preview + main)
160
+ ├── HLS.vue # HLS video streaming
161
+ └── Logout.vue # Logout handler
101
162
  ```
102
163
 
103
164
  ## Key Code Examples
@@ -105,94 +166,102 @@ src/
105
166
  ### Fetching Live Images (LiveCamera.vue)
106
167
 
107
168
  ```typescript
108
- import { getLiveImage, type LiveImageParams } from 'een-api-toolkit'
169
+ import { getLiveImage } from 'een-api-toolkit'
109
170
 
110
171
  async function fetchLiveImage() {
111
- const result = await getLiveImage(selectedCameraId.value, {
112
- type: 'preview'
113
- })
172
+ const result = await getLiveImage({ deviceId: selectedCameraId.value })
114
173
 
115
174
  if (result.error) {
116
175
  error.value = result.error.message
117
176
  } else {
118
- imageData.value = result.data.image
177
+ imageData.value = result.data.imageData
119
178
  timestamp.value = result.data.timestamp
120
179
  }
121
180
  }
122
181
  ```
123
182
 
124
- ### Auto-Refresh for Live Images
125
-
126
- ```typescript
127
- let refreshInterval: number | null = null
128
-
129
- function startAutoRefresh() {
130
- refreshInterval = window.setInterval(() => {
131
- fetchLiveImage()
132
- }, 2000) // Refresh every 2 seconds
133
- }
134
-
135
- function stopAutoRefresh() {
136
- if (refreshInterval) {
137
- clearInterval(refreshInterval)
138
- refreshInterval = null
139
- }
140
- }
141
- ```
142
-
143
183
  ### Fetching Recorded Images (RecordedImage.vue)
144
184
 
145
- ```typescript
146
- import { getRecordedImage, type RecordedImageParams } from 'een-api-toolkit'
147
-
148
- async function fetchRecordedImage() {
149
- const result = await getRecordedImage(selectedCameraId.value, {
150
- timestamp__gte: selectedTimestamp.value,
151
- type: 'preview'
152
- })
185
+ Fetches both preview and main quality images for comparison:
153
186
 
154
- if (result.error) {
155
- error.value = result.error.message
156
- } else {
157
- imageData.value = result.data.image
158
- actualTimestamp.value = result.data.timestamp
159
- prevToken.value = result.data.prevToken
160
- nextToken.value = result.data.nextToken
161
- }
162
- }
187
+ ```typescript
188
+ import { getRecordedImage } from 'een-api-toolkit'
189
+
190
+ // Fetch preview image
191
+ const result = await getRecordedImage({
192
+ deviceId: selectedCameraId.value,
193
+ type: 'preview',
194
+ timestamp__gte: timestamp
195
+ })
196
+
197
+ // Fetch main image at the same timestamp
198
+ const mainResult = await getRecordedImage({
199
+ deviceId: selectedCameraId.value,
200
+ type: 'main',
201
+ timestamp__gte: result.data.timestamp
202
+ })
163
203
  ```
164
204
 
165
- ### Navigating Recorded Images
205
+ ### Streaming HLS Video (HLS.vue)
166
206
 
167
207
  ```typescript
168
- async function navigateNext() {
169
- if (!nextToken.value) return
170
-
171
- const result = await getRecordedImage(selectedCameraId.value, {
172
- next: nextToken.value,
173
- type: 'preview'
174
- })
175
-
176
- if (!result.error) {
177
- imageData.value = result.data.image
178
- actualTimestamp.value = result.data.timestamp
179
- prevToken.value = result.data.prevToken
180
- nextToken.value = result.data.nextToken
208
+ import { listMedia, initMediaSession, useAuthStore } from 'een-api-toolkit'
209
+ import Hls from 'hls.js'
210
+
211
+ // Initialize media session first
212
+ await initMediaSession()
213
+
214
+ // Get HLS URL from listMedia
215
+ const result = await listMedia({
216
+ deviceId: selectedCameraId.value,
217
+ type: 'main',
218
+ mediaType: 'video',
219
+ startTimestamp: timestamp,
220
+ include: ['hlsUrl']
221
+ })
222
+
223
+ // Configure HLS.js with Authorization header
224
+ const authStore = useAuthStore()
225
+ const hls = new Hls({
226
+ xhrSetup: (xhr: XMLHttpRequest) => {
227
+ xhr.setRequestHeader('Authorization', `Bearer ${authStore.token}`)
181
228
  }
182
- }
229
+ })
230
+
231
+ hls.loadSource(result.data.results[0].hlsUrl)
232
+ hls.attachMedia(videoElement)
183
233
  ```
184
234
 
185
235
  ### Displaying Images
186
236
 
237
+ The toolkit returns `imageData` as a complete data URL (including the `data:image/jpeg;base64,` prefix), so it can be used directly with `:src`:
238
+
187
239
  ```vue
188
240
  <template>
189
241
  <img
190
242
  v-if="imageData"
191
- :src="`data:image/jpeg;base64,${imageData}`"
243
+ :src="imageData"
192
244
  alt="Camera image"
193
245
  />
194
- <p v-if="timestamp">
195
- Timestamp: {{ new Date(timestamp).toLocaleString() }}
196
- </p>
197
246
  </template>
198
247
  ```
248
+
249
+ ### Shared State with Composables
250
+
251
+ Camera selection and datetime are shared across pages using Vue composables:
252
+
253
+ ```typescript
254
+ // useSelectedCamera.ts
255
+ import { ref } from 'vue'
256
+
257
+ const selectedCameraId = ref<string | null>(null)
258
+
259
+ export function useSelectedCamera() {
260
+ return {
261
+ selectedCameraId,
262
+ setSelectedCamera: (id: string) => { selectedCameraId.value = id }
263
+ }
264
+ }
265
+ ```
266
+
267
+ This allows users to switch between Live Camera, Recorded Image, and HLS Video pages while maintaining their camera and time selection.
@@ -52,6 +52,7 @@ test.describe('vue-media example app', () => {
52
52
  await expect(page.getByText('getCameras()')).toBeVisible()
53
53
  await expect(page.getByText('getLiveImage()')).toBeVisible()
54
54
  await expect(page.getByText('getRecordedImage()')).toBeVisible()
55
+ await expect(page.getByText('listMedia()')).toBeVisible()
55
56
  })
56
57
 
57
58
  test('recorded route redirects to login when not authenticated', async ({ page }) => {
@@ -60,4 +61,11 @@ test.describe('vue-media example app', () => {
60
61
  // Should redirect to login page (auth guard)
61
62
  await expect(page).toHaveURL('/login')
62
63
  })
64
+
65
+ test('hls route redirects to login when not authenticated', async ({ page }) => {
66
+ await page.goto('/hls')
67
+
68
+ // Should redirect to login page (auth guard)
69
+ await expect(page).toHaveURL('/login')
70
+ })
63
71
  })
@@ -201,7 +201,7 @@ test.describe('Vue Media Example - Auth', () => {
201
201
  await page.click('[data-testid="nav-live"]')
202
202
  await page.waitForURL('/live')
203
203
 
204
- await expect(page.getByRole('heading', { name: 'Live Camera View' })).toBeVisible()
204
+ await expect(page.getByRole('heading', { name: 'Live Camera Image (preview)' })).toBeVisible()
205
205
 
206
206
  // Wait for cameras to load
207
207
  await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
@@ -243,7 +243,7 @@ test.describe('Vue Media Example - Auth', () => {
243
243
  await page.click('[data-testid="nav-recorded"]')
244
244
  await page.waitForURL('/recorded')
245
245
 
246
- await expect(page.getByRole('heading', { name: 'Recorded Images' })).toBeVisible()
246
+ await expect(page.getByRole('heading', { name: 'Recorded Image (Preview and Main)' })).toBeVisible()
247
247
 
248
248
  // Wait for cameras to load
249
249
  await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
@@ -269,6 +269,7 @@ test.describe('Vue Media Example - Auth', () => {
269
269
  if (hasCameras) {
270
270
  await expect(page.getByTestId('datetime-input')).toBeVisible()
271
271
  await expect(page.getByTestId('go-button')).toBeVisible()
272
+ await expect(page.getByTestId('now-button')).toBeVisible()
272
273
  await expect(page.getByTestId('prev-button')).toBeVisible()
273
274
  await expect(page.getByTestId('next-button')).toBeVisible()
274
275
  console.log('Recorded image controls visible')
@@ -277,6 +278,157 @@ test.describe('Vue Media Example - Auth', () => {
277
278
  }
278
279
  })
279
280
 
281
+ test('Now button resets datetime picker to current time', async ({ page }) => {
282
+ skipIfNoProxy()
283
+ skipIfNoCredentials()
284
+
285
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
286
+ await expect(page.getByTestId('nav-recorded')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
287
+
288
+ await page.click('[data-testid="nav-recorded"]')
289
+ await page.waitForURL('/recorded')
290
+
291
+ await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
292
+ timeout: TIMEOUTS.MEDIA_LOAD
293
+ })
294
+
295
+ const hasCameras = await page.getByTestId('camera-select').isVisible().catch(() => false)
296
+ if (hasCameras) {
297
+ const datetimeInput = page.getByTestId('datetime-input')
298
+ await expect(datetimeInput).toBeVisible()
299
+
300
+ // Set datetime to a past time (1 hour ago)
301
+ const pastTime = new Date(Date.now() - 60 * 60 * 1000)
302
+ const pastTimeStr = pastTime.toISOString().slice(0, 19) // Format: YYYY-MM-DDTHH:mm:ss
303
+ await datetimeInput.fill(pastTimeStr)
304
+
305
+ // Verify the input has the past time
306
+ const valueBeforeClick = await datetimeInput.inputValue()
307
+ expect(valueBeforeClick).toContain(pastTimeStr.slice(0, 16)) // Check date and time portion
308
+
309
+ // Click the Now button
310
+ await page.click('[data-testid="now-button"]')
311
+
312
+ // Get the new value and verify it's closer to current time
313
+ const valueAfterClick = await datetimeInput.inputValue()
314
+ const nowTime = new Date()
315
+ const selectedTime = new Date(valueAfterClick)
316
+
317
+ // The selected time should be within 2 minutes of now
318
+ const timeDiffMs = Math.abs(nowTime.getTime() - selectedTime.getTime())
319
+ expect(timeDiffMs).toBeLessThan(2 * 60 * 1000) // 2 minutes tolerance
320
+
321
+ // Verify UTC timestamp is visible and in valid EEN API format
322
+ const utcTimestamp = page.getByTestId('utc-timestamp')
323
+ await expect(utcTimestamp).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
324
+
325
+ const utcText = await utcTimestamp.textContent()
326
+ expect(utcText).toContain('Timestamp for API (UTC):')
327
+
328
+ // Extract the timestamp value and verify EEN API format: YYYY-MM-DDTHH:mm:ss.sss+00:00
329
+ const apiTimestampMatch = utcText?.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+00:00/)
330
+ expect(apiTimestampMatch).not.toBeNull()
331
+
332
+ console.log('Now button correctly reset datetime to current time')
333
+ console.log('UTC timestamp visible and valid:', apiTimestampMatch?.[0])
334
+ } else {
335
+ console.log('No cameras in account - skipping Now button test')
336
+ }
337
+ })
338
+
339
+ test('datetime selection persists between recorded and video pages', async ({ page }) => {
340
+ skipIfNoProxy()
341
+ skipIfNoCredentials()
342
+
343
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
344
+ await expect(page.getByTestId('nav-recorded')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
345
+
346
+ // Navigate to recorded page
347
+ await page.click('[data-testid="nav-recorded"]')
348
+ await page.waitForURL('/recorded')
349
+
350
+ await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
351
+ timeout: TIMEOUTS.MEDIA_LOAD
352
+ })
353
+
354
+ const hasCameras = await page.getByTestId('camera-select').isVisible().catch(() => false)
355
+ if (hasCameras) {
356
+ const datetimeInput = page.getByTestId('datetime-input')
357
+ await expect(datetimeInput).toBeVisible()
358
+
359
+ // Set a specific datetime (2 hours ago to ensure it's different from default)
360
+ const specificTime = new Date(Date.now() - 2 * 60 * 60 * 1000)
361
+ const specificTimeStr = specificTime.toISOString().slice(0, 19) // Format: YYYY-MM-DDTHH:mm:ss
362
+ await datetimeInput.fill(specificTimeStr)
363
+
364
+ // Verify the input has the specific time
365
+ const valueOnRecorded = await datetimeInput.inputValue()
366
+ expect(valueOnRecorded).toContain(specificTimeStr.slice(0, 16)) // Check date and time portion
367
+
368
+ // Navigate to HLS video page
369
+ await page.click('[data-testid="nav-hls"]')
370
+ await page.waitForURL('/hls')
371
+
372
+ await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
373
+ timeout: TIMEOUTS.MEDIA_LOAD
374
+ })
375
+
376
+ // Verify the datetime is persisted on HLS page
377
+ const hlsDatetimeInput = page.getByTestId('datetime-input')
378
+ await expect(hlsDatetimeInput).toBeVisible()
379
+
380
+ const valueOnHls = await hlsDatetimeInput.inputValue()
381
+ expect(valueOnHls).toContain(specificTimeStr.slice(0, 16)) // Should match the time set on recorded page
382
+
383
+ console.log('Datetime persistence verified: recorded →', valueOnRecorded, '| hls →', valueOnHls)
384
+ } else {
385
+ console.log('No cameras in account - skipping datetime persistence test')
386
+ }
387
+ })
388
+
389
+ test('can view HLS video after login', async ({ page }) => {
390
+ skipIfNoProxy()
391
+ skipIfNoCredentials()
392
+
393
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
394
+ await expect(page.getByTestId('nav-hls')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
395
+
396
+ await page.click('[data-testid="nav-hls"]')
397
+ await page.waitForURL('/hls')
398
+
399
+ await expect(page.getByRole('heading', { name: 'HLS Video Streaming (Main)' })).toBeVisible()
400
+
401
+ // Wait for cameras to load
402
+ await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
403
+ timeout: TIMEOUTS.MEDIA_LOAD
404
+ })
405
+ })
406
+
407
+ test('HLS page shows controls when cameras available', async ({ page }) => {
408
+ skipIfNoProxy()
409
+ skipIfNoCredentials()
410
+
411
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
412
+ await expect(page.getByTestId('nav-hls')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
413
+
414
+ await page.click('[data-testid="nav-hls"]')
415
+ await page.waitForURL('/hls')
416
+
417
+ await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
418
+ timeout: TIMEOUTS.MEDIA_LOAD
419
+ })
420
+
421
+ const hasCameras = await page.getByTestId('camera-select').isVisible().catch(() => false)
422
+ if (hasCameras) {
423
+ await expect(page.getByTestId('datetime-input')).toBeVisible()
424
+ await expect(page.getByTestId('go-button')).toBeVisible()
425
+ await expect(page.getByTestId('now-button')).toBeVisible()
426
+ console.log('HLS video controls visible')
427
+ } else {
428
+ console.log('No cameras in account - skipping camera-specific checks')
429
+ }
430
+ })
431
+
280
432
  test('can logout after login', async ({ page }) => {
281
433
  skipIfNoProxy()
282
434
  skipIfNoCredentials()