/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ /** @jsxRuntime automatic */ /** @jsxImportSource hono/jsx */ import {getErrorMessage} from '@fluxer/admin/src/api/Errors'; import {getGuildMemoryStats, getNodeStats} from '@fluxer/admin/src/api/System'; import {ErrorAlert} from '@fluxer/admin/src/components/ErrorDisplay'; import {Layout} from '@fluxer/admin/src/components/Layout'; import {PageHeader} from '@fluxer/admin/src/components/ui/Layout/PageHeader'; import {VStack} from '@fluxer/admin/src/components/ui/Layout/VStack'; import {Heading, Text} from '@fluxer/admin/src/components/ui/Typography'; import type {Session} from '@fluxer/admin/src/types/App'; import type {AdminConfig as Config} from '@fluxer/admin/src/types/Config'; import {MEDIA_PROXY_ICON_SIZE_DEFAULT} from '@fluxer/constants/src/MediaProxyAssetSizes'; import type {Flash} from '@fluxer/hono/src/Flash'; import {formatNumber} from '@fluxer/number_utils/src/NumberFormatting'; import type {GuildMemoryStatsResponse, NodeStatsResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas'; import type {UserAdminResponse} from '@fluxer/schema/src/domains/admin/AdminUserSchemas'; import {Alert} from '@fluxer/ui/src/components/Alert'; import {Button} from '@fluxer/ui/src/components/Button'; import {CardElevated} from '@fluxer/ui/src/components/Card'; import {CsrfInput} from '@fluxer/ui/src/components/CsrfInput'; import type {FC} from 'hono/jsx'; interface GatewayPageProps { config: Config; session: Session; currentAdmin: UserAdminResponse | undefined; flash: Flash | undefined; adminAcls: Array; reloadResult?: number | undefined; assetVersion: string; csrfToken: string; } function formatNumberLocal(n: number): string { return formatNumber(n, {locale: 'en-US'}); } function formatMemory(memoryMb: number): string { if (memoryMb < 1.0) { const kb = memoryMb * 1024.0; return `${kb.toFixed(2)} KB`; } if (memoryMb < 1024.0) { return `${memoryMb.toFixed(2)} MB`; } const gb = memoryMb / 1024.0; return `${gb.toFixed(2)} GB`; } function formatMemoryFromBytes(bytesStr: string): string { const bytes = BigInt(bytesStr); const mbWith2Decimals = Number((bytes * 100n) / 1_048_576n) / 100; return formatMemory(mbWith2Decimals); } function getFirstChar(s: string): string { if (s === '') return '?'; return s.charAt(0); } function getGuildIconUrl( mediaEndpoint: string, guildId: string, guildIcon: string | null, forceStatic: boolean, ): string | null { if (!guildIcon) return null; const isAnimated = guildIcon.startsWith('a_'); const extension = isAnimated && !forceStatic ? 'gif' : 'webp'; return `${mediaEndpoint}/icons/${guildId}/${guildIcon}.${extension}?size=${MEDIA_PROXY_ICON_SIZE_DEFAULT}`; } const StatCard: FC<{label: string; value: string}> = ({label, value}) => (
{label}
{value}
); type ProcessMemoryStats = GuildMemoryStatsResponse['guilds'][number]; const NodeStatsSection: FC<{stats: NodeStatsResponse}> = ({stats}) => ( Gateway Statistics
); const GuildRow: FC<{config: Config; guild: ProcessMemoryStats; rank: number}> = ({config, guild, rank}) => { const iconUrl = guild.guild_id ? getGuildIconUrl(config.mediaEndpoint, guild.guild_id, guild.guild_icon, true) : null; return ( #{rank} {guild.guild_id ? ( {iconUrl ? ( {guild.guild_name} ) : (
{getFirstChar(guild.guild_name)}
)}
{guild.guild_name}
{guild.guild_id}
) : (
?
{guild.guild_name}
)} {formatMemoryFromBytes(guild.memory)} {formatNumberLocal(guild.member_count)} {formatNumberLocal(guild.session_count)} {formatNumberLocal(guild.presence_count)} ); }; const GuildTable: FC<{config: Config; guilds: Array}> = ({config, guilds}) => { if (guilds.length === 0) { return
No guilds in memory
; } return (
{guilds.map((guild, index) => ( ))}
Rank Guild RAM Usage Members Sessions Presences
); }; const SuccessView: FC<{ config: Config; adminAcls: Array; nodeStats: NodeStatsResponse | null; guilds: Array; reloadResult: number | undefined; csrfToken: string; }> = ({config, adminAcls, nodeStats, guilds, reloadResult, csrfToken}) => { const canReloadAll = adminAcls.includes('gateway:reload_all') || adminAcls.includes('*'); return ( ) } /> {reloadResult !== undefined && Successfully reloaded {reloadResult} guilds!} {nodeStats && }
Guild Memory Leaderboard (Top 100) Guilds ranked by memory usage, showing the top 100 consumers
); }; export async function GatewayPage({ config, session, currentAdmin, flash, adminAcls, reloadResult, assetVersion, csrfToken, }: GatewayPageProps) { const nodeStatsResult = await getNodeStats(config, session); const guildStatsResult = await getGuildMemoryStats(config, session, 100); const content = guildStatsResult.ok ? ( ) : ( <> Gateway ); return ( {content} ); }