This commit is contained in:
Gabriel Peron 2025-03-23 01:32:13 +01:00
parent cc98497da0
commit 85b482a431
13 changed files with 328 additions and 187 deletions

3
.bolt/config.json Normal file
View file

@ -0,0 +1,3 @@
{
"template": "bolt-vite-react-ts"
}

8
.bolt/prompt Normal file
View file

@ -0,0 +1,8 @@
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
Use icons from lucide-react for logos.
Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.

View file

@ -0,0 +1,53 @@
/*
# Create servers table for Proxmox dashboard
1. New Tables
- `servers`
- `id` (uuid, primary key)
- `name` (text, server name)
- `model` (text, server model)
- `cpu_model` (text, CPU model)
- `cpu_cores` (integer, number of CPU cores)
- `ram_gb` (integer, RAM in GB)
- `created_at` (timestamp)
- `updated_at` (timestamp)
2. Security
- Enable RLS on `servers` table
- Add policies for authenticated users to manage their servers
*/
CREATE TABLE IF NOT EXISTS servers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
model text NOT NULL,
cpu_model text NOT NULL,
cpu_cores integer NOT NULL,
ram_gb integer NOT NULL,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
ALTER TABLE servers ENABLE ROW LEVEL SECURITY;
-- Allow authenticated users to read all servers
CREATE POLICY "Users can read all servers"
ON servers
FOR SELECT
TO authenticated
USING (true);
-- Allow authenticated users to insert their own servers
CREATE POLICY "Users can insert servers"
ON servers
FOR INSERT
TO authenticated
WITH CHECK (true);
-- Allow authenticated users to update their own servers
CREATE POLICY "Users can update their own servers"
ON servers
FOR UPDATE
TO authenticated
USING (true)
WITH CHECK (true);

View file

@ -0,0 +1,62 @@
/*
# Create servers table
1. New Tables
- `servers`
- `id` (uuid, primary key)
- `name` (text)
- `model` (text)
- `cpus` (jsonb array)
- `ram_gb` (integer)
- `proxmox_url` (text)
- `user_id` (uuid, foreign key)
- `created_at` (timestamp)
- `status` (text)
- `specs` (jsonb)
- `last_ping` (integer)
2. Security
- Enable RLS on `servers` table
- Add policies for authenticated users to manage their own servers
*/
CREATE TABLE IF NOT EXISTS servers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
model text NOT NULL,
cpus jsonb NOT NULL DEFAULT '[]'::jsonb,
ram_gb integer NOT NULL,
proxmox_url text NOT NULL,
user_id uuid NOT NULL REFERENCES auth.users(id),
created_at timestamptz DEFAULT now(),
status text NOT NULL DEFAULT 'checking',
specs jsonb NOT NULL DEFAULT '{}'::jsonb,
last_ping integer
);
ALTER TABLE servers ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read their own servers"
ON servers
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert their own servers"
ON servers
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own servers"
ON servers
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete their own servers"
ON servers
FOR DELETE
TO authenticated
USING (auth.uid() = user_id);

0
.env
View file

128
package-lock.json generated
View file

