een-api-toolkit 0.0.13 → 0.0.18
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 +5 -318
- package/README.md +2 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +897 -0
- package/dist/index.js +395 -262
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +362 -64
- package/examples/vue-cameras/.env.example +13 -0
- package/examples/vue-cameras/e2e/app.spec.ts +73 -0
- package/examples/vue-cameras/index.html +13 -0
- package/examples/vue-cameras/package-lock.json +1583 -0
- package/examples/vue-cameras/package.json +28 -0
- package/examples/vue-cameras/playwright.config.ts +46 -0
- package/examples/vue-cameras/src/App.vue +108 -0
- package/examples/vue-cameras/src/main.ts +23 -0
- package/examples/vue-cameras/src/router/index.ts +68 -0
- package/examples/vue-cameras/src/views/Callback.vue +76 -0
- package/examples/vue-cameras/src/views/CameraDetail.vue +315 -0
- package/examples/vue-cameras/src/views/Cameras.vue +249 -0
- package/examples/vue-cameras/src/views/Home.vue +125 -0
- package/examples/vue-cameras/src/views/Login.vue +33 -0
- package/examples/vue-cameras/src/views/Logout.vue +66 -0
- package/examples/vue-cameras/src/vite-env.d.ts +12 -0
- package/examples/vue-cameras/tsconfig.json +21 -0
- package/examples/vue-cameras/tsconfig.node.json +10 -0
- package/examples/vue-cameras/vite.config.ts +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "een-api-toolkit-cameras-example",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"stop": "npx kill-port 3333",
|
|
8
|
+
"dev": "npm run stop && vite",
|
|
9
|
+
"build": "vue-tsc && vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"test:e2e": "playwright test",
|
|
12
|
+
"test:e2e:ui": "playwright test --ui"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"een-api-toolkit": "file:../..",
|
|
16
|
+
"pinia": "^2.1.7",
|
|
17
|
+
"vue": "^3.4.0",
|
|
18
|
+
"vue-router": "^4.2.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@playwright/test": "^1.57.0",
|
|
22
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
23
|
+
"dotenv": "^17.2.3",
|
|
24
|
+
"typescript": "~5.8.0",
|
|
25
|
+
"vite": "^7.3.0",
|
|
26
|
+
"vue-tsc": "^3.2.1"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
2
|
+
import dotenv from 'dotenv'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
// Load .env files: parent first, then local with override to replace any conflicts
|
|
9
|
+
dotenv.config({ path: path.resolve(__dirname, '../../.env') })
|
|
10
|
+
dotenv.config({ path: path.resolve(__dirname, '.env'), override: true })
|
|
11
|
+
|
|
12
|
+
const redirectUri = process.env.VITE_REDIRECT_URI || 'http://127.0.0.1:3333'
|
|
13
|
+
if (!redirectUri.startsWith('http://127.0.0.1:') && !redirectUri.startsWith('http://localhost:')) {
|
|
14
|
+
throw new Error('VITE_REDIRECT_URI must use localhost or 127.0.0.1 for security')
|
|
15
|
+
}
|
|
16
|
+
const baseURL = redirectUri
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
testDir: './e2e',
|
|
20
|
+
testMatch: '**/*.spec.ts',
|
|
21
|
+
fullyParallel: false,
|
|
22
|
+
forbidOnly: !!process.env.CI,
|
|
23
|
+
retries: 0,
|
|
24
|
+
maxFailures: 1,
|
|
25
|
+
workers: 1,
|
|
26
|
+
reporter: [['html', { open: 'never' }]],
|
|
27
|
+
timeout: 30000,
|
|
28
|
+
use: {
|
|
29
|
+
baseURL,
|
|
30
|
+
trace: 'on-first-retry',
|
|
31
|
+
video: 'retain-on-failure'
|
|
32
|
+
},
|
|
33
|
+
outputDir: './e2e-results/',
|
|
34
|
+
projects: [
|
|
35
|
+
{
|
|
36
|
+
name: 'chromium',
|
|
37
|
+
use: { ...devices['Desktop Chrome'] }
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
webServer: {
|
|
41
|
+
command: 'npm run dev',
|
|
42
|
+
url: baseURL,
|
|
43
|
+
reuseExistingServer: !process.env.CI,
|
|
44
|
+
timeout: 30000
|
|
45
|
+
}
|
|
46
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useAuthStore } from 'een-api-toolkit'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
const authStore = useAuthStore()
|
|
6
|
+
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div class="app">
|
|
11
|
+
<header>
|
|
12
|
+
<h1>EEN Cameras Example</h1>
|
|
13
|
+
<nav>
|
|
14
|
+
<router-link to="/">Home</router-link>
|
|
15
|
+
<router-link v-if="isAuthenticated" to="/cameras">Cameras</router-link>
|
|
16
|
+
<router-link v-if="!isAuthenticated" to="/login">Login</router-link>
|
|
17
|
+
<router-link v-if="isAuthenticated" to="/logout">Logout</router-link>
|
|
18
|
+
</nav>
|
|
19
|
+
</header>
|
|
20
|
+
<main>
|
|
21
|
+
<router-view />
|
|
22
|
+
</main>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
* {
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
body {
|
|
34
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
|
35
|
+
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
36
|
+
line-height: 1.6;
|
|
37
|
+
color: #333;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.app {
|
|
41
|
+
max-width: 1200px;
|
|
42
|
+
margin: 0 auto;
|
|
43
|
+
padding: 20px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
header {
|
|
47
|
+
display: flex;
|
|
48
|
+
justify-content: space-between;
|
|
49
|
+
align-items: center;
|
|
50
|
+
margin-bottom: 30px;
|
|
51
|
+
padding-bottom: 20px;
|
|
52
|
+
border-bottom: 1px solid #eee;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
header h1 {
|
|
56
|
+
font-size: 1.5rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
nav {
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 20px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
nav a {
|
|
65
|
+
color: #42b883;
|
|
66
|
+
text-decoration: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
nav a:hover {
|
|
70
|
+
text-decoration: underline;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
nav a.router-link-active {
|
|
74
|
+
font-weight: bold;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
button {
|
|
78
|
+
background: #42b883;
|
|
79
|
+
color: white;
|
|
80
|
+
border: none;
|
|
81
|
+
padding: 10px 20px;
|
|
82
|
+
border-radius: 4px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
font-size: 1rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
button:hover {
|
|
88
|
+
background: #3aa876;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
button:disabled {
|
|
92
|
+
background: #ccc;
|
|
93
|
+
cursor: not-allowed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.error {
|
|
97
|
+
color: #e74c3c;
|
|
98
|
+
padding: 10px;
|
|
99
|
+
background: #fdf2f2;
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
margin: 10px 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.loading {
|
|
105
|
+
color: #666;
|
|
106
|
+
font-style: italic;
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import { createPinia } from 'pinia'
|
|
3
|
+
import { initEenToolkit } from 'een-api-toolkit'
|
|
4
|
+
import App from './App.vue'
|
|
5
|
+
import router from './router'
|
|
6
|
+
|
|
7
|
+
const app = createApp(App)
|
|
8
|
+
|
|
9
|
+
// Install Pinia (required before initEenToolkit)
|
|
10
|
+
app.use(createPinia())
|
|
11
|
+
|
|
12
|
+
// Initialize EEN API Toolkit
|
|
13
|
+
initEenToolkit({
|
|
14
|
+
proxyUrl: import.meta.env.VITE_PROXY_URL,
|
|
15
|
+
clientId: import.meta.env.VITE_EEN_CLIENT_ID,
|
|
16
|
+
redirectUri: import.meta.env.VITE_REDIRECT_URI,
|
|
17
|
+
debug: import.meta.env.VITE_DEBUG === 'true'
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Install router
|
|
21
|
+
app.use(router)
|
|
22
|
+
|
|
23
|
+
app.mount('#app')
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
import { useAuthStore } from 'een-api-toolkit'
|
|
3
|
+
import Home from '../views/Home.vue'
|
|
4
|
+
import Login from '../views/Login.vue'
|
|
5
|
+
import Callback from '../views/Callback.vue'
|
|
6
|
+
import Cameras from '../views/Cameras.vue'
|
|
7
|
+
import CameraDetail from '../views/CameraDetail.vue'
|
|
8
|
+
import Logout from '../views/Logout.vue'
|
|
9
|
+
|
|
10
|
+
const router = createRouter({
|
|
11
|
+
history: createWebHistory(),
|
|
12
|
+
routes: [
|
|
13
|
+
{
|
|
14
|
+
path: '/',
|
|
15
|
+
name: 'home',
|
|
16
|
+
component: Home,
|
|
17
|
+
// Handle OAuth callback on root path (EEN IDP redirects to http://127.0.0.1:3333)
|
|
18
|
+
beforeEnter: (to, _from, next) => {
|
|
19
|
+
// If URL has code and state params, it's an OAuth callback
|
|
20
|
+
if (to.query.code && to.query.state) {
|
|
21
|
+
next({ name: 'callback', query: to.query })
|
|
22
|
+
} else {
|
|
23
|
+
next()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
path: '/login',
|
|
29
|
+
name: 'login',
|
|
30
|
+
component: Login
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: '/callback',
|
|
34
|
+
name: 'callback',
|
|
35
|
+
component: Callback
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
path: '/cameras',
|
|
39
|
+
name: 'cameras',
|
|
40
|
+
component: Cameras,
|
|
41
|
+
meta: { requiresAuth: true }
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: '/cameras/:id',
|
|
45
|
+
name: 'camera-detail',
|
|
46
|
+
component: CameraDetail,
|
|
47
|
+
meta: { requiresAuth: true }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: '/logout',
|
|
51
|
+
name: 'logout',
|
|
52
|
+
component: Logout
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Navigation guard for protected routes
|
|
58
|
+
router.beforeEach((to, _from, next) => {
|
|
59
|
+
const authStore = useAuthStore()
|
|
60
|
+
|
|
61
|
+
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
62
|
+
next({ name: 'login' })
|
|
63
|
+
} else {
|
|
64
|
+
next()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export default router
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, ref } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
import { handleAuthCallback } from 'een-api-toolkit'
|
|
5
|
+
|
|
6
|
+
const router = useRouter()
|
|
7
|
+
const error = ref<string | null>(null)
|
|
8
|
+
const processing = ref(true)
|
|
9
|
+
|
|
10
|
+
onMounted(async () => {
|
|
11
|
+
const url = new URL(window.location.href)
|
|
12
|
+
const code = url.searchParams.get('code')
|
|
13
|
+
const state = url.searchParams.get('state')
|
|
14
|
+
const errorParam = url.searchParams.get('error')
|
|
15
|
+
|
|
16
|
+
if (errorParam) {
|
|
17
|
+
error.value = `OAuth error: ${errorParam}`
|
|
18
|
+
processing.value = false
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!code || !state) {
|
|
23
|
+
error.value = 'Missing authorization code or state parameter'
|
|
24
|
+
processing.value = false
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await handleAuthCallback(code, state)
|
|
29
|
+
|
|
30
|
+
if (result.error) {
|
|
31
|
+
error.value = result.error.message
|
|
32
|
+
processing.value = false
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Success - redirect to cameras
|
|
37
|
+
router.push('/cameras')
|
|
38
|
+
})
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div class="callback">
|
|
43
|
+
<div v-if="processing" class="loading">
|
|
44
|
+
<h2>Authenticating...</h2>
|
|
45
|
+
<p>Please wait while we complete the login process.</p>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div v-else-if="error" class="error-state">
|
|
49
|
+
<h2>Authentication Failed</h2>
|
|
50
|
+
<p class="error">{{ error }}</p>
|
|
51
|
+
<router-link to="/login">
|
|
52
|
+
<button>Try Again</button>
|
|
53
|
+
</router-link>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<style scoped>
|
|
59
|
+
.callback {
|
|
60
|
+
text-align: center;
|
|
61
|
+
max-width: 400px;
|
|
62
|
+
margin: 0 auto;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
h2 {
|
|
66
|
+
margin-bottom: 20px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.loading p {
|
|
70
|
+
color: #666;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.error-state .error {
|
|
74
|
+
margin-bottom: 20px;
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useRoute } from 'vue-router'
|
|
3
|
+
import { useCamera, type CameraStatus } from 'een-api-toolkit'
|
|
4
|
+
|
|
5
|
+
const route = useRoute()
|
|
6
|
+
|
|
7
|
+
// Fetch camera with all details
|
|
8
|
+
const { camera, loading, error, refresh } = useCamera(
|
|
9
|
+
() => route.params.id as string,
|
|
10
|
+
{
|
|
11
|
+
include: ['deviceInfo', 'status', 'shareDetails', 'devicePosition', 'networkInfo', 'tags']
|
|
12
|
+
}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// Get status badge class
|
|
16
|
+
function getStatusClass(status?: CameraStatus): string {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'online':
|
|
19
|
+
case 'streaming':
|
|
20
|
+
return 'status-online'
|
|
21
|
+
case 'offline':
|
|
22
|
+
case 'deviceOffline':
|
|
23
|
+
case 'bridgeOffline':
|
|
24
|
+
return 'status-offline'
|
|
25
|
+
case 'error':
|
|
26
|
+
case 'invalidCredentials':
|
|
27
|
+
return 'status-error'
|
|
28
|
+
default:
|
|
29
|
+
return 'status-unknown'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div class="camera-detail">
|
|
36
|
+
<div class="nav-back">
|
|
37
|
+
<router-link to="/cameras">← Back to Cameras</router-link>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div v-if="loading" class="loading">
|
|
41
|
+
Loading camera details...
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div v-else-if="error" class="error">
|
|
45
|
+
Error: {{ error.message }}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div v-else-if="camera" class="camera-info">
|
|
49
|
+
<div class="header">
|
|
50
|
+
<div>
|
|
51
|
+
<h2>{{ camera.name }}</h2>
|
|
52
|
+
<p class="camera-id">ID: {{ camera.id }}</p>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="header-actions">
|
|
55
|
+
<span :class="['status-badge', getStatusClass(camera.status)]">
|
|
56
|
+
{{ camera.status || 'Unknown' }}
|
|
57
|
+
</span>
|
|
58
|
+
<button @click="refresh">Refresh</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="sections">
|
|
63
|
+
<!-- Basic Info -->
|
|
64
|
+
<section class="info-section">
|
|
65
|
+
<h3>Basic Information</h3>
|
|
66
|
+
<dl>
|
|
67
|
+
<dt>Account ID</dt>
|
|
68
|
+
<dd>{{ camera.accountId }}</dd>
|
|
69
|
+
|
|
70
|
+
<dt>Bridge ID</dt>
|
|
71
|
+
<dd>{{ camera.bridgeId || 'Direct to Cloud' }}</dd>
|
|
72
|
+
|
|
73
|
+
<dt>Location ID</dt>
|
|
74
|
+
<dd>{{ camera.locationId || 'Not assigned' }}</dd>
|
|
75
|
+
|
|
76
|
+
<dt v-if="camera.timezone">Timezone</dt>
|
|
77
|
+
<dd v-if="camera.timezone">{{ camera.timezone }}</dd>
|
|
78
|
+
|
|
79
|
+
<dt v-if="camera.tags && camera.tags.length > 0">Tags</dt>
|
|
80
|
+
<dd v-if="camera.tags && camera.tags.length > 0">
|
|
81
|
+
<span v-for="tag in camera.tags" :key="tag" class="tag">{{ tag }}</span>
|
|
82
|
+
</dd>
|
|
83
|
+
</dl>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<!-- Device Info -->
|
|
87
|
+
<section v-if="camera.deviceInfo" class="info-section">
|
|
88
|
+
<h3>Device Information</h3>
|
|
89
|
+
<dl>
|
|
90
|
+
<dt v-if="camera.deviceInfo.make">Manufacturer</dt>
|
|
91
|
+
<dd v-if="camera.deviceInfo.make">{{ camera.deviceInfo.make }}</dd>
|
|
92
|
+
|
|
93
|
+
<dt v-if="camera.deviceInfo.model">Model</dt>
|
|
94
|
+
<dd v-if="camera.deviceInfo.model">{{ camera.deviceInfo.model }}</dd>
|
|
95
|
+
|
|
96
|
+
<dt v-if="camera.deviceInfo.firmwareVersion">Firmware</dt>
|
|
97
|
+
<dd v-if="camera.deviceInfo.firmwareVersion">{{ camera.deviceInfo.firmwareVersion }}</dd>
|
|
98
|
+
|
|
99
|
+
<dt v-if="camera.deviceInfo.serialNumber">Serial Number</dt>
|
|
100
|
+
<dd v-if="camera.deviceInfo.serialNumber">{{ camera.deviceInfo.serialNumber }}</dd>
|
|
101
|
+
|
|
102
|
+
<dt v-if="camera.deviceInfo.resolution">Resolution</dt>
|
|
103
|
+
<dd v-if="camera.deviceInfo.resolution">{{ camera.deviceInfo.resolution }}</dd>
|
|
104
|
+
|
|
105
|
+
<dt>Direct to Cloud</dt>
|
|
106
|
+
<dd>{{ camera.deviceInfo.directToCloud ? 'Yes' : 'No' }}</dd>
|
|
107
|
+
</dl>
|
|
108
|
+
</section>
|
|
109
|
+
|
|
110
|
+
<!-- Network Info -->
|
|
111
|
+
<section v-if="camera.ipAddress || camera.macAddress" class="info-section">
|
|
112
|
+
<h3>Network Information</h3>
|
|
113
|
+
<dl>
|
|
114
|
+
<dt v-if="camera.ipAddress">IP Address</dt>
|
|
115
|
+
<dd v-if="camera.ipAddress">{{ camera.ipAddress }}</dd>
|
|
116
|
+
|
|
117
|
+
<dt v-if="camera.macAddress">MAC Address</dt>
|
|
118
|
+
<dd v-if="camera.macAddress">{{ camera.macAddress }}</dd>
|
|
119
|
+
|
|
120
|
+
<dt v-if="camera.guid">GUID</dt>
|
|
121
|
+
<dd v-if="camera.guid">{{ camera.guid }}</dd>
|
|
122
|
+
</dl>
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
<!-- Share Details -->
|
|
126
|
+
<section v-if="camera.shareDetails" class="info-section">
|
|
127
|
+
<h3>Sharing</h3>
|
|
128
|
+
<dl>
|
|
129
|
+
<dt>Shared</dt>
|
|
130
|
+
<dd>{{ camera.shareDetails.shared ? 'Yes' : 'No' }}</dd>
|
|
131
|
+
|
|
132
|
+
<dt v-if="camera.shareDetails.accountId">Shared with Account</dt>
|
|
133
|
+
<dd v-if="camera.shareDetails.accountId">{{ camera.shareDetails.accountId }}</dd>
|
|
134
|
+
|
|
135
|
+
<dt v-if="camera.shareDetails.firstResponder !== undefined">First Responder</dt>
|
|
136
|
+
<dd v-if="camera.shareDetails.firstResponder !== undefined">
|
|
137
|
+
{{ camera.shareDetails.firstResponder ? 'Yes' : 'No' }}
|
|
138
|
+
</dd>
|
|
139
|
+
</dl>
|
|
140
|
+
</section>
|
|
141
|
+
|
|
142
|
+
<!-- Position -->
|
|
143
|
+
<section v-if="camera.devicePosition" class="info-section">
|
|
144
|
+
<h3>Location</h3>
|
|
145
|
+
<dl>
|
|
146
|
+
<dt v-if="camera.devicePosition.latitude !== undefined">Coordinates</dt>
|
|
147
|
+
<dd v-if="camera.devicePosition.latitude !== undefined">
|
|
148
|
+
{{ camera.devicePosition.latitude }}, {{ camera.devicePosition.longitude }}
|
|
149
|
+
</dd>
|
|
150
|
+
|
|
151
|
+
<dt v-if="camera.devicePosition.floor !== undefined">Floor</dt>
|
|
152
|
+
<dd v-if="camera.devicePosition.floor !== undefined">{{ camera.devicePosition.floor }}</dd>
|
|
153
|
+
|
|
154
|
+
<dt v-if="camera.devicePosition.azimuth !== undefined">Azimuth</dt>
|
|
155
|
+
<dd v-if="camera.devicePosition.azimuth !== undefined">{{ camera.devicePosition.azimuth }}deg</dd>
|
|
156
|
+
</dl>
|
|
157
|
+
</section>
|
|
158
|
+
|
|
159
|
+
<!-- Analytics -->
|
|
160
|
+
<section v-if="camera.enabledAnalytics && camera.enabledAnalytics.length > 0" class="info-section">
|
|
161
|
+
<h3>Analytics</h3>
|
|
162
|
+
<div class="analytics-list">
|
|
163
|
+
<span v-for="analytic in camera.enabledAnalytics" :key="analytic" class="analytic-badge">
|
|
164
|
+
{{ analytic }}
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
</section>
|
|
168
|
+
|
|
169
|
+
<!-- Timestamps -->
|
|
170
|
+
<section class="info-section">
|
|
171
|
+
<h3>Timestamps</h3>
|
|
172
|
+
<dl>
|
|
173
|
+
<dt v-if="camera.createdAt">Created</dt>
|
|
174
|
+
<dd v-if="camera.createdAt">{{ new Date(camera.createdAt).toLocaleString() }}</dd>
|
|
175
|
+
|
|
176
|
+
<dt v-if="camera.updatedAt">Updated</dt>
|
|
177
|
+
<dd v-if="camera.updatedAt">{{ new Date(camera.updatedAt).toLocaleString() }}</dd>
|
|
178
|
+
</dl>
|
|
179
|
+
</section>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</template>
|
|
184
|
+
|
|
185
|
+
<style scoped>
|
|
186
|
+
.camera-detail {
|
|
187
|
+
max-width: 800px;
|
|
188
|
+
margin: 0 auto;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.nav-back {
|
|
192
|
+
margin-bottom: 20px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.nav-back a {
|
|
196
|
+
color: #42b883;
|
|
197
|
+
text-decoration: none;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.nav-back a:hover {
|
|
201
|
+
text-decoration: underline;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.header {
|
|
205
|
+
display: flex;
|
|
206
|
+
justify-content: space-between;
|
|
207
|
+
align-items: flex-start;
|
|
208
|
+
margin-bottom: 30px;
|
|
209
|
+
padding-bottom: 20px;
|
|
210
|
+
border-bottom: 1px solid #eee;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.header h2 {
|
|
214
|
+
margin: 0 0 5px 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.camera-id {
|
|
218
|
+
color: #666;
|
|
219
|
+
font-size: 0.9rem;
|
|
220
|
+
margin: 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.header-actions {
|
|
224
|
+
display: flex;
|
|
225
|
+
gap: 10px;
|
|
226
|
+
align-items: center;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.status-badge {
|
|
230
|
+
padding: 6px 12px;
|
|
231
|
+
border-radius: 4px;
|
|
232
|
+
font-size: 0.8rem;
|
|
233
|
+
font-weight: 600;
|
|
234
|
+
text-transform: uppercase;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.status-online {
|
|
238
|
+
background: #d4edda;
|
|
239
|
+
color: #155724;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.status-offline {
|
|
243
|
+
background: #f8d7da;
|
|
244
|
+
color: #721c24;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.status-error {
|
|
248
|
+
background: #fff3cd;
|
|
249
|
+
color: #856404;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.status-unknown {
|
|
253
|
+
background: #e2e3e5;
|
|
254
|
+
color: #383d41;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.sections {
|
|
258
|
+
display: grid;
|
|
259
|
+
gap: 25px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.info-section {
|
|
263
|
+
background: #f9f9f9;
|
|
264
|
+
padding: 20px;
|
|
265
|
+
border-radius: 8px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.info-section h3 {
|
|
269
|
+
margin: 0 0 15px 0;
|
|
270
|
+
font-size: 1rem;
|
|
271
|
+
color: #333;
|
|
272
|
+
border-bottom: 1px solid #ddd;
|
|
273
|
+
padding-bottom: 10px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
dl {
|
|
277
|
+
display: grid;
|
|
278
|
+
grid-template-columns: 150px 1fr;
|
|
279
|
+
gap: 10px 15px;
|
|
280
|
+
margin: 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
dt {
|
|
284
|
+
font-weight: 600;
|
|
285
|
+
color: #555;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
dd {
|
|
289
|
+
margin: 0;
|
|
290
|
+
color: #333;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.tag {
|
|
294
|
+
display: inline-block;
|
|
295
|
+
background: #e0e0e0;
|
|
296
|
+
padding: 2px 8px;
|
|
297
|
+
border-radius: 3px;
|
|
298
|
+
margin-right: 5px;
|
|
299
|
+
font-size: 0.85rem;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.analytics-list {
|
|
303
|
+
display: flex;
|
|
304
|
+
flex-wrap: wrap;
|
|
305
|
+
gap: 8px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.analytic-badge {
|
|
309
|
+
background: #42b883;
|
|
310
|
+
color: white;
|
|
311
|
+
padding: 4px 10px;
|
|
312
|
+
border-radius: 4px;
|
|
313
|
+
font-size: 0.85rem;
|
|
314
|
+
}
|
|
315
|
+
</style>
|