created prototype of tasks for device and dialog, that shows all tasks for admin per each device
This commit is contained in:
@@ -74,15 +74,6 @@ function onSelect(ev: CustomEvent) {
|
|||||||
// When parent switches between external/internal data at runtime, refetch if needed.
|
// When parent switches between external/internal data at runtime, refetch if needed.
|
||||||
watch(() => props.allUsers, () => { if (!usingExternal.value) loadUsers() })
|
watch(() => props.allUsers, () => { if (!usingExternal.value) loadUsers() })
|
||||||
|
|
||||||
// Debug: whenever modelValue changes (from dialog)
|
|
||||||
watch(() => props.modelValue, (v) => {
|
|
||||||
console.log('[AssignDevice:props.modelValue]', v)
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// Debug: when effective users list is ready
|
|
||||||
watch(() => effectiveUsers.value, (v) => {
|
|
||||||
console.log('[AssignDevice:effectiveUsers]', v?.length, 'items')
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
124
management-ui/src/customcompometns/DataTableNoCheckboxScroll.vue
Normal file
124
management-ui/src/customcompometns/DataTableNoCheckboxScroll.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts" generic="TData, TValue">
|
||||||
|
import { defineProps } from 'vue'
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table'
|
||||||
|
import {
|
||||||
|
useVueTable,
|
||||||
|
getCoreRowModel,
|
||||||
|
FlexRender,
|
||||||
|
} from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table, TableHeader, TableRow, TableHead, TableBody, TableCell,
|
||||||
|
} from '@/components/ui/table'
|
||||||
|
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' // <-- add ScrollBar
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
columns: ColumnDef<TData, TValue>[]
|
||||||
|
data: TData[]
|
||||||
|
dropdownComponent?: DefineComponent<{ row: TData }, any, any>
|
||||||
|
dropdownProps?: Record<string, any>
|
||||||
|
/** Optional tailwind class to control min table width for horizontal scrolling */
|
||||||
|
minTableWidth?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const table = useVueTable({
|
||||||
|
get data() { return props.data },
|
||||||
|
get columns() { return props.columns },
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'row-updated', row: TData, payload: any): void
|
||||||
|
(e: 'row-deleted', row: TData): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const minWidthClass = props.minTableWidth ?? 'min-w-[1100px]' // tweak as needed
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full border rounded-md flex flex-col">
|
||||||
|
<!-- Both-direction scroll area -->
|
||||||
|
<ScrollArea class="flex-1 w-full">
|
||||||
|
<!-- The min-width container enables horizontal scroll on small displays -->
|
||||||
|
<div :class="['w-full', minWidthClass]">
|
||||||
|
<Table class="w-full">
|
||||||
|
<!-- header -->
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow
|
||||||
|
v-for="headerGroup in table.getHeaderGroups()"
|
||||||
|
:key="headerGroup.id"
|
||||||
|
>
|
||||||
|
<TableHead
|
||||||
|
v-for="header in headerGroup.headers"
|
||||||
|
:key="header.id"
|
||||||
|
class="sticky top-0 bg-background z-10 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<FlexRender
|
||||||
|
v-if="!header.isPlaceholder"
|
||||||
|
:render="header.column.columnDef.header"
|
||||||
|
:props="header.getContext()"
|
||||||
|
/>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<!-- extra empty head for dropdown column -->
|
||||||
|
<TableHead
|
||||||
|
v-if="props.dropdownComponent"
|
||||||
|
class="sticky top-0 bg-background z-10 w-12"
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<!-- body -->
|
||||||
|
<TableBody>
|
||||||
|
<template v-if="table.getRowModel().rows.length">
|
||||||
|
<TableRow
|
||||||
|
v-for="row in table.getRowModel().rows"
|
||||||
|
:key="row.id"
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<!-- data cells -->
|
||||||
|
<TableCell
|
||||||
|
v-for="cell in row.getVisibleCells()"
|
||||||
|
:key="cell.id"
|
||||||
|
>
|
||||||
|
<FlexRender
|
||||||
|
:render="cell.column.columnDef.cell"
|
||||||
|
:props="cell.getContext()"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<!-- dropdown cell -->
|
||||||
|
<TableCell v-if="props.dropdownComponent" class="text-right">
|
||||||
|
<component
|
||||||
|
:is="props.dropdownComponent"
|
||||||
|
:row="row.original"
|
||||||
|
v-bind="props.dropdownProps"
|
||||||
|
@updated="(payload: any) => emit('row-updated', row.original, payload)"
|
||||||
|
@deleted="() => emit('row-deleted', row.original)"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- no-data row -->
|
||||||
|
<template v-else>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
:colspan="props.columns.length + (props.dropdownComponent ? 1 : 0)"
|
||||||
|
class="h-24 text-center"
|
||||||
|
>
|
||||||
|
No data.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scrollbars -->
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
<ScrollBar orientation="vertical" />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -3,12 +3,50 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||||||
import DeviceDashboard from './DeviceDashboard.vue';
|
import DeviceDashboard from './DeviceDashboard.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import DeviceRecordings from './DeviceRecordings.vue';
|
import DeviceRecordings from './DeviceRecordings.vue';
|
||||||
import { type PropType } from 'vue';
|
import { ref, type PropType } from 'vue';
|
||||||
|
import type { Task } from '@/lib/interfaces';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
guid: { type: String as PropType<string>, required: true },
|
guid: { type: String as PropType<string>, required: true },
|
||||||
})
|
})
|
||||||
|
const sending = ref(false)
|
||||||
|
|
||||||
|
async function startStreaming() {
|
||||||
|
const dto: Task = {
|
||||||
|
type: 'start_stream',
|
||||||
|
payload: '' // empty as requested
|
||||||
|
}
|
||||||
|
// debug
|
||||||
|
console.log('CreateTaskDto →', dto)
|
||||||
|
|
||||||
|
try {
|
||||||
|
sending.value = true
|
||||||
|
await api.post(`/device/${encodeURIComponent(props.guid)}/task`, dto)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create task:', e)
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopStreaming() {
|
||||||
|
const dto: Task = {
|
||||||
|
type: 'stop_stream',
|
||||||
|
payload: '' // empty as requested
|
||||||
|
}
|
||||||
|
// debug
|
||||||
|
console.log('CreateTaskDto →', dto)
|
||||||
|
|
||||||
|
try {
|
||||||
|
sending.value = true
|
||||||
|
await api.post(`/device/${encodeURIComponent(props.guid)}/task`, dto)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create task:', e)
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Tabs default-value="records">
|
<Tabs default-value="records">
|
||||||
@@ -24,18 +62,26 @@ const props = defineProps({
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="dashboard">
|
<TabsContent value="dashboard">
|
||||||
<DeviceDashboard/>
|
<DeviceDashboard :guid="guid" />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="records">
|
<TabsContent value="records">
|
||||||
<DeviceRecordings :guid="guid"/>
|
<DeviceRecordings :guid="guid" />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="livestream">
|
<TabsContent value="livestream">
|
||||||
<Button>
|
<!-- <Button>
|
||||||
Start streaming
|
Start streaming
|
||||||
</Button>
|
</Button>
|
||||||
<Button>
|
<Button>
|
||||||
Stop streaming
|
Stop streaming
|
||||||
|
</Button> -->
|
||||||
|
<div class="flex space-x-4 pt-2 gap-4">
|
||||||
|
<Button :disabled="sending" @click="startStreaming">
|
||||||
|
{{ sending ? 'Starting…' : 'Start streaming' }}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button :disabled="sending" @click="stopStreaming">
|
||||||
|
{{ sending ? 'Stopping…' : 'Stop streaming' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</template>
|
</template>
|
||||||
@@ -18,11 +18,54 @@ import {
|
|||||||
NumberFieldInput,
|
NumberFieldInput,
|
||||||
} from '@/components/ui/number-field'
|
} from '@/components/ui/number-field'
|
||||||
import AssignDevice from "./AssignDevice.vue";
|
import AssignDevice from "./AssignDevice.vue";
|
||||||
import { ref } from "vue";
|
import { ref, type PropType } from "vue";
|
||||||
import Separator from "@/components/ui/separator/Separator.vue";
|
import Separator from "@/components/ui/separator/Separator.vue";
|
||||||
import DataRangePicker from "./DataRangePicker.vue";
|
import DataRangePicker from "./DataRangePicker.vue";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
import type { Task } from "@/lib/interfaces";
|
||||||
const selectedUserIds = ref<string[]>([])
|
const selectedUserIds = ref<string[]>([])
|
||||||
const usrIDs = selectedUserIds.value
|
const usrIDs = selectedUserIds.value
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
guid: { type: String as PropType<string>, required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const sending = ref(false)
|
||||||
|
async function startRecording() {
|
||||||
|
const dto: Task = {
|
||||||
|
type: 'start_recording',
|
||||||
|
payload: '' // empty as requested
|
||||||
|
}
|
||||||
|
// debug
|
||||||
|
console.log('CreateTaskDto →', dto)
|
||||||
|
|
||||||
|
try {
|
||||||
|
sending.value = true
|
||||||
|
await api.post(`/device/${encodeURIComponent(props.guid)}/task`, dto)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create task:', e)
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopRecording() {
|
||||||
|
const dto: Task = {
|
||||||
|
type: 'stop_recording',
|
||||||
|
payload: '' // empty as requested
|
||||||
|
}
|
||||||
|
// debug
|
||||||
|
console.log('CreateTaskDto →', dto)
|
||||||
|
|
||||||
|
try {
|
||||||
|
sending.value = true
|
||||||
|
await api.post(`/device/${encodeURIComponent(props.guid)}/task`, dto)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create task:', e)
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -41,8 +84,12 @@ const usrIDs = selectedUserIds.value
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex space-x-4 pt-2 gap-4">
|
<div class="flex space-x-4 pt-2 gap-4">
|
||||||
<Button>Start recording</Button>
|
<Button :disabled="sending" @click="startRecording">
|
||||||
<Button>Stop recording</Button>
|
{{ sending ? 'Starting…' : 'Start recording' }}
|
||||||
|
</Button>
|
||||||
|
<Button :disabled="sending" @click="stopRecording">
|
||||||
|
{{ sending ? 'Stopping…' : 'Stop recording' }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2 gap-4">
|
<div class="space-y-2 gap-4">
|
||||||
<NumberField id="duration" :default-value="120" :min="30">
|
<NumberField id="duration" :default-value="120" :min="30">
|
||||||
|
|||||||
@@ -1,47 +1,129 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import type { Device } from '@/lib/interfaces';
|
import type { Device, TaskDto, TaskListResp } from '@/lib/interfaces'
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue'
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table'
|
||||||
|
import DataTableNoCheckbox from './DataTableNoCheckbox.vue'
|
||||||
|
import DataTableNoCheckboxScroll from './DataTableNoCheckboxScroll.vue'
|
||||||
|
import { ref, watch, h } from 'vue' // <-- import h
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: { type: Boolean as PropType<boolean>, required: true },
|
||||||
type: Boolean as PropType<boolean>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
device: { type: Object as PropType<Device>, required: false },
|
device: { type: Object as PropType<Device>, required: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', v: boolean): void
|
(e: 'update:modelValue', v: boolean): void
|
||||||
(e: 'confirm'): void
|
(e: 'confirm'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function onSave() {
|
const tasks = ref<TaskDto[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
async function fetchTasks() {
|
||||||
|
if (!props.device?.guid) return
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<TaskListResp>(`/device/${encodeURIComponent(props.device.guid)}/tasks`)
|
||||||
|
// server returns { limit, offset, total, tasks: [...] }
|
||||||
|
tasks.value = Array.isArray(data?.tasks) ? data.tasks : []
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
error.value = e?.message ?? 'Failed to load tasks'
|
||||||
|
tasks.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (open) => { if (open) fetchTasks() })
|
||||||
|
watch(() => props.device?.guid, (n, o) => {
|
||||||
|
if (props.modelValue && n && n !== o) fetchTasks()
|
||||||
|
})
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
emit('confirm')
|
emit('confirm')
|
||||||
// close the dialog
|
|
||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmt(ts?: string | null) {
|
||||||
|
if (!ts) return ''
|
||||||
|
try { return new Date(ts).toLocaleString() } catch { return ts ?? '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const task_columns: ColumnDef<TaskDto, any>[] = [
|
||||||
|
{ accessorKey: 'id', header: 'ID' },
|
||||||
|
// { accessorKey: 'deviceGuid', header: 'GUID' },
|
||||||
|
{ accessorKey: 'type', header: 'Task' },
|
||||||
|
{
|
||||||
|
accessorKey: 'payload',
|
||||||
|
header: 'Command',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const p = row.original.payload ?? ''
|
||||||
|
return p.length > 80 ? `${p.slice(0, 80)}…` : p
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
// Return a VNode, not an HTML string
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const s = row.original.status
|
||||||
|
const cls =
|
||||||
|
s === 'finished' ? 'px-2 py-0.5 rounded text-xs text-green-700 bg-green-100'
|
||||||
|
: s === 'running' ? 'px-2 py-0.5 rounded text-xs text-blue-700 bg-blue-100'
|
||||||
|
: s === 'error' ? 'px-2 py-0.5 rounded text-xs text-red-700 bg-red-100'
|
||||||
|
: 'px-2 py-0.5 rounded text-xs text-amber-700 bg-amber-100'
|
||||||
|
return h('span', { class: cls }, s)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ accessorKey: 'error', header: 'Error', cell: ({ row }) => row.original.error ?? '' },
|
||||||
|
{
|
||||||
|
accessorKey: 'result',
|
||||||
|
header: 'Result',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const r = row.original.result ?? ''
|
||||||
|
return r.length > 80 ? `${r.slice(0, 80)}…` : r
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ accessorKey: 'createdAt', header: 'Created', cell: ({ row }) => fmt(row.original.createdAt) },
|
||||||
|
{ accessorKey: 'startedAt', header: 'Started', cell: ({ row }) => fmt(row.original.startedAt) },
|
||||||
|
{ accessorKey: 'finishedAt', header: 'Finished', cell: ({ row }) => fmt(row.original.finishedAt) },
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :open="props.modelValue" @update:open="(v: boolean) => emit('update:modelValue', v)">
|
<Dialog :open="props.modelValue" @update:open="(v: boolean) => emit('update:modelValue', v)">
|
||||||
<DialogContent class="sm:min-w-[800px]">
|
<DialogContent class="sm:min-w-[1000px]">
|
||||||
<DialogHeader>
|
<DialogHeader class="flex flex-row items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
<DialogTitle>Tasks</DialogTitle>
|
<DialogTitle>Tasks</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>{{ props.device?.guid }}</DialogDescription>
|
||||||
{{ props.device?.guid }}
|
</div>
|
||||||
</DialogDescription>
|
<div class="flex gap-2">
|
||||||
|
<Button variant="outline" :disabled="loading || !props.device?.guid" @click="fetchTasks">
|
||||||
|
{{ loading ? 'Loading…' : 'Refresh' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="error" class="text-sm text-red-600 mb-3">{{ error }}</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="text-sm text-muted-foreground py-8 text-center">
|
||||||
|
Loading tasks…
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<DataTableNoCheckboxScroll :columns="task_columns" :data="tasks" minTableWidth="min-w-[800px]"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button @click="onSave">Close</Button>
|
<Button @click="onClose">Close</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -9,3 +9,39 @@ export interface Users {
|
|||||||
username: string,
|
username: string,
|
||||||
role: string
|
role: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeviceTaskType =
|
||||||
|
| 'start_stream'
|
||||||
|
| 'stop_stream'
|
||||||
|
| 'start_recording'
|
||||||
|
| 'stop_recording'
|
||||||
|
| 'update_config'
|
||||||
|
| 'set_deep_sleep'
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
type: DeviceTaskType
|
||||||
|
/** Raw JSON string (e.g. '{"sleepTimeout":5}') */
|
||||||
|
payload: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TaskStatus = 'pending' | 'running' | 'finished' | 'error'
|
||||||
|
|
||||||
|
export interface TaskDto {
|
||||||
|
id: number
|
||||||
|
deviceGuid: string
|
||||||
|
type: DeviceTaskType
|
||||||
|
payload: string // raw JSON string
|
||||||
|
status: TaskStatus
|
||||||
|
error?: string // from `ErrorMsg` -> `error`
|
||||||
|
result?: string
|
||||||
|
createdAt: string // ISO string from API
|
||||||
|
startedAt?: string | null // ISO string or null
|
||||||
|
finishedAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskListResp {
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
total: number
|
||||||
|
tasks: TaskDto[]
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ type CreateTaskDto struct {
|
|||||||
Type models.DeviceTaskType `json:"type" binding:"required,oneof=start_stream stop_stream start_recording stop_recording update_config set_deep_sleep"`
|
Type models.DeviceTaskType `json:"type" binding:"required,oneof=start_stream stop_stream start_recording stop_recording update_config set_deep_sleep"`
|
||||||
// Pass raw JSON string as payload (e.g. {"sleepTimeout":5,"jitterMs":50,"recordingDurationSec":60})
|
// Pass raw JSON string as payload (e.g. {"sleepTimeout":5,"jitterMs":50,"recordingDurationSec":60})
|
||||||
// Keep it string to let device/server evolve freely.
|
// Keep it string to let device/server evolve freely.
|
||||||
Payload string `json:"payload" binding:"required"`
|
Payload string `json:"payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device polls: single next task
|
// Device polls: single next task
|
||||||
|
|||||||
Reference in New Issue
Block a user