Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/PieChart/PieChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</svg>
<div class="absolute inset-0 bg-zinc-900 rounded-full m-12" />
</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
v-for="slice in slices"
:key="slice.value"
Expand Down
8 changes: 3 additions & 5 deletions components/SourceTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@
v-if="source.fsStats"
:percentage="
getPercentage(
source.fsStats.freeSpace,
source.fsStats.totalSpace - source.fsStats.freeSpace,
source.fsStats.totalSpace,
)
"
:color="
getBarColor(
getPercentage(
source.fsStats.freeSpace,
source.fsStats.totalSpace - source.fsStats.freeSpace,
source.fsStats.totalSpace,
),
)
Expand Down Expand Up @@ -148,6 +148,7 @@ import { BackwardIcon, CheckIcon, XMarkIcon } from "@heroicons/vue/24/outline";
import { DropLogo } from "#components";
import { formatBytes } from "~/server/internal/utils/files";
import { getBarColor } from "~/utils/colors";
import { getPercentage } from "~/utils/utils";

const {
sources,
Expand Down Expand Up @@ -183,7 +184,4 @@ const optionsMetadata: {
icon: BackwardIcon,
},
};

const getPercentage = (value: number, total: number) =>
((total - value) * 100) / total;
</script>
21 changes: 21 additions & 0 deletions composables/admin-home.ts
Original file line number Diff line number Diff line change
@@ -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;
});
4 changes: 4 additions & 0 deletions i18n/locales/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,17 @@
"activeInactiveUsers": "Active/inactive users",
"activeUsers": "Active users",
"allVersionsCombined": "All versions combined",
"availableRam": "({freeRam} / {totalRam})",
"biggestGamesOnServer": "Biggest games on server",
"biggestGamesToDownload": "Biggest games to download",
"cpuUsage": "CPU usage",
"games": "Games",
"goToUsers": "Go to users",
"inactiveUsers": "Inactive users",
"latestVersionOnly": "Latest version only",
"librarySources": "Library sources",
"numberCores": "({count} cores) | ({count} core) | ({count} cores)",
"ramUsage": "RAM usage",
"subheader": "Instance summary",
"title": "Home",
"users": "Users",
Expand Down
59 changes: 59 additions & 0 deletions pages/admin/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,61 @@
<PieChart :data="pieChartData" />
</TileWithLink>
</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">
<TileWithLink
title="Library"
Expand Down Expand Up @@ -139,6 +194,8 @@ import { formatBytes } from "~/server/internal/utils/files";
import GamepadIcon from "~/components/Icons/GamepadIcon.vue";
import DropLogo from "~/components/DropLogo.vue";
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 { RankItem } from "~/components/RankingList.vue";

Expand All @@ -152,6 +209,8 @@ useHead({

const { t } = useI18n();

const systemData = useSystemData();

const {
version,
gameCount,
Expand Down
36 changes: 36 additions & 0 deletions server/api/v1/admin/system-data/ws.get.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
2 changes: 2 additions & 0 deletions server/api/v1/auth/odic/callback.get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sessionHandler from "~/server/internal/session";
import authManager from "~/server/internal/auth";
import type { Session } from "~/server/internal/session/types";
import userStatsManager from "~/server/internal/userstats";

defineRouteMeta({
openAPI: {
Expand Down Expand Up @@ -61,6 +62,7 @@ export default defineEventHandler(async (h3) => {
`/auth/mfa?redirect=${result.options.redirect ? encodeURIComponent(result.options.redirect) : "/"}`,
);
}
await userStatsManager.cacheUserSessions();

if (result.options.redirect) {
return sendRedirect(h3, result.options.redirect);
Expand Down
5 changes: 5 additions & 0 deletions server/internal/acls/descriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const userACLDescriptions: ObjectFromList<typeof userACLs> = {
"emoji:read": "Read built in emojis",

"settings:read": "Read system settings.",
"system-data:listen":
"Connect to a websocket to receive system data updates.",
};

export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
Expand Down Expand Up @@ -108,4 +110,7 @@ export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {

"depot:new": "Create a new download depot",
"depot:delete": "Remove a download depot",

"system-data:listen":
"Connect to a websocket to receive system data updates.",
};
4 changes: 4 additions & 0 deletions server/internal/acls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const userACLs = [
"news:read",

"settings:read",

"system-data:listen",
] as const;
const userACLPrefix = "user:";

Expand Down Expand Up @@ -100,6 +102,8 @@ export const systemACLs = [
"maintenance:read",

"settings:update",

"system-data:listen",
] as const;
const systemACLPrefix = "system:";

Expand Down
4 changes: 2 additions & 2 deletions server/internal/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class NotificationSystem {
},
});
for (const notification of notifications) {
await listener.callback(notification);
listener.callback(notification);
}
}

Expand All @@ -69,7 +69,7 @@ class NotificationSystem {
notification.acls.findIndex(
(e) => listener.acls.findIndex((v) => v === e) != -1,
) != -1;
if (hasSome) await listener.callback(notification);
if (hasSome) listener.callback(notification);
}
}

Expand Down
5 changes: 5 additions & 0 deletions server/internal/session/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export default function createCacheSessionProvider() {
const session = await sessions.get(token);
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) {
return (await this.setSession(token, data)) !== undefined;
},
Expand Down
16 changes: 16 additions & 0 deletions server/internal/session/db.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DateTime } from "luxon";

import prisma from "../db/database";
import type { SessionProvider, SessionWithToken } from "./types";
import cacheHandler from "../cache";
Expand Down Expand Up @@ -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) {
const search: SessionWhereInput[] = [];
if (options.userId) {
Expand Down
10 changes: 8 additions & 2 deletions server/internal/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { parse as parseCookies } from "cookie-es";
import type { MinimumRequestObject } from "~/server/h3";
import type { DurationLike } from "luxon";
import { DateTime } from "luxon";
import createDBSessionHandler from "./db";
import prisma from "../db/database";
// import createMemorySessionHandler from "./memory";
import createDBSessionHandler from "./db";
// import createCacheSessionProvider from "./cache";

/*
This implementation may need work.
Expand Down Expand Up @@ -49,7 +51,7 @@ export class SessionHandler {
// Create a new provider
// this.sessionProvider = createCacheSessionProvider();
this.sessionProvider = createDBSessionHandler();
// this.sessionProvider = createMemorySessionProvider();
// this.sessionProvider = createMemorySessionHandler();
}

async signin(
Expand Down Expand Up @@ -217,6 +219,10 @@ export class SessionHandler {
return await this.sessionProvider.findSessions(terms);
}

async getNumberActiveSessions() {
return this.sessionProvider.getNumberActiveSessions();
}

/**
* Update session info
* @param token session token
Expand Down
9 changes: 9 additions & 0 deletions server/internal/session/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ export default function createMemorySessionHandler() {
sessions.delete(token);
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() {
const now = new Date();
for (const [token, session] of sessions) {
Expand Down
1 change: 1 addition & 0 deletions server/internal/session/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ export interface SessionProvider {
removeSession: (token: string) => Promise<boolean>;
cleanupSessions: () => Promise<void>;
findSessions: (options: SessionSearchTerms) => Promise<SessionWithToken[]>;
getNumberActiveSessions: () => Promise<number>;
}
Loading
Loading