diff --git a/app/assets/css/tailwind.css b/app/assets/css/tailwind.css index 280d20f..64d822f 100644 --- a/app/assets/css/tailwind.css +++ b/app/assets/css/tailwind.css @@ -42,8 +42,8 @@ --vis-tooltip-backdrop-filter: none !important; --vis-tooltip-padding: none !important; - --vis-primary-color: 198 93% 60%; - --vis-secondary-color: 158 64% 52%; + --vis-primary-color: 158 64% 52%; + --vis-secondary-color: 198 93% 60%; /* --vis-secondary-color: 160 81% 40%; */ /* --vis-secondary-color: var(--primary); */ --vis-text-color: var(--muted-foreground); diff --git a/app/components/dashboard/TimePicker.vue b/app/components/dashboard/TimePicker.vue new file mode 100644 index 0000000..d67b9f5 --- /dev/null +++ b/app/components/dashboard/TimePicker.vue @@ -0,0 +1,94 @@ + + + + + + + + + + {{ $t('dashboard.time_picker.today') }} + + + {{ $t('dashboard.time_picker.last_5m') }} + + + {{ $t('dashboard.time_picker.last_10m') }} + + + {{ $t('dashboard.time_picker.last_30m') }} + + + {{ $t('dashboard.time_picker.last_1h') }} + + + {{ $t('dashboard.time_picker.last_6h') }} + + + {{ $t('dashboard.time_picker.last_12h') }} + + + {{ $t('dashboard.time_picker.last_24h') }} + + + + diff --git a/app/components/dashboard/analysis/Views.vue b/app/components/dashboard/analysis/Views.vue index 9ed866a..01555f9 100644 --- a/app/components/dashboard/analysis/Views.vue +++ b/app/components/dashboard/analysis/Views.vue @@ -2,15 +2,30 @@ import { AreaChart } from '@/components/ui/chart-area' import { BarChart } from '@/components/ui/chart-bar' +const props = defineProps({ + mode: { + type: String, + default: 'full', + }, + chartType: { + type: String, + default: 'area', + }, +}) + const views = ref([]) -const chart = computed(() => views.value.length > 1 ? AreaChart : BarChart) +const chart = computed(() => (props.chartType === 'area' && views.value.length > 1) ? AreaChart : BarChart) const id = inject('id') const time = inject('time') const filters = inject('filters') +const OneHour = 60 * 60 // 1 hour in seconds const OneDay = 24 * 60 * 60 // 1 day in seconds function getUnit(startAt, endAt) { + if (startAt && endAt && endAt - startAt <= OneHour) + return 'minute' + if (startAt && endAt && endAt - startAt <= OneDay) return 'hour' @@ -61,16 +76,20 @@ function formatTime(tick) { - + {{ $t('dashboard.views') }} diff --git a/app/components/dashboard/realtime/Chart.vue b/app/components/dashboard/realtime/Chart.vue new file mode 100644 index 0000000..7e248bf --- /dev/null +++ b/app/components/dashboard/realtime/Chart.vue @@ -0,0 +1,56 @@ + + + + + + + + + {{ $t('dashboard.visits') }} + + + + + + + + + + diff --git a/app/components/dashboard/realtime/Globe.vue b/app/components/dashboard/realtime/Globe.vue index b07f527..08985e8 100644 --- a/app/components/dashboard/realtime/Globe.vue +++ b/app/components/dashboard/realtime/Globe.vue @@ -13,6 +13,9 @@ const props = defineProps({ }, }) +const time = inject('time') +const filters = inject('filters') + const countries = ref({}) const locations = ref([]) @@ -39,7 +42,9 @@ async function getGlobeJSON() { async function getLiveLocations() { const { data } = await useAPI('/api/logs/locations', { query: { - startAt: Math.floor(Date.now() / 1000) - 60 * props.minutes, + startAt: time.value.startAt, + endAt: time.value.endAt, + ...filters.value, }, }) locations.value = data?.map(e => ({ @@ -59,7 +64,7 @@ function initGlobe() { // .globeOffset([width.value > 768 ? -100 : 0, width.value > 768 ? 0 : 100]) .atmosphereColor('rgba(170, 170, 200, 1)') .globeMaterial(new MeshPhongMaterial({ - color: 'hsl(220.9, 30.3%, 16%)', + color: 'rgb(228, 228, 231)', transparent: false, opacity: 1, })) @@ -99,6 +104,10 @@ function stopRotation() { } } +const stopWatchQueryChange = watch([time, filters], getLiveLocations, { + deep: true, +}) + watch(width, () => { if (globe) { globe.width(size.value.width) @@ -106,10 +115,20 @@ watch(width, () => { } }) +watch(locations, () => { + if (globe) { + globe.hexBinPointsData(locations.value) + } +}) + onMounted(async () => { await Promise.all([getGlobeJSON(), getLiveLocations()]) initGlobe() }) + +onBeforeUnmount(() => { + stopWatchQueryChange() +}) diff --git a/app/components/dashboard/realtime/Index.vue b/app/components/dashboard/realtime/Index.vue index cb59ed4..5e8dee5 100644 --- a/app/components/dashboard/realtime/Index.vue +++ b/app/components/dashboard/realtime/Index.vue @@ -1,12 +1,70 @@ + + - Mode & TimeRange + - + + + + + + - - diff --git a/app/components/dashboard/realtime/Logs.vue b/app/components/dashboard/realtime/Logs.vue index 0c504ea..362f6d4 100644 --- a/app/components/dashboard/realtime/Logs.vue +++ b/app/components/dashboard/realtime/Logs.vue @@ -1,111 +1,49 @@ - - - - - - - - - - - - - - - - - - - - No results. - - - - - + + + + + diff --git a/app/components/spark-ui/AnimatedList.vue b/app/components/spark-ui/AnimatedList.vue new file mode 100644 index 0000000..fc9bf86 --- /dev/null +++ b/app/components/spark-ui/AnimatedList.vue @@ -0,0 +1,93 @@ + + + + + + + + + + + + + diff --git a/app/components/spark-ui/Notification.vue b/app/components/spark-ui/Notification.vue new file mode 100644 index 0000000..41e3cd8 --- /dev/null +++ b/app/components/spark-ui/Notification.vue @@ -0,0 +1,45 @@ + + + + + + + {{ props.icon }} + + + + {{ props.name }} + · + {{ shortTime(props.time) }} + + + {{ props.description }} + + + + + diff --git a/app/components/ui/chart/ChartCrosshair.vue b/app/components/ui/chart/ChartCrosshair.vue index ff7f354..c9e3bf2 100644 --- a/app/components/ui/chart/ChartCrosshair.vue +++ b/app/components/ui/chart/ChartCrosshair.vue @@ -25,7 +25,7 @@ function template(d: any) { const omittedData = Object.entries(omit(d, [props.index])).map(([key, value]) => { const legendReference = props.items.find(i => i.name === key) return { ...legendReference, value } - }) + }).filter(i => i.name) const TooltipComponent = props.customTooltip ?? ChartTooltip createApp(TooltipComponent, { title: d[props.index].toString(), data: omittedData }).mount(componentDiv) wm.set(d, componentDiv.innerHTML) diff --git a/app/utils/color.ts b/app/utils/color.ts index 7a9b7f4..c455d9b 100644 --- a/app/utils/color.ts +++ b/app/utils/color.ts @@ -1,3 +1,3 @@ export function colorGradation(count: number) { - return Array.from({ length: count }, (_, i) => `hsl(var(--vis-secondary-color) / ${1 - (1 / count) * i})`) + return Array.from({ length: count }, (_, i) => `hsl(var(--vis-primary-color) / ${1 - (1 / count) * i})`) } diff --git a/app/utils/time.ts b/app/utils/time.ts index cff40c0..9c35623 100644 --- a/app/utils/time.ts +++ b/app/utils/time.ts @@ -25,7 +25,14 @@ export function longDate(unix = 0) { return new Date(unix * 1000).toLocaleString() } -export function date2unix(dateValue: DateValue | Date, type: string) { +export function shortTime(unix = 0) { + const shortTime = new Intl.DateTimeFormat(undefined, { + timeStyle: 'short', + }) + return shortTime.format(unix * 1000) +} + +export function date2unix(dateValue: DateValue | Date, type?: string) { const date = dateValue instanceof Date ? dateValue : dateValue.toDate(getTimeZone()) if (type === 'start') return Math.floor(date.setHours(0, 0, 0) / 1000) diff --git a/eslint.config.mjs b/eslint.config.mjs index c1f4d60..a64fb52 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ import withNuxt from './.nuxt/eslint.config.mjs' export default withNuxt( antfu(), { - ignores: ['app/components/ui', '.data', 'public/world.json'], + ignores: ['app/components/ui', '.data', 'public/*.json'], }, { rules: { diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index bb2aa9b..7c48f74 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -138,6 +138,16 @@ "os": "OS", "browser": "Browser", "browserType": "Browser Type" + }, + "time_picker": { + "today": "Today", + "last_5m": "Last 5 minutes", + "last_10m": "Last 10 minutes", + "last_30m": "Last 30 minutes", + "last_1h": "Last 1 hour", + "last_6h": "Last 6 hours", + "last_12h": "Last 12 hours", + "last_24h": "Last 24 hours" } }, "links": { diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 40c8c93..abbc750 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -138,6 +138,16 @@ "os": "OS", "browser": "Navigateur", "browserType": "Type de navigateur" + }, + "time_picker": { + "today": "Aujourd'hui", + "last_5m": "5 minutes", + "last_10m": "10 minutes", + "last_30m": "30 minutes", + "last_1h": "1 heure", + "last_6h": "6 heures", + "last_12h": "12 heures", + "last_24h": "24 heures" } }, "links": { diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index 43b826d..1751723 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -138,6 +138,16 @@ "os": "Hệ điều hành", "browser": "Trình duyệt", "browserType": "Loại trình duyệt" + }, + "time_picker": { + "today": "Hôm nay", + "last_5m": "5 phút qua", + "last_10m": "10 phút qua", + "last_30m": "30 phút qua", + "last_1h": "1 giờ qua", + "last_6h": "6 giờ qua", + "last_12h": "12 giờ qua", + "last_24h": "24 giờ qua" } }, "links": { diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index f69abf8..3557a31 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -138,6 +138,16 @@ "os": "操作系统", "browser": "浏览器", "browserType": "浏览器类型" + }, + "time_picker": { + "today": "今天", + "last_5m": "最近5分钟", + "last_10m": "最近10分钟", + "last_30m": "最近30分钟", + "last_1h": "最近1小时", + "last_6h": "最近6小时", + "last_12h": "最近12小时", + "last_24h": "最近24小时" } }, "links": { diff --git a/i18n/locales/zh-TW.json b/i18n/locales/zh-TW.json index 719c101..51ccb4d 100644 --- a/i18n/locales/zh-TW.json +++ b/i18n/locales/zh-TW.json @@ -138,6 +138,16 @@ "os": "操作系統", "browser": "瀏覽器", "browserType": "瀏覽器類型" + }, + "time_picker": { + "today": "今天", + "last_5m": "最近5分鐘", + "last_10m": "最近10分鐘", + "last_30m": "最近30分鐘", + "last_1h": "最近1小時", + "last_6h": "最近6小時", + "last_12h": "最近12小時", + "last_24h": "最近24小時" } }, "links": { diff --git a/nuxt.config.ts b/nuxt.config.ts index 19519c9..5f9c922 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -8,6 +8,7 @@ export default defineNuxtConfig({ 'shadcn-nuxt', '@nuxt/eslint', '@nuxtjs/tailwindcss', + '@vueuse/motion/nuxt', '@nuxtjs/color-mode', '@nuxtjs/i18n', ], @@ -41,8 +42,10 @@ export default defineNuxtConfig({ routeRules: { '/': { prerender: true, + ssr: false, }, '/dashboard/**': { + prerender: true, ssr: false, }, '/dashboard': { diff --git a/package.json b/package.json index 7d00a37..3f4f0f5 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "dependencies": { "@intlify/message-compiler": "^11.1.3", "@number-flow/vue": "^0.3.3", - "@tanstack/vue-table": "^8.21.3", "@unovis/ts": "^1.5.0", "@unovis/vue": "^1.5.0", "@vee-validate/zod": "^4.15.0", "@vueuse/core": "^12.2.0", + "@vueuse/motion": "^3.0.3", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.1.0", "fuse.js": "^7.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a09c9d..1ab9d76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: '@number-flow/vue': specifier: ^0.3.3 version: 0.3.3(vue@3.5.13(typescript@5.7.2)) - '@tanstack/vue-table': - specifier: ^8.21.3 - version: 8.21.3(vue@3.5.13(typescript@5.7.2)) '@unovis/ts': specifier: ^1.5.0 version: 1.5.0 @@ -29,6 +26,9 @@ importers: '@vueuse/core': specifier: ^12.2.0 version: 12.2.0(typescript@5.7.2) + '@vueuse/motion': + specifier: ^3.0.3 + version: 3.0.3(magicast@0.3.5)(vue@3.5.13(typescript@5.7.2)) d3-scale: specifier: ^4.0.2 version: 4.0.2 @@ -2114,22 +2114,12 @@ packages: '@swc/helpers@0.5.11': resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} - '@tanstack/table-core@8.21.3': - resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} - engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.2': resolution: {integrity: sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==} '@tanstack/virtual-core@3.8.1': resolution: {integrity: sha512-uNtAwenT276M9QYCjTBoHZ8X3MUeCRoGK59zPi92hMIxdfS9AyHjkDWJ94WroDxnv48UE+hIeo21BU84jKc8aQ==} - '@tanstack/vue-table@8.21.3': - resolution: {integrity: sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==} - engines: {node: '>=12'} - peerDependencies: - vue: '>=3.2' - '@tanstack/vue-virtual@3.13.2': resolution: {integrity: sha512-z4swzjdhzCh95n9dw9lTvw+t3iwSkYRlVkYkra3C9mul/m5fTzHR7KmtkwH4qXMTXGJUbngtC/bz2cHQIHkO8g==} peerDependencies: @@ -2354,6 +2344,9 @@ packages: '@types/web-bluetooth@0.0.20': resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@typescript-eslint/eslint-plugin@8.18.2': resolution: {integrity: sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2622,6 +2615,11 @@ packages: '@vueuse/core@12.7.0': resolution: {integrity: sha512-jtK5B7YjZXmkGNHjviyGO4s3ZtEhbzSgrbX+s5o+Lr8i2nYqNyHuPVOeTdM1/hZ5Tkxg/KktAuAVDDiHMraMVA==} + '@vueuse/core@13.2.0': + resolution: {integrity: sha512-n5TZoIAxbWAQ3PqdVPDzLgIRQOujFfMlatdI+f7ditSmoEeNpPBvp7h2zamzikCmrhFIePAwdEQB6ENccHr7Rg==} + peerDependencies: + vue: ^3.5.0 + '@vueuse/integrations@12.2.0': resolution: {integrity: sha512-Bc0unXiGNZ0w7xqSvzCuP7AflBRKcZX6ib7tGi7vAjOxhLd6GtN3J8qizIbSPnI62XyPy5fauOkq2i2HUPq6MQ==} peerDependencies: @@ -2672,6 +2670,14 @@ packages: '@vueuse/metadata@12.7.0': resolution: {integrity: sha512-4VvTH9mrjXqFN5LYa5YfqHVRI6j7R00Vy4995Rw7PQxyCL3z0Lli86iN4UemWqixxEvYfRjG+hF9wL8oLOn+3g==} + '@vueuse/metadata@13.2.0': + resolution: {integrity: sha512-kPpzuQCU0+D8DZCzK0iPpIcXI+6ufWSgwnjJ6//GNpEn+SHViaCtR+XurzORChSgvpHO9YC8gGM97Y1kB+UabA==} + + '@vueuse/motion@3.0.3': + resolution: {integrity: sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==} + peerDependencies: + vue: '>=3.0.0' + '@vueuse/shared@10.11.1': resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} @@ -2681,6 +2687,11 @@ packages: '@vueuse/shared@12.7.0': resolution: {integrity: sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==} + '@vueuse/shared@13.2.0': + resolution: {integrity: sha512-vx9ZPDF5HcU9up3Jgt3G62dMUfZEdk6tLyBAHYAG4F4n73vpaA7J5hdncDI/lS9Vm7GA/FPlbOmh9TrDZROTpg==} + peerDependencies: + vue: ^3.5.0 + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -4097,6 +4108,9 @@ packages: frame-ticker@1.0.3: resolution: {integrity: sha512-E0X2u2JIvbEMrqEg5+4BpTqaD22OwojJI63K7MdKHdncjtAhGRbCR8nJCr2vwEt9NWBPCPcu70X9smPviEBy8Q==} + framesync@6.1.2: + resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -4316,6 +4330,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + hey-listen@1.0.8: + resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -5447,6 +5464,9 @@ packages: resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} engines: {node: '>=10'} + popmotion@11.0.5: + resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==} + portfinder@1.0.32: resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} engines: {node: '>= 0.12.0'} @@ -6146,6 +6166,9 @@ packages: striptags@3.2.0: resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} + style-value-types@5.1.2: + resolution: {integrity: sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==} + stylehacks@7.0.4: resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} @@ -6349,6 +6372,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} @@ -9026,17 +9052,10 @@ snapshots: dependencies: tslib: 2.6.3 - '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.2': {} '@tanstack/virtual-core@3.8.1': {} - '@tanstack/vue-table@8.21.3(vue@3.5.13(typescript@5.7.2))': - dependencies: - '@tanstack/table-core': 8.21.3 - vue: 3.5.13(typescript@5.7.2) - '@tanstack/vue-virtual@3.13.2(vue@3.5.13(typescript@5.7.2))': dependencies: '@tanstack/virtual-core': 3.13.2 @@ -9299,6 +9318,8 @@ snapshots: '@types/web-bluetooth@0.0.20': {} + '@types/web-bluetooth@0.0.21': {} + '@typescript-eslint/eslint-plugin@8.18.2(@typescript-eslint/parser@8.18.2(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.2)': dependencies: '@eslint-community/regexpp': 4.11.0 @@ -9764,6 +9785,13 @@ snapshots: transitivePeerDependencies: - typescript + '@vueuse/core@13.2.0(vue@3.5.13(typescript@5.7.2))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.2.0 + '@vueuse/shared': 13.2.0(vue@3.5.13(typescript@5.7.2)) + vue: 3.5.13(typescript@5.7.2) + '@vueuse/integrations@12.2.0(fuse.js@7.0.0)(typescript@5.7.2)': dependencies: '@vueuse/core': 12.2.0(typescript@5.7.2) @@ -9780,6 +9808,23 @@ snapshots: '@vueuse/metadata@12.7.0': {} + '@vueuse/metadata@13.2.0': {} + + '@vueuse/motion@3.0.3(magicast@0.3.5)(vue@3.5.13(typescript@5.7.2))': + dependencies: + '@vueuse/core': 13.2.0(vue@3.5.13(typescript@5.7.2)) + '@vueuse/shared': 13.2.0(vue@3.5.13(typescript@5.7.2)) + defu: 6.1.4 + framesync: 6.1.2 + popmotion: 11.0.5 + style-value-types: 5.1.2 + vue: 3.5.13(typescript@5.7.2) + optionalDependencies: + '@nuxt/kit': 3.15.4(magicast@0.3.5) + transitivePeerDependencies: + - magicast + - supports-color + '@vueuse/shared@10.11.1(vue@3.5.13(typescript@5.7.2))': dependencies: vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.2)) @@ -9799,6 +9844,10 @@ snapshots: transitivePeerDependencies: - typescript + '@vueuse/shared@13.2.0(vue@3.5.13(typescript@5.7.2))': + dependencies: + vue: 3.5.13(typescript@5.7.2) + abbrev@1.1.1: {} abort-controller@3.0.0: @@ -11387,6 +11436,10 @@ snapshots: dependencies: simplesignal: 2.1.7 + framesync@6.1.2: + dependencies: + tslib: 2.4.0 + fresh@0.5.2: {} fs-extra@11.2.0: @@ -11644,6 +11697,8 @@ snapshots: he@1.2.0: {} + hey-listen@1.0.8: {} + hookable@5.5.3: {} hosted-git-info@2.8.9: {} @@ -13155,6 +13210,13 @@ snapshots: dependencies: '@babel/runtime': 7.24.5 + popmotion@11.0.5: + dependencies: + framesync: 6.1.2 + hey-listen: 1.0.8 + style-value-types: 5.1.2 + tslib: 2.4.0 + portfinder@1.0.32: dependencies: async: 2.6.4 @@ -13898,6 +13960,11 @@ snapshots: striptags@3.2.0: {} + style-value-types@5.1.2: + dependencies: + hey-listen: 1.0.8 + tslib: 2.4.0 + stylehacks@7.0.4(postcss@8.4.49): dependencies: browserslist: 4.24.3 @@ -14152,6 +14219,8 @@ snapshots: ts-interface-checker@0.1.13: {} + tslib@2.4.0: {} + tslib@2.6.3: {} tslib@2.8.1: {} diff --git a/server/api/logs/events.ts b/server/api/logs/events.ts index 98b266e..663ac54 100644 --- a/server/api/logs/events.ts +++ b/server/api/logs/events.ts @@ -1,12 +1,13 @@ import type { H3Event } from 'h3' import { QuerySchema } from '@@/schemas/query' +import { date2unix } from '~/utils/time' const { select } = SqlBricks function query2sql(query: Query, event: H3Event): string { const filter = query2filter(query) const { dataset } = useRuntimeConfig(event) - const sql = select(`*`).from(dataset).where(filter) + const sql = select(`*`).from(dataset).where(filter).orderBy('timestamp DESC') appendTimeFilter(sql, query) return sql.toString() } @@ -23,8 +24,8 @@ function events2logs(events: WAEEvents[]) { }, []) return { ...blobs2logs(blobs), - id: event.index1, - timestamp: event.timestamp, + id: crypto.randomUUID(), + timestamp: date2unix(new Date(`${event.timestamp}Z`)), } }) } diff --git a/server/api/stats/views.get.ts b/server/api/stats/views.get.ts index 09ec7ef..c492c36 100644 --- a/server/api/stats/views.get.ts +++ b/server/api/stats/views.get.ts @@ -5,6 +5,7 @@ import { z } from 'zod' const { select } = SqlBricks const unitMap: { [x: string]: string } = { + minute: '%H:%M', hour: '%Y-%m-%d %H', day: '%Y-%m-%d', }
+ {{ props.description }} +