mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-01-31 15:37:09 +01:00
Adds new tile on the admin home page with system data. (#301)
* Adds new tile on the admin home page with system data. Also fixes the active users bug in the pie chart * Fixes missing parentheses * Updates user stats cache when signing in * Reads active number of users from session provider * Removes unused variable * Small improvements * Removes acl properties from system data websocket and performs initial push of data * fix: remove acl fetch --------- Co-authored-by: DecDuck <declanahofmeyr@gmail.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<div class="absolute inset-0 bg-zinc-900 rounded-full m-12" />
|
<div class="absolute inset-0 bg-zinc-900 rounded-full m-12" />
|
||||||
</div>
|
</div>
|
||||||
<ul class="flex flex-col gap-y-1 justify-center text-left">
|
<ul class="flex flex-col gap-y-1 my-auto text-left">
|
||||||
<li
|
<li
|
||||||
v-for="slice in slices"
|
v-for="slice in slices"
|
||||||
:key="slice.value"
|
:key="slice.value"
|
||||||
|
|||||||
@@ -92,14 +92,14 @@
|
|||||||
v-if="source.fsStats"
|
v-if="source.fsStats"
|
||||||
:percentage="
|
:percentage="
|
||||||
getPercentage(
|
getPercentage(
|
||||||
source.fsStats.freeSpace,
|
source.fsStats.totalSpace - source.fsStats.freeSpace,
|
||||||
source.fsStats.totalSpace,
|
source.fsStats.totalSpace,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
:color="
|
:color="
|
||||||
getBarColor(
|
getBarColor(
|
||||||
getPercentage(
|
getPercentage(
|
||||||
source.fsStats.freeSpace,
|
source.fsStats.totalSpace - source.fsStats.freeSpace,
|
||||||
source.fsStats.totalSpace,
|
source.fsStats.totalSpace,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -148,6 +148,7 @@ import { BackwardIcon, CheckIcon, XMarkIcon } from "@heroicons/vue/24/outline";
|
|||||||
import { DropLogo } from "#components";
|
import { DropLogo } from "#components";
|
||||||
import { formatBytes } from "~/server/internal/utils/files";
|
import { formatBytes } from "~/server/internal/utils/files";
|
||||||
import { getBarColor } from "~/utils/colors";
|
import { getBarColor } from "~/utils/colors";
|
||||||
|
import { getPercentage } from "~/utils/utils";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
sources,
|
sources,
|
||||||
@@ -183,7 +184,4 @@ const optionsMetadata: {
|
|||||||
icon: BackwardIcon,
|
icon: BackwardIcon,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPercentage = (value: number, total: number) =>
|
|
||||||
((total - value) * 100) / total;
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
21
composables/admin-home.ts
Normal file
21
composables/admin-home.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { SerializeObject } from "nitropack";
|
||||||
|
import type { SystemData } from "~/server/internal/system-data";
|
||||||
|
|
||||||
|
const ws = new WebSocketHandler("/api/v1/admin/system-data/ws");
|
||||||
|
|
||||||
|
export const useSystemData = () =>
|
||||||
|
useState<SerializeObject<SystemData>>(
|
||||||
|
"system-data",
|
||||||
|
(): SystemData => ({
|
||||||
|
totalRam: 0,
|
||||||
|
freeRam: 0,
|
||||||
|
cpuLoad: 0,
|
||||||
|
cpuCores: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
ws.listen((systemDataString) => {
|
||||||
|
const data = JSON.parse(systemDataString) as SerializeObject<SystemData>;
|
||||||
|
const systemData = useSystemData();
|
||||||
|
systemData.value = data;
|
||||||
|
});
|
||||||
@@ -280,13 +280,17 @@
|
|||||||
"activeInactiveUsers": "Active/inactive users",
|
"activeInactiveUsers": "Active/inactive users",
|
||||||
"activeUsers": "Active users",
|
"activeUsers": "Active users",
|
||||||
"allVersionsCombined": "All versions combined",
|
"allVersionsCombined": "All versions combined",
|
||||||
|
"availableRam": "({freeRam} / {totalRam})",
|
||||||
"biggestGamesOnServer": "Biggest games on server",
|
"biggestGamesOnServer": "Biggest games on server",
|
||||||
"biggestGamesToDownload": "Biggest games to download",
|
"biggestGamesToDownload": "Biggest games to download",
|
||||||
|
"cpuUsage": "CPU usage",
|
||||||
"games": "Games",
|
"games": "Games",
|
||||||
"goToUsers": "Go to users",
|
"goToUsers": "Go to users",
|
||||||
"inactiveUsers": "Inactive users",
|
"inactiveUsers": "Inactive users",
|
||||||
"latestVersionOnly": "Latest version only",
|
"latestVersionOnly": "Latest version only",
|
||||||
"librarySources": "Library sources",
|
"librarySources": "Library sources",
|
||||||
|
"numberCores": "({count} cores) | ({count} core) | ({count} cores)",
|
||||||
|
"ramUsage": "RAM usage",
|
||||||
"subheader": "Instance summary",
|
"subheader": "Instance summary",
|
||||||
"title": "Home",
|
"title": "Home",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
|
|||||||
@@ -105,6 +105,61 @@
|
|||||||
<PieChart :data="pieChartData" />
|
<PieChart :data="pieChartData" />
|
||||||
</TileWithLink>
|
</TileWithLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-6 row-span-1 lg:col-span-2 lg:row-span-2">
|
||||||
|
<TileWithLink title="System">
|
||||||
|
<div class="h-full pb-15 content-center">
|
||||||
|
<div class="grid grid-cols-1 text-center gap-4">
|
||||||
|
<h3 class="col-span-1 text-lg font-semibold flex">
|
||||||
|
<div class="flex-1 text-left">
|
||||||
|
{{ $t("home.admin.cpuUsage") }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-sm grow text-right self-center">
|
||||||
|
{{ $t("home.admin.numberCores", systemData.cpuCores) }}
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<div class="col-span-1">
|
||||||
|
<ProgressBar
|
||||||
|
:color="getBarColor(systemData.cpuLoad)"
|
||||||
|
:percentage="systemData.cpuLoad"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 class="col-span-1 text-lg font-semibold my-2 flex">
|
||||||
|
<div class="flex-none text-left">
|
||||||
|
{{ $t("home.admin.ramUsage") }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-sm grow text-right self-center">
|
||||||
|
{{
|
||||||
|
$t("home.admin.availableRam", {
|
||||||
|
freeRam: formatBytes(systemData.freeRam),
|
||||||
|
totalRam: formatBytes(systemData.totalRam),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<div class="col-span-1">
|
||||||
|
<ProgressBar
|
||||||
|
:color="
|
||||||
|
getBarColor(
|
||||||
|
getPercentage(
|
||||||
|
systemData.totalRam - systemData.freeRam,
|
||||||
|
systemData.totalRam,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:percentage="
|
||||||
|
getPercentage(
|
||||||
|
systemData.totalRam - systemData.freeRam,
|
||||||
|
systemData.totalRam,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TileWithLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-span-6">
|
<div class="col-span-6">
|
||||||
<TileWithLink
|
<TileWithLink
|
||||||
title="Library"
|
title="Library"
|
||||||
@@ -139,6 +194,8 @@ import { formatBytes } from "~/server/internal/utils/files";
|
|||||||
import GamepadIcon from "~/components/Icons/GamepadIcon.vue";
|
import GamepadIcon from "~/components/Icons/GamepadIcon.vue";
|
||||||
import DropLogo from "~/components/DropLogo.vue";
|
import DropLogo from "~/components/DropLogo.vue";
|
||||||
import { ServerStackIcon, UserGroupIcon } from "@heroicons/vue/24/outline";
|
import { ServerStackIcon, UserGroupIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { getPercentage } from "~/utils/utils";
|
||||||
|
import { getBarColor } from "~/utils/colors";
|
||||||
import type { GameSize } from "~/server/internal/gamesize";
|
import type { GameSize } from "~/server/internal/gamesize";
|
||||||
import type { RankItem } from "~/components/RankingList.vue";
|
import type { RankItem } from "~/components/RankingList.vue";
|
||||||
|
|
||||||
@@ -152,6 +209,8 @@ useHead({
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const systemData = useSystemData();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
version,
|
version,
|
||||||
gameCount,
|
gameCount,
|
||||||
|
|||||||
36
server/api/v1/admin/system-data/ws.get.ts
Normal file
36
server/api/v1/admin/system-data/ws.get.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import systemManager from "~/server/internal/system-data";
|
||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import { logger } from "~/server/internal/logging";
|
||||||
|
|
||||||
|
// TODO add web socket sessions for horizontal scaling
|
||||||
|
// Peer ID to user ID
|
||||||
|
const socketSessions = new Map<string, string>();
|
||||||
|
|
||||||
|
export default defineWebSocketHandler({
|
||||||
|
async open(peer) {
|
||||||
|
const h3 = { headers: peer.request?.headers ?? new Headers() };
|
||||||
|
const userId = await aclManager.getUserIdACL(h3, ["system-data:listen"]);
|
||||||
|
if (!userId) {
|
||||||
|
peer.send("unauthenticated");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socketSessions.set(peer.id, userId);
|
||||||
|
|
||||||
|
systemManager.listen(userId, peer.id, (systemData) => {
|
||||||
|
peer.send(JSON.stringify(systemData));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async close(peer, _details) {
|
||||||
|
const userId = socketSessions.get(peer.id);
|
||||||
|
if (!userId) {
|
||||||
|
logger.info(`skipping websocket close for ${peer.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
systemManager.unlisten(userId, peer.id);
|
||||||
|
systemManager.unlisten("system", peer.id); // In case we were listening as 'system'
|
||||||
|
socketSessions.delete(peer.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import sessionHandler from "~/server/internal/session";
|
import sessionHandler from "~/server/internal/session";
|
||||||
import authManager from "~/server/internal/auth";
|
import authManager from "~/server/internal/auth";
|
||||||
import type { Session } from "~/server/internal/session/types";
|
import type { Session } from "~/server/internal/session/types";
|
||||||
|
import userStatsManager from "~/server/internal/userstats";
|
||||||
|
|
||||||
defineRouteMeta({
|
defineRouteMeta({
|
||||||
openAPI: {
|
openAPI: {
|
||||||
@@ -61,6 +62,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
`/auth/mfa?redirect=${result.options.redirect ? encodeURIComponent(result.options.redirect) : "/"}`,
|
`/auth/mfa?redirect=${result.options.redirect ? encodeURIComponent(result.options.redirect) : "/"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await userStatsManager.cacheUserSessions();
|
||||||
|
|
||||||
if (result.options.redirect) {
|
if (result.options.redirect) {
|
||||||
return sendRedirect(h3, result.options.redirect);
|
return sendRedirect(h3, result.options.redirect);
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export const userACLDescriptions: ObjectFromList<typeof userACLs> = {
|
|||||||
"emoji:read": "Read built in emojis",
|
"emoji:read": "Read built in emojis",
|
||||||
|
|
||||||
"settings:read": "Read system settings.",
|
"settings:read": "Read system settings.",
|
||||||
|
"system-data:listen":
|
||||||
|
"Connect to a websocket to receive system data updates.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||||
@@ -108,4 +110,7 @@ export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
|||||||
|
|
||||||
"depot:new": "Create a new download depot",
|
"depot:new": "Create a new download depot",
|
||||||
"depot:delete": "Remove a download depot",
|
"depot:delete": "Remove a download depot",
|
||||||
|
|
||||||
|
"system-data:listen":
|
||||||
|
"Connect to a websocket to receive system data updates.",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export const userACLs = [
|
|||||||
"news:read",
|
"news:read",
|
||||||
|
|
||||||
"settings:read",
|
"settings:read",
|
||||||
|
|
||||||
|
"system-data:listen",
|
||||||
] as const;
|
] as const;
|
||||||
const userACLPrefix = "user:";
|
const userACLPrefix = "user:";
|
||||||
|
|
||||||
@@ -100,6 +102,8 @@ export const systemACLs = [
|
|||||||
"maintenance:read",
|
"maintenance:read",
|
||||||
|
|
||||||
"settings:update",
|
"settings:update",
|
||||||
|
|
||||||
|
"system-data:listen",
|
||||||
] as const;
|
] as const;
|
||||||
const systemACLPrefix = "system:";
|
const systemACLPrefix = "system:";
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class NotificationSystem {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
for (const notification of notifications) {
|
for (const notification of notifications) {
|
||||||
await listener.callback(notification);
|
listener.callback(notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ class NotificationSystem {
|
|||||||
notification.acls.findIndex(
|
notification.acls.findIndex(
|
||||||
(e) => listener.acls.findIndex((v) => v === e) != -1,
|
(e) => listener.acls.findIndex((v) => v === e) != -1,
|
||||||
) != -1;
|
) != -1;
|
||||||
if (hasSome) await listener.callback(notification);
|
if (hasSome) listener.callback(notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export default function createCacheSessionProvider() {
|
|||||||
const session = await sessions.get(token);
|
const session = await sessions.get(token);
|
||||||
return session ? (session as T) : undefined; // Ensure undefined is returned if session is not found
|
return session ? (session as T) : undefined; // Ensure undefined is returned if session is not found
|
||||||
},
|
},
|
||||||
|
async getNumberActiveSessions() {
|
||||||
|
const now = new Date();
|
||||||
|
const allSessions = await sessions.getItems(await sessions.getKeys());
|
||||||
|
return allSessions.filter(({ value }) => value.expiresAt > now).length;
|
||||||
|
},
|
||||||
async updateSession(token, data) {
|
async updateSession(token, data) {
|
||||||
return (await this.setSession(token, data)) !== undefined;
|
return (await this.setSession(token, data)) !== undefined;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
import prisma from "../db/database";
|
import prisma from "../db/database";
|
||||||
import type { SessionProvider, SessionWithToken } from "./types";
|
import type { SessionProvider, SessionWithToken } from "./types";
|
||||||
import cacheHandler from "../cache";
|
import cacheHandler from "../cache";
|
||||||
@@ -76,6 +78,20 @@ export default function createDBSessionHandler(): SessionProvider {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async getNumberActiveSessions() {
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
await prisma.session.groupBy({
|
||||||
|
by: ["userId"],
|
||||||
|
where: {
|
||||||
|
expiresAt: {
|
||||||
|
gt: DateTime.now().toJSDate(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).length || 0
|
||||||
|
);
|
||||||
|
},
|
||||||
async findSessions(options) {
|
async findSessions(options) {
|
||||||
const search: SessionWhereInput[] = [];
|
const search: SessionWhereInput[] = [];
|
||||||
if (options.userId) {
|
if (options.userId) {
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import { parse as parseCookies } from "cookie-es";
|
|||||||
import type { MinimumRequestObject } from "~/server/h3";
|
import type { MinimumRequestObject } from "~/server/h3";
|
||||||
import type { DurationLike } from "luxon";
|
import type { DurationLike } from "luxon";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import createDBSessionHandler from "./db";
|
|
||||||
import prisma from "../db/database";
|
import prisma from "../db/database";
|
||||||
|
// import createMemorySessionHandler from "./memory";
|
||||||
|
import createDBSessionHandler from "./db";
|
||||||
|
// import createCacheSessionProvider from "./cache";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This implementation may need work.
|
This implementation may need work.
|
||||||
@@ -49,7 +51,7 @@ export class SessionHandler {
|
|||||||
// Create a new provider
|
// Create a new provider
|
||||||
// this.sessionProvider = createCacheSessionProvider();
|
// this.sessionProvider = createCacheSessionProvider();
|
||||||
this.sessionProvider = createDBSessionHandler();
|
this.sessionProvider = createDBSessionHandler();
|
||||||
// this.sessionProvider = createMemorySessionProvider();
|
// this.sessionProvider = createMemorySessionHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
async signin(
|
async signin(
|
||||||
@@ -217,6 +219,10 @@ export class SessionHandler {
|
|||||||
return await this.sessionProvider.findSessions(terms);
|
return await this.sessionProvider.findSessions(terms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getNumberActiveSessions() {
|
||||||
|
return this.sessionProvider.getNumberActiveSessions();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update session info
|
* Update session info
|
||||||
* @param token session token
|
* @param token session token
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ export default function createMemorySessionHandler() {
|
|||||||
sessions.delete(token);
|
sessions.delete(token);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
async getNumberActiveSessions() {
|
||||||
|
let activeSessions = 0;
|
||||||
|
for (const [_key, session] of sessions) {
|
||||||
|
if (session.expiresAt.getDate() > Date.now()) {
|
||||||
|
activeSessions += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return activeSessions;
|
||||||
|
},
|
||||||
async cleanupSessions() {
|
async cleanupSessions() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
for (const [token, session] of sessions) {
|
for (const [token, session] of sessions) {
|
||||||
|
|||||||
1
server/internal/session/types.d.ts
vendored
1
server/internal/session/types.d.ts
vendored
@@ -50,4 +50,5 @@ export interface SessionProvider {
|
|||||||
removeSession: (token: string) => Promise<boolean>;
|
removeSession: (token: string) => Promise<boolean>;
|
||||||
cleanupSessions: () => Promise<void>;
|
cleanupSessions: () => Promise<void>;
|
||||||
findSessions: (options: SessionSearchTerms) => Promise<SessionWithToken[]>;
|
findSessions: (options: SessionSearchTerms) => Promise<SessionWithToken[]>;
|
||||||
|
getNumberActiveSessions: () => Promise<number>;
|
||||||
}
|
}
|
||||||
|
|||||||
58
server/internal/system-data/index.ts
Normal file
58
server/internal/system-data/index.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import os from "os";
|
||||||
|
|
||||||
|
export type SystemData = {
|
||||||
|
totalRam: number;
|
||||||
|
freeRam: number;
|
||||||
|
cpuLoad: number;
|
||||||
|
cpuCores: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SystemManager {
|
||||||
|
// userId to acl to listenerId
|
||||||
|
private listeners = new Map<
|
||||||
|
string,
|
||||||
|
Map<string, { callback: (systemData: SystemData) => void }>
|
||||||
|
>();
|
||||||
|
|
||||||
|
listen(
|
||||||
|
userId: string,
|
||||||
|
id: string,
|
||||||
|
callback: (systemData: SystemData) => void,
|
||||||
|
) {
|
||||||
|
if (!this.listeners.has(userId)) this.listeners.set(userId, new Map());
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-extra-non-null-assertion
|
||||||
|
this.listeners.get(userId)!!.set(id, { callback });
|
||||||
|
this.pushUpdate(userId, id);
|
||||||
|
setInterval(() => this.pushUpdate(userId, id), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
unlisten(userId: string, id: string) {
|
||||||
|
this.listeners.get(userId)?.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pushUpdate(userId: string, id: string) {
|
||||||
|
const listener = this.listeners.get(userId)?.get(id);
|
||||||
|
if (!listener) {
|
||||||
|
throw new Error("Failed to catch-up listener: callback does not exist");
|
||||||
|
}
|
||||||
|
listener.callback(this.getSystemData());
|
||||||
|
}
|
||||||
|
|
||||||
|
getSystemData(): SystemData {
|
||||||
|
return {
|
||||||
|
cpuLoad: this.cpuLoad(),
|
||||||
|
totalRam: os.totalmem(),
|
||||||
|
freeRam: os.freemem(),
|
||||||
|
cpuCores: os.cpus().length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private cpuLoad() {
|
||||||
|
const [oneMinLoad, _fiveMinLoad, _fiftenMinLoad] = os.loadavg();
|
||||||
|
const numberCpus = os.cpus().length;
|
||||||
|
return 100 - ((numberCpus - oneMinLoad) / numberCpus) * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const systemManager = new SystemManager();
|
||||||
|
export default systemManager;
|
||||||
@@ -4,25 +4,14 @@ Handles managing collections
|
|||||||
|
|
||||||
import cacheHandler from "../cache";
|
import cacheHandler from "../cache";
|
||||||
import prisma from "../db/database";
|
import prisma from "../db/database";
|
||||||
import { DateTime } from "luxon";
|
import sessionHandler from "../session";
|
||||||
|
|
||||||
class UserStatsManager {
|
class UserStatsManager {
|
||||||
// Caches the user's core library
|
// Caches the user's core library
|
||||||
private userStatsCache = cacheHandler.createCache<number>("userStats");
|
private userStatsCache = cacheHandler.createCache<number>("userStats");
|
||||||
|
|
||||||
async cacheUserSessions() {
|
async cacheUserSessions() {
|
||||||
const activeSessions =
|
const activeSessions = await sessionHandler.getNumberActiveSessions();
|
||||||
(
|
|
||||||
await prisma.client.groupBy({
|
|
||||||
by: ["userId"],
|
|
||||||
where: {
|
|
||||||
id: { not: "system" },
|
|
||||||
lastConnected: {
|
|
||||||
gt: DateTime.now().minus({ months: 1 }).toISO(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).length || 0;
|
|
||||||
await this.userStatsCache.set("activeSessions", activeSessions);
|
await this.userStatsCache.set("activeSessions", activeSessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
utils/utils.ts
Normal file
7
utils/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const getPercentage = (value: number, total: number) => {
|
||||||
|
const percentage = (value * 100) / total;
|
||||||
|
if (!isNaN(percentage)) {
|
||||||
|
return percentage;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user