@ -8,7 +8,6 @@
"name": "proxmox-dashboard",
"version": "0.0.0",
"dependencies": {
"@supabase/supabase-js": "^2.39.7",
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -1205,73 +1204,6 @@
"win32"
]
},
"node_modules/@supabase/auth-js": {
"version": "2.68.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.68.0.tgz",
"integrity": "sha512-odG7nb7aOmZPUXk6SwL2JchSsn36Ppx11i2yWMIc/meUO2B2HK9YwZHPK06utD9Ql9ke7JKDbwGin/8prHKxxQ==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz",
"integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/node-fetch": {
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.2.tgz",
"integrity": "sha512-MXRbk4wpwhWl9IN6rIY1mR8uZCCG4MZAEji942ve6nMwIqnBgBnZhZlON6zTTs6fgveMnoCILpZv1+K91jN+ow==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz",
"integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14",
"@types/phoenix": "^1.5.4",
"@types/ws": "^8.5.10",
"ws": "^8.18.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
"integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.49.1",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.1.tgz",
"integrity": "sha512-lKaptKQB5/juEF5+jzmBeZlz69MdHZuxf+0f50NwhL+IE//m4ZnOeWlsKRjjsM0fVayZiQKqLvYdBn0RLkhGiQ==",
"dependencies": {
"@supabase/auth-js": "2.68.0",
"@supabase/functions-js": "2.4.4",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.19.2",
"@supabase/realtime-js": "2.11.2",
"@supabase/storage-js": "2.7.1"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -1329,15 +1261,13 @@
"version": "22.13.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.11.tgz",
"integrity": "sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A=="
},
"node_modules/@types/prop-types": {
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
@ -1363,14 +1293,6 @@
"@types/react": "*"
}
},
"node_modules/@types/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.8.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz",
@ -3815,11 +3737,6 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@ -3889,7 +3806,10 @@
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/update-browserslist-db": {
"version": "1.1.1",
@ -3995,20 +3915,6 @@
}
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -4154,26 +4060,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View file

