Merge branch 'preview'

This commit is contained in:
ccbikai 2025-05-18 21:18:21 +08:00
commit 6a98a4b719
36 changed files with 27191 additions and 64 deletions

2
.gitignore vendored
View file

@ -10,7 +10,7 @@ dist
node_modules
# Logs
logs
./logs
*.log
# Misc

View file

@ -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);

View file

@ -18,9 +18,9 @@ const route = useRoute()
<TabsTrigger value="/dashboard/analysis">
{{ $t('nav.analysis') }}
</TabsTrigger>
<!-- <TabsTrigger value="/dashboard/realtime">
<TabsTrigger value="/dashboard/realtime">
{{ $t('nav.realtime') }}
</TabsTrigger> -->
</TabsTrigger>
</TabsList>
</Tabs>
<slot name="left" />

View file

@ -0,0 +1,95 @@
<script setup>
import { now } from '@internationalized/date'
import { useUrlSearchParams } from '@vueuse/core'
const emit = defineEmits(['update:timeRange'])
const timeRange = ref('last-1h')
watch(timeRange, (newValue) => {
switch (newValue) {
case 'today':
emit('update:timeRange', [date2unix(now(), 'start'), date2unix(now())], newValue)
break
case 'last-5m':
emit('update:timeRange', [date2unix(now().subtract({ minutes: 5 })), date2unix(now())], newValue)
break
case 'last-10m':
emit('update:timeRange', [date2unix(now().subtract({ minutes: 10 })), date2unix(now())], newValue)
break
case 'last-30m':
emit('update:timeRange', [date2unix(now().subtract({ minutes: 30 })), date2unix(now())], newValue)
break
case 'last-1h':
emit('update:timeRange', [date2unix(now().subtract({ hours: 1 })), date2unix(now())], newValue)
break
case 'last-6h':
emit('update:timeRange', [date2unix(now().subtract({ hours: 6 })), date2unix(now())], newValue)
break
case 'last-12h':
emit('update:timeRange', [date2unix(now().subtract({ hours: 12 })), date2unix(now())], newValue)
break
case 'last-24h':
emit('update:timeRange', [date2unix(now().subtract({ hours: 24 })), date2unix(now())], newValue)
break
default:
break
}
}, { deep: true })
function restoreTimeRange() {
try {
const searchParams = useUrlSearchParams('history')
if (searchParams.time) {
timeRange.value = searchParams.time
triggerRef(timeRange)
}
}
catch (error) {
console.error('restore searchParams error', error)
}
}
defineExpose({
restoreTimeRange,
})
onBeforeMount(() => {
restoreTimeRange()
})
</script>
<template>
<Select v-model="timeRange">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="last-5m">
{{ $t('dashboard.time_picker.last_5m') }}
</SelectItem>
<SelectItem value="last-10m">
{{ $t('dashboard.time_picker.last_10m') }}
</SelectItem>
<SelectItem value="last-30m">
{{ $t('dashboard.time_picker.last_30m') }}
</SelectItem>
<SelectItem value="last-1h">
{{ $t('dashboard.time_picker.last_1h') }}
</SelectItem>
<SelectItem value="last-6h">
{{ $t('dashboard.time_picker.last_6h') }}
</SelectItem>
<SelectItem value="last-12h">
{{ $t('dashboard.time_picker.last_12h') }}
</SelectItem>
<SelectItem value="last-24h">
{{ $t('dashboard.time_picker.last_24h') }}
</SelectItem>
<SelectSeparator />
<SelectItem value="today">
{{ $t('dashboard.time_picker.today') }}
</SelectItem>
</SelectContent>
</Select>
</template>

View file

