diff --git a/.gitignore b/.gitignore index 0d319cf..4261dc7 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? -.env \ No newline at end of file +.env +.vs \ No newline at end of file diff --git a/management-ui/package-lock.json b/management-ui/package-lock.json index fed64d4..bed06ce 100644 --- a/management-ui/package-lock.json +++ b/management-ui/package-lock.json @@ -10,12 +10,12 @@ "dependencies": { "@tailwindcss/vite": "^4.1.11", "@tanstack/vue-table": "^8.21.3", - "@vueuse/core": "^13.5.0", + "@vueuse/core": "^13.9.0", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-vue-next": "^0.525.0", - "reka-ui": "^2.4.0", + "reka-ui": "^2.5.0", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.6", @@ -1495,14 +1495,14 @@ } }, "node_modules/@vueuse/core": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.5.0.tgz", - "integrity": "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz", + "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "13.5.0", - "@vueuse/shared": "13.5.0" + "@vueuse/metadata": "13.9.0", + "@vueuse/shared": "13.9.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -1512,18 +1512,18 @@ } }, "node_modules/@vueuse/metadata": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.5.0.tgz", - "integrity": "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz", + "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.5.0.tgz", - "integrity": "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz", + "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -2413,9 +2413,9 @@ "license": "MIT" }, "node_modules/reka-ui": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.4.0.tgz", - "integrity": "sha512-5WHLEquWI5W67NnjH9F+RhpzRAcwjAEQHtHZMa5wYdhClcYDr59q0RAAcymcnfndFqv0MiBxyFfzbGSRyIZG5g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.5.0.tgz", + "integrity": "sha512-81aMAmJeVCy2k0E6x7n1kypDY6aM1ldLis5+zcdV1/JtoAlSDck5OBsyLRJU9CfgbrQp1ImnRnBSmC4fZ2fkZQ==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.6.13", diff --git a/management-ui/package.json b/management-ui/package.json index 6c5912e..c3f96ee 100644 --- a/management-ui/package.json +++ b/management-ui/package.json @@ -11,12 +11,12 @@ "dependencies": { "@tailwindcss/vite": "^4.1.11", "@tanstack/vue-table": "^8.21.3", - "@vueuse/core": "^13.5.0", + "@vueuse/core": "^13.9.0", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-vue-next": "^0.525.0", - "reka-ui": "^2.4.0", + "reka-ui": "^2.5.0", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.6", diff --git a/management-ui/src/components/ui/button/Button.vue b/management-ui/src/components/ui/button/Button.vue index 6498db9..d9c7ded 100644 --- a/management-ui/src/components/ui/button/Button.vue +++ b/management-ui/src/components/ui/button/Button.vue @@ -1,27 +1,29 @@ - - + + + diff --git a/management-ui/src/components/ui/button/index.ts b/management-ui/src/components/ui/button/index.ts index 23edd68..bc13b1c 100644 --- a/management-ui/src/components/ui/button/index.ts +++ b/management-ui/src/components/ui/button/index.ts @@ -1,34 +1,35 @@ -import { cva, type VariantProps } from 'class-variance-authority' +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" -export { default as Button } from './Button.vue' +export { default as Button } from "./Button.vue" export const buttonVariants = cva( - 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: - 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: - 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: - 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: - 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', - link: 'text-primary underline-offset-4 hover:underline', + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", }, size: { - default: 'h-9 px-4 py-2 has-[>svg]:px-3', - sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', - lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', - icon: 'size-9', + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, }, ) diff --git a/management-ui/src/components/ui/number-field/NumberField.vue b/management-ui/src/components/ui/number-field/NumberField.vue new file mode 100644 index 0000000..d0f15a2 --- /dev/null +++ b/management-ui/src/components/ui/number-field/NumberField.vue @@ -0,0 +1,20 @@ + + + diff --git a/management-ui/src/components/ui/number-field/NumberFieldContent.vue b/management-ui/src/components/ui/number-field/NumberFieldContent.vue new file mode 100644 index 0000000..35c69b2 --- /dev/null +++ b/management-ui/src/components/ui/number-field/NumberFieldContent.vue @@ -0,0 +1,14 @@ + + + diff --git a/management-ui/src/components/ui/number-field/NumberFieldDecrement.vue b/management-ui/src/components/ui/number-field/NumberFieldDecrement.vue new file mode 100644 index 0000000..c743d18 --- /dev/null +++ b/management-ui/src/components/ui/number-field/NumberFieldDecrement.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/number-field/NumberFieldIncrement.vue b/management-ui/src/components/ui/number-field/NumberFieldIncrement.vue new file mode 100644 index 0000000..982581e --- /dev/null +++ b/management-ui/src/components/ui/number-field/NumberFieldIncrement.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/number-field/NumberFieldInput.vue b/management-ui/src/components/ui/number-field/NumberFieldInput.vue new file mode 100644 index 0000000..8e4df3c --- /dev/null +++ b/management-ui/src/components/ui/number-field/NumberFieldInput.vue @@ -0,0 +1,16 @@ + + + diff --git a/management-ui/src/components/ui/number-field/index.ts b/management-ui/src/components/ui/number-field/index.ts new file mode 100644 index 0000000..5808279 --- /dev/null +++ b/management-ui/src/components/ui/number-field/index.ts @@ -0,0 +1,5 @@ +export { default as NumberField } from "./NumberField.vue" +export { default as NumberFieldContent } from "./NumberFieldContent.vue" +export { default as NumberFieldDecrement } from "./NumberFieldDecrement.vue" +export { default as NumberFieldIncrement } from "./NumberFieldIncrement.vue" +export { default as NumberFieldInput } from "./NumberFieldInput.vue" diff --git a/management-ui/src/components/ui/popover/Popover.vue b/management-ui/src/components/ui/popover/Popover.vue new file mode 100644 index 0000000..732ae06 --- /dev/null +++ b/management-ui/src/components/ui/popover/Popover.vue @@ -0,0 +1,18 @@ + + + diff --git a/management-ui/src/components/ui/popover/PopoverAnchor.vue b/management-ui/src/components/ui/popover/PopoverAnchor.vue new file mode 100644 index 0000000..fbb2beb --- /dev/null +++ b/management-ui/src/components/ui/popover/PopoverAnchor.vue @@ -0,0 +1,15 @@ + + + diff --git a/management-ui/src/components/ui/popover/PopoverContent.vue b/management-ui/src/components/ui/popover/PopoverContent.vue new file mode 100644 index 0000000..9fd1460 --- /dev/null +++ b/management-ui/src/components/ui/popover/PopoverContent.vue @@ -0,0 +1,46 @@ + + + diff --git a/management-ui/src/components/ui/popover/PopoverTrigger.vue b/management-ui/src/components/ui/popover/PopoverTrigger.vue new file mode 100644 index 0000000..431713b --- /dev/null +++ b/management-ui/src/components/ui/popover/PopoverTrigger.vue @@ -0,0 +1,15 @@ + + + diff --git a/management-ui/src/components/ui/popover/index.ts b/management-ui/src/components/ui/popover/index.ts new file mode 100644 index 0000000..ea8eb33 --- /dev/null +++ b/management-ui/src/components/ui/popover/index.ts @@ -0,0 +1,4 @@ +export { default as Popover } from "./Popover.vue" +export { default as PopoverAnchor } from "./PopoverAnchor.vue" +export { default as PopoverContent } from "./PopoverContent.vue" +export { default as PopoverTrigger } from "./PopoverTrigger.vue" diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendar.vue b/management-ui/src/components/ui/range-calendar/RangeCalendar.vue new file mode 100644 index 0000000..9372fdf --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendar.vue @@ -0,0 +1,62 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarCell.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarCell.vue new file mode 100644 index 0000000..37eecc3 --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarCell.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarCellTrigger.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarCellTrigger.vue new file mode 100644 index 0000000..17f5cb1 --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarCellTrigger.vue @@ -0,0 +1,41 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarGrid.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarGrid.vue new file mode 100644 index 0000000..43b8704 --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarGrid.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarGridBody.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarGridBody.vue new file mode 100644 index 0000000..a29fb80 --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarGridBody.vue @@ -0,0 +1,15 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarGridHead.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarGridHead.vue new file mode 100644 index 0000000..54cad9c --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarGridHead.vue @@ -0,0 +1,15 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarGridRow.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarGridRow.vue new file mode 100644 index 0000000..2e79c52 --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarGridRow.vue @@ -0,0 +1,22 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarHeadCell.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarHeadCell.vue new file mode 100644 index 0000000..f611b5f --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarHeadCell.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarHeader.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarHeader.vue new file mode 100644 index 0000000..45386d7 --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarHeader.vue @@ -0,0 +1,23 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarHeading.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarHeading.vue new file mode 100644 index 0000000..00840ed --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarHeading.vue @@ -0,0 +1,30 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarNextButton.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarNextButton.vue new file mode 100644 index 0000000..0db72fa --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarNextButton.vue @@ -0,0 +1,32 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/RangeCalendarPrevButton.vue b/management-ui/src/components/ui/range-calendar/RangeCalendarPrevButton.vue new file mode 100644 index 0000000..d98b175 --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/RangeCalendarPrevButton.vue @@ -0,0 +1,32 @@ + + + diff --git a/management-ui/src/components/ui/range-calendar/index.ts b/management-ui/src/components/ui/range-calendar/index.ts new file mode 100644 index 0000000..8155287 --- /dev/null +++ b/management-ui/src/components/ui/range-calendar/index.ts @@ -0,0 +1,12 @@ +export { default as RangeCalendar } from "./RangeCalendar.vue" +export { default as RangeCalendarCell } from "./RangeCalendarCell.vue" +export { default as RangeCalendarCellTrigger } from "./RangeCalendarCellTrigger.vue" +export { default as RangeCalendarGrid } from "./RangeCalendarGrid.vue" +export { default as RangeCalendarGridBody } from "./RangeCalendarGridBody.vue" +export { default as RangeCalendarGridHead } from "./RangeCalendarGridHead.vue" +export { default as RangeCalendarGridRow } from "./RangeCalendarGridRow.vue" +export { default as RangeCalendarHeadCell } from "./RangeCalendarHeadCell.vue" +export { default as RangeCalendarHeader } from "./RangeCalendarHeader.vue" +export { default as RangeCalendarHeading } from "./RangeCalendarHeading.vue" +export { default as RangeCalendarNextButton } from "./RangeCalendarNextButton.vue" +export { default as RangeCalendarPrevButton } from "./RangeCalendarPrevButton.vue" diff --git a/management-ui/src/customcompometns/DataRangePicker.vue b/management-ui/src/customcompometns/DataRangePicker.vue new file mode 100644 index 0000000..9c20a6b --- /dev/null +++ b/management-ui/src/customcompometns/DataRangePicker.vue @@ -0,0 +1,54 @@ + + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/DeviceComponent.vue b/management-ui/src/customcompometns/DeviceComponent.vue index a5d6645..62ddb09 100644 --- a/management-ui/src/customcompometns/DeviceComponent.vue +++ b/management-ui/src/customcompometns/DeviceComponent.vue @@ -1,10 +1,21 @@ \ No newline at end of file diff --git a/management-ui/src/customcompometns/DeviceDashboard.vue b/management-ui/src/customcompometns/DeviceDashboard.vue new file mode 100644 index 0000000..d757b36 --- /dev/null +++ b/management-ui/src/customcompometns/DeviceDashboard.vue @@ -0,0 +1,70 @@ + + + \ No newline at end of file diff --git a/management-ui/src/customcompometns/DeviceRecordings.vue b/management-ui/src/customcompometns/DeviceRecordings.vue new file mode 100644 index 0000000..de17522 --- /dev/null +++ b/management-ui/src/customcompometns/DeviceRecordings.vue @@ -0,0 +1,158 @@ + + \ No newline at end of file diff --git a/management-ui/src/pages/DeviceView.vue b/management-ui/src/pages/DeviceView.vue index 53c087b..0de30aa 100644 --- a/management-ui/src/pages/DeviceView.vue +++ b/management-ui/src/pages/DeviceView.vue @@ -2,12 +2,15 @@ import { useColorMode } from '@vueuse/core' import Navbar from '@/customcompometns/Navbar.vue'; import DeviceComponent from '@/customcompometns/DeviceComponent.vue'; +import { useRoute } from 'vue-router'; +import { computed } from 'vue'; const mode = useColorMode() - +const route = useRoute() +const guid = computed(() => String(route.params.guid ?? route.query.guid ?? '')) \ No newline at end of file diff --git a/server/internal/handlers/records.go b/server/internal/handlers/records.go index 6110f64..f9771f5 100644 --- a/server/internal/handlers/records.go +++ b/server/internal/handlers/records.go @@ -1,8 +1,8 @@ package handlers import ( - "context" "fmt" + "io" "mime/multipart" "net/http" "path" @@ -111,13 +111,35 @@ func (h *RecordsHandler) File(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var rec models.Record if err := h.db.First(&rec, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) + c.Status(http.StatusNotFound) return } - u, err := h.minio.PresignedGetObject(context.Background(), h.recordsBucket, rec.ObjectKey, h.presignTTL, nil) + // Stream from MinIO to the client to avoid redirecting to an internal host like "minio". + obj, err := h.minio.GetObject(c.Request.Context(), h.recordsBucket, rec.ObjectKey, minio.GetObjectOptions{}) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "presign failed"}) + c.Status(http.StatusInternalServerError) return } - c.Redirect(http.StatusFound, u.String()) + defer obj.Close() + + info, err := h.minio.StatObject(c.Request.Context(), h.recordsBucket, rec.ObjectKey, minio.StatObjectOptions{}) + if err != nil { + c.Status(http.StatusInternalServerError) + return + } + + if ct := info.ContentType; ct != "" { + c.Header("Content-Type", ct) + } else { + c.Header("Content-Type", "application/octet-stream") + } + if info.Size >= 0 { + c.Header("Content-Length", strconv.FormatInt(info.Size, 10)) + } + if filename := path.Base(rec.ObjectKey); filename != "" { + c.Header("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename)) + } + + c.Status(http.StatusOK) + _, _ = io.Copy(c.Writer, obj) } diff --git a/server/internal/router/router.go b/server/internal/router/router.go index 99c2702..3d3ae20 100644 --- a/server/internal/router/router.go +++ b/server/internal/router/router.go @@ -48,7 +48,7 @@ func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine { r.POST("/devices/:guid/set_users", authMW, adminOnly, devH.SetUsers) r.POST("/devices/:guid/remove_from_user", authMW, devH.RemoveFromUser) - r.POST("/records/upload", authMW, recH.Upload) + r.POST("/records/upload", recH.Upload) r.GET("/records", authMW, recH.List) r.GET("/records/:id/file", authMW, recH.File)