Alter Maintenance-Screen hinzugefügt

This commit is contained in:
PxlLoewe
2025-07-03 21:17:23 -07:00
parent 2ff2c3274a
commit 7c050cf42e
16 changed files with 168 additions and 199 deletions

View File

@@ -1,22 +1,13 @@
import { prisma } from "@repo/db"; import { prisma } from "@repo/db";
import { MessageCircleWarning } from "lucide-react"; import { MessageCircleWarning } from "lucide-react";
const fetchMainMessage = async () => {
return await prisma.notam.findFirst({
where: {
active: true,
},
});
};
export const WarningAlert = async () => { export const WarningAlert = async () => {
const mainMessage = await fetchMainMessage(); const config = await prisma.notam.findFirst({
orderBy: [{ createdAt: "desc" }],
if (mainMessage?.showUntilActive && new Date(mainMessage.showUntil) < new Date()) { });
return <></>;
}
let msgColor; let msgColor;
switch (mainMessage?.color) { switch (config?.color) {
case "WARNING": case "WARNING":
msgColor = "alert alert-soft alert-warning ml-3 py-2 flex items-center gap-2"; msgColor = "alert alert-soft alert-warning ml-3 py-2 flex items-center gap-2";
break; break;
@@ -33,15 +24,11 @@ export const WarningAlert = async () => {
msgColor = "alert alert-soft ml-3 py-2 flex items-center gap-2"; msgColor = "alert alert-soft ml-3 py-2 flex items-center gap-2";
} }
if ((mainMessage?.message == "" && !mainMessage?.wartungsmodus) || !mainMessage) { if (config?.message || config?.maintenanceEnabled) {
return <></>;
} else {
return ( return (
<div role="alert" className={msgColor}> <div className={msgColor}>
<MessageCircleWarning /> <MessageCircleWarning className="w-5 h-5" />
<span className="font-bold m-0"> {config?.message}
{mainMessage?.wartungsmodus ? "Wartungsmodus aktiv!" : mainMessage?.message}
</span>
</div> </div>
); );
} }

View File

@@ -7,6 +7,7 @@ import { Toaster } from "react-hot-toast";
import { QueryProvider } from "_components/QueryProvider"; import { QueryProvider } from "_components/QueryProvider";
import { prisma } from "@repo/db"; import { prisma } from "@repo/db";
import { Error as ErrorComp } from "_components/Error"; import { Error as ErrorComp } from "_components/Error";
import { Maintenance } from "@repo/shared-components";
const geistSans = localFont({ const geistSans = localFont({
src: "./fonts/GeistVF.woff", src: "./fonts/GeistVF.woff",
@@ -29,21 +30,10 @@ export default async function RootLayout({
}>) { }>) {
const session = await getServerSession(); const session = await getServerSession();
const latestNotam = await prisma.notam.findFirst({ const config = await prisma.notam.findFirst({
orderBy: { createdAt: "desc" }, orderBy: [{ createdAt: "desc" }],
}); });
let wartungsarbeiten = false;
if (
latestNotam &&
latestNotam.wartungsmodus &&
latestNotam.active &&
((latestNotam.showUntilActive && new Date(latestNotam.showUntil) > new Date()) ||
!latestNotam.showUntilActive)
) {
wartungsarbeiten = true;
}
return ( return (
<html lang="de" data-theme="dark"> <html lang="de" data-theme="dark">
<body <body
@@ -70,19 +60,12 @@ export default async function RootLayout({
{session?.user.isBanned && ( {session?.user.isBanned && (
<ErrorComp title="You are banned from using this service" statusCode={403} /> <ErrorComp title="You are banned from using this service" statusCode={403} />
)} )}
{!session?.user.isBanned && {config?.maintenanceEnabled && !session?.user.permissions.includes("ADMIN_MESSAGE") && (
wartungsarbeiten && <Maintenance message={config?.message} />
!session?.user.permissions.includes("ADMIN_MESSAGE") && (
<ErrorComp
title={
latestNotam?.message ||
"Wir führen aktuell Wartungsarbeiten am System durch, versuche es später erneut."
}
statusCode={503}
/>
)} )}
{!session?.user.isBanned && {!session?.user.isBanned &&
(!wartungsarbeiten || session?.user.permissions.includes("ADMIN_MESSAGE")) && (!config?.maintenanceEnabled ||
session?.user.permissions.includes("ADMIN_MESSAGE")) &&
children} children}
</NextAuthSessionProvider> </NextAuthSessionProvider>
</QueryProvider> </QueryProvider>

View File

@@ -5,54 +5,39 @@ import { Notam } from "@repo/db";
import { NotamOptionalDefaults, NotamOptionalDefaultsSchema } from "@repo/db/zod"; import { NotamOptionalDefaults, NotamOptionalDefaultsSchema } from "@repo/db/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { addMessage, disableMessage } from "../action"; import { addMessage, disableMessage } from "../action";
import { useState } from "react";
import { DateInput } from "_components/ui/DateInput";
import "react-datepicker/dist/react-datepicker.css"; // <-- Add this line at the top if using react-datepicker import "react-datepicker/dist/react-datepicker.css"; // <-- Add this line at the top if using react-datepicker
import { Button } from "_components/ui/Button";
import { PaginatedTableRef } from "_components/PaginatedTable";
import { RefObject } from "react";
export const MessageForm = ({ message }: { message?: Notam }) => { export const MessageForm = ({ tableRef }: { tableRef: RefObject<PaginatedTableRef | null> }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const getDefaultShowUntilDate = () => {
const date = new Date();
return date;
};
const disableMessageClient = async () => { const disableMessageClient = async () => {
await disableMessage(); await disableMessage();
window.location.reload(); tableRef?.current?.refresh();
}; };
const form = useForm<NotamOptionalDefaults>({ const form = useForm<NotamOptionalDefaults>({
resolver: zodResolver(NotamOptionalDefaultsSchema), resolver: zodResolver(NotamOptionalDefaultsSchema),
defaultValues: {
message: message?.message,
color: message?.color,
active: true,
wartungsmodus: message?.wartungsmodus,
disableHPG: message?.disableHPG,
showUntil: getDefaultShowUntilDate(),
},
}); });
return ( return (
<form <form
onSubmit={form.handleSubmit(async (values) => { onSubmit={form.handleSubmit(async (values) => {
setIsSubmitting(true);
try { try {
await addMessage(values); await addMessage(values);
window.location.reload(); tableRef?.current?.refresh();
form.reset();
} catch (error) { } catch (error) {
setIsSubmitting(false);
console.error("Failed to add message", error); console.error("Failed to add message", error);
} }
})} })}
className="grid grid-cols-6 gap-3" className="grid grid-cols-6 gap-3"
> >
<label className="floating-label col-span-6"> <label className="floating-label col-span-6">
<span>Globale Service Nachricht</span> <span>Notam</span>
<input <input
type="text" type="text"
placeholder="Globale Service Nachricht" placeholder="Globales Notam"
className="input input-md w-full mb-2" className="input input-md w-full mb-2"
{...form.register("message")} {...form.register("message")}
/> />
@@ -94,7 +79,7 @@ export const MessageForm = ({ message }: { message?: Notam }) => {
<input <input
type="checkbox" type="checkbox"
className="checkbox checkbox-primary" className="checkbox checkbox-primary"
{...form.register("wartungsmodus")} {...form.register("maintenanceEnabled")}
/> />
Wartungsmodus einschalten Wartungsmodus einschalten
</label> </label>
@@ -107,26 +92,24 @@ export const MessageForm = ({ message }: { message?: Notam }) => {
HPG Alarmierung deaktivieren HPG Alarmierung deaktivieren
</label> </label>
</div> </div>
<div className="flex flex-col gap-2 ml-2">
<label className="label">Nachricht & Effekte bis (optional)</label>
<DateInput
control={form.control}
name="showUntil"
showTimeInput
timeCaption="Uhrzeit"
showTimeCaption
className="input input-md"
/>
</div>
</div> </div>
<div className="flex flex-col justify-end"> <div className="flex flex-col justify-end">
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
<button type="submit" className="btn btn-soft" onClick={disableMessageClient}> <Button
Aktuelle Nachricht deaktivieren type="button"
</button> onSubmit={() => false}
<button type="submit" className="btn btn-primary" disabled={isSubmitting}> className="btn btn-soft"
onClick={disableMessageClient}
>
Config zurücksetzen
</Button>
<Button
type="submit"
className="btn btn-primary"
isLoading={form.formState.isSubmitting}
>
Speichern Speichern
</button> </Button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,22 @@
"use server";
import { prisma, Prisma } from "@repo/db";
export const addMessage = async (notam: Prisma.NotamCreateInput) => {
try {
await prisma.notam.create({
data: notam,
});
} catch (error) {
throw new Error("Failed to add message");
}
};
export const disableMessage = async () => {
try {
await prisma.notam.create({
data: {},
});
} catch (error) {
throw new Error("Failed to disable message");
}
};

View File

@@ -1,27 +1,30 @@
"use client"; "use client";
import { Check, MessageSquareWarning } from "lucide-react"; import { Check, MessageSquareWarning, Settings } from "lucide-react";
import { MessageForm } from "./_components/messageForm"; import { MessageForm } from "./_components/MessageForm";
import { PaginatedTable } from "_components/PaginatedTable"; import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { Notam } from "@repo/db"; import { Notam } from "@repo/db";
import { useRef } from "react";
export default function MessagePage() { export default function MessagePage() {
const tableRef = useRef<PaginatedTableRef | null>(null);
return ( return (
<> <>
<div className="grid grid-cols-6 gap-4"> <div className="grid grid-cols-6 gap-4">
<div className="col-span-full"> <div className="col-span-full">
<p className="text-2xl font-semibold text-left flex items-center gap-2"> <p className="text-2xl font-semibold text-left flex items-center gap-2">
<MessageSquareWarning className="w-5 h-5" /> Service Nachrichten <Settings className="w-5 h-5" /> Config
</p> </p>
</div> </div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6"> <div className="card bg-base-200 shadow-xl mb-4 col-span-6">
<div className="card-body"> <div className="card-body">
<MessageForm /> <MessageForm tableRef={tableRef} />
</div> </div>
</div> </div>
</div> </div>
<PaginatedTable <PaginatedTable
ref={tableRef}
prismaModel="notam" prismaModel="notam"
initialOrderBy={[{ id: "createdAt", desc: true }]} initialOrderBy={[{ id: "createdAt", desc: true }]}
columns={ columns={
@@ -42,7 +45,7 @@ export default function MessagePage() {
accessorKey: "wartungsmodus", accessorKey: "wartungsmodus",
header: "Wartungsmodus", header: "Wartungsmodus",
cell: ({ row }) => { cell: ({ row }) => {
const wartungsmodus = row.getValue("wartungsmodus"); const wartungsmodus = row.original.maintenanceEnabled;
return wartungsmodus ? <Check /> : ""; return wartungsmodus ? <Check /> : "";
}, },
}, },
@@ -50,28 +53,11 @@ export default function MessagePage() {
accessorKey: "disableHPG", accessorKey: "disableHPG",
header: "HPG deaktiviert", header: "HPG deaktiviert",
cell: ({ row }) => { cell: ({ row }) => {
const disableHPG = row.getValue("disableHPG"); const disableHPG = row.original.disableHPG;
return disableHPG ? <Check /> : ""; return disableHPG ? <Check /> : "";
}, },
}, },
{
accessorKey: "showUntil",
header: "Zeitlimit",
cell: ({ row, cell }) => {
const showUntil = new Date(cell.getValue() as string);
const createdAt = new Date(row.getValue("createdAt") as string);
if (showUntil > createdAt) {
return showUntil.toLocaleDateString("de-DE", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
return "";
},
},
{ {
accessorKey: "createdAt", accessorKey: "createdAt",
header: "Erstellt am", header: "Erstellt am",

View File

@@ -1,39 +0,0 @@
"use server";
import { prisma, Prisma } from "@repo/db";
export const addMessage = async (message: Prisma.NotamCreateInput) => {
try {
await prisma.notam.updateMany({
where: { active: true },
data: { active: false },
});
const showUntil = new Date(message.showUntil);
const showUntilActive = showUntil > new Date();
await prisma.notam.create({
data: {
message: message.message,
color: message.color,
active: true,
wartungsmodus: message.wartungsmodus,
disableHPG: message.disableHPG,
showUntilActive,
showUntil,
},
});
} catch (error) {
throw new Error("Failed to add message");
}
};
export const disableMessage = async () => {
try {
await prisma.notam.updateMany({
where: { active: true },
data: { active: false },
});
} catch (error) {
throw new Error("Failed to disable message");
}
};

View File

@@ -80,7 +80,7 @@ export const VerticalNav = async () => {
)} )}
{session.user.permissions.includes("ADMIN_MESSAGE") && ( {session.user.permissions.includes("ADMIN_MESSAGE") && (
<li> <li>
<Link href="/admin/message">Service Nachrichten</Link> <Link href="/admin/config">Config</Link>
</li> </li>
)} )}
{session.user.permissions.includes("ADMIN_USER") && ( {session.user.permissions.includes("ADMIN_USER") && (

View File

@@ -1,19 +1,12 @@
import { prisma } from "@repo/db"; import { prisma } from "@repo/db";
import { MessageCircleWarning } from "lucide-react"; import { MessageCircleWarning } from "lucide-react";
const fetchMainMessage = async () => {
return await prisma.notam.findFirst({
where: {
active: true,
},
});
};
export const WarningAlert = async () => { export const WarningAlert = async () => {
const mainMessage = await fetchMainMessage(); const mainMessage = await await prisma.notam.findFirst({
orderBy: {
if (mainMessage?.showUntilActive && new Date(mainMessage.showUntil) < new Date()) { createdAt: "desc",
return <></>; },
} });
let msgColor; let msgColor;
switch (mainMessage?.color) { switch (mainMessage?.color) {
@@ -33,14 +26,14 @@ export const WarningAlert = async () => {
msgColor = "alert alert-soft ml-3 py-2 flex items-center gap-2"; msgColor = "alert alert-soft ml-3 py-2 flex items-center gap-2";
} }
if ((mainMessage?.message == "" && !mainMessage?.wartungsmodus) || !mainMessage) { if ((mainMessage?.message == "" && !mainMessage?.maintenanceEnabled) || !mainMessage) {
return <></>; return null;
} else { } else {
return ( return (
<div role="alert" className={msgColor}> <div role="alert" className={msgColor}>
<MessageCircleWarning /> <MessageCircleWarning />
<span className="font-bold m-0"> <span className="font-bold m-0">
{mainMessage?.wartungsmodus ? "Wartungsmodus aktiv!" : mainMessage?.message} {mainMessage?.maintenanceEnabled ? "Wartungsmodus aktiv!" : mainMessage?.message}
</span> </span>
</div> </div>
); );

View File

@@ -8,6 +8,7 @@ import { QueryProvider } from "_components/QueryClient";
import { prisma } from "@repo/db"; import { prisma } from "@repo/db";
import React from "react"; import React from "react";
import { Error as ErrorComp } from "_components/Error"; import { Error as ErrorComp } from "_components/Error";
import { Maintenance } from "@repo/shared-components";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -30,31 +31,16 @@ const RootLayout = async ({
orderBy: { createdAt: "desc" }, orderBy: { createdAt: "desc" },
}); });
let wartungsarbeiten = false;
if (
latestNotam &&
latestNotam.wartungsmodus &&
latestNotam.active &&
((latestNotam.showUntilActive && new Date(latestNotam.showUntil) > new Date()) ||
!latestNotam.showUntilActive)
) {
wartungsarbeiten = true;
}
return ( return (
<html lang="en" data-theme="dark"> <html lang="en" data-theme="dark">
<NextAuthSessionProvider session={session}> <NextAuthSessionProvider session={session}>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{wartungsarbeiten && !session?.user.permissions.includes("ADMIN_MESSAGE") && ( {latestNotam?.maintenanceEnabled &&
<ErrorComp !session?.user.permissions.includes("ADMIN_MESSAGE") && (
title={ <Maintenance message={latestNotam?.message} />
latestNotam?.message ||
"Wir führen aktuell Wartungsarbeiten am System durch, versuche es später erneut."
}
statusCode={503}
/>
)} )}
{(!wartungsarbeiten || session?.user.permissions.includes("ADMIN_MESSAGE")) && ( {(!latestNotam?.maintenanceEnabled ||
session?.user.permissions.includes("ADMIN_MESSAGE")) && (
<> <>
<div> <div>
<Toaster <Toaster

BIN
apps/hub/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

View File

@@ -16,5 +16,5 @@
".next/types/**/*.ts", ".next/types/**/*.ts",
"types/.d.ts" "types/.d.ts"
], ],
"exclude": ["node_modules"] "exclude": ["node_modules", ".next"]
} }

View File

@@ -0,0 +1,17 @@
/*
Warnings:
- You are about to drop the column `active` on the `Notam` table. All the data in the column will be lost.
- You are about to drop the column `showUntil` on the `Notam` table. All the data in the column will be lost.
- You are about to drop the column `updatedAt` on the `Notam` table. All the data in the column will be lost.
- You are about to drop the column `wartungsmodus` on the `Notam` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Notam" DROP COLUMN "active",
DROP COLUMN "showUntil",
DROP COLUMN "updatedAt",
DROP COLUMN "wartungsmodus",
ADD COLUMN "maintenanceEnabled" BOOLEAN NOT NULL DEFAULT false,
ALTER COLUMN "color" DROP NOT NULL,
ALTER COLUMN "message" SET DEFAULT '';

View File

@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `showUntilActive` on the `Notam` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Notam" DROP COLUMN "showUntilActive";

View File

@@ -9,13 +9,9 @@ enum GlobalColor {
model Notam { model Notam {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
color GlobalColor color GlobalColor?
message String message String @default("")
showUntil DateTime maintenanceEnabled Boolean @default(false)
showUntilActive Boolean @default(false)
wartungsmodus Boolean @default(false)
disableHPG Boolean @default(false) disableHPG Boolean @default(false)
active Boolean
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} }

View File

@@ -0,0 +1,46 @@
"use client";
import { useEffect, useState } from "react";
export const Maintenance = ({ message }: { message?: string }) => {
const [rotationSpeed, setRotationSpeed] = useState(0);
const [rotationDeg, setRotationDeg] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setRotationDeg(rotationDeg + rotationSpeed);
});
return () => {
clearInterval(interval);
};
}, [rotationDeg, rotationSpeed]);
return (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-[10px] min-h-0 bg-base-300 shadow-lg p-8">
<h1 className="text-4xl font-bold text-center">Wartungsarbeiten</h1>
<p className="text-base text-center mx-6">
{message || "Unsere Website wird derzeit gewartet. Bitte versuchen Sie es später erneut."}
</p>
<div className="flex flex-col items-center gap-6 mt-6">
<img
src={`${process.env.NEXT_PUBLIC_HUB_URL}/logo.png`}
width={150}
alt="logo"
style={{
transform: `rotate(${rotationDeg}deg)`,
cursor: "pointer",
boxShadow: `0 0 ${rotationSpeed * 20}px rgba(255, 0, 0, ${rotationSpeed - 0})`,
borderRadius: "50%",
}}
onClick={() => {
setRotationSpeed(rotationSpeed + 0.1);
}}
onContextMenu={(e) => {
e.preventDefault();
setRotationSpeed(Math.max(rotationSpeed - 0.1, 0));
}}
/>
</div>
</div>
);
};

View File

@@ -1,2 +1,3 @@
export * from "./Badge"; export * from "./Badge";
export * from "./PenaltyDropdown"; export * from "./PenaltyDropdown";
export * from "./Maintenance";