mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 10:21:03 +00:00
622c1f9262
* feat(dashboard): drop CPU column and relative timestamp from Stack Health and status bar The Stack Health table's CPU column duplicated data already surfaced in the top ResourceGauges and the CPU Usage historical chart. The health status bar's 'just now' timestamp was cosmetic: no consumer relied on lastUpdated state for polling, staleness detection, or conditional rendering. Removing both tightens the dashboard and eliminates a dead prop chain through useDashboardData. * refactor: remove dead admin_email field from setup flow The Setup form captured an admin email under 'Used for license recovery. Never shared with third parties.' but the value was written to global_settings and read nowhere: no license recovery, SMTP, or support contact flow consumed it. Rather than building UI on top of the dead field, delete the input, the payload key, and the backend persistence. Any orphaned row from prior setups is harmless and the frontend ignores unknown settings keys. * feat(settings): use Radix ScrollArea with per-section scroll memory Settings Hub used a native-scroll div that snapped to the top every time the user switched subsections and exposed the default browser scrollbar. Wrap the nav and content panes with the shadcn ScrollArea (Radix under the hood, type='hover') and expose a viewportRef so the modal can stash each section's scrollTop in a ref and restore it via useLayoutEffect on switch. Style the thumb with translucent foreground tokens so it reads as glass against popovers and dialogs. Replaces a hand-rolled scroll hook and ad-hoc CSS utility.
168 lines
5.3 KiB
TypeScript
168 lines
5.3 KiB
TypeScript
import { useState } from 'react';
|
|
import { cn } from "@/lib/utils";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
interface SetupProps {
|
|
onComplete: () => void;
|
|
}
|
|
|
|
export function Setup({
|
|
onComplete,
|
|
className,
|
|
...props
|
|
}: SetupProps & React.ComponentPropsWithoutRef<"div">) {
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
// Client-side validation
|
|
if (password !== confirmPassword) {
|
|
setError('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
if (username.length < 3) {
|
|
setError('Username must be at least 3 characters');
|
|
return;
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
setError('Password must be at least 8 characters');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/setup', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
username,
|
|
password,
|
|
confirmPassword,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.success) {
|
|
onComplete();
|
|
} else {
|
|
setError(data.error || 'Setup failed');
|
|
}
|
|
} catch {
|
|
setError('Network error. Please try again.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={cn("grid min-h-svh md:grid-cols-2", className)} {...props}>
|
|
{/* ── Left: Branding Panel (desktop only) ── */}
|
|
<div className="relative hidden md:flex flex-col items-center justify-center bg-zinc-950 overflow-hidden">
|
|
{/* Dot grid texture */}
|
|
<div
|
|
className="absolute inset-0 opacity-[0.15]"
|
|
style={{
|
|
backgroundImage: 'radial-gradient(circle, rgba(255,255,255,0.7) 1px, transparent 1px)',
|
|
backgroundSize: '24px 24px',
|
|
}}
|
|
/>
|
|
|
|
{/* Branding content */}
|
|
<div className="relative z-10 flex flex-col items-center gap-6 px-12">
|
|
<img
|
|
src="/sencho-logo-dark.png"
|
|
alt="Sencho"
|
|
className="w-28 h-28"
|
|
draggable={false}
|
|
/>
|
|
<div className="text-center">
|
|
<h1 className="text-4xl font-medium text-foreground tracking-tight">Sencho</h1>
|
|
<p className="text-base text-zinc-400 mt-2">Docker Compose Management</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Brand accent line */}
|
|
<div className="absolute bottom-0 left-0 right-0 h-px bg-brand" />
|
|
</div>
|
|
|
|
{/* ── Right: Form Panel ── */}
|
|
<div className="flex flex-col items-center justify-center bg-background px-6 py-12">
|
|
{/* Mobile logo header */}
|
|
<div className="flex items-center gap-2.5 mb-10 md:hidden">
|
|
<img src="/sencho-logo-light.png" alt="Sencho" className="w-8 h-8 dark:hidden" draggable={false} />
|
|
<img src="/sencho-logo-dark.png" alt="Sencho" className="w-8 h-8 hidden dark:block" draggable={false} />
|
|
<span className="text-lg font-semibold tracking-tight">Sencho</span>
|
|
</div>
|
|
|
|
<div className="w-full max-w-sm">
|
|
<div className="mb-8">
|
|
<h2 className="text-2xl font-bold tracking-tight">Create your account</h2>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Set up the admin credentials for your Sencho instance
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="flex flex-col gap-5">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="username">Username</Label>
|
|
<Input
|
|
id="username"
|
|
type="text"
|
|
placeholder="admin"
|
|
required
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="password">Password</Label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
|
<Input
|
|
id="confirmPassword"
|
|
type="password"
|
|
required
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
/>
|
|
</div>
|
|
{error && (
|
|
<div className="text-sm text-red-500 text-center">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
{isLoading ? 'Setting up...' : 'Complete Setup'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|