Backend (nvcurve/): - hal/fans.py, hal/limits.py, hal/gpu.py: replace conditional pynvml imports with the established 'pynvml: Any = _pynvml_import' pattern (fixes ~50 'possibly unbound' errors); type the result dicts; guard query_interface() results; explicit uuid/pci-bus parsing (int, hex convention documented); modernize Optional[T] -> T | None - cli.py: fix 'curve_state' possibly-unbound and snap_path None handling in cmd_setup; wrap unchecked int()/open()/makedirs() calls in try/except with clean CLI errors; add module logger for silent except-pass blocks; raise ... from exc; fix unused loop vars and set-comprehension - hal/snapshot.py: filepath: str | None; wrap all file ops; sorted imports; remove unused CT_POINTS import - daemon.py: extract 0o666 to _SOCKET_MODE constant (intentional for /run sockets) with nosemgrep - server.py: nosemgrep for Python 3.7-compat false positive (project requires >= 3.12); log previously-swallowed exception - profiles/native.py, profiles/apply.py: wrap file ops and int(k) profile-key parsing; sorted imports; modernize typing Frontend (frontend/src): - Add .js extensions to all relative imports (standard TS-ESM; Vite resolves .js -> .ts) - React.FormEvent (deprecated in React 19 types) -> React.SubmitEvent - catch (e: any) -> catch (e: unknown) + instanceof Error narrowing - React-hooks: move ref writes from render into effects; convert viewport reset to render-phase state adjustment; split selectPoint(index, multi) into selectPoint + togglePoint (no flag argument); remove non-null assertion - Static inline styles -> Tailwind classes (dynamic positioning/cursor styles kept) - Remove non-standard 'container' option from scrollIntoView (browsers ignore unknown options) which had orphaned a @ts-expect-error - Object.fromEntries for Map -> Record conversion Tooling: - .gitignore: ignore .codegraph/ local tool data Verified: tsc --noEmit, vite production build, python imports, and full LSP scan (0 errors/warnings in both projects).
125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
import { useState } from "react";
|
|
import { Loader, Lock, User } from "lucide-react";
|
|
import { api, ApiError } from "../../api/client.js";
|
|
|
|
interface Props {
|
|
onSuccess: (username: string) => void;
|
|
}
|
|
|
|
export function LoginScreen({ onSuccess }: Props) {
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function submit(e: React.SubmitEvent) {
|
|
e.preventDefault();
|
|
if (busy) return;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const res = await api.login(username, password);
|
|
onSuccess(res.username);
|
|
} catch (err) {
|
|
if (err instanceof ApiError && err.status === 401) {
|
|
setError("Invalid username or password.");
|
|
} else if (err instanceof ApiError && err.status === 429) {
|
|
setError(
|
|
"Too many failed attempts. Please wait a moment and try again.",
|
|
);
|
|
} else {
|
|
setError(err instanceof Error ? err.message : "Login failed.");
|
|
}
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-zinc-950 text-zinc-100 flex items-center justify-center p-4">
|
|
<div className="w-full max-w-sm">
|
|
<div className="flex flex-col items-center mb-8">
|
|
<img src="/logo.svg" alt="NVCurve" className="w-12 h-12 mb-3" />
|
|
<h1 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-fuchsia-400 tracking-tight">
|
|
NVCurve
|
|
</h1>
|
|
<p className="text-sm text-zinc-500 mt-1">Sign in to continue</p>
|
|
</div>
|
|
|
|
<form
|
|
onSubmit={submit}
|
|
className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 shadow-[0_8px_30px_rgb(0,0,0,0.6)] flex flex-col gap-4"
|
|
>
|
|
<div className="flex flex-col gap-1.5">
|
|
<label
|
|
htmlFor="username"
|
|
className="text-xs font-medium text-zinc-400"
|
|
>
|
|
Username
|
|
</label>
|
|
<div className="relative">
|
|
<User
|
|
size={15}
|
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500"
|
|
/>
|
|
<input
|
|
id="username"
|
|
type="text"
|
|
autoComplete="username"
|
|
autoFocus
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
className="w-full bg-zinc-950 border border-zinc-700 rounded-lg pl-9 pr-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/40"
|
|
placeholder="username"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<label
|
|
htmlFor="password"
|
|
className="text-xs font-medium text-zinc-400"
|
|
>
|
|
Password
|
|
</label>
|
|
<div className="relative">
|
|
<Lock
|
|
size={15}
|
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500"
|
|
/>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="w-full bg-zinc-950 border border-zinc-700 rounded-lg pl-9 pr-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/40"
|
|
placeholder="••••••••"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={busy || !username || !password}
|
|
className="mt-1 flex items-center justify-center gap-2 bg-violet-600 hover:bg-violet-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg py-2.5 transition-colors"
|
|
>
|
|
{busy && <Loader size={15} className="animate-spin" />}
|
|
{busy ? "Signing in…" : "Sign in"}
|
|
</button>
|
|
</form>
|
|
|
|
<p className="text-center text-xs text-zinc-600 mt-4">
|
|
Sessions last 24 hours, then you will be asked to sign in again.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|