feat: activity screen in the SPA, route /activity

This commit is contained in:
fsylvain committed 2026-10-07 01:48:21 +02:00
1 parent e37a46a027
commit 35db442cd3
47 files changed
+7582 -11

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
import { formatNumber, t } from '@/i18n'
import type { ActivitySummary, RangeKey } from '@/lib/activity'
export type StripKey = 'total' | 'active' | 'failed' | 'security'
export type StripTone = 'neutral' | 'error' | 'warning' | 'calm'
export interface StripCard {
key: StripKey
label: string
value: string
subline: string
icon: string | null
tone: StripTone
series: number[]
}
export const STRIP_KEYS: StripKey[] = ['total', 'active', 'failed', 'security']
export const BAR_COUNT = 7
export const MIN_BAR_HEIGHT = 2
function percent(part: number, whole: number) {
return Math.round((Math.abs(part - whole) / whole) * 100)
}
function totalSubline(summary: ActivitySummary, range: RangeKey) {
if (summary.total === 0) return { text: t('No data yet'), icon: null }
if (summary.previous_total === 0) return { text: t('No earlier data'), icon: null }
if (summary.total === summary.previous_total) return { text: t('No change'), icon: null }
const value = formatNumber(percent(summary.total, summary.previous_total))
const text =
range === 'today'
? t('{0}% vs previous day', [value])
: range === '30d'
? t('{0}% vs previous 30 days', [value])
: range === 'custom'
? t('{0}% vs previous period', [value])
: t('{0}% vs previous 7 days', [value])
const icon =
summary.total >= summary.previous_total ? 'i-lucide-trending-up' : 'i-lucide-trending-down'
return { text, icon }
}
function peopleSubline(summary: ActivitySummary) {
if (summary.total === 0) return t('No data yet')
const { users, api_keys: keys } = summary.active_people
const people = users === 1 ? t('1 person') : t('{0} people', [formatNumber(users)])
const apiKeys = keys === 1 ? t('1 API key') : t('{0} API keys', [formatNumber(keys)])
return `${people}, ${apiKeys}`
}
function failedSubline(summary: ActivitySummary) {
const ratio = (summary.failed / summary.total) * 100
return t('{0}% of all actions', [
formatNumber(ratio, { minimumFractionDigits: 1, maximumFractionDigits: 1 }),
])
}
function flaggedSubline(count: number) {
return count === 1 ? t('1 flagged') : t('{0} flagged', [formatNumber(count)])
}
export function stripCards(summary: ActivitySummary, range: RangeKey): StripCard[] {
const total = totalSubline(summary, range)
const failedCalm = summary.failed === 0
const securityCalm = summary.security === 0
return [
{
key: 'total',
label: t('Total actions'),
value: formatNumber(summary.total),
subline: total.text,
icon: total.icon,
tone: 'neutral',
series: summary.series.total,
},
{
key: 'active',
label: t('Active people'),
value: formatNumber(summary.active_people.users + summary.active_people.api_keys),
subline: peopleSubline(summary),
icon: null,
tone: 'neutral',
series: summary.series.active,
},
{
key: 'failed',
label: t('Failed actions'),
value: formatNumber(summary.failed),
subline: failedCalm ? t('Nothing failed') : failedSubline(summary),
icon: failedCalm ? 'i-lucide-circle-check' : 'i-lucide-circle-x',
tone: failedCalm ? 'calm' : 'error',
series: summary.series.failed,
},
{
key: 'security',
label: t('Security events'),
value: formatNumber(summary.security),
subline: securityCalm ? t('Nothing to flag') : flaggedSubline(summary.security),
icon: securityCalm ? 'i-lucide-circle-check' : 'i-lucide-shield-alert',
tone: securityCalm ? 'calm' : 'warning',
series: summary.series.security,
},
]
}
export function barHeights(series: number[], height: number) {
const peak = Math.max(...series, 1)
return series.map((value) => Math.max(MIN_BAR_HEIGHT, Math.round((value / peak) * height)))
}