Admin home page #128 (#259)

* First iteration on the new PieChart component

* #128 Adds new admin home page

* Fixes code after merging conflicts

* Removes empty file

* Uses real data for admin home page, and improves style

* Reverts debugging code

* Defines missing variable

* Caches user stats data for admin home page

* Typo

* Styles improvements

* Invalidates cache on signup/signin

* Implements top 5 biggest games

* Improves styling

* Improves style

* Using generateManifest to get the proper size

* Reading data from cache

* Removes unnecessary import

* Improves caching mechanism for game sizes

* Removes lint errors

* Replaces piechart tooltip with colors in legend

* Fixes caching

* Fixes caching and slight improvement on pie chart colours

* Fixes a few bugs related to caching

* Fixes bug where app signin didn't refresh cache

* feat: style improvements

* fix: lint

---------

Co-authored-by: DecDuck <declanahofmeyr@gmail.com>
This commit is contained in:
Paco
2025-11-07 22:14:45 +00:00
committed by GitHub
parent 289034d0c8
commit dfa30c8a65
40 changed files with 1352 additions and 150 deletions

View File

@ -15,6 +15,8 @@ import { GameNotFoundError, type LibraryProvider } from "./provider";
import { logger } from "../logging";
import type { GameModel } from "~/prisma/client/models";
import { createHash } from "node:crypto";
import type { WorkingLibrarySource } from "~/server/api/v1/admin/library/sources/index.get";
import gameSizeManager from "~/server/internal/gamesize";
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
return createHash("md5")
@ -39,13 +41,19 @@ class LibraryManager {
this.libraries.delete(id);
}
async fetchLibraries() {
async fetchLibraries(): Promise<WorkingLibrarySource[]> {
const libraries = await prisma.library.findMany({});
const libraryWithMetadata = libraries.map((e) => ({
...e,
working: this.libraries.has(e.id),
}));
return libraryWithMetadata;
const libraryWithMetadata = libraries.map(async (library) => {
const theLibrary = this.libraries.get(library.id);
const working = this.libraries.has(library.id);
return {
...library,
working,
fsStats: working ? theLibrary?.fsStats() : undefined,
};
});
return await Promise.all(libraryWithMetadata);
}
async fetchGamesByLibrary() {
@ -334,6 +342,8 @@ class LibraryManager {
acls: ["system:import:version:read"],
});
await libraryManager.cacheCombinedGameSize(gameId);
await libraryManager.cacheGameVersionSize(gameId, versionName);
progress(100);
},
});
@ -363,6 +373,68 @@ class LibraryManager {
if (!library) return undefined;
return await library.readFile(game, version, filename, options);
}
async deleteGameVersion(gameId: string, version: string) {
await prisma.gameVersion.delete({
where: {
gameId_versionName: {
gameId: gameId,
versionName: version,
},
},
});
await gameSizeManager.deleteGameVersion(gameId, version);
}
async deleteGame(gameId: string) {
await prisma.game.delete({
where: {
id: gameId,
},
});
gameSizeManager.deleteGame(gameId);
}
async getGameVersionSize(
gameId: string,
versionName?: string,
): Promise<number | null> {
return gameSizeManager.getGameVersionSize(gameId, versionName);
}
async getBiggestGamesCombinedVersions(top: number) {
if (await gameSizeManager.isGameSizesCacheEmpty()) {
await gameSizeManager.cacheAllCombinedGames();
}
return gameSizeManager.getBiggestGamesAllVersions(top);
}
async getBiggestGamesLatestVersions(top: number) {
if (await gameSizeManager.isGameVersionsSizesCacheEmpty()) {
await gameSizeManager.cacheAllGameVersions();
}
return gameSizeManager.getBiggestGamesLatestVersion(top);
}
async cacheCombinedGameSize(gameId: string) {
const game = await prisma.game.findFirst({ where: { id: gameId } });
if (!game) {
return;
}
await gameSizeManager.cacheCombinedGame(game);
}
async cacheGameVersionSize(gameId: string, versionName: string) {
const game = await prisma.game.findFirst({
where: { id: gameId },
include: { versions: true },
});
if (!game) {
return;
}
await gameSizeManager.cacheGameVersion(game, versionName);
}
}
export const libraryManager = new LibraryManager();

View File

@ -57,6 +57,8 @@ export abstract class LibraryProvider<CFG> {
filename: string,
options?: { start?: number; end?: number },
): Promise<ReadableStream | undefined>;
abstract fsStats(): { freeSpace: number; totalSpace: number } | undefined;
}
export class GameNotFoundError extends Error {}

View File

@ -8,6 +8,7 @@ import { LibraryBackend } from "~/prisma/client/enums";
import fs from "fs";
import path from "path";
import droplet, { DropletHandler } from "@drop-oss/droplet";
import { fsStats } from "~/server/internal/utils/files";
export const FilesystemProviderConfig = type({
baseDir: "string",
@ -122,4 +123,8 @@ export class FilesystemProvider
return stream;
}
fsStats() {
return fsStats(this.config.baseDir);
}
}

View File

@ -6,6 +6,7 @@ import fs from "fs";
import path from "path";
import droplet from "@drop-oss/droplet";
import { DROPLET_HANDLER } from "./filesystem";
import { fsStats } from "~/server/internal/utils/files";
export const FlatFilesystemProviderConfig = type({
baseDir: "string",
@ -113,4 +114,8 @@ export class FlatFilesystemProvider
return stream.getStream();
}
fsStats() {
return fsStats(this.config.baseDir);
}
}