Settings as panels, the way the portal does it

Four tabs over three cards was navigation for its own sake, and the two shells
disagreed about what a settings screen looks like. It is one grid of cards now,
matching the portal: the account card carries changing a password and deleting
the account in its footer, appearance is a card with one row, and remote access
— an operator's concern, not a personal preference — is a card of its own for a
superuser.

SettingRow, alert-dialog and UserAvatar come across from the portal, so an
account renders the same face in both shells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 16:04:42 +02:00
co-authored by Claude Opus 5
parent 6d09500a73
commit 5da5606f79
12 changed files with 717 additions and 367 deletions
@@ -3,6 +3,13 @@ import { useState } from "react"
import { CloudService } from "@/client"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Dialog,
DialogContent,
@@ -13,6 +20,7 @@ import {
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
@@ -96,125 +104,130 @@ export function RemoteAccess() {
if (!status) return null
return (
<div className="flex max-w-2xl flex-col gap-6 py-4">
<div>
<h2 className="text-lg font-medium">Remote access</h2>
<p className="text-sm text-muted-foreground">
<Card>
<CardHeader>
<CardTitle>Remote access</CardTitle>
<CardDescription>
Reach this installation from fluksio.com. Entirely optional without
it, this installation talks to nothing outside your network.
</p>
</div>
{status.enrolled ? (
<>
<div className="flex flex-col gap-4 rounded-lg border border-border p-4">
<dl className="grid gap-3 sm:grid-cols-2">
<Field label="Status">
{status.connected
? "Connected"
: status.last_error
? `Reconnecting — ${status.last_error}`
: "Reconnecting…"}
</Field>
<Field label="Portal">{status.portal_url ?? "—"}</Field>
<Field label="Acting as">
{status.portal_account ?? "—"}
<span className="mt-1 block text-xs text-muted-foreground">
Portal sessions of this account get this account's rights
here. Anyone else gets in only once added below, as their own
user.
</span>
</Field>
<Field label="Installation">
<span className="font-mono text-xs">
{status.installation_id ?? "—"}
</span>
</Field>
</dl>
<div>
<Button
variant="destructive"
className="rounded-full"
onClick={() => setConfirmDisconnect(true)}
>
Disconnect
</Button>
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
{status.enrolled ? (
<>
<div className="flex flex-col gap-4">
<dl className="grid gap-3 sm:grid-cols-2">
<Field label="Status">
{status.connected
? "Connected"
: status.last_error
? `Reconnecting — ${status.last_error}`
: "Reconnecting…"}
</Field>
<Field label="Portal">{status.portal_url ?? "—"}</Field>
<Field label="Acting as">
{status.portal_account ?? "—"}
<span className="mt-1 block text-xs text-muted-foreground">
Portal sessions of this account get this account's rights
here. Anyone else gets in only once added below, as their
own user.
</span>
</Field>
<Field label="Installation">
<span className="font-mono text-xs">
{status.installation_id ?? "—"}
</span>
</Field>
</dl>
<div>
<Button
variant="destructive"
className="rounded-full"
onClick={() => setConfirmDisconnect(true)}
>
Disconnect
</Button>
</div>
</div>
</div>
<div className="flex flex-col gap-4 rounded-lg border border-border p-4">
<div>
<h3 className="font-medium">Remote users</h3>
<p className="text-sm text-muted-foreground">
Let someone else reach this installation through the portal.
They get a user of their own here — not yours, and never a
superuser, so they cannot pass access on.
</p>
<Separator />
<div className="flex flex-col gap-4">
<div>
<h3 className="font-medium">Remote users</h3>
<p className="text-sm text-muted-foreground">
Let someone else reach this installation through the portal.
They get a user of their own here — not yours, and never a
superuser, so they cannot pass access on.
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="join-code">Code</Label>
<Input
id="join-code"
value={joinCode}
placeholder="XXXX-XXXX"
className="font-mono tracking-widest"
onChange={(event) =>
setJoinCode(event.target.value.toUpperCase())
}
/>
<p className="text-xs text-muted-foreground">
They get a code at fluksio.com → Installations → Join an
installation. Added users appear under Admin → Users; deleting
them there ends their access.
</p>
</div>
<div>
<Button
className="rounded-full"
variant="outline"
disabled={!joinCode.trim() || addRemoteUser.isPending}
onClick={() => addRemoteUser.mutate()}
>
{addRemoteUser.isPending ? "Adding…" : "Add remote user"}
</Button>
</div>
</div>
</>
) : (
<div className="flex flex-col gap-4">
<div className="grid gap-2">
<Label htmlFor="portal-url">Portal</Label>
<Input
id="portal-url"
value={portalUrl}
onChange={(event) => setPortalUrl(event.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="join-code">Code</Label>
<Label htmlFor="claim-code">Code</Label>
<Input
id="join-code"
value={joinCode}
id="claim-code"
value={code}
placeholder="XXXX-XXXX"
className="font-mono tracking-widest"
onChange={(event) =>
setJoinCode(event.target.value.toUpperCase())
}
onChange={(event) => setCode(event.target.value.toUpperCase())}
/>
<p className="text-xs text-muted-foreground">
They get a code at fluksio.com → Installations → Join an
installation. Added users appear under Admin → Users; deleting
them there ends their access.
Get a code at fluksio.com → Installations → Add installation.
</p>
</div>
<div>
<Button
variant="brand"
className="rounded-full"
variant="outline"
disabled={!joinCode.trim() || addRemoteUser.isPending}
onClick={() => addRemoteUser.mutate()}
disabled={
!code.trim() || !portalUrl.trim() || connect.isPending
}
onClick={() => connect.mutate()}
>
{addRemoteUser.isPending ? "Adding…" : "Add remote user"}
{connect.isPending ? "Connecting…" : "Connect"}
</Button>
</div>
</div>
</>
) : (
<div className="flex flex-col gap-4 rounded-lg border border-border p-4">
<div className="grid gap-2">
<Label htmlFor="portal-url">Portal</Label>
<Input
id="portal-url"
value={portalUrl}
onChange={(event) => setPortalUrl(event.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="claim-code">Code</Label>
<Input
id="claim-code"
value={code}
placeholder="XXXX-XXXX"
className="font-mono tracking-widest"
onChange={(event) => setCode(event.target.value.toUpperCase())}
/>
<p className="text-xs text-muted-foreground">
Get a code at fluksio.com → Installations → Add installation.
</p>
</div>
<div>
<Button
variant="brand"
className="rounded-full"
disabled={!code.trim() || !portalUrl.trim() || connect.isPending}
onClick={() => connect.mutate()}
>
{connect.isPending ? "Connecting…" : "Connect"}
</Button>
</div>
</div>
)}
)}
</CardContent>
<Dialog open={confirmDisconnect} onOpenChange={setConfirmDisconnect}>
<DialogContent>
@@ -245,7 +258,7 @@ export function RemoteAccess() {
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Card>
)
}