fix: resolve frontend routing, API mismatches, and missing UI components

- Add servers list page and missing routes (servers, settings redirect, account security)
- Fix members page .map error (API returns { data } wrapper, not flat array)
- Fix auth store fetchUser expecting flat User but API returns { user } wrapper
- Add node token display dialog after creation
- Add allocation management UI to node detail page
- Add account security page with password change
- Add change-password API endpoint
- Add node servers and stats API endpoints
- Fix config save using PATCH instead of PUT, add api.put method
- Fix audit logs field name mismatch (userName vs username)
- Replace admin nodes page to avoid orgId dependency
- Remove duplicate sidebar nav items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hibna
2026-02-22 13:07:00 +03:00
parent d7d8fd5339
commit c9fe2bd9fe
14 changed files with 618 additions and 17 deletions
+62 -3
View File
@@ -1,7 +1,8 @@
import { useState } from 'react';
import { useParams, Link } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Plus, Network, Wifi, WifiOff } from 'lucide-react';
import { Plus, Network, Wifi, WifiOff, Copy, Check } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/lib/api';
import { formatBytes } from '@/lib/utils';
import { Button } from '@/components/ui/button';
@@ -16,6 +17,7 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
DialogDescription,
} from '@/components/ui/dialog';
interface NodeItem {
@@ -29,6 +31,10 @@ interface NodeItem {
isOnline: boolean;
}
interface CreatedNode extends NodeItem {
daemonToken: string;
}
interface PaginatedResponse<T> {
data: T[];
meta: { total: number };
@@ -38,6 +44,9 @@ export function NodesPage() {
const { orgId } = useParams();
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [tokenDialog, setTokenDialog] = useState(false);
const [createdToken, setCreatedToken] = useState('');
const [copied, setCopied] = useState(false);
const [name, setName] = useState('');
const [fqdn, setFqdn] = useState('');
const [daemonPort, setDaemonPort] = useState(8443);
@@ -52,17 +61,28 @@ export function NodesPage() {
const createMutation = useMutation({
mutationFn: (body: Record<string, unknown>) =>
api.post(`/organizations/${orgId}/nodes`, body),
onSuccess: () => {
api.post<CreatedNode>(`/organizations/${orgId}/nodes`, body),
onSuccess: (node) => {
queryClient.invalidateQueries({ queryKey: ['nodes', orgId] });
setOpen(false);
setName('');
setFqdn('');
// Show token dialog
setCreatedToken(node.daemonToken);
setTokenDialog(true);
setCopied(false);
},
});
const nodes = data?.data ?? [];
const handleCopyToken = async () => {
await navigator.clipboard.writeText(createdToken);
setCopied(true);
toast.success('Token copied to clipboard');
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@@ -151,6 +171,45 @@ export function NodesPage() {
</Dialog>
</div>
{/* Token display dialog */}
<Dialog open={tokenDialog} onOpenChange={setTokenDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Node Created Successfully</DialogTitle>
<DialogDescription>
Save this daemon token now. It will not be shown again.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<Label>Daemon Token</Label>
<div className="flex gap-2">
<Input
readOnly
value={createdToken}
className="font-mono text-xs"
/>
<Button
variant="outline"
size="icon"
onClick={handleCopyToken}
>
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Use this token in your daemon configuration file (config.yml) to authenticate with the panel.
</p>
</div>
<DialogFooter>
<Button onClick={() => setTokenDialog(false)}>Done</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className="grid gap-4 sm:grid-cols-2">
{nodes.map((node) => (
<Link key={node.id} to={`/org/${orgId}/nodes/${node.id}`}>