coldown fix

This commit is contained in:
2025-08-21 16:05:29 +02:00
parent 49923adcd2
commit 68eeaa063d
13 changed files with 1287 additions and 133 deletions

View File

@@ -4,12 +4,14 @@ import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import CanvasPage from "@/pages/canvas";
import AdminPage from "@/pages/admin";
import NotFound from "@/pages/not-found";
function Router() {
return (
<Switch>
<Route path="/" component={CanvasPage} />
<Route path="/admin" component={AdminPage} />
<Route component={NotFound} />
</Switch>
);

View File

@@ -0,0 +1,12 @@
import { apiRequest } from "./queryClient";
export interface ExpandCanvasRequest {
canvasWidth: number;
canvasHeight: number;
}
export const expandCanvas = async (dimensions: ExpandCanvasRequest) => {
const response = await apiRequest("POST", "/api/config/expand", dimensions);
return response.json();
};

382
client/src/pages/admin.tsx Normal file
View File

@@ -0,0 +1,382 @@
import { useState, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient } from "@/lib/queryClient";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/hooks/use-toast";
import { Pixel, CanvasConfig } from "@shared/schema";
import { apiRequest } from "@/lib/queryClient";
import { AuthBanner } from "@/components/auth-banner";
export default function AdminPage() {
const [adminKey, setAdminKey] = useState("");
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [selectedPixelId, setSelectedPixelId] = useState("");
const [canvasWidth, setCanvasWidth] = useState(500);
const [canvasHeight, setCanvasHeight] = useState(200);
const { toast } = useToast();
// Auth check
const checkAuth = async (key: string) => {
try {
const response = await apiRequest("POST", "/api/admin/auth", { adminKey: key });
if (response.ok) {
setIsAuthenticated(true);
localStorage.setItem("adminKey", key);
toast({ title: "Admin-Zugang gewährt" });
} else {
throw new Error("Invalid key");
}
} catch (error) {
toast({
title: "Ungültiger Admin-Schlüssel",
variant: "destructive"
});
}
};
// Check saved auth on load
useEffect(() => {
const savedKey = localStorage.getItem("adminKey");
if (savedKey) {
setAdminKey(savedKey);
checkAuth(savedKey);
}
}, []);
// Fetch data
const { data: pixels = [], refetch: refetchPixels } = useQuery<Pixel[]>({
queryKey: ['/api/pixels'],
enabled: isAuthenticated,
});
const { data: config } = useQuery<CanvasConfig>({
queryKey: ['/api/config'],
enabled: isAuthenticated,
});
const { data: adminStats } = useQuery({
queryKey: ['/api/admin/stats'],
enabled: isAuthenticated,
queryFn: async () => {
const response = await apiRequest("GET", "/api/admin/stats", undefined, {
'X-Admin-Key': adminKey
});
return response.json();
},
});
// Mutations
const deletePixelMutation = useMutation({
mutationFn: async (pixelId: string) => {
const response = await apiRequest("DELETE", `/api/admin/pixels/${pixelId}`, undefined, {
'X-Admin-Key': adminKey
});
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
queryClient.invalidateQueries({ queryKey: ['/api/admin/stats'] });
toast({ title: "Pixel gelöscht" });
},
});
const clearCanvasMutation = useMutation({
mutationFn: async () => {
const response = await apiRequest("DELETE", "/api/admin/canvas", undefined, {
'X-Admin-Key': adminKey
});
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
queryClient.invalidateQueries({ queryKey: ['/api/admin/stats'] });
toast({ title: "Canvas geleert" });
},
});
const expandCanvasMutation = useMutation({
mutationFn: async ({ width, height }: { width: number; height: number }) => {
const response = await apiRequest("POST", "/api/config/expand", {
canvasWidth: width,
canvasHeight: height
}, {
'X-Admin-Key': adminKey
});
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/config'] });
toast({ title: "Canvas erweitert" });
},
});
const exportCanvasMutation = useMutation({
mutationFn: async () => {
const response = await apiRequest("POST", "/api/admin/export", undefined, {
'X-Admin-Key': adminKey
});
return response.json();
},
onSuccess: (data) => {
toast({ title: "Canvas exportiert", description: `Datei: ${data.filename}` });
},
});
if (!isAuthenticated) {
return (
<div className="min-h-screen bg-canvas-bg text-white flex items-center justify-center">
<Card className="w-96 bg-panel-bg border-gray-700">
<CardHeader>
<CardTitle className="text-white">Admin-Zugang</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input
type="password"
placeholder="Admin-Schlüssel eingeben"
value={adminKey}
onChange={(e) => setAdminKey(e.target.value)}
className="bg-panel-hover border-gray-600 text-white"
/>
<Button
onClick={() => checkAuth(adminKey)}
className="w-full"
disabled={!adminKey}
>
Anmelden
</Button>
</CardContent>
</Card>
</div>
);
}
return (
<div className="min-h-screen bg-canvas-bg text-white">
<AuthBanner />
<div className="container mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold">Admin-Panel</h1>
<Button
variant="outline"
onClick={() => {
setIsAuthenticated(false);
localStorage.removeItem("adminKey");
}}
>
Abmelden
</Button>
</div>
<Tabs defaultValue="overview" className="space-y-6">
<TabsList className="bg-panel-bg">
<TabsTrigger value="overview">Übersicht</TabsTrigger>
<TabsTrigger value="moderation">Moderation</TabsTrigger>
<TabsTrigger value="canvas">Canvas-Verwaltung</TabsTrigger>
<TabsTrigger value="export">Export</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="bg-panel-bg border-gray-700">
<CardHeader>
<CardTitle className="text-white">Statistiken</CardTitle>
</CardHeader>
<CardContent className="text-white">
<div className="space-y-2">
<div className="flex justify-between">
<span>Gesamte Pixel:</span>
<Badge>{adminStats?.totalPixels || 0}</Badge>
</div>
<div className="flex justify-between">
<span>Einzigartige Nutzer:</span>
<Badge>{adminStats?.uniqueUsers || 0}</Badge>
</div>
<div className="flex justify-between">
<span>Canvas-Größe:</span>
<Badge>{config?.canvasWidth}×{config?.canvasHeight}</Badge>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-panel-bg border-gray-700">
<CardHeader>
<CardTitle className="text-white">Letzte Aktivität</CardTitle>
</CardHeader>
<CardContent className="text-white">
<div className="space-y-2 max-h-40 overflow-y-auto">
{pixels.slice(0, 5).map((pixel) => (
<div key={pixel.id} className="text-sm">
<span className="text-muted">{pixel.username}</span> -
<span className="ml-1">({pixel.x}, {pixel.y})</span>
</div>
))}
</div>
</CardContent>
</Card>
<Card className="bg-panel-bg border-gray-700">
<CardHeader>
<CardTitle className="text-white">System</CardTitle>
</CardHeader>
<CardContent className="text-white">
<div className="space-y-2">
<div className="flex justify-between">
<span>Cooldown:</span>
<Badge>{config?.defaultCooldown}s</Badge>
</div>
<div className="flex justify-between">
<span>Auto-Events:</span>
<Badge variant={config?.enableAutomaticEvents ? "default" : "secondary"}>
{config?.enableAutomaticEvents ? "An" : "Aus"}
</Badge>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="moderation">
<div className="space-y-6">
<Card className="bg-panel-bg border-gray-700">
<CardHeader>
<CardTitle className="text-white">Pixel-Moderation</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex space-x-2">
<Input
placeholder="Pixel-ID eingeben"
value={selectedPixelId}
onChange={(e) => setSelectedPixelId(e.target.value)}
className="bg-panel-hover border-gray-600 text-white"
/>
<Button
onClick={() => deletePixelMutation.mutate(selectedPixelId)}
disabled={!selectedPixelId}
variant="destructive"
>
Pixel löschen
</Button>
</div>
<div className="border-t border-gray-600 pt-4">
<Button
onClick={() => clearCanvasMutation.mutate()}
variant="destructive"
className="w-full"
>
Gesamtes Canvas leeren
</Button>
</div>
</CardContent>
</Card>
<Card className="bg-panel-bg border-gray-700">
<CardHeader>
<CardTitle className="text-white">Pixel-Liste</CardTitle>
</CardHeader>
<CardContent>
<div className="max-h-60 overflow-y-auto space-y-2">
{pixels.slice().reverse().slice(0, 20).map((pixel) => (
<div key={pixel.id} className="flex items-center justify-between p-2 bg-panel-hover rounded">
<div className="text-white text-sm">
<span className="font-mono">{pixel.id.slice(0, 8)}</span> -
<span className="ml-2">{pixel.username}</span> -
<span className="ml-2">({pixel.x}, {pixel.y})</span> -
<span
className="ml-2 inline-block w-4 h-4 rounded"
style={{ backgroundColor: pixel.color }}
/>
</div>
<Button
size="sm"
variant="destructive"
onClick={() => deletePixelMutation.mutate(pixel.id)}
>
Löschen
</Button>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="canvas">
<Card className="bg-panel-bg border-gray-700">
<CardHeader>
<CardTitle className="text-white">Canvas-Verwaltung</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-white text-sm">Breite</label>
<Input
type="number"
value={canvasWidth}
onChange={(e) => setCanvasWidth(Number(e.target.value))}
className="bg-panel-hover border-gray-600 text-white"
/>
</div>
<div>
<label className="text-white text-sm">Höhe</label>
<Input
type="number"
value={canvasHeight}
onChange={(e) => setCanvasHeight(Number(e.target.value))}
className="bg-panel-hover border-gray-600 text-white"
/>
</div>
</div>
<Button
onClick={() => expandCanvasMutation.mutate({
width: canvasWidth,
height: canvasHeight
})}
className="w-full"
>
Canvas erweitern
</Button>
<div className="text-sm text-muted">
Aktuelle Größe: {config?.canvasWidth}×{config?.canvasHeight}
<br />
Hinweis: Canvas kann nur erweitert, nicht verkleinert werden.
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="export">
<Card className="bg-panel-bg border-gray-700">
<CardHeader>
<CardTitle className="text-white">Export-Verwaltung</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Button
onClick={() => exportCanvasMutation.mutate()}
className="w-full"
>
Manueller SVG-Export
</Button>
<div className="text-sm text-muted">
Automatische Exports erfolgen alle {config?.exportInterval || 60} Sekunden.
Dateien werden im exports/ Verzeichnis gespeichert.
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
}

View File

@@ -43,13 +43,15 @@ export default function CanvasPage() {
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
queryClient.invalidateQueries({ queryKey: ['/api/recent'] });
//toast({
// title: "Pixel placed",
// description: `${message.data.username} placed a pixel at (${message.data.x}, ${message.data.y})`,
// });
//break;
// Aktualisiere Cooldown nach jedem Pixel-Placement (auch von anderen Nutzern)
// um sicherzustellen, dass lokaler State mit Server synchron ist
if (userId) {
fetch(`/api/cooldown/${userId}`)
.then(res => res.json())
.then(data => setCooldownSeconds(data.remainingSeconds))
.catch(error => console.error("Failed to sync cooldown:", error));
}
break;
case "cooldown_update":
if (message.data.userId === userId) {
@@ -67,12 +69,21 @@ export default function CanvasPage() {
const response = await apiRequest("POST", "/api/pixels", pixel);
return response.json();
},
onSuccess: () => {
onSuccess: async () => {
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
queryClient.invalidateQueries({ queryKey: ['/api/recent'] });
// Start cooldown countdown
if (config && !config.enableAutomaticEvents) {
setCooldownSeconds(config.defaultCooldown);
// Hole den aktuellen Cooldown-Status vom Server
try {
const cooldownResponse = await fetch(`/api/cooldown/${userId}`);
const cooldownData = await cooldownResponse.json();
setCooldownSeconds(cooldownData.remainingSeconds);
} catch (error) {
console.error("Failed to fetch cooldown after placement:", error);
// Fallback: Setze Standard-Cooldown wenn Server-Abfrage fehlschlägt
if (config && !config.enableAutomaticEvents) {
setCooldownSeconds(config.defaultCooldown);
}
}
},
onError: (error: any) => {
@@ -118,10 +129,14 @@ export default function CanvasPage() {
}
};
if (userId) {
if (userId && config) {
fetchCooldown();
// Aktualisiere Cooldown regelmäßig für den Fall, dass er anderswo gesetzt wurde
const interval = setInterval(fetchCooldown, 2000);
return () => clearInterval(interval);
}
}, [userId]);
}, [userId, config]);
const handlePixelClick = (x: number, y: number) => {
if (cooldownSeconds > 0) return;
@@ -160,9 +175,14 @@ export default function CanvasPage() {
</div>
<div className="flex items-center space-x-3">
{/* Admin Link */}
<Button
variant="outline"
size="sm"
onClick={() => window.location.href = '/admin'}
>
Admin
</Button>
{/* User Info */}
<div className="flex items-center space-x-2 px-3 py-2 bg-panel-hover rounded-lg">