@ -26,17 +26,13 @@ async function getLinkCounters() {
counters.value = data?.[0]
}
const stopWatchQueryChange = watch([time, filters], getLinkCounters, {
watch([time, filters], getLinkCounters, {
deep: true,
})
onMounted(async () => {
getLinkCounters()
})
onBeforeUnmount(() => {
stopWatchQueryChange()
})
</script>
<template>

View file

@ -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'
@ -36,7 +51,7 @@ async function getLinkViews() {
})
}
const stopWatchQueryChange = watch([time, filters], getLinkViews, {
watch([time, filters], getLinkViews, {
deep: true,
})
@ -44,10 +59,6 @@ onMounted(async () => {
getLinkViews()
})
onBeforeUnmount(() => {
stopWatchQueryChange()
})
function formatTime(tick) {
if (Number.isInteger(tick) && views.value[tick]) {
if (getUnit(time.value.startAt, time.value.endAt) === 'hour')
@ -61,16 +72,20 @@ function formatTime(tick) {
<template>
<Card class="px-0 py-6 md:px-6">
<CardTitle class="px-6 md:px-0">
<CardTitle v-if="mode === 'full'" class="px-6 md:px-0">
{{ $t('dashboard.views') }}
</CardTitle>
<component
:is="chart"
:data="views"
v-if="views.length"
class="w-full h-full"
index="time"
:categories="['visitors', 'visits']"
:data="views"
:categories="mode === 'full' ? ['visits', 'visitors'] : ['visits']"
:x-formatter="formatTime"
:y-formatter="formatNumber"
:show-grid-line="mode === 'full'"
:show-legend="mode === 'full'"
/>
</Card>
</template>

View file

@ -33,7 +33,7 @@ async function getMapData() {
}
}
const stopWatchQueryChange = watch([time, filters], getMapData, {
watch([time, filters], getMapData, {
deep: true,
})
@ -42,10 +42,6 @@ onMounted(() => {
getMapData()
})
onBeforeUnmount(() => {
stopWatchQueryChange()
})
const valueFormatter = v => v
const Tooltip = {
props: ['title', 'data'],

View file

@ -45,17 +45,13 @@ async function getLinkMetrics() {
}
}
const stopWatchQueryChange = watch([time, filters], getLinkMetrics, {
watch([time, filters], getLinkMetrics, {
deep: true,
})
onMounted(() => {
getLinkMetrics()
})
onBeforeUnmount(() => {
stopWatchQueryChange()
})
</script>
<template>

View file

@ -0,0 +1,52 @@
<script setup>
import NumberFlow from '@number-flow/vue'
import { MousePointerClick } from 'lucide-vue-next'
provide('id', ref())
const time = inject('time')
const filters = inject('filters')
const stats = ref({ visits: 0 })
async function getRealtimeStats() {
const { data } = await useAPI('/api/stats/counters', {
query: {
startAt: time.value.startAt,
endAt: time.value.endAt,
...filters.value,
},
})
stats.value = data?.[0] || {}
}
watch([time, filters], getRealtimeStats, {
deep: true,
})
onMounted(async () => {
getRealtimeStats()
})
</script>
<template>
<Card class="md:w-80 h-72 flex flex-col p-4 md:m-2">
<div class="h-24">
<CardHeader v-if="stats.visits" class="flex flex-row justify-between items-center pb-2 space-y-0 px-0 pt-2">
<CardTitle class="text-sm font-medium flex items-center gap-2">
<span class="size-1.5 inline-flex animate-ping rounded-full bg-green-400 opacity-75" />
{{ $t('dashboard.visits') }}
</CardTitle>
<MousePointerClick class="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent class="px-0 pb-4">
<NumberFlow class="text-2xl font-bold" :class="{ 'blur-md opacity-60': !stats.visits }" :value="stats.visits" />
</CardContent>
</div>
<DashboardAnalysisViews
class="w-full h-40 border-none !p-0"
mode="simple"
chart-type="bar"
/>
</Card>
</template>

View file

@ -0,0 +1,183 @@
<script setup>
import { useElementSize } from '@vueuse/core'
import { scaleSequentialSqrt } from 'd3-scale'
import { interpolateYlOrRd } from 'd3-scale-chromatic'
import Globe from 'globe.gl'
import { debounce } from 'lodash-es'
import { MeshPhongMaterial } from 'three'
const props = defineProps({
minutes: {
type: Number,
default: 60,
},
})
const time = inject('time')
const filters = inject('filters')
const countries = ref({})
const locations = ref([])
const colos = ref({})
const currentLocation = ref({})
const el = useTemplateRef('globeEl')
const { width } = useElementSize(el)
const size = computed(() => ({
width: width.value,
height: width.value > 768 ? width.value * 0.6 : width.value,
}))
const globeEl = ref()
const hexAltitude = ref(0.001)
const highest = computed(() => {
return locations.value.reduce((acc, curr) => Math.max(acc, curr.count), 0) || 1
})
let globe = null
async function getGlobeJSON() {
const data = await $fetch('/countries.geojson')
countries.value = data
}
async function getColosJSON() {
const data = await $fetch('/colos.json')
colos.value = data
}
async function getCurrentLocation() {
const data = await useAPI('/api/location')
currentLocation.value = data
}
async function getLiveLocations() {
const { data } = await useAPI('/api/logs/locations', {
query: {
startAt: time.value.startAt,
endAt: time.value.endAt,
...filters.value,
},
})
locations.value = data?.map(e => ({
lat: e.latitude,
lng: e.longitude,
count: Math.max(1, +e.count / highest.value),
}))
}
let cleanArcsDataTimer = null
function trafficEvent({ props }, { delay = 0 }) {
const arc = {
startLat: props.item.latitude,
startLng: props.item.longitude,
endLat: colos.value[props.item.COLO]?.lat,
endLng: colos.value[props.item.COLO]?.lon,
color: 'red',
arcAltitude: 0.5,
}
console.info(`from ${props.item.city}(${props.item.latitude}, ${props.item.longitude}) to ${props.item.COLO}(${colos.value[props.item.COLO]?.lat}, ${colos.value[props.item.COLO]?.lon})`)
const random = Math.random()
globe.arcsData([arc])
.arcColor('color')
.arcDashLength(() => random + 0.2)
.arcDashGap(() => random - 0.2)
.arcDashAnimateTime(2000)
clearTimeout(cleanArcsDataTimer)
cleanArcsDataTimer = setTimeout(() => {
globe.arcsData([])
}, delay + 100)
}
const normalized = 5 / props.minutes
const weightColor = scaleSequentialSqrt(interpolateYlOrRd).domain([0, highest.value * normalized * 15])
function initGlobe() {
globe = new Globe(globeEl.value)
.width(size.value.width)
.height(size.value.height)
// .globeOffset([width.value > 768 ? -100 : 0, width.value > 768 ? 0 : 100])
.atmosphereColor('rgba(170, 170, 200, 0.8)')
.globeMaterial(new MeshPhongMaterial({
color: 'rgb(228, 228, 231)',
transparent: false,
opacity: 1,
}))
.backgroundColor('rgba(0,0,0,0)')
.hexPolygonsData(countries.value.features)
.hexPolygonResolution(3)
.hexPolygonMargin(0.2)
.hexBinResolution(3)
.hexBinPointsData(locations.value)
.hexPolygonAltitude(() => hexAltitude.value)
.hexBinMerge(true)
.hexBinPointWeight('count')
.hexPolygonColor(() => `rgba(54, 211, 153, ${Math.random() / 1.5 + 0.5})`)
.onGlobeReady(() => {
globe.pointOfView({ lat: currentLocation.value.latitude, lng: currentLocation.value.longitude, altitude: width.value > 768 ? 2 : 3 })
globe.controls().autoRotate = true
globe.controls().autoRotateSpeed = 0.3
})
globe.controls().addEventListener('end', debounce(() => {
const distance = Math.round(globe.controls().getDistance())
let nextAlt = 0.005
if (distance <= 300)
nextAlt = 0.001
else if (distance >= 600)
nextAlt = 0.02
if (nextAlt !== hexAltitude.value)
hexAltitude.value = nextAlt
}, 200))
globalTrafficEvent.on(trafficEvent)
}
function stopRotation() {
if (globe) {
globe.controls().autoRotate = false
}
}
watch([time, filters], getLiveLocations, {
deep: true,
})
watch(width, () => {
if (globe) {
globe.width(size.value.width)
globe.height(size.value.height)
}
})
watch(locations, () => {
if (globe) {
globe.hexBinPointsData(locations.value)
globe.hexTopColor(d => weightColor(d.sumWeight))
globe.hexSideColor(d => weightColor(d.sumWeight))
}
})
onMounted(async () => {
await Promise.all([
getLiveLocations(),
getCurrentLocation(),
getGlobeJSON(),
getColosJSON(),
])
initGlobe()
})
onBeforeUnmount(() => {
globalTrafficEvent.off(trafficEvent)
if (globe) {
globe._destructor?.()
globe = null
}
})
</script>
<template>
<div ref="globeEl" @mousedown="stopRotation" />
</template>

View file

@ -1,10 +1,70 @@
<script setup>
import { now } from '@internationalized/date'
import { useIntervalFn, useUrlSearchParams } from '@vueuse/core'
import { safeDestr } from 'destr'
const searchParams = useUrlSearchParams('history')
const timePicker = ref(null)
const time = ref({
startAt: date2unix(now().subtract({ hours: 1 })),
endAt: date2unix(now()),
})
provide('time', time)
function changeTime(timeRange, timeName) {
console.log('changeTime', timeRange, timeName)
time.value.startAt = timeRange[0]
time.value.endAt = timeRange[1]
searchParams.time = timeName
}
const filters = ref({})
provide('filters', filters)
function changeFilter(type, value) {
console.log('changeFilter', type, value)
filters.value[type] = value
searchParams.filters = JSON.stringify(filters.value)
}
useIntervalFn(() => {
timePicker.value?.restoreTimeRange()
}, 5 * 60 * 1000)
function restoreSearchParams() {
try {
if (searchParams.filters) {
filters.value = safeDestr(searchParams.filters)
}
}
catch (error) {
console.error('restore searchParams error', error)
}
}
onBeforeMount(() => {
restoreSearchParams()
})
</script>
<template>
<main class="space-y-6">
<div class="flex flex-col gap-6 sm:gap-2 sm:flex-row sm:justify-between">
<DashboardNav />
<DashboardNav class="flex-1">
<DashboardTimePicker ref="timePicker" @update:time-range="changeTime" />
</DashboardNav>
<DashboardFilters @change="changeFilter" />
</div>
<div>
Realtime
<div class="relative space-y-4">
<DashboardRealtimeChart class="md:absolute top-0 left-0 z-10" />
<DashboardRealtimeGlobe />
<DashboardRealtimeLogs class="md:absolute top-0 right-0 h-full z-10" />
</div>
</main>
</template>

View file

@ -0,0 +1,50 @@
<script setup>
import AnimatedList from '@/components/spark-ui/AnimatedList.vue'
import Notification from '@/components/spark-ui/Notification.vue'
const time = inject('time')
const filters = inject('filters')
const logs = ref([])
const logskey = ref(0)
async function getEvents() {
const data = await useAPI('/api/logs/events', {
query: {
startAt: time.value.startAt,
endAt: time.value.endAt,
...filters.value,
},
})
logs.value = data?.reverse()
logskey.value = Date.now()
}
watch([time, filters], getEvents, {
deep: true,
})
onMounted(async () => {
getEvents()
})
function onUpdateItems(...args) {
globalTrafficEvent.emit(...args)
}
</script>
<template>
<AnimatedList v-if="logs.length" :key="logskey" class="md:w-72" @update:items="onUpdateItems">
<template #default>
<Notification
v-for="item in logs"
:key="item.id"
:name="item.slug"
:description="[item.os, item.browser].filter(Boolean).join(' ')"
:icon="getFlag(item.country)"
:time="item.timestamp"
:item="item"
class="w-full"
/>
</template>
</AnimatedList>
</template>

View file

@ -0,0 +1,99 @@
<script lang="ts" setup>
import { cn } from '@/lib/utils'
import { computed, onMounted, ref, useSlots } from 'vue'
const props = withDefaults(defineProps<{
class?: string
delay?: number
}>(), {
delay: 1000,
})
const emit = defineEmits(['update:items'])
const slots = useSlots()
const index = ref(0)
const slotsArray = ref<any>([])
const maxShowItems = 100
const itemsToShow = computed(() => {
const start = index.value - maxShowItems < 0 ? 0 : index.value - maxShowItems
return slotsArray.value.slice(start, index.value)
})
watch([itemsToShow], () => {
emit('update:items', itemsToShow.value.at(-1), props)
})
async function loadComponents() {
slotsArray.value = slots.default ? slots.default()?.[0]?.children : []
while (index.value < slotsArray.value.length) {
index.value++
await delay(props.delay)
}
}
async function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
function getInitial(idx: number) {
return idx === index.value - 1
? {
scale: 0,
opacity: 0,
}
: undefined
}
function getEnter(idx: number) {
return idx === index.value - 1
? {
scale: 1,
opacity: 1,
y: 0,
transition: {
type: 'spring',
stiffness: 250,
damping: 40,
},
}
: undefined
}
function getLeave() {
return {
scale: 0,
opacity: 0,
y: 0,
transition: {
type: 'spring',
stiffness: 350,
damping: 40,
},
}
};
onMounted(() => loadComponents())
</script>
<template>
<div :class="cn('overflow-auto', $props.class)">
<transition-group name="list" tag="div" class="flex flex-col-reverse items-center p-2" move-class="move">
<div
v-for="(item, idx) in itemsToShow"
:key="item.props.key"
v-motion :initial="getInitial(idx)" :enter="getEnter(idx)" :leave="getLeave()"
:class="cn('mx-auto w-full')"
>
<component :is="item" />
</div>
</transition-group>
</div>
</template>
<style scoped>
.move {
transition: transform 0.4s ease-out;
}
</style>

View file

@ -0,0 +1,45 @@
<script setup lang='ts'>
import { cn } from '@/lib/utils'
const props = defineProps<{
name: string
class?: string
description: string
icon?: string
color?: string
time: number
}>()
const className = cn(
'relative mx-auto min-h-fit w-full cursor-pointer border rounded-2xl my-1',
// animation styles
'transition-all duration-200 ease-in-out hover:scale-[103%] transform-gpu',
// light styles
'bg-white',
// dark styles
'dark:bg-transparent dark:backdrop-blur-md dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]',
)
</script>
<template>
<figure :class="className">
<div class="flex flex-row py-2 items-center px-2 gap-2">
<div
class="flex size-10 items-center justify-center rounded-2xl"
:style="{ backgroundColor: props.color || 'transparent' }"
>
<span class="text-lg">{{ props.icon }}</span>
</div>
<div class="flex flex-col overflow-hidden">
<div class="flex flex-row items-center whitespace-pre text-lg font-medium ">
<span class="text-sm text-foreground sm:text-lg">{{ props.name }}</span>
<span class="mx-1">·</span>
<span class="text-xs text-gray-500">{{ shortTime(props.time) }}</span>
</div>
<p class="text-sm font-normal">
{{ props.description }}
</p>
</div>
</div>
</figure>
</template>

View file

@ -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)

View file

@ -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})`)
}

3
app/utils/events.ts Normal file
View file

@ -0,0 +1,3 @@
import { useEventBus } from '@vueuse/core'
export const globalTrafficEvent = useEventBus(Symbol('traffic'))

View file

@ -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)

View file

@ -8,7 +8,6 @@
"baseColor": "zinc",
"cssVariables": true
},
"framework": "nuxt",
"aliases": {
"components": "@/components",
"utils": "@/utils"

View file

@ -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: {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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': {

View file

@ -11,6 +11,7 @@
"dev": "nuxt dev",
"build": "nuxt build",
"build:map": "node scripts/build-map.js",
"build:colo": "node scripts/build-colo.js",
"preview": "wrangler pages dev dist",
"deploy": "wrangler pages deploy dist",
"postinstall": "npm run build:map && nuxt prepare",
@ -21,13 +22,19 @@
"lint-staged": "lint-staged"
},
"dependencies": {
"@intlify/message-compiler": "^11.1.3",
"@number-flow/vue": "^0.3.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",
"globe.gl": "^2.41.4",
"intl-parse-accept-language": "^1.0.0",
"lodash-es": "^4.17.21",
"lucide-vue-next": "^0.469.0",
"mysql-bricks": "^1.1.2",
"nanoid": "^5.0.9",

View file

@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@intlify/message-compiler':
specifier: ^11.1.3
version: 11.1.3
'@number-flow/vue':
specifier: ^0.3.3
version: 0.3.3(vue@3.5.13(typescript@5.7.2))
@ -23,12 +26,27 @@ 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
d3-scale-chromatic:
specifier: ^3.1.0
version: 3.1.0
fuse.js:
specifier: ^7.0.0
version: 7.0.0
globe.gl:
specifier: ^2.41.4
version: 2.41.4
intl-parse-accept-language:
specifier: ^1.0.0
version: 1.0.0
lodash-es:
specifier: ^4.17.21
version: 4.17.21
lucide-vue-next:
specifier: ^0.469.0
version: 0.469.0(vue@3.5.13(typescript@5.7.2))
@ -1418,20 +1436,24 @@ packages:
resolution: {integrity: sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.0.0-rc.1':
resolution: {integrity: sha512-TGw2uBfuTFTegZf/BHtUQBEKxl7Q/dVGLoqRIdw8lFsp9g/53sYn5iD+0HxIzdYjbWL6BTJMXCPUHp9PxDTRPw==}
'@intlify/message-compiler@11.1.3':
resolution: {integrity: sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw==}
engines: {node: '>= 16'}
'@intlify/message-compiler@12.0.0-alpha.2':
resolution: {integrity: sha512-PD9C+oQbb7BF52hec0+vLnScaFkvnfX+R7zSbODYuRo/E2niAtGmHd0wPvEMsDhf9Z9b8f/qyDsVeZnD/ya9Ug==}
engines: {node: '>= 16'}
'@intlify/shared@10.0.5':
resolution: {integrity: sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==}
engines: {node: '>= 16'}
'@intlify/shared@11.0.0-rc.1':
resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==}
'@intlify/shared@11.1.3':
resolution: {integrity: sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.2':
resolution: {integrity: sha512-dF2iMMy8P9uKVHV/20LA1ulFLL+MKSbfMiixSmn6fpwqzvix38OIc7ebgnFbBqElvghZCW9ACtzKTGKsTGTWGA==}
'@intlify/shared@12.0.0-alpha.2':
resolution: {integrity: sha512-P2DULVX9nz3y8zKNqLw9Es1aAgQ1JGC+kgpx5q7yLmrnAKkPR5MybQWoEhxanefNJgUY5ehsgo+GKif59SrncA==}
engines: {node: '>= 16'}
'@intlify/unplugin-vue-i18n@6.0.3':
@ -2112,6 +2134,18 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
'@turf/boolean-point-in-polygon@7.2.0':
resolution: {integrity: sha512-lvEOjxeXIp+wPXgl9kJA97dqzMfNexjqHou+XHVcfxQgolctoJiRYmcVCWGpiZ9CBf/CJha1KmD1qQoRIsjLaA==}
'@turf/helpers@7.2.0':
resolution: {integrity: sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw==}
'@turf/invariant@7.2.0':
resolution: {integrity: sha512-kV4u8e7Gkpq+kPbAKNC21CmyrXzlbBgFjO1PhrHPgEdNqXqDawoZ3i6ivE3ULJj2rSesCjduUaC/wyvH/sNr2Q==}
'@tweenjs/tween.js@25.0.0':
resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==}
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@ -2310,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}
@ -2578,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:
@ -2628,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==}
@ -2637,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==}
@ -2648,6 +2703,10 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
accessor-fn@1.5.3:
resolution: {integrity: sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==}
engines: {node: '>=12'}
acorn-import-attributes@1.9.5:
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
peerDependencies:
@ -3285,6 +3344,10 @@ packages:
engines: {node: '>=12'}
hasBin: true
d3-geo-voronoi@2.1.0:
resolution: {integrity: sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q==}
engines: {node: '>=12'}
d3-geo@3.1.1:
resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
engines: {node: '>=12'}
@ -3300,6 +3363,9 @@ packages:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-octree@1.1.0:
resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==}
d3-path@1.0.9:
resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
@ -3359,6 +3425,10 @@ packages:
peerDependencies:
d3-selection: 2 - 3
d3-tricontour@1.0.2:
resolution: {integrity: sha512-HIRxHzHagPtUPNabjOlfcyismJYIsc+Xlq4mlsts4e8eAcwyq9Tgk/sYdyhlBpQ0MHwVquc/8j+e29YjXnmxeA==}
engines: {node: '>=12'}
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
@ -3367,6 +3437,10 @@ packages:
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
engines: {node: '>=12'}
data-bind-mapper@1.0.3:
resolution: {integrity: sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==}
engines: {node: '>=12'}
data-uri-to-buffer@2.0.2:
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
@ -3560,6 +3634,9 @@ packages:
earcut@2.2.4:
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
earcut@3.0.1:
resolution: {integrity: sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -4013,6 +4090,10 @@ packages:
flatted@3.3.2:
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
float-tooltip@1.7.5:
resolution: {integrity: sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==}
engines: {node: '>=12'}
foreground-child@3.1.1:
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
engines: {node: '>=14'}
@ -4024,6 +4105,12 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
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'}
@ -4181,6 +4268,10 @@ packages:
resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==}
engines: {node: '>=18'}
globe.gl@2.41.4:
resolution: {integrity: sha512-ExddewF46ncxFSG+ci62Vz7QO4NmLmTtJ386zLoZSgLdbKl9bWz6DN7DxXNkvo6M6mSV94E18uOvLbvr7YmsGw==}
engines: {node: '>=12'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@ -4195,6 +4286,10 @@ packages:
resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
h3-js@4.2.1:
resolution: {integrity: sha512-HYiUrq5qTRFqMuQu3jEHqxXLk1zsSJiby9Lja/k42wHjabZG7tN9rOuzT/PEFf+Wa7rsnHLMHRWIu0mgcJ0ewQ==}
engines: {node: '>=4', npm: '>=3', yarn: '>=1.3.0'}
h3@1.12.0:
resolution: {integrity: sha512-Zi/CcNeWBXDrFNlV0hUBJQR9F7a96RjMeAZweW/ZWkR9fuXrMcvKnSA63f/zZ9l0GgQOZDVHGvXivNN9PWOwhA==}
@ -4235,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==}
@ -4321,6 +4419,10 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
index-array-by@1.4.2:
resolution: {integrity: sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==}
engines: {node: '>=12'}
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
@ -4542,6 +4644,10 @@ packages:
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
kapsule@1.16.3:
resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==}
engines: {node: '>=12'}
kdbush@3.0.0:
resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==}
@ -5351,6 +5457,16 @@ packages:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
point-in-polygon-hao@1.2.4:
resolution: {integrity: sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==}
polished@4.3.1:
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'}
@ -5575,6 +5691,9 @@ packages:
potpack@1.0.2:
resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
preact@10.26.6:
resolution: {integrity: sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g==}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@ -5882,6 +6001,9 @@ packages:
simple-git@3.27.0:
resolution: {integrity: sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==}
simplesignal@2.1.7:
resolution: {integrity: sha512-PEo2qWpUke7IMhlqiBxrulIFvhJRLkl1ih52Rwa+bPjzhJepcd4GIjn2RiQmFSx3dQvsEAgF0/lXMwMN7vODaA==}
sirv@3.0.0:
resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==}
engines: {node: '>=18'}
@ -5915,10 +6037,6 @@ packages:
smob@1.5.0:
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@ -6048,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}
@ -6148,9 +6269,42 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
three-conic-polygon-geometry@2.1.2:
resolution: {integrity: sha512-NaP3RWLJIyPGI+zyaZwd0Yj6rkoxm4FJHqAX1Enb4L64oNYLCn4bz1ESgOEYavgcUwCNYINu1AgEoUBJr1wZcA==}
engines: {node: '>=12'}
peerDependencies:
three: '>=0.72.0'
three-geojson-geometry@2.1.1:
resolution: {integrity: sha512-dC7bF3ri1goDcihYhzACHOBQqu7YNNazYLa2bSydVIiJUb3jDFojKSy+gNj2pMkqZNSVjssSmdY9zlmnhEpr1w==}
engines: {node: '>=12'}
peerDependencies:
three: '>=0.72.0'
three-globe@2.42.4:
resolution: {integrity: sha512-YWWFtl2MNT3CDDjgE4blmWaIgSjVOqJdtx9BaLIwWwVo4oTto6dU6w/tHkLKx/hCpGCQfhWJFszvereUaeknEg==}
engines: {node: '>=12'}
peerDependencies:
three: '>=0.154'
three-render-objects@1.40.0:
resolution: {integrity: sha512-Ub2IebRGrV+ctxkOe7lkLzIraJDTtz5s31Z2rvaQb7sHAXfofv02CwtEJmIPIkEy6jpGreuqJbCGEnQpvKKDFw==}
engines: {node: '>=12'}
peerDependencies:
three: '>=0.168'
three-slippy-map-globe@1.0.3:
resolution: {integrity: sha512-Y9WCA/tTL8yH8FHVSXVQss/P0V36utTNhuixzFPj0Bs0SXxO+Vui133oAQmMpx4BLXYZpWZwcqHM2i3MfFlYWw==}
engines: {node: '>=12'}
peerDependencies:
three: '>=0.154'
three@0.135.0:
resolution: {integrity: sha512-kuEpuuxRzLv0MDsXai9huCxOSQPZ4vje6y0gn80SRmQvgz6/+rI0NAvCRAw56zYaWKMGMfqKWsxF9Qa2Z9xymQ==}
three@0.176.0:
resolution: {integrity: sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==}
throttle-debounce@5.0.0:
resolution: {integrity: sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==}
engines: {node: '>=12.22'}
@ -6158,6 +6312,9 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
tinycolor2@1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
tinyexec@0.3.1:
resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==}
@ -6215,9 +6372,15 @@ 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==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsscmp@1.0.6:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'}
@ -7887,8 +8050,8 @@ snapshots:
'@intlify/bundle-utils@10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))':
dependencies:
'@intlify/message-compiler': 11.0.0-rc.1
'@intlify/shared': 11.0.0-rc.1
'@intlify/message-compiler': 12.0.0-alpha.2
'@intlify/shared': 12.0.0-alpha.2
acorn: 8.14.0
escodegen: 2.1.0
estree-walker: 2.0.2
@ -7919,23 +8082,28 @@ snapshots:
'@intlify/shared': 10.0.5
source-map-js: 1.2.1
'@intlify/message-compiler@11.0.0-rc.1':
'@intlify/message-compiler@11.1.3':
dependencies:
'@intlify/shared': 11.0.0-rc.1
'@intlify/shared': 11.1.3
source-map-js: 1.2.1
'@intlify/message-compiler@12.0.0-alpha.2':
dependencies:
'@intlify/shared': 12.0.0-alpha.2
source-map-js: 1.2.1
'@intlify/shared@10.0.5': {}
'@intlify/shared@11.0.0-rc.1': {}
'@intlify/shared@11.1.3': {}
'@intlify/shared@11.1.2': {}
'@intlify/shared@12.0.0-alpha.2': {}
'@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.17.0(jiti@2.4.2))(rollup@4.18.0)(typescript@5.7.2)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2))
'@intlify/bundle-utils': 10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))
'@intlify/shared': 11.1.2
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))
'@intlify/shared': 11.1.3
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.3)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))
'@rollup/pluginutils': 5.1.4(rollup@4.18.0)
'@typescript-eslint/scope-manager': 8.18.2
'@typescript-eslint/typescript-estree': 8.18.2(typescript@5.7.2)
@ -7959,11 +8127,11 @@ snapshots:
'@intlify/utils@0.13.0': {}
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))':
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.3)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.2)))(vue@3.5.13(typescript@5.7.2))':
dependencies:
'@babel/parser': 7.26.3
optionalDependencies:
'@intlify/shared': 11.1.2
'@intlify/shared': 11.1.3
'@vue/compiler-dom': 3.5.13
vue: 3.5.13(typescript@5.7.2)
vue-i18n: 10.0.5(vue@3.5.13(typescript@5.7.2))
@ -8900,6 +9068,27 @@ snapshots:
'@trysound/sax@0.2.0': {}
'@turf/boolean-point-in-polygon@7.2.0':
dependencies:
'@turf/helpers': 7.2.0
'@turf/invariant': 7.2.0
'@types/geojson': 7946.0.14
point-in-polygon-hao: 1.2.4
tslib: 2.8.1
'@turf/helpers@7.2.0':
dependencies:
'@types/geojson': 7946.0.14
tslib: 2.8.1
'@turf/invariant@7.2.0':
dependencies:
'@turf/helpers': 7.2.0
'@types/geojson': 7946.0.14
tslib: 2.8.1
'@tweenjs/tween.js@25.0.0': {}
'@types/d3-array@3.2.1': {}
'@types/d3-axis@3.0.6':
@ -9129,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
@ -9468,7 +9659,7 @@ snapshots:
'@vue/shared': 3.5.13
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.0
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.13':
dependencies:
@ -9594,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)
@ -9610,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))
@ -9629,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:
@ -9640,6 +9859,8 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
accessor-fn@1.5.3: {}
acorn-import-attributes@1.9.5(acorn@8.12.1):
dependencies:
acorn: 8.12.1
@ -10289,6 +10510,13 @@ snapshots:
d3-array: 3.2.4
d3-geo: 3.1.1
d3-geo-voronoi@2.1.0:
dependencies:
d3-array: 3.2.4
d3-delaunay: 6.0.4
d3-geo: 3.1.1
d3-tricontour: 1.0.2
d3-geo@3.1.1:
dependencies:
d3-array: 3.2.4
@ -10301,6 +10529,8 @@ snapshots:
dependencies:
d3-color: 3.1.0
d3-octree@1.1.0: {}
d3-path@1.0.9: {}
d3-path@3.1.0: {}
@ -10358,6 +10588,11 @@ snapshots:
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-tricontour@1.0.2:
dependencies:
d3-delaunay: 6.0.4
d3-scale: 4.0.2
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
@ -10399,6 +10634,10 @@ snapshots:
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-zoom: 3.0.0
data-bind-mapper@1.0.3:
dependencies:
accessor-fn: 1.5.3
data-uri-to-buffer@2.0.2: {}
date-fns@4.1.0: {}
@ -10528,6 +10767,8 @@ snapshots:
earcut@2.2.4: {}
earcut@3.0.1: {}
eastasianwidth@0.2.0: {}
ee-first@1.1.1: {}
@ -11171,6 +11412,12 @@ snapshots:
flatted@3.3.2: {}
float-tooltip@1.7.5:
dependencies:
d3-selection: 3.0.0
kapsule: 1.16.3
preact: 10.26.6
foreground-child@3.1.1:
dependencies:
cross-spawn: 7.0.3
@ -11185,6 +11432,14 @@ snapshots:
fraction.js@4.3.7: {}
frame-ticker@1.0.3:
dependencies:
simplesignal: 2.1.7
framesync@6.1.2:
dependencies:
tslib: 2.4.0
fresh@0.5.2: {}
fs-extra@11.2.0:
@ -11369,6 +11624,15 @@ snapshots:
slash: 5.1.0
unicorn-magic: 0.1.0
globe.gl@2.41.4:
dependencies:
'@tweenjs/tween.js': 25.0.0
accessor-fn: 1.5.3
kapsule: 1.16.3
three: 0.176.0
three-globe: 2.42.4(three@0.176.0)
three-render-objects: 1.40.0(three@0.176.0)
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
@ -11379,6 +11643,8 @@ snapshots:
dependencies:
duplexer: 0.1.2
h3-js@4.2.1: {}
h3@1.12.0:
dependencies:
cookie-es: 1.1.0
@ -11431,6 +11697,8 @@ snapshots:
he@1.2.0: {}
hey-listen@1.0.8: {}
hookable@5.5.3: {}
hosted-git-info@2.8.9: {}
@ -11515,6 +11783,8 @@ snapshots:
indent-string@4.0.0: {}
index-array-by@1.4.2: {}
inflight@1.0.6:
dependencies:
once: 1.4.0
@ -11695,6 +11965,10 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
kapsule@1.16.3:
dependencies:
lodash-es: 4.17.21
kdbush@3.0.0: {}
keygrip@1.1.0:
@ -12928,6 +13202,21 @@ snapshots:
pluralize@8.0.0: {}
point-in-polygon-hao@1.2.4:
dependencies:
robust-predicates: 3.0.2
polished@4.3.1:
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
@ -13141,6 +13430,8 @@ snapshots:
potpack@1.0.2: {}
preact@10.26.6: {}
prelude-ls@1.2.1: {}
pretty-bytes@6.1.1: {}
@ -13517,6 +13808,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
simplesignal@2.1.7: {}
sirv@3.0.0:
dependencies:
'@polka/url': 1.0.0-next.25
@ -13545,8 +13838,6 @@ snapshots:
smob@1.5.0: {}
source-map-js@1.2.0: {}
source-map-js@1.2.1: {}
source-map-support@0.5.21:
@ -13669,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
@ -13809,12 +14105,71 @@ snapshots:
dependencies:
any-promise: 1.3.0
three-conic-polygon-geometry@2.1.2(three@0.176.0):
dependencies:
'@turf/boolean-point-in-polygon': 7.2.0
d3-array: 3.2.4
d3-geo: 3.1.1
d3-geo-voronoi: 2.1.0
d3-scale: 4.0.2
delaunator: 5.0.1
earcut: 3.0.1
three: 0.176.0
three-geojson-geometry@2.1.1(three@0.176.0):
dependencies:
d3-geo: 3.1.1
d3-interpolate: 3.0.1
earcut: 3.0.1
three: 0.176.0
three-globe@2.42.4(three@0.176.0):
dependencies:
'@tweenjs/tween.js': 25.0.0
accessor-fn: 1.5.3
d3-array: 3.2.4
d3-color: 3.1.0
d3-geo: 3.1.1
d3-interpolate: 3.0.1
d3-scale: 4.0.2
d3-scale-chromatic: 3.1.0
data-bind-mapper: 1.0.3
frame-ticker: 1.0.3
h3-js: 4.2.1
index-array-by: 1.4.2
kapsule: 1.16.3
three: 0.176.0
three-conic-polygon-geometry: 2.1.2(three@0.176.0)
three-geojson-geometry: 2.1.1(three@0.176.0)
three-slippy-map-globe: 1.0.3(three@0.176.0)
tinycolor2: 1.6.0
three-render-objects@1.40.0(three@0.176.0):
dependencies:
'@tweenjs/tween.js': 25.0.0
accessor-fn: 1.5.3
float-tooltip: 1.7.5
kapsule: 1.16.3
polished: 4.3.1
three: 0.176.0
three-slippy-map-globe@1.0.3(three@0.176.0):
dependencies:
d3-geo: 3.1.1
d3-octree: 1.1.0
d3-scale: 4.0.2
three: 0.176.0
three@0.135.0: {}
three@0.176.0: {}
throttle-debounce@5.0.0: {}
tiny-invariant@1.3.3: {}
tinycolor2@1.6.0: {}
tinyexec@0.3.1: {}
tinyglobby@0.2.10:
@ -13864,8 +14219,12 @@ snapshots:
ts-interface-checker@0.1.13: {}
tslib@2.4.0: {}
tslib@2.6.3: {}
tslib@2.8.1: {}
tsscmp@1.0.6: {}
type-check@0.4.0:

1
public/colos.json Normal file

File diff suppressed because one or more lines are too long

26017
public/countries.geojson Normal file

File diff suppressed because it is too large Load diff

19
scripts/build-colo.js Normal file
View file

@ -0,0 +1,19 @@
import { writeFileSync } from 'node:fs'
import { join } from 'node:path'
async function main() {
const locations = await fetch('https://raw.githubusercontent.com/Netrvin/cloudflare-colo-list/refs/heads/main/locations.json')
if (!locations.ok) {
throw new Error('Failed to fetch locations')
}
const colos = await locations.json()
writeFileSync(join(import.meta.dirname, '../public/colos.json'), JSON.stringify(colos.reduce((acc, c) => {
acc[c.iata] = {
lat: c.lat,
lon: c.lon,
}
return acc
}, {}), 'utf8'))
}
main().catch(console.error)

7
server/api/location.ts Normal file
View file

@ -0,0 +1,7 @@
export default eventHandler((event) => {
const { request: { cf } } = event.context.cloudflare
return {
latitude: cf.latitude,
longitude: cf.longitude,
}
})

45
server/api/logs/events.ts Normal file
View file

@ -0,0 +1,45 @@
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).orderBy('timestamp DESC')
appendTimeFilter(sql, query)
return sql.toString()
}
interface WAEEvents {
[key: string]: string
}
function events2logs(events: WAEEvents[]) {
return events.map((event) => {
const blobs = Array.from({ length: Object.keys(blobsMap).length }).fill(0).reduce<string[]>((_, _c, i) => {
_.push(event[`blob${i + 1}`])
return _
}, [])
const doubles = Array.from({ length: Object.keys(doublesMap).length }).fill(0).reduce<number[]>((_, _c, i) => {
_.push(+event[`double${i + 1}`])
return _
}, [])
return {
...blobs2logs(blobs),
...doubles2logs(doubles),
ip: undefined,
id: crypto.randomUUID(),
timestamp: date2unix(new Date(`${event.timestamp}Z`)),
}
})
}
export default eventHandler(async (event) => {
const query = await getValidatedQuery(event, QuerySchema.parse)
const sql = query2sql(query, event)
const logs = await useWAE(event, sql) as { data: WAEEvents[] }
return events2logs(logs?.data || [])
})

View file

@ -0,0 +1,22 @@
import type { H3Event } from 'h3'
import { QuerySchema } from '@@/schemas/query'
const { select, and, notEq } = SqlBricks
function query2sql(query: Query, event: H3Event): string {
const filter = query2filter(query)
const { dataset } = useRuntimeConfig(event)
const sql = select(`blob8 as ${blobsMap.blob8},double1 as ${doublesMap.double1},double2 as ${doublesMap.double2},count() as count`)
.from(dataset)
.where(and([notEq('double1', 0), notEq('double2', 0), filter]))
.groupBy([blobsMap.blob8, doublesMap.double1, doublesMap.double2])
appendTimeFilter(sql, query)
return sql.toString()
}
export default eventHandler(async (event) => {
const query = await getValidatedQuery(event, QuerySchema.parse)
const sql = query2sql(query, event)
return useWAE(event, sql)
})

View file

@ -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',
}

View file

@ -60,13 +60,13 @@ export const logsMap = {
...Object.entries(doublesMap).reduce((acc, [k, v]) => ({ ...acc, [v]: k }), {}),
} as LogsMap
function logs2blobs(logs: LogsMap) {
export function logs2blobs(logs: LogsMap) {
return (Object.keys(blobsMap) as BlobsKey[])
.sort((a, b) => toBlobNumber(a) - toBlobNumber(b))
.map(key => String(logs[blobsMap[key] as LogsKey] || ''))
}
function blobs2logs(blobs: string[]) {
export function blobs2logs(blobs: string[]) {
const logsList = Object.keys(blobsMap)
return blobs.reduce((logs, blob, i) => {
@ -76,13 +76,13 @@ function blobs2logs(blobs: string[]) {
}, {} as Partial<LogsMap>)
}
function logs2doubles(logs: LogsMap) {
export function logs2doubles(logs: LogsMap) {
return (Object.keys(doublesMap) as DoublesKey[])
.sort((a, b) => toBlobNumber(a) - toBlobNumber(b))
.map(key => Number(logs[doublesMap[key] as LogsKey] || 0))
}
function doubles2logs(doubles: number[]) {
export function doubles2logs(doubles: number[]) {
const logsList = Object.keys(doublesMap)
return doubles.reduce((logs, double, i) => {