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

View File

@@ -7,6 +7,7 @@ import { Toaster } from "react-hot-toast";
import { QueryProvider } from "_components/QueryProvider";
import { prisma } from "@repo/db";
import { Error as ErrorComp } from "_components/Error";
import { Maintenance } from "@repo/shared-components";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
@@ -29,21 +30,10 @@ export default async function RootLayout({
}>) {
const session = await getServerSession();
const latestNotam = await prisma.notam.findFirst({
orderBy: { createdAt: "desc" },
const config = await prisma.notam.findFirst({
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 (
<html lang="de" data-theme="dark">
<body
@@ -70,19 +60,12 @@ export default async function RootLayout({
{session?.user.isBanned && (
<ErrorComp title="You are banned from using this service" statusCode={403} />
)}
{config?.maintenanceEnabled && !session?.user.permissions.includes("ADMIN_MESSAGE") && (
<Maintenance message={config?.message} />
)}
{!session?.user.isBanned &&
wartungsarbeiten &&
!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 &&
(!wartungsarbeiten || session?.user.permissions.includes("ADMIN_MESSAGE")) &&
(!config?.maintenanceEnabled ||
session?.user.permissions.includes("ADMIN_MESSAGE")) &&
children}
</NextAuthSessionProvider>
</QueryProvider>

View File

@@ -5,54 +5,39 @@ import { Notam } from "@repo/db";
import { NotamOptionalDefaults, NotamOptionalDefaultsSchema } from "@repo/db/zod";
import { useForm } from "react-hook-form";
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 { Button } from "_components/ui/Button";
import { PaginatedTableRef } from "_components/PaginatedTable";
import { RefObject } from "react";
export const MessageForm = ({ message }: { message?: Notam }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const getDefaultShowUntilDate = () => {
const date = new Date();
return date;
};
export const MessageForm = ({ tableRef }: { tableRef: RefObject<PaginatedTableRef | null> }) => {
const disableMessageClient = async () => {
await disableMessage();
window.location.reload();
tableRef?.current?.refresh();
};
const form = useForm<NotamOptionalDefaults>({
resolver: zodResolver(NotamOptionalDefaultsSchema),
defaultValues: {
message: message?.message,
color: message?.color,
active: true,
wartungsmodus: message?.wartungsmodus,
disableHPG: message?.disableHPG,
showUntil: getDefaultShowUntilDate(),
},
});
return (
<form
onSubmit={form.handleSubmit(async (values) => {
setIsSubmitting(true);
try {
await addMessage(values);
window.location.reload();
tableRef?.current?.refresh();
form.reset();
} catch (error) {
setIsSubmitting(false);
console.error("Failed to add message", error);
}
})}
className="grid grid-cols-6 gap-3"
>
<label className="floating-label col-span-6">
<span>Globale Service Nachricht</span>
<span>Notam</span>
<input
type="text"
placeholder="Globale Service Nachricht"
placeholder="Globales Notam"
className="input input-md w-full mb-2"
{...form.register("message")}
/>
@@ -94,7 +79,7 @@ export const MessageForm = ({ message }: { message?: Notam }) => {
<input
type="checkbox"
className="checkbox checkbox-primary"
{...form.register("wartungsmodus")}
{...form.register("maintenanceEnabled")}
/>
Wartungsmodus einschalten
</label>
@@ -107,26 +92,24 @@ export const MessageForm = ({ message }: { message?: Notam }) => {
HPG Alarmierung deaktivieren
</label>
</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 className="flex flex-col justify-end">
<div className="flex justify-center gap-2">
<button type="submit" className="btn btn-soft" onClick={disableMessageClient}>
Aktuelle Nachricht deaktivieren
</button>
<button type="submit" className="btn btn-primary" disabled={isSubmitting}>
<Button
type="button"
onSubmit={() => false}
className="btn btn-soft"
onClick={disableMessageClient}
>
Config zurücksetzen
</Button>
<Button
type="submit"
className="btn btn-primary"
isLoading={form.formState.isSubmitting}
>
Speichern
</button>
</Button>
</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";
import { Check, MessageSquareWarning } from "lucide-react";
import { MessageForm } from "./_components/messageForm";
import { PaginatedTable } from "_components/PaginatedTable";
import { Check, MessageSquareWarning, Settings } from "lucide-react";
import { MessageForm } from "./_components/MessageForm";
import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
import { ColumnDef } from "@tanstack/react-table";
import { Notam } from "@repo/db";
import { useRef } from "react";
export default function MessagePage() {
const tableRef = useRef<PaginatedTableRef | null>(null);
return (
<>
<div className="grid grid-cols-6 gap-4">
<div className="col-span-full">
<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>
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6">
<div className="card-body">
<MessageForm />
<MessageForm tableRef={tableRef} />
</div>
</div>
</div>
<PaginatedTable
ref={tableRef}
prismaModel="notam"
initialOrderBy={[{ id: "createdAt", desc: true }]}
columns={
@@ -42,7 +45,7 @@ export default function MessagePage() {
accessorKey: "wartungsmodus",
header: "Wartungsmodus",
cell: ({ row }) => {
const wartungsmodus = row.getValue("wartungsmodus");
const wartungsmodus = row.original.maintenanceEnabled;
return wartungsmodus ? <Check /> : "";
},
},
@@ -50,28 +53,11 @@ export default function MessagePage() {
accessorKey: "disableHPG",
header: "HPG deaktiviert",
cell: ({ row }) => {
const disableHPG = row.getValue("disableHPG");
const disableHPG = row.original.disableHPG;
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",
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") && (
<li>
<Link href="/admin/message">Service Nachrichten</Link>
<Link href="/admin/config">Config</Link>
</li>
)}
{session.user.permissions.includes("ADMIN_USER") && (

View File

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

View File

@@ -8,6 +8,7 @@ import { QueryProvider } from "_components/QueryClient";
import { prisma } from "@repo/db";
import React from "react";
import { Error as ErrorComp } from "_components/Error";
import { Maintenance } from "@repo/shared-components";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -30,31 +31,16 @@ const RootLayout = async ({
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 (
<html lang="en" data-theme="dark">
<NextAuthSessionProvider session={session}>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{wartungsarbeiten && !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}
/>
)}
{(!wartungsarbeiten || session?.user.permissions.includes("ADMIN_MESSAGE")) && (
{latestNotam?.maintenanceEnabled &&
!session?.user.permissions.includes("ADMIN_MESSAGE") && (
<Maintenance message={latestNotam?.message} />
)}
{(!latestNotam?.maintenanceEnabled ||
session?.user.permissions.includes("ADMIN_MESSAGE")) && (
<>
<div>
<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",
"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

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

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 "./PenaltyDropdown";
export * from "./Maintenance";