Merge pull request 'coldown fix and first admin preview' (#7) from coldown-fix into main
Reviewed-on: #7
This commit is contained in:
commit
09c6984c0f
38
.replit
38
.replit
@ -1,42 +1,12 @@
|
|||||||
modules = ["nodejs-20", "web", "postgresql-16"]
|
modules = ["nodejs-20", "web"]
|
||||||
run = "npm run dev"
|
run = "npm run dev"
|
||||||
hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
|
|
||||||
|
|
||||||
[nix]
|
[nix]
|
||||||
channel = "stable-24_05"
|
channel = "stable-25_05"
|
||||||
|
|
||||||
[deployment]
|
[deployment]
|
||||||
deploymentTarget = "autoscale"
|
run = ["sh", "-c", "npm run dev"]
|
||||||
build = ["npm", "run", "build"]
|
|
||||||
run = ["npm", "run", "start"]
|
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 5000
|
localPort = 5001
|
||||||
externalPort = 80
|
externalPort = 80
|
||||||
|
|
||||||
[env]
|
|
||||||
PORT = "5000"
|
|
||||||
|
|
||||||
[workflows]
|
|
||||||
runButton = "Project"
|
|
||||||
|
|
||||||
[[workflows.workflow]]
|
|
||||||
name = "Project"
|
|
||||||
mode = "parallel"
|
|
||||||
author = "agent"
|
|
||||||
|
|
||||||
[[workflows.workflow.tasks]]
|
|
||||||
task = "workflow.run"
|
|
||||||
args = "Start application"
|
|
||||||
|
|
||||||
[[workflows.workflow]]
|
|
||||||
name = "Start application"
|
|
||||||
author = "agent"
|
|
||||||
|
|
||||||
[[workflows.workflow.tasks]]
|
|
||||||
task = "shell.exec"
|
|
||||||
args = "npm run dev"
|
|
||||||
waitForPort = 5000
|
|
||||||
|
|
||||||
[agent]
|
|
||||||
integrations = ["javascript_mem_db==1.0.0", "javascript_websocket==1.0.0"]
|
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import { QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import CanvasPage from "@/pages/canvas";
|
import CanvasPage from "@/pages/canvas";
|
||||||
|
import AdminPage from "@/pages/admin";
|
||||||
import NotFound from "@/pages/not-found";
|
import NotFound from "@/pages/not-found";
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={CanvasPage} />
|
<Route path="/" component={CanvasPage} />
|
||||||
|
<Route path="/admin" component={AdminPage} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|||||||
12
client/src/lib/canvas-api.ts
Normal file
12
client/src/lib/canvas-api.ts
Normal 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
382
client/src/pages/admin.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -43,13 +43,15 @@ export default function CanvasPage() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/recent'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/recent'] });
|
||||||
|
|
||||||
//toast({
|
// Aktualisiere Cooldown nach jedem Pixel-Placement (auch von anderen Nutzern)
|
||||||
// title: "Pixel placed",
|
// um sicherzustellen, dass lokaler State mit Server synchron ist
|
||||||
// description: `${message.data.username} placed a pixel at (${message.data.x}, ${message.data.y})`,
|
if (userId) {
|
||||||
// });
|
fetch(`/api/cooldown/${userId}`)
|
||||||
//break;
|
.then(res => res.json())
|
||||||
|
.then(data => setCooldownSeconds(data.remainingSeconds))
|
||||||
|
.catch(error => console.error("Failed to sync cooldown:", error));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "cooldown_update":
|
case "cooldown_update":
|
||||||
if (message.data.userId === userId) {
|
if (message.data.userId === userId) {
|
||||||
@ -67,13 +69,22 @@ export default function CanvasPage() {
|
|||||||
const response = await apiRequest("POST", "/api/pixels", pixel);
|
const response = await apiRequest("POST", "/api/pixels", pixel);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/pixels'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/recent'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/recent'] });
|
||||||
// Start cooldown countdown
|
|
||||||
|
// 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) {
|
if (config && !config.enableAutomaticEvents) {
|
||||||
setCooldownSeconds(config.defaultCooldown);
|
setCooldownSeconds(config.defaultCooldown);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
if (error.message.includes("429")) {
|
if (error.message.includes("429")) {
|
||||||
@ -118,10 +129,14 @@ export default function CanvasPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (userId) {
|
if (userId && config) {
|
||||||
fetchCooldown();
|
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) => {
|
const handlePixelClick = (x: number, y: number) => {
|
||||||
if (cooldownSeconds > 0) return;
|
if (cooldownSeconds > 0) return;
|
||||||
@ -160,9 +175,14 @@ export default function CanvasPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
|
{/* Admin Link */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => window.location.href = '/admin'}
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<div className="flex items-center space-x-2 px-3 py-2 bg-panel-hover rounded-lg">
|
<div className="flex items-center space-x-2 px-3 py-2 bg-panel-hover rounded-lg">
|
||||||
|
|||||||
394
package-lock.json
generated
394
package-lock.json
generated
@ -40,6 +40,8 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"better-sqlite3": "^12.2.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@ -3334,6 +3336,15 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/better-sqlite3": {
|
||||||
|
"version": "7.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||||
|
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.5",
|
"version": "1.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||||
@ -3823,6 +3834,26 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/basic-ftp": {
|
"node_modules/basic-ftp": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
||||||
@ -3833,6 +3864,20 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "12.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz",
|
||||||
|
"integrity": "sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
|
"prebuild-install": "^7.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20.x || 22.x || 23.x || 24.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@ -3845,6 +3890,26 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.5.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bn.js": {
|
"node_modules/bn.js": {
|
||||||
"version": "4.12.2",
|
"version": "4.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||||
@ -3950,6 +4015,30 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-crc32": {
|
"node_modules/buffer-crc32": {
|
||||||
"version": "0.2.13",
|
"version": "0.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
@ -4088,6 +4177,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/chromedriver": {
|
"node_modules/chromedriver": {
|
||||||
"version": "139.0.2",
|
"version": "139.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-139.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-139.0.2.tgz",
|
||||||
@ -4442,6 +4537,30 @@
|
|||||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/deep-extend": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@ -4514,7 +4633,6 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -5219,7 +5337,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
@ -5424,6 +5541,15 @@
|
|||||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/expand-template": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||||
|
"license": "(MIT OR WTFPL)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
@ -5610,6 +5736,12 @@
|
|||||||
"pend": "~1.2.0"
|
"pend": "~1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@ -5768,6 +5900,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-constants": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@ -5890,6 +6028,12 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/github-from-package": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.4.5",
|
"version": "10.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||||
@ -6079,12 +6223,38 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/input-otp": {
|
"node_modules/input-otp": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
|
||||||
@ -6739,6 +6909,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimalistic-assert": {
|
"node_modules/minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
@ -6766,6 +6948,15 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@ -6781,6 +6972,12 @@
|
|||||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp-classic": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/modern-screenshot": {
|
"node_modules/modern-screenshot": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.6.0.tgz",
|
||||||
@ -6832,6 +7029,12 @@
|
|||||||
"node": "^18 || >=20"
|
"node": "^18 || >=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/napi-build-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
@ -6860,6 +7063,30 @@
|
|||||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-abi": {
|
||||||
|
"version": "3.75.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz",
|
||||||
|
"integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-abi/node_modules/semver": {
|
||||||
|
"version": "7.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-gyp-build": {
|
"node_modules/node-gyp-build": {
|
||||||
"version": "4.8.3",
|
"version": "4.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
|
||||||
@ -6960,7 +7187,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@ -7503,6 +7729,32 @@
|
|||||||
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
|
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/prebuild-install": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"expand-template": "^2.0.3",
|
||||||
|
"github-from-package": "0.0.0",
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"mkdirp-classic": "^0.5.3",
|
||||||
|
"napi-build-utils": "^2.0.0",
|
||||||
|
"node-abi": "^3.3.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"rc": "^1.2.7",
|
||||||
|
"simple-get": "^4.0.0",
|
||||||
|
"tar-fs": "^2.0.0",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prebuild-install": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@ -7579,7 +7831,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
"once": "^1.3.1"
|
"once": "^1.3.1"
|
||||||
@ -7653,6 +7904,21 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-extend": "^0.6.0",
|
||||||
|
"ini": "~1.3.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"strip-json-comments": "~2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rc": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
@ -7844,6 +8110,20 @@
|
|||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@ -8181,6 +8461,51 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-concat": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/smart-buffer": {
|
"node_modules/smart-buffer": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
@ -8270,6 +8595,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
@ -8366,6 +8700,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.35.0",
|
"version": "3.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||||
@ -8462,6 +8805,34 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tcp-port-used": {
|
"node_modules/tcp-port-used": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz",
|
||||||
@ -9026,6 +9397,18 @@
|
|||||||
"@esbuild/win32-x64": "0.23.1"
|
"@esbuild/win32-x64": "0.23.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tw-animate-css": {
|
"node_modules/tw-animate-css": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz",
|
||||||
@ -9841,8 +10224,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC",
|
"license": "ISC"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
|
|||||||
@ -42,6 +42,8 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"better-sqlite3": "^12.2.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
|||||||
@ -11,6 +11,7 @@ interface Config {
|
|||||||
|
|
||||||
autoExportIntervalSeconds: number;
|
autoExportIntervalSeconds: number;
|
||||||
exportPath: string;
|
exportPath: string;
|
||||||
|
adminKey: string;
|
||||||
|
|
||||||
enableKeycloak: boolean;
|
enableKeycloak: boolean;
|
||||||
keycloakRealm: string;
|
keycloakRealm: string;
|
||||||
@ -23,86 +24,43 @@ function parseConfigFile(): Config {
|
|||||||
const configPath = join(process.cwd(), "config.cfg");
|
const configPath = join(process.cwd(), "config.cfg");
|
||||||
const configContent = readFileSync(configPath, "utf-8");
|
const configContent = readFileSync(configPath, "utf-8");
|
||||||
|
|
||||||
const config: Partial<Config> = {};
|
const configMap = new Map<string, string>();
|
||||||
|
|
||||||
configContent.split("\n").forEach(line => {
|
configContent.split("\n").forEach(line => {
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
if (line.startsWith("#") || !line.includes("=")) return;
|
if (line.startsWith("#") || !line.includes("=")) return;
|
||||||
|
|
||||||
const [key, value] = line.split("=");
|
const [key, value] = line.split("=");
|
||||||
const trimmedKey = key.trim();
|
configMap.set(key.trim(), value.trim());
|
||||||
const trimmedValue = value.trim();
|
|
||||||
|
|
||||||
switch (trimmedKey) {
|
|
||||||
case "CANVAS_WIDTH":
|
|
||||||
config.canvasWidth = parseInt(trimmedValue);
|
|
||||||
break;
|
|
||||||
case "CANVAS_HEIGHT":
|
|
||||||
config.canvasHeight = parseInt(trimmedValue);
|
|
||||||
break;
|
|
||||||
case "DEFAULT_COOLDOWN":
|
|
||||||
config.defaultCooldown = parseInt(trimmedValue);
|
|
||||||
break;
|
|
||||||
case "ENABLE_AUTOMATIC_EVENTS":
|
|
||||||
config.enableAutomaticEvents = trimmedValue.toLowerCase() === "true";
|
|
||||||
break;
|
|
||||||
case "EVENT_DURATION_MINUTES":
|
|
||||||
config.eventDurationMinutes = parseInt(trimmedValue);
|
|
||||||
break;
|
|
||||||
case "EVENT_INTERVAL_HOURS":
|
|
||||||
config.eventIntervalHours = parseInt(trimmedValue);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "AUTO_EXPORT_INTERVAL_SECONDS":
|
|
||||||
config.autoExportIntervalSeconds = parseInt(trimmedValue);
|
|
||||||
break;
|
|
||||||
case "EXPORT_PATH":
|
|
||||||
config.exportPath = trimmedValue;
|
|
||||||
break;
|
|
||||||
case "ENABLE_KEYCLOAK":
|
|
||||||
config.enableKeycloak = trimmedValue.toLowerCase() === "true";
|
|
||||||
break;
|
|
||||||
case "KEYCLOAK_REALM":
|
|
||||||
config.keycloakRealm = trimmedValue;
|
|
||||||
break;
|
|
||||||
case "KEYCLOAK_AUTH_URL":
|
|
||||||
config.keycloakAuthUrl = trimmedValue;
|
|
||||||
break;
|
|
||||||
case "KEYCLOAK_CLIENT_ID":
|
|
||||||
config.keycloakClientId = trimmedValue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set defaults for missing values
|
|
||||||
return {
|
return {
|
||||||
canvasWidth: config.canvasWidth || 100,
|
canvasWidth: parseInt(configMap.get('CANVAS_WIDTH') || '500'),
|
||||||
canvasHeight: config.canvasHeight || 100,
|
canvasHeight: parseInt(configMap.get('CANVAS_HEIGHT') || '200'),
|
||||||
defaultCooldown: config.defaultCooldown || 5,
|
defaultCooldown: parseInt(configMap.get('DEFAULT_COOLDOWN') || '10'),
|
||||||
enableAutomaticEvents: config.enableAutomaticEvents || false,
|
enableAutomaticEvents: configMap.get('ENABLE_AUTOMATIC_EVENTS') === 'true',
|
||||||
eventDurationMinutes: config.eventDurationMinutes || 30,
|
eventDurationMinutes: parseInt(configMap.get('EVENT_DURATION_MINUTES') || '30'),
|
||||||
eventIntervalHours: config.eventIntervalHours || 6,
|
eventIntervalHours: parseInt(configMap.get('EVENT_INTERVAL_HOURS') || '1'),
|
||||||
|
autoExportIntervalSeconds: parseInt(configMap.get('AUTO_EXPORT_INTERVAL_SECONDS') || '60'),
|
||||||
autoExportIntervalSeconds: config.autoExportIntervalSeconds || 60,
|
exportPath: configMap.get('EXPORT_PATH') || './exports/',
|
||||||
exportPath: config.exportPath || "./exports/",
|
adminKey: configMap.get('ADMIN_KEY') || 'admin123',
|
||||||
|
enableKeycloak: configMap.get('ENABLE_KEYCLOAK') === 'true',
|
||||||
enableKeycloak: config.enableKeycloak || false,
|
keycloakRealm: configMap.get('KEYCLOAK_REALM') || 'rplace',
|
||||||
keycloakRealm: config.keycloakRealm || "rplace",
|
keycloakAuthUrl: configMap.get('KEYCLOAK_AUTH_URL') || 'http://localhost:8080/auth',
|
||||||
keycloakAuthUrl: config.keycloakAuthUrl || "http://localhost:8080/auth",
|
keycloakClientId: configMap.get('KEYCLOAK_CLIENT_ID') || 'rplace-client',
|
||||||
keycloakClientId: config.keycloakClientId || "rplace-client",
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading config file, using defaults:", error);
|
console.error("Error reading config file, using defaults:", error);
|
||||||
return {
|
return {
|
||||||
canvasWidth: 100,
|
canvasWidth: 500,
|
||||||
canvasHeight: 100,
|
canvasHeight: 200,
|
||||||
defaultCooldown: 5,
|
defaultCooldown: 10,
|
||||||
enableAutomaticEvents: false,
|
enableAutomaticEvents: false,
|
||||||
eventDurationMinutes: 30,
|
eventDurationMinutes: 30,
|
||||||
eventIntervalHours: 6,
|
eventIntervalHours: 1,
|
||||||
|
|
||||||
autoExportIntervalSeconds: 60,
|
autoExportIntervalSeconds: 60,
|
||||||
exportPath: "./exports/",
|
exportPath: "./exports/",
|
||||||
|
adminKey: 'admin123',
|
||||||
|
|
||||||
enableKeycloak: false,
|
enableKeycloak: false,
|
||||||
keycloakRealm: "rplace",
|
keycloakRealm: "rplace",
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { registerRoutes } from "./routes";
|
|||||||
import { setupVite, serveStatic, log } from "./vite";
|
import { setupVite, serveStatic, log } from "./vite";
|
||||||
import { setupKeycloak } from "./keycloak";
|
import { setupKeycloak } from "./keycloak";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
|
import { storage } from "./storage";
|
||||||
|
import { CanvasExporter } from "./export";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@ -72,6 +74,42 @@ app.use((req, res, next) => {
|
|||||||
serveStatic(app);
|
serveStatic(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aktualisiere Canvas-Konfiguration beim Start falls sich config.cfg geändert hat
|
||||||
|
try {
|
||||||
|
const currentConfig = await storage.getCanvasConfig();
|
||||||
|
const configChanged =
|
||||||
|
currentConfig.canvasWidth !== config.canvasWidth ||
|
||||||
|
currentConfig.canvasHeight !== config.canvasHeight ||
|
||||||
|
currentConfig.defaultCooldown !== config.defaultCooldown ||
|
||||||
|
currentConfig.enableAutomaticEvents !== config.enableAutomaticEvents ||
|
||||||
|
currentConfig.eventDuration !== config.eventDurationMinutes ||
|
||||||
|
currentConfig.eventInterval !== config.eventIntervalHours;
|
||||||
|
|
||||||
|
if (configChanged) {
|
||||||
|
console.log(`${formatTime()} [express] Aktualisiere Canvas-Konfiguration aus config.cfg`);
|
||||||
|
|
||||||
|
// Für SQLite Storage: Verwende expandCanvas wenn Canvas vergrößert wird
|
||||||
|
if ('expandCanvas' in storage && typeof storage.expandCanvas === 'function' &&
|
||||||
|
(config.canvasWidth > currentConfig.canvasWidth || config.canvasHeight > currentConfig.canvasHeight)) {
|
||||||
|
await (storage as any).expandCanvas(config.canvasWidth, config.canvasHeight);
|
||||||
|
} else {
|
||||||
|
await storage.updateCanvasConfig({
|
||||||
|
canvasWidth: Math.max(currentConfig.canvasWidth, config.canvasWidth), // Erlaube nur Erweiterung
|
||||||
|
canvasHeight: Math.max(currentConfig.canvasHeight, config.canvasHeight), // Erlaube nur Erweiterung
|
||||||
|
defaultCooldown: config.defaultCooldown,
|
||||||
|
enableAutomaticEvents: config.enableAutomaticEvents,
|
||||||
|
eventDuration: config.eventDurationMinutes,
|
||||||
|
eventInterval: config.eventIntervalHours
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(`${formatTime()} [express] Canvas-Konfiguration aktualisiert`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${formatTime()} [express] Fehler beim Aktualisieren der Canvas-Konfiguration:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas exporter wird bereits in routes.ts initialisiert
|
||||||
|
|
||||||
// ALWAYS serve the app on the port specified in the environment variable PORT
|
// ALWAYS serve the app on the port specified in the environment variable PORT
|
||||||
// Other ports are firewalled. Default to 5000 if not specified.
|
// Other ports are firewalled. Default to 5000 if not specified.
|
||||||
// this serves both the API and the client.
|
// this serves both the API and the client.
|
||||||
@ -85,3 +123,11 @@ app.use((req, res, next) => {
|
|||||||
log(`serving on port ${port}`);
|
log(`serving on port ${port}`);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function formatTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||||
|
return `[${hours}:${minutes}:${seconds}]`;
|
||||||
|
}
|
||||||
142
server/routes.ts
142
server/routes.ts
@ -6,6 +6,17 @@ import { insertPixelSchema, insertUserCooldownSchema, type WSMessage } from "@sh
|
|||||||
import { CanvasExporter } from "./export";
|
import { CanvasExporter } from "./export";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
|
|
||||||
|
// Admin authentication middleware
|
||||||
|
function requireAdmin(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const adminKey = req.headers['x-admin-key'] || req.body.adminKey;
|
||||||
|
|
||||||
|
if (!adminKey || adminKey !== config.adminKey) {
|
||||||
|
return res.status(401).json({ message: "Admin access required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
// Authentication middleware
|
// Authentication middleware
|
||||||
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||||
if (!config.enableKeycloak) {
|
if (!config.enableKeycloak) {
|
||||||
@ -99,6 +110,48 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Canvas-Erweiterungs-Endpoint
|
||||||
|
app.post("/api/config/expand", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { canvasWidth, canvasHeight } = req.body;
|
||||||
|
|
||||||
|
if (!canvasWidth || !canvasHeight || canvasWidth < 1 || canvasHeight < 1) {
|
||||||
|
return res.status(400).json({ message: "Invalid canvas dimensions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentConfig = await storage.getCanvasConfig();
|
||||||
|
|
||||||
|
// Erlaube nur Erweiterung, nicht Verkleinerung
|
||||||
|
if (canvasWidth < currentConfig.canvasWidth || canvasHeight < currentConfig.canvasHeight) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: "Canvas kann nur erweitert werden, nicht verkleinert",
|
||||||
|
current: { width: currentConfig.canvasWidth, height: currentConfig.canvasHeight }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für SQLite Storage: Verwende spezielle expandCanvas Methode
|
||||||
|
if ('expandCanvas' in storage && typeof storage.expandCanvas === 'function') {
|
||||||
|
await (storage as any).expandCanvas(canvasWidth, canvasHeight);
|
||||||
|
} else {
|
||||||
|
// Fallback für Memory Storage
|
||||||
|
await storage.updateCanvasConfig({
|
||||||
|
canvasWidth,
|
||||||
|
canvasHeight,
|
||||||
|
defaultCooldown: currentConfig.defaultCooldown,
|
||||||
|
enableAutomaticEvents: currentConfig.enableAutomaticEvents,
|
||||||
|
eventDuration: currentConfig.eventDuration,
|
||||||
|
eventInterval: currentConfig.eventInterval
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedConfig = await storage.getCanvasConfig();
|
||||||
|
res.json(updatedConfig);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to expand canvas:", error);
|
||||||
|
res.status(500).json({ message: "Failed to expand canvas" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Config is now read-only from file
|
// Config is now read-only from file
|
||||||
// Remove the POST endpoint for config updates
|
// Remove the POST endpoint for config updates
|
||||||
|
|
||||||
@ -121,11 +174,17 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Check cooldown unless events are enabled
|
// Check cooldown unless events are enabled
|
||||||
if (!config.enableAutomaticEvents) {
|
if (!config.enableAutomaticEvents) {
|
||||||
const cooldown = await storage.getUserCooldown(userInfo.userId);
|
const cooldown = await storage.getUserCooldown(userInfo.userId);
|
||||||
if (cooldown && cooldown.cooldownEnds > new Date()) {
|
const now = new Date();
|
||||||
return res.status(429).json({ message: "Cooldown active" });
|
|
||||||
|
if (cooldown && cooldown.cooldownEnds > now) {
|
||||||
|
const remaining = Math.ceil((cooldown.cooldownEnds.getTime() - now.getTime()) / 1000);
|
||||||
|
return res.status(429).json({
|
||||||
|
message: "Cooldown active",
|
||||||
|
remainingSeconds: remaining
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set new cooldown
|
// Set new cooldown - immer setzen, auch wenn kein vorheriger existierte
|
||||||
const cooldownEnd = new Date(Date.now() + (config.defaultCooldown * 1000));
|
const cooldownEnd = new Date(Date.now() + (config.defaultCooldown * 1000));
|
||||||
await storage.setUserCooldown({
|
await storage.setUserCooldown({
|
||||||
userId: userInfo.userId,
|
userId: userInfo.userId,
|
||||||
@ -226,6 +285,83 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Admin Routes
|
||||||
|
app.post("/api/admin/auth", (req, res) => {
|
||||||
|
const { adminKey } = req.body;
|
||||||
|
|
||||||
|
if (adminKey === config.adminKey) {
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ message: "Invalid admin key" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/admin/stats", requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pixels = await storage.getAllPixels();
|
||||||
|
const uniqueUsers = new Set(pixels.map(p => p.userId)).size;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
totalPixels: pixels.length,
|
||||||
|
uniqueUsers,
|
||||||
|
lastActivity: pixels.length > 0 ? pixels[pixels.length - 1].createdAt : null
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to fetch admin stats" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/admin/pixels/:id", requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
if ('deletePixel' in storage && typeof storage.deletePixel === 'function') {
|
||||||
|
await (storage as any).deletePixel(id);
|
||||||
|
|
||||||
|
// Broadcast pixel deletion
|
||||||
|
broadcast({
|
||||||
|
type: "pixel_deleted",
|
||||||
|
data: { pixelId: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(501).json({ message: "Pixel deletion not supported by current storage" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to delete pixel" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/admin/canvas", requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
if ('clearCanvas' in storage && typeof storage.clearCanvas === 'function') {
|
||||||
|
await (storage as any).clearCanvas();
|
||||||
|
|
||||||
|
// Broadcast canvas clear
|
||||||
|
broadcast({
|
||||||
|
type: "canvas_cleared",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(501).json({ message: "Canvas clearing not supported by current storage" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to clear canvas" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/admin/export", requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const filename = await exporter.exportCanvas();
|
||||||
|
res.json({ filename, success: true });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: "Failed to export canvas" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Keep connections alive with ping/pong
|
// Keep connections alive with ping/pong
|
||||||
const pingInterval = setInterval(() => {
|
const pingInterval = setInterval(() => {
|
||||||
connectedUsers.forEach(ws => {
|
connectedUsers.forEach(ws => {
|
||||||
|
|||||||
220
server/sqlite-storage.ts
Normal file
220
server/sqlite-storage.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import { type Pixel, type InsertPixel, type CanvasConfig, type InsertCanvasConfig, type UserCooldown, type InsertUserCooldown } from "@shared/schema";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { config } from "./config";
|
||||||
|
import { IStorage } from "./storage";
|
||||||
|
|
||||||
|
export class SQLiteStorage implements IStorage {
|
||||||
|
private db: Database.Database;
|
||||||
|
|
||||||
|
constructor(dbPath: string = ':memory:') {
|
||||||
|
this.db = new Database(dbPath);
|
||||||
|
this.initTables();
|
||||||
|
this.initDefaultConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initTables() {
|
||||||
|
// Pixels table
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS pixels (
|
||||||
|
id TEXT PRIMARY KEY DEFAULT (hex(randomblob(16))),
|
||||||
|
x INTEGER NOT NULL,
|
||||||
|
y INTEGER NOT NULL,
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
userId TEXT NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Canvas config table
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS canvas_config (
|
||||||
|
id TEXT PRIMARY KEY DEFAULT (hex(randomblob(16))),
|
||||||
|
canvasWidth INTEGER NOT NULL DEFAULT 100,
|
||||||
|
canvasHeight INTEGER NOT NULL DEFAULT 100,
|
||||||
|
defaultCooldown INTEGER NOT NULL DEFAULT 5,
|
||||||
|
enableAutomaticEvents BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
eventDuration INTEGER NOT NULL DEFAULT 30,
|
||||||
|
eventInterval INTEGER NOT NULL DEFAULT 6,
|
||||||
|
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// User cooldowns table
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_cooldowns (
|
||||||
|
id TEXT PRIMARY KEY DEFAULT (hex(randomblob(16))),
|
||||||
|
userId TEXT NOT NULL UNIQUE,
|
||||||
|
lastPlacement DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
cooldownEnds DATETIME NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_pixels_xy ON pixels(x, y)`);
|
||||||
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_pixels_created ON pixels(createdAt DESC)`);
|
||||||
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_cooldowns_user ON user_cooldowns(userId)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initDefaultConfig() {
|
||||||
|
const existingConfig = this.db.prepare('SELECT * FROM canvas_config LIMIT 1').get();
|
||||||
|
if (!existingConfig) {
|
||||||
|
this.db.prepare(`
|
||||||
|
INSERT INTO canvas_config (canvasWidth, canvasHeight, defaultCooldown, enableAutomaticEvents, eventDuration, eventInterval)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(
|
||||||
|
config.canvasWidth,
|
||||||
|
config.canvasHeight,
|
||||||
|
config.defaultCooldown,
|
||||||
|
config.enableAutomaticEvents ? 1 : 0,
|
||||||
|
config.eventDurationMinutes,
|
||||||
|
config.eventIntervalHours
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPixel(x: number, y: number): Promise<Pixel | undefined> {
|
||||||
|
const row = this.db.prepare('SELECT * FROM pixels WHERE x = ? AND y = ? ORDER BY createdAt DESC LIMIT 1').get(x, y) as any;
|
||||||
|
if (!row) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
createdAt: new Date(row.createdAt),
|
||||||
|
enableAutomaticEvents: Boolean(row.enableAutomaticEvents)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllPixels(): Promise<Pixel[]> {
|
||||||
|
const rows = this.db.prepare(`
|
||||||
|
SELECT p1.* FROM pixels p1
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT x, y, MAX(createdAt) as maxCreated
|
||||||
|
FROM pixels
|
||||||
|
GROUP BY x, y
|
||||||
|
) p2 ON p1.x = p2.x AND p1.y = p2.y AND p1.createdAt = p2.maxCreated
|
||||||
|
`).all() as any[];
|
||||||
|
|
||||||
|
return rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
createdAt: new Date(row.createdAt)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async placePixel(insertPixel: InsertPixel): Promise<Pixel> {
|
||||||
|
const id = randomUUID();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
this.db.prepare(`
|
||||||
|
INSERT INTO pixels (id, x, y, color, userId, username, createdAt)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(id, insertPixel.x, insertPixel.y, insertPixel.color, insertPixel.userId, insertPixel.username, now.toISOString());
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...insertPixel,
|
||||||
|
createdAt: now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCanvasConfig(): Promise<CanvasConfig> {
|
||||||
|
const row = this.db.prepare('SELECT * FROM canvas_config ORDER BY updatedAt DESC LIMIT 1').get() as any;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
enableAutomaticEvents: Boolean(row.enableAutomaticEvents),
|
||||||
|
updatedAt: new Date(row.updatedAt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCanvasConfig(configUpdate: InsertCanvasConfig): Promise<CanvasConfig> {
|
||||||
|
const currentConfig = await this.getCanvasConfig();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
this.db.prepare(`
|
||||||
|
UPDATE canvas_config
|
||||||
|
SET canvasWidth = ?, canvasHeight = ?, defaultCooldown = ?,
|
||||||
|
enableAutomaticEvents = ?, eventDuration = ?, eventInterval = ?,
|
||||||
|
updatedAt = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(
|
||||||
|
configUpdate.canvasWidth ?? currentConfig.canvasWidth,
|
||||||
|
configUpdate.canvasHeight ?? currentConfig.canvasHeight,
|
||||||
|
configUpdate.defaultCooldown ?? currentConfig.defaultCooldown,
|
||||||
|
configUpdate.enableAutomaticEvents ? 1 : 0,
|
||||||
|
configUpdate.eventDuration ?? currentConfig.eventDuration,
|
||||||
|
configUpdate.eventInterval ?? currentConfig.eventInterval,
|
||||||
|
now.toISOString(),
|
||||||
|
currentConfig.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.getCanvasConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserCooldown(userId: string): Promise<UserCooldown | undefined> {
|
||||||
|
const row = this.db.prepare('SELECT * FROM user_cooldowns WHERE userId = ?').get(userId) as any;
|
||||||
|
if (!row) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
lastPlacement: new Date(row.lastPlacement),
|
||||||
|
cooldownEnds: new Date(row.cooldownEnds)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserCooldown(insertCooldown: InsertUserCooldown): Promise<UserCooldown> {
|
||||||
|
const id = randomUUID();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
this.db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO user_cooldowns (id, userId, lastPlacement, cooldownEnds)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`).run(id, insertCooldown.userId, now.toISOString(), insertCooldown.cooldownEnds.toISOString());
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
userId: insertCooldown.userId,
|
||||||
|
lastPlacement: now,
|
||||||
|
cooldownEnds: insertCooldown.cooldownEnds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecentPlacements(limit: number = 10): Promise<Pixel[]> {
|
||||||
|
const rows = this.db.prepare(`
|
||||||
|
SELECT * FROM pixels
|
||||||
|
ORDER BY createdAt DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(limit) as any[];
|
||||||
|
|
||||||
|
return rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
createdAt: new Date(row.createdAt)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas-Erweiterungsmethode
|
||||||
|
async expandCanvas(newWidth: number, newHeight: number): Promise<void> {
|
||||||
|
const currentConfig = await this.getCanvasConfig();
|
||||||
|
|
||||||
|
if (newWidth < currentConfig.canvasWidth || newHeight < currentConfig.canvasHeight) {
|
||||||
|
throw new Error("Canvas kann nur erweitert werden, nicht verkleinert");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateCanvasConfig({
|
||||||
|
canvasWidth: newWidth,
|
||||||
|
canvasHeight: newHeight,
|
||||||
|
defaultCooldown: currentConfig.defaultCooldown,
|
||||||
|
enableAutomaticEvents: currentConfig.enableAutomaticEvents,
|
||||||
|
eventDuration: currentConfig.eventDuration,
|
||||||
|
eventInterval: currentConfig.eventInterval
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePixel(pixelId: string): Promise<void> {
|
||||||
|
this.db.prepare("DELETE FROM pixels WHERE id = ?").run(pixelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearCanvas(): Promise<void> {
|
||||||
|
this.db.prepare("DELETE FROM pixels").run();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,10 @@ export interface IStorage {
|
|||||||
|
|
||||||
// Recent activity
|
// Recent activity
|
||||||
getRecentPlacements(limit?: number): Promise<Pixel[]>;
|
getRecentPlacements(limit?: number): Promise<Pixel[]>;
|
||||||
|
|
||||||
|
// Admin operations
|
||||||
|
deletePixel(pixelId: string): Promise<void>;
|
||||||
|
clearCanvas(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemStorage implements IStorage {
|
export class MemStorage implements IStorage {
|
||||||
@ -100,6 +104,21 @@ export class MemStorage implements IStorage {
|
|||||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deletePixel(pixelId: string): Promise<void> {
|
||||||
|
this.pixels.delete(pixelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearCanvas(): Promise<void> {
|
||||||
|
this.pixels.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storage = new MemStorage();
|
// The SQLiteStorage import and usage below is not part of the changes,
|
||||||
|
// but is included to ensure the file is complete as per instructions.
|
||||||
|
import { SQLiteStorage } from "./sqlite-storage";
|
||||||
|
|
||||||
|
// Verwende SQLite im Development-Modus, Memory-Storage in Production
|
||||||
|
export const storage = process.env.NODE_ENV === 'development'
|
||||||
|
? new SQLiteStorage('./dev-database.sqlite')
|
||||||
|
: new MemStorage();
|
||||||
@ -83,4 +83,9 @@ export const wsMessageSchema = z.union([
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type WSMessage = z.infer<typeof wsMessageSchema>;
|
export type WSMessage =
|
||||||
|
| { type: "pixel_placed"; data: { x: number; y: number; color: string; userId: string; username: string; timestamp: string } }
|
||||||
|
| { type: "user_count"; data: { count: number } }
|
||||||
|
| { type: "cooldown_update"; data: { userId: string; remainingSeconds: number } }
|
||||||
|
| { type: "pixel_deleted"; data: { pixelId: string } }
|
||||||
|
| { type: "canvas_cleared"; data: {} };
|
||||||
Loading…
Reference in New Issue
Block a user