@ -10,7 +10,6 @@
"preview": "vite preview"
},
"dependencies": {
"@supabase/supabase-js": "^2.39.7",
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",

View file

@ -11,8 +11,13 @@ function App() {
const [servers, setServers] = useState<Server[]>([]);
const fetchServers = () => {
const data = storage.getServers();
setServers(data);
const serverData = storage.getServers();
setServers(serverData);
};
const handleDeleteServer = (id: string) => {
storage.deleteServer(id);
fetchServers();
};
useEffect(() => {
@ -53,10 +58,7 @@ function App() {
<ServerCard
key={server.id}
server={server}
onDelete={(id) => {
storage.deleteServer(id);
fetchServers();
}}
onDelete={handleDeleteServer}
/>
))}
</div>
@ -72,4 +74,4 @@ function App() {
);
}
export default App
export default App;

View file

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { X, Server, Cpu, MemoryStick as Memory, Link } from 'lucide-react';
import { storage } from '../utils/storage';
import toast from 'react-hot-toast';
import type { CPU } from '../types';
interface AddServerModalProps {
isOpen: boolean;
@ -13,23 +14,29 @@ export function AddServerModal({ isOpen, onClose, onServerAdded }: AddServerModa
const [formData, setFormData] = useState({
name: '',
model: '',
cpu_model: '',
cpu_cores: '',
cpuModel: '',
cpuCores: 1,
cpuCount: 1,
ram_gb: '',
proxmox_url: ''
});
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
try {
// Create array of identical CPUs based on count
const cpus: CPU[] = Array(formData.cpuCount).fill({
model: formData.cpuModel,
cores: formData.cpuCores
});
storage.addServer({
name: formData.name,
model: formData.model,
cpu_model: formData.cpu_model,
cpu_cores: parseInt(formData.cpu_cores),
cpus,
ram_gb: parseInt(formData.ram_gb),
proxmox_url: formData.proxmox_url
});
@ -44,8 +51,8 @@ export function AddServerModal({ isOpen, onClose, onServerAdded }: AddServerModa
};
return (
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-gray-800/90 backdrop-blur rounded-xl p-6 w-full max-w-md border border-gray-700/50">
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4 z-50 animate-fadeIn">
<div className="bg-gray-800/90 backdrop-blur rounded-xl p-6 w-full max-w-md border border-gray-700/50 animate-slideIn">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-semibold text-white flex items-center gap-2">
<Server className="text-purple-400" size={24} />
@ -88,51 +95,65 @@ export function AddServerModal({ isOpen, onClose, onServerAdded }: AddServerModa
/>
</div>
<div className="bg-gray-700/30 p-4 rounded-lg space-y-4">
<div className="flex items-center gap-2 mb-2">
<Cpu size={18} className="text-purple-400" />
<span className="text-gray-300 font-medium">CPU Configuration</span>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">CPU Model</label>
<input
type="text"
value={formData.cpuModel}
onChange={(e) => setFormData({ ...formData, cpuModel: e.target.value })}
className="w-full bg-gray-700/50 text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-purple-500 focus:outline-none transition-all duration-200"
placeholder="e.g. Intel Xeon E5-2680 v2"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">Cores per CPU</label>
<input
type="number"
min="1"
value={formData.cpuCores}
onChange={(e) => setFormData({ ...formData, cpuCores: parseInt(e.target.value) })}
className="w-full bg-gray-700/50 text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-purple-500 focus:outline-none transition-all duration-200"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">Number of CPUs</label>
<input
type="number"
min="1"
max="8"
value={formData.cpuCount}
onChange={(e) => setFormData({ ...formData, cpuCount: parseInt(e.target.value) })}
className="w-full bg-gray-700/50 text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-purple-500 focus:outline-none transition-all duration-200"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">
<div className="flex items-center gap-2 mb-2">
<Cpu size={16} className="text-purple-400" />
CPU Model
<Memory size={16} className="text-purple-400" />
RAM (GB)
</div>
</label>
<input
type="text"
value={formData.cpu_model}
onChange={(e) => setFormData({ ...formData, cpu_model: e.target.value })}
type="number"
value={formData.ram_gb}
onChange={(e) => setFormData({ ...formData, ram_gb: e.target.value })}
className="w-full bg-gray-700/50 text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-purple-500 focus:outline-none transition-all duration-200"
placeholder="e.g. Intel Xeon E5-2680"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">CPU Cores</label>
<input
type="number"
value={formData.cpu_cores}
onChange={(e) => setFormData({ ...formData, cpu_cores: e.target.value })}
className="w-full bg-gray-700/50 text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-purple-500 focus:outline-none transition-all duration-200"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">
<div className="flex items-center gap-2 mb-2">
<Memory size={16} className="text-purple-400" />
RAM (GB)
</div>
</label>
<input
type="number"
value={formData.ram_gb}
onChange={(e) => setFormData({ ...formData, ram_gb: e.target.value })}
className="w-full bg-gray-700/50 text-white rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-purple-500 focus:outline-none transition-all duration-200"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">
@ -153,7 +174,7 @@ export function AddServerModal({ isOpen, onClose, onServerAdded }: AddServerModa
<button
type="submit"
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-medium py-3 px-4 rounded-lg transform hover:scale-102 transition-all duration-200 mt-6"
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-medium py-3 px-4 rounded-lg transform hover:scale-102 transition-all duration-200 mt-6 animate-pulse hover:animate-none"
>
Add Server
</button>

View file

@ -12,14 +12,17 @@ export function ServerCard({ server, onDelete }: ServerCardProps) {
window.open(server.proxmox_url, '_blank');
};
const totalCores = server.cpus.reduce((sum, cpu) => sum + cpu.cores, 0);
const firstCpu = server.cpus[0];
return (
<div
className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 hover:bg-gray-700/50 transition-all duration-300 cursor-pointer transform hover:scale-102 hover:shadow-xl hover:shadow-purple-500/10 border border-gray-700/50"
className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 hover:bg-gray-700/50 transition-all duration-300 cursor-pointer transform hover:scale-102 hover:shadow-xl hover:shadow-purple-500/10 border border-gray-700/50 animate-fadeIn hover:animate-glow"
onClick={handleProxmoxClick}
>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-white flex items-center gap-2 group">
<Server className="text-purple-400 group-hover:text-purple-300 transition-colors" size={24} />
<Server className="text-purple-400 group-hover:text-purple-300 transition-colors animate-pulse" size={24} />
<span className="group-hover:text-purple-300 transition-colors">{server.name}</span>
<ExternalLink size={16} className="text-purple-400 group-hover:text-purple-300 transition-colors" />
</h3>
@ -31,36 +34,43 @@ export function ServerCard({ server, onDelete }: ServerCardProps) {
className="text-gray-400 hover:text-red-400 transition-colors p-2 hover:bg-red-400/10 rounded-lg"
title="Delete server"
>
<Trash2 size={18} />
<Trash2 size={18} className="transform hover:rotate-12 transition-transform" />
</button>
</div>
<div className="space-y-4">
<div className="flex items-center gap-2 text-gray-300 bg-gray-800/50 p-3 rounded-lg">
<div className="flex items-center gap-2 text-gray-300 bg-gray-800/50 p-3 rounded-lg hover:bg-gray-700/50 transition-all duration-300">
<span className="text-gray-400 min-w-[4rem]">Model:</span>
<span className="font-medium">{server.model}</span>
</div>
<div className="bg-gray-800/50 p-4 rounded-lg space-y-3">
<div className="bg-gray-800/50 p-4 rounded-lg space-y-3 hover:bg-gray-700/50 transition-all duration-300">
<div className="flex items-center gap-2 mb-2">
<Cpu size={18} className="text-purple-400" />
<Cpu size={18} className="text-purple-400 animate-spin-slow" />
<span className="text-gray-300 font-medium">CPU Information</span>
</div>
<div className="pl-7 space-y-2">
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-gray-400">Model:</span>
<span className="text-gray-200 font-medium">{server.cpu_model}</span>
<span className="text-gray-200 font-medium">{firstCpu.model}</span>
<span className="text-purple-400 font-bold">×{server.cpus.length}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-400">Cores:</span>
<span className="text-gray-200 font-medium">{server.cpu_cores} cores</span>
<span className="text-gray-400">Cores per CPU:</span>
<span className="text-gray-200 font-medium">{firstCpu.cores}</span>
</div>
<div className="pt-2 border-t border-gray-700/50 mt-2">
<div className="flex items-center gap-2">
<span className="text-gray-400">Total Cores:</span>
<span className="text-purple-400 font-bold">{totalCores}</span>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-2 text-gray-300 bg-gray-800/50 p-3 rounded-lg">
<Memory size={18} className="text-purple-400 min-w-[1.5rem]" />
<div className="flex items-center gap-2 text-gray-300 bg-gray-800/50 p-3 rounded-lg hover:bg-gray-700/50 transition-all duration-300">
<Memory size={18} className="text-purple-400 min-w-[1.5rem] animate-bounce-slow" />
<div>
<div className="font-medium">{server.ram_gb} GB</div>
<div className="text-sm text-gray-400">RAM</div>

View file

@ -28,4 +28,74 @@ body {
background: linear-gradient(-45deg, #2d1b69, #1a1a2e, #16213e, #1f2937);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes glow {
0% {
box-shadow: 0 0 5px rgba(139, 92, 246, 0.2);
}
50% {
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
}
100% {
box-shadow: 0 0 5px rgba(139, 92, 246, 0.2);
}
}
@keyframes spin-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes bounce-slow {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
.animate-fadeIn {
animation: fadeIn 0.5s ease-out;
}
.animate-slideIn {
animation: slideIn 0.5s ease-out;
}
.animate-glow {
animation: glow 2s ease-in-out infinite;
}
.animate-spin-slow {
animation: spin-slow 4s linear infinite;
}
.animate-bounce-slow {
animation: bounce-slow 2s ease-in-out infinite;
}

View file

@ -1,9 +1,13 @@
export interface CPU {
model: string;
cores: number;
}
export interface Server {
id: string;
name: string;
model: string;
cpu_model: string;
cpu_cores: number;
cpus: CPU[];
ram_gb: number;
proxmox_url: string;
created_at: string;

View file

@ -6,7 +6,30 @@ export const storage = {
getServers: (): Server[] => {
try {
const data = localStorage.getItem(STORAGE_FILE);
return data ? JSON.parse(data) : [];
if (!data) return [];
// Parse the stored data
const servers = JSON.parse(data);
// Migrate old format to new format
return servers.map((server: any) => {
// If server already has cpus array, return as is
if (Array.isArray(server.cpus)) {
return server;
}
// Migrate old format to new format
return {
...server,
cpus: [{
model: server.cpu_model || '',
cores: server.cpu_cores || 1
}],
// Remove old properties
cpu_model: undefined,
cpu_cores: undefined
};
});
} catch (error) {
console.error('Error reading servers:', error);
return [];