From 24ebe8c3ffdd130f1a9f2e2df0cb7d6e0e9b672c Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Sat, 22 Aug 2026 17:08:51 +0200 Subject: [PATCH] ENH - add live admin dashboard statistics --- .../components/AdminPanel.contract.test.ts | 31 +- frontend/src/components/AdminPanel.vue | 752 +++++------------- frontend/src/stores/admin.ts | 13 +- frontend/src/stores/phone.ts | 49 +- frontend/src/types/admin.ts | 7 + frontend/testserver/index.cjs | 13 +- sky_phone/config/locales/de.lua | 28 +- sky_phone/config/locales/en.lua | 28 +- sky_phone/config/locales/es.lua | 29 +- sky_phone/source/server/admin.lua | 25 +- 10 files changed, 288 insertions(+), 687 deletions(-) diff --git a/frontend/src/components/AdminPanel.contract.test.ts b/frontend/src/components/AdminPanel.contract.test.ts index d6f1f33..a0d391c 100644 --- a/frontend/src/components/AdminPanel.contract.test.ts +++ b/frontend/src/components/AdminPanel.contract.test.ts @@ -152,24 +152,23 @@ describe('standalone admin panel contracts', () => { } }) - it('removes manual reload and applies a persistent global accent choice', () => { + it('uses a fixed blue accent and replaces appearance controls with live statistics', () => { expect(source).not.toContain(' import { + ChartNoAxesCombined, BadgeDollarSign, BriefcaseBusiness, Check, - ChevronDown, ChevronRight, CircleUserRound, Clipboard, @@ -16,7 +16,6 @@ import { LoaderCircle, LockKeyhole, MessageSquare, - Palette, PhoneCall, PhoneForwarded, Save, @@ -81,17 +80,6 @@ type AdminTab = type ConfiguratorScope = 'config' | 'media' type DeviceAction = 'reset-passcode' | 'change-number' | 'factory-reset' type PendingAction = { kind: 'close' } | { kind: 'player'; source: number } -type AccentChannel = 'blue' | 'green' | 'red' -type AdminFontFamily = - | 'classic' - | 'georgia' - | 'inter' - | 'mono' - | 'system' - | 'tahoma' - | 'trebuchet' - | 'verdana' - const emit = defineEmits<{ close: [] }>() const admin = useAdminStore() const phone = usePhoneStore() @@ -109,61 +97,11 @@ const revealDialogImei = ref('') const deviceAction = ref(null) const deviceActionInput = ref('') const discardDialog = ref(false) -const fontMenuOpen = ref(false) const pendingAction = ref(null) const toast = ref('') const toastTone = ref<'error' | 'success'>('success') let toastTimer: number | undefined - -const accentOptions = [ - { color: '#74d66f', key: 'emerald' }, - { color: '#4f9cff', key: 'blue' }, - { color: '#a875ff', key: 'violet' }, - { color: '#f0a24b', key: 'orange' }, - { color: '#ef6969', key: 'red' }, -] as const -const accentChannels = ['red', 'green', 'blue'] as const -const fontFamilyOptions: Array<{ - key: AdminFontFamily - value: string -}> = [ - { key: 'inter', value: 'var(--sky-font-family, Inter, sans-serif)' }, - { - key: 'system', - value: - 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', - }, - { key: 'classic', value: 'Arial, Helvetica, sans-serif' }, - { key: 'verdana', value: 'Verdana, Geneva, sans-serif' }, - { key: 'tahoma', value: 'Tahoma, Geneva, sans-serif' }, - { - key: 'trebuchet', - value: '"Trebuchet MS", "Lucida Grande", sans-serif', - }, - { key: 'georgia', value: 'Georgia, "Times New Roman", serif' }, - { - key: 'mono', - value: - 'ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace', - }, -] -const accentColor = ref('#74d66f') -const adminFontFamily = ref('inter') -const adminFontSize = ref(100) -const adminFontSizePreview = ref(100) -const activeAdminFontFamily = computed( - () => - fontFamilyOptions.find((option) => option.key === adminFontFamily.value) - ?.value ?? fontFamilyOptions[0].value, -) -const accentRgb = computed(() => { - const color = accentColor.value.replace('#', '') - return { - red: Number.parseInt(color.slice(0, 2), 16), - green: Number.parseInt(color.slice(2, 4), 16), - blue: Number.parseInt(color.slice(4, 6), 16), - } -}) +let statisticsTimer: number | undefined const filteredPlayers = computed(() => { const needle = playerQuery.value.trim().toLocaleLowerCase(phone.lang) @@ -595,7 +533,9 @@ function queueAction(action: PendingAction): void { function selectTab(nextTab: AdminTab): void { tab.value = nextTab - fontMenuOpen.value = false + if (nextTab === 'overview' && admin.initialized && !admin.loading) { + void admin.load() + } if (nextTab === 'configurator' && !admin.configurator) { void admin.loadConfigurator().then((loaded) => { if (!loaded) showToast(errorText(), 'error') @@ -603,61 +543,13 @@ function selectTab(nextTab: AdminTab): void { } } -function selectAccent(color: string): void { - if (!/^#[0-9a-f]{6}$/i.test(color)) return - accentColor.value = color.toLowerCase() - window.localStorage.setItem('sky-phone-admin-accent', accentColor.value) +function formatStatistic(value: number): string { + return value.toLocaleString(phone.lang) } -function updateAccent(event: Event): void { - const input = event.currentTarget as HTMLInputElement - if (/^#[0-9a-f]{6}$/i.test(input.value)) selectAccent(input.value) - else input.value = accentColor.value.toUpperCase() -} - -function updateAccentChannel(channel: AccentChannel, event: Event): void { - const input = event.currentTarget as HTMLInputElement - const value = Math.min(255, Math.max(0, Number(input.value) || 0)) - const channels = { ...accentRgb.value, [channel]: value } - selectAccent( - `#${channels.red.toString(16).padStart(2, '0')}${channels.green - .toString(16) - .padStart(2, '0')}${channels.blue.toString(16).padStart(2, '0')}`, - ) -} - -function accentChannelGradient(channel: AccentChannel): string { - const { red, green, blue } = accentRgb.value - if (channel === 'red') { - return `linear-gradient(90deg, rgb(0, ${green}, ${blue}), rgb(255, ${green}, ${blue}))` - } - if (channel === 'green') { - return `linear-gradient(90deg, rgb(${red}, 0, ${blue}), rgb(${red}, 255, ${blue}))` - } - return `linear-gradient(90deg, rgb(${red}, ${green}, 0), rgb(${red}, ${green}, 255))` -} - -function selectAdminFontFamily(family: AdminFontFamily): void { - adminFontFamily.value = family - fontMenuOpen.value = false - window.localStorage.setItem('sky-phone-admin-font-family', family) -} - -function closeFontMenu(event: FocusEvent): void { - const menu = event.currentTarget as HTMLElement - if (!menu.contains(event.relatedTarget as Node | null)) { - fontMenuOpen.value = false - } -} - -function updateAdminFontSize(event: Event): void { - const size = Number((event.currentTarget as HTMLInputElement).value) - adminFontSize.value = Math.min(115, Math.max(90, size)) - adminFontSizePreview.value = adminFontSize.value - window.localStorage.setItem( - 'sky-phone-admin-font-size', - String(adminFontSize.value), - ) +function statisticPercentage(value: number, total: number): number { + if (total <= 0) return 0 + return Math.min(100, Math.round((value / total) * 100)) } async function runAction(action: PendingAction): Promise { @@ -918,10 +810,6 @@ function auditDescription(entry: AdminAuditEntry): string { function onKeydown(event: KeyboardEvent): void { if (event.key !== 'Escape') return event.preventDefault() - if (fontMenuOpen.value) { - fontMenuOpen.value = false - return - } if (revealDialogImei.value) { revealDialogImei.value = '' return @@ -941,46 +829,21 @@ function onKeydown(event: KeyboardEvent): void { onMounted(() => { document.addEventListener('keydown', onKeydown) - const storedAccent = window.localStorage.getItem('sky-phone-admin-accent') - if (storedAccent && /^#[0-9a-f]{6}$/i.test(storedAccent)) { - accentColor.value = storedAccent.toLowerCase() - } - const storedFontFamily = window.localStorage.getItem( - 'sky-phone-admin-font-family', - ) - if ( - storedFontFamily && - fontFamilyOptions.some((option) => option.key === storedFontFamily) - ) { - adminFontFamily.value = storedFontFamily as AdminFontFamily - } - const storedFontSize = Number( - window.localStorage.getItem('sky-phone-admin-font-size'), - ) - if (storedFontSize >= 90 && storedFontSize <= 115) { - adminFontSize.value = storedFontSize - adminFontSizePreview.value = storedFontSize - } void refreshData() + statisticsTimer = window.setInterval(() => { + if (tab.value === 'overview' && !admin.loading) void admin.load() + }, 15_000) }) onBeforeUnmount(() => { document.removeEventListener('keydown', onKeydown) if (toastTimer) window.clearTimeout(toastTimer) + if (statisticsTimer) window.clearInterval(statisticsTimer) })