feat: make the Users tab operational, drop the degraded empty state

With user management now wired through the control-plane API, the Users
tab is always functional against a live gateway. Remove the "Gestión de
users no disponible" alert and the writable gating (button disabled,
revoke hidden) that were driven by the old users_backend === "none"
case. The backend badge now reads the wiring in use ("control-plane" or
"sqlite"). Add user (handle + 64-hex sign-pub + role) and revoke (with
explicit confirmation) consume the gateway REST unchanged. Includes the
rebuilt SPA bundle embedded by the binary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 21:10:35 +02:00
parent c412941e4c
commit c7631074cb
4 changed files with 169 additions and 182 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>unibus · admin</title>
<script type="module" crossorigin src="/assets/index-D7Qf15Sh.js"></script>
<script type="module" crossorigin src="/assets/index-CGRScjCy.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ndvieWwa.css">
</head>
<body>
+10 -18
View File
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import {
ActionIcon,
Alert,
Badge,
Button,
Card,
@@ -18,7 +17,7 @@ import {
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
import { IconPlus, IconRefresh, IconUserOff, IconInfoCircle } from "@tabler/icons-react";
import { IconPlus, IconRefresh, IconUserOff } from "@tabler/icons-react";
import { api, ApiError } from "../api";
import type { UserView } from "../types";
import { fmtTime, trunc } from "../util";
@@ -32,7 +31,6 @@ export function UsersPage({ usersBackend }: { usersBackend: string }) {
const [err, setErr] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [addOpen, addCtl] = useDisclosure(false);
const writable = usersBackend !== "none";
const load = useCallback(() => {
setLoading(true);
@@ -62,9 +60,11 @@ export function UsersPage({ usersBackend }: { usersBackend: string }) {
<Group gap="sm">
<Title order={3}>Users</Title>
{users && <Badge color="brand" variant="light">{users.length}</Badge>}
<Badge variant="outline" color={writable ? "teal" : "gray"} style={{ textTransform: "none" }}>
store: {usersBackend}
</Badge>
<Tooltip label="Vía de gestión de la allowlist del bus">
<Badge variant="outline" color="teal" style={{ textTransform: "none" }}>
backend: {usersBackend}
</Badge>
</Tooltip>
</Group>
<Group gap="xs">
<Tooltip label="Refrescar">
@@ -72,22 +72,14 @@ export function UsersPage({ usersBackend }: { usersBackend: string }) {
<IconRefresh size={18} />
</ActionIcon>
</Tooltip>
<Button leftSection={<IconPlus size={16} />} onClick={addCtl.open} disabled={!writable}>
<Button leftSection={<IconPlus size={16} />} onClick={addCtl.open}>
Añadir user
</Button>
</Group>
</Group>
{!writable && (
<Alert icon={<IconInfoCircle size={18} />} color="yellow" variant="light" title="Gestión de users no disponible">
El plano de control no expone endpoint de users; viven solo en el store. Arranca el gateway con <code>--db</code>
(single-node) o con acceso KV admin del cluster para listar/dar de alta/revocar. Coordinar con la vía KV que
añade <code>quick/0011-deploy-gaps</code>.
</Alert>
)}
{err && writable && <Text c="red">{err}</Text>}
{!users && !err && writable && <Loader color="brand" />}
{err && <Text c="red">{err}</Text>}
{!users && !err && <Loader color="brand" />}
{users && (
<Card withBorder bg="dark.7" p={0} radius="md">
@@ -117,7 +109,7 @@ export function UsersPage({ usersBackend }: { usersBackend: string }) {
</Table.Td>
<Table.Td><Text size="xs" c="dimmed">{fmtTime(u.created_at)}</Text></Table.Td>
<Table.Td>
{writable && u.status === "active" && (
{u.status === "active" && (
<Tooltip label="Revocar acceso">
<ActionIcon variant="subtle" color="red" onClick={() => revoke(u)}>
<IconUserOff size={16} />