"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, issueMountProfile, logout, getMe, type StorageExport, type MountProfile, 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 [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, exps] = await Promise.all([getMe(), listExports()]); setUser(me); 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; return (

betterNAS

Control Plane

{user && (
{user.username}
)}
{process.env.NEXT_PUBLIC_BETTERNAS_API_URL || "local"} {exports.length === 1 ? "1 export" : `${exports.length} exports`}
{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.
                  
                    {`BETTERNAS_USERNAME=${user.username} BETTERNAS_PASSWORD=... BETTERNAS_EXPORT_PATH=/path/to/export betternas-node`}
                  
                
)}
{feedback !== null && ( Error {feedback} )}
Exports Storage exports registered with this control plane. {exports.length === 1 ? "1 export" : `${exports.length} exports`} {exports.length === 0 ? (

No exports registered yet. Start the node agent and connect it to this control plane.

) : (
{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. ))}
)}
); }