Compare commits

..

2 Commits

Author SHA1 Message Date
cb7143ed3d should be all good now (and added api status notifications)
Some checks failed
Deploy Backend / deploy (push) Successful in 3m38s
Deploy Website / deploy (push) Has been cancelled
2024-10-16 08:15:11 +01:00
1eed0e1e99 im dumb 2024-10-16 08:03:36 +01:00
7 changed files with 107 additions and 7 deletions

@ -25,6 +25,6 @@ RUN bun --filter '@ssr/common' build
COPY --from=depends /app/projects/backend ./projects/backend COPY --from=depends /app/projects/backend ./projects/backend
# Lint before starting # Lint before starting
RUN bun --filter 'website' lint RUN bun --filter 'backend' lint
CMD ["bun", "run", "--filter", "backend", "start"] CMD ["bun", "run", "--filter", "backend", "start"]

@ -12,6 +12,13 @@ export default class AppController {
}; };
} }
@Get("/health")
public async getHealth() {
return {
status: "OK",
};
}
@Get("/statistics") @Get("/statistics")
public async getStatistics() { public async getStatistics() {
return await AppService.getAppStatistics(); return await AppService.getAppStatistics();

@ -0,0 +1,27 @@
import ky from "ky";
type ApiHealth = {
online: boolean;
};
/**
* Gets the health of the api server.
*
* @param url the url of the api
*/
export async function getApiHealth(url: string): Promise<ApiHealth> {
try {
await ky
.get(url, {
cache: "no-cache",
})
.json();
return {
online: true,
};
} catch {
return {
online: false,
};
}
}

@ -1,3 +1,5 @@
import ky from "ky";
/** /**
* Checks if we're in production * Checks if we're in production
*/ */
@ -24,3 +26,17 @@ export function delay(ms: number) {
export function getPageFromRank(rank: number, itemsPerPage: number) { export function getPageFromRank(rank: number, itemsPerPage: number) {
return Math.floor(rank / itemsPerPage) + 1; return Math.floor(rank / itemsPerPage) + 1;
} }
/**
* Fetches data from the given url.
*
* @param url the url to fetch
*/
export async function kyFetch<T>(url: string): Promise<T | undefined> {
try {
return await ky.get<T>(url).json();
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
return undefined;
}
}

@ -1,14 +1,14 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
import ky from "ky";
import { config } from "../../../config"; import { config } from "../../../config";
import { AppStatistics } from "@ssr/common/types/backend/app-statistics"; import { AppStatistics } from "@ssr/common/types/backend/app-statistics";
import Statistic from "@/components/home/statistic"; import Statistic from "@/components/home/statistic";
import { kyFetch } from "@ssr/common/utils/utils";
export const dynamic = "force-dynamic"; // Always generate the page on load export const dynamic = "force-dynamic"; // Always generate the page on load
export default async function HomePage() { export default async function HomePage() {
const statistics = await ky.get(config.siteApi + "/statistics").json<AppStatistics>(); const statistics = await kyFetch<AppStatistics>(config.siteApi + "/statistics");
return ( return (
<main className="flex flex-col items-center w-full gap-6 text-center"> <main className="flex flex-col items-center w-full gap-6 text-center">
@ -21,10 +21,12 @@ export default async function HomePage() {
<p>ScoreSaber Reloaded is a website that allows you to track your ScoreSaber data over time.</p> <p>ScoreSaber Reloaded is a website that allows you to track your ScoreSaber data over time.</p>
</div> </div>
<div className="flex items-center flex-col"> {statistics && (
<p className="font-semibold">Site Statistics</p> <div className="flex items-center flex-col">
<Statistic title="Total Tracked Players" value={statistics.trackedPlayers} /> <p className="font-semibold">Site Statistics</p>
</div> <Statistic title="Total Tracked Players" value={statistics.trackedPlayers} />
</div>
)}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Link href="/search"> <Link href="/search">

@ -14,6 +14,7 @@ import NavBar from "../components/navbar/navbar";
import { Colors } from "@/common/colors"; import { Colors } from "@/common/colors";
import OfflineNetwork from "@/components/offline-network"; import OfflineNetwork from "@/components/offline-network";
import Script from "next/script"; import Script from "next/script";
import { ApiHealth } from "@/components/api/api-health";
const siteFont = localFont({ const siteFont = localFont({
src: "./fonts/JetBrainsMono.ttf", src: "./fonts/JetBrainsMono.ttf",
@ -79,6 +80,7 @@ export default function RootLayout({
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange> <ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
<QueryProvider> <QueryProvider>
<AnimatePresence> <AnimatePresence>
<ApiHealth />
<main className="flex flex-col min-h-screen gap-2 text-white w-full"> <main className="flex flex-col min-h-screen gap-2 text-white w-full">
<NavBar /> <NavBar />
<div className="z-[1] m-auto flex flex-col flex-grow items-center w-full md:max-w-[1600px]"> <div className="z-[1] m-auto flex flex-col flex-grow items-center w-full md:max-w-[1600px]">

@ -0,0 +1,46 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import { getApiHealth } from "@ssr/common/utils/api-utils";
import { config } from "../../../config";
import { useToast } from "@/hooks/use-toast";
import { useIsFirstRender } from "@uidotdev/usehooks";
export function ApiHealth() {
const { toast } = useToast();
const firstRender = useIsFirstRender();
const [online, setOnline] = useState<boolean>(true);
const previousOnlineStatus = useRef<boolean>(true);
useQuery({
queryKey: ["api-health"],
queryFn: async () => {
const status = (await getApiHealth(config.siteApi)).online;
setOnline(status);
return status;
},
refetchInterval: 1000 * 15,
});
useEffect(() => {
if (firstRender) {
return;
}
// Trigger toast only if the online status changes
if (previousOnlineStatus.current !== online) {
toast({
title: `The API is now ${online ? "Online" : "Offline"}!`,
description: online ? "The API has recovered connectivity." : "The API has lost connectivity.",
variant: online ? "success" : "destructive",
duration: 10_000, // 10 seconds
});
}
// Update the previous online status
previousOnlineStatus.current = online;
}, [firstRender, online, toast]);
return <></>;
}