feat: enforce singleton local node per instance (#1567)

Only one local node is allowed. Creating a second local returns 409,
and the last local node cannot be deleted or converted to a remote type.
Existing duplicate local nodes from older versions are preserved and can
be cleaned up individually. Zero-local recovery auto-assigns the default
flag. Frontend delete surfaces and the Add Node form respect the new
invariant.

Enforced in DatabaseService (addNode/updateNode/deleteNode guards) and
routes (error translations). Legacy test fixtures use raw SQL helpers.
This commit is contained in:
Anso
2026-07-05 05:30:56 -04:00
committed by GitHub
parent 33231089c3
commit a7e856f447
11 changed files with 507 additions and 119 deletions
+3 -2
View File
@@ -288,7 +288,7 @@ export function NodeManager() {
{resettingAnchor === nodeId ? 'Resetting...' : 'Reset anchor on peer'}
</Button>
)}
{node && !node.is_default && (isAdmin || can('node:manage', 'node', String(nodeId))) && (
{node && !node.is_default && nodes.filter(n => n.type === 'local').length > 1 && (isAdmin || can('node:manage', 'node', String(nodeId))) && (
<Button
size="sm"
variant="outline"
@@ -324,6 +324,7 @@ export function NodeManager() {
<TableBody>
{nodes.map((node) => {
const canManageThis = isAdmin || can('node:manage', 'node', String(node.id));
const isLastLocal = node.type === 'local' && nodes.filter(n => n.type === 'local').length <= 1;
return (
<TableRow key={node.id}>
<TableCell>
@@ -493,7 +494,7 @@ export function NodeManager() {
</TooltipProvider>
)}
{!node.is_default && canManageThis && (
{!node.is_default && !isLastLocal && canManageThis && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>