"use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { Globe, HardDrives, LinkSimple, SignOut, Warning, } from "@phosphor-icons/react"; import { isAuthenticated, listExports, listNodes, issueMountProfile, logout, getMe, type StorageExport, type MountProfile, type NasNode, type User, ApiError, } from "@/lib/api"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import { CopyField } from "./copy-field"; export default function Home() { const router = useRouter(); const [user, setUser] = useState(null); const [nodes, setNodes] = useState([]); const [exports, setExports] = useState([]); const [selectedExportId, setSelectedExportId] = useState(null); const [mountProfile, setMountProfile] = useState(null); const [feedback, setFeedback] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { if (!isAuthenticated()) { router.replace("/login"); return; } async function load() { try { const [me, registeredNodes, exps] = await Promise.all([ getMe(), listNodes(), listExports(), ]); setUser(me); setNodes(registeredNodes); setExports(exps); } catch (err) { if (err instanceof ApiError && err.status === 401) { router.replace("/login"); return; } setFeedback(err instanceof Error ? err.message : "Failed to load"); } finally { setLoading(false); } } load(); }, [router]); async function handleSelectExport(exportId: string) { setSelectedExportId(exportId); setMountProfile(null); setFeedback(null); try { const profile = await issueMountProfile(exportId); setMountProfile(profile); } catch (err) { setFeedback( err instanceof Error ? err.message : "Failed to issue mount profile", ); } } async function handleLogout() { await logout(); router.replace("/login"); } if (loading) { return (

Loading...

); } const selectedExport = selectedExportId ? (exports.find((e) => e.id === selectedExportId) ?? null) : null; const onlineNodeCount = nodes.filter((node) => node.status === "online").length; const degradedNodeCount = nodes.filter( (node) => node.status === "degraded", ).length; const offlineNodeCount = nodes.filter( (node) => node.status === "offline", ).length; return (

betterNAS

Control Plane

{user && (
{user.username}
)}
{process.env.NEXT_PUBLIC_BETTERNAS_API_URL || "local"} {exports.length === 1 ? "1 export" : `${exports.length} exports`} {nodes.length === 1 ? "1 node" : `${nodes.length} nodes`}
{user && ( Node agent setup Run the node binary on the machine that owns the files with the same account credentials you use here and in Finder.
                    
                      {`curl -fsSL https://raw.githubusercontent.com/harivansh-afk/betterNAS/main/scripts/install-betternas-node.sh | sh`}
                    
                  
                    
                      {`BETTERNAS_USERNAME=${user.username} BETTERNAS_PASSWORD=... BETTERNAS_EXPORT_PATH=/path/to/export BETTERNAS_NODE_DIRECT_ADDRESS=https://your-public-node-url betternas-node`}
                    
                  
)}
{feedback !== null && ( Error {feedback} )} Nodes Machines registered to your account and their current connection state.
{onlineNodeCount} online {degradedNodeCount > 0 && ( {degradedNodeCount} degraded )} {offlineNodeCount > 0 && ( {offlineNodeCount} offline )}
{nodes.length === 0 ? (

No nodes registered yet. Install and start the node agent on the machine that owns your files.

) : (
{nodes.map((node) => (
{node.displayName} {node.machineId}
{node.status}
Node ID
{node.id}
Last seen
{formatTimestamp(node.lastSeenAt)}
Address
{node.directAddress ?? node.relayAddress ?? "Unavailable"}
))}
)}
Exports Connected storage exports that are currently mountable. {exports.length === 1 ? "1 export" : `${exports.length} exports`} {exports.length === 0 ? (

{nodes.length === 0 ? "No exports registered yet. Start the node agent and connect it to this control plane." : "No connected exports right now. Start the node agent or wait for a disconnected node to reconnect."}

) : (
{exports.map((storageExport) => { const isSelected = storageExport.id === selectedExportId; return ( ); })}
)}
{selectedExport !== null ? `Mount ${selectedExport.label}` : "Mount instructions"} {selectedExport !== null ? "WebDAV mount details for Finder." : "Select an export to see the mount URL and account login details."} {mountProfile === null ? (

Pick an export to see the Finder mount URL and the username to use with your betterNAS account password.

) : (
Issued profile {mountProfile.readonly ? "Read-only" : "Read-write"}
Use your betterNAS account password Enter the same password you use to sign in to betterNAS and run the node agent. v1 does not issue a separate WebDAV password.
Mode
{mountProfile.credential.mode}
Password source
Your betterNAS account password

Finder steps

    {[ "Open Finder and choose Go, then Connect to Server.", "Paste the mount URL into the server address field.", "Enter your betterNAS username and account password when prompted.", "Save to Keychain only if you want Finder to reuse that same account password.", ].map((step, index) => (
  1. {index + 1} {step}
  2. ))}
)}
); } function formatTimestamp(value: string): string { const trimmedValue = value.trim(); if (trimmedValue === "") { return "Never"; } const parsed = new Date(trimmedValue); if (Number.isNaN(parsed.getTime())) { return trimmedValue; } return parsed.toLocaleString(); }