milestone 25: client names learned over reverse dns
Gates / test (push) Successful in 2m58s
Gates / frontend (push) Successful in 3m57s
Gates / test-aarch64 (push) Successful in 8m20s
Gates / package (push) Successful in 7m27s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 19m9s

This commit is contained in:
2026-08-15 11:18:44 +02:00
parent c428bc2398
commit 3c794b645b
20 changed files with 2638 additions and 36 deletions
@@ -47,9 +47,12 @@ const styles = stylex.create({
export default function ClientEditDialog({ client, groups, onClose }: Props) {
const queryClient = useQueryClient();
const mutation = useMutation(clientUpdateMutation(queryClient));
const [name, setName] = useState(client.name);
const [groupId, setGroupId] = useState(client.group_id);
const readOnly = useReadOnlyConfig();
// Adopting the learned name as a typed one is the natural gesture, but only
// where the save can land: under file authority the PUT answers 403, and the
// file's declared name is the one that wins.
const [name, setName] = useState(client.name === "" && !readOnly ? client.learned_name : client.name);
const [groupId, setGroupId] = useState(client.group_id);
return (
<Dialog label={`Edit client ${client.ip}`} isOpen onClose={onClose}>
@@ -18,6 +18,7 @@ const CLIENTS = {
id: 1,
ip: "192.168.1.10",
name: "laptop",
learned_name: "laptop-1.lan",
group_id: 1,
group: "default",
hand_edited: true,
@@ -28,6 +29,7 @@ const CLIENTS = {
id: 2,
ip: "192.168.1.11",
name: "",
learned_name: "kids-tablet.lan",
group_id: 2,
group: "kids",
hand_edited: false,
@@ -95,6 +97,25 @@ test("renders the client table with group names and one hand-edited badge", asyn
expect(screen.getAllByRole("cell", { name: "kids" })).toHaveLength(1);
});
test("a named row shows the typed name and hides the learned one", async () => {
await renderClientsPage(BASE);
expect(screen.getByText("laptop")).toBeTruthy();
expect(screen.queryByText("laptop-1.lan")).toBeNull();
});
test("an unnamed row shows the learned name with the learned affordance", async () => {
await renderClientsPage(BASE);
// The cell holds the learned name followed by the tag, so the match is on
// the containing span rather than on a bare text node.
const learned = screen.getByText(
(content, element) => element?.tagName === "SPAN" && content.startsWith("kids-tablet.lan"),
);
// The affordance is text, not colour, so a screen reader announces it too.
expect(within(learned).getByText("learned")).toBeTruthy();
});
test("shows the DNS-activity empty state when there are no clients", async () => {
await renderClientsPage({ ...BASE, "GET /api/clients": { clients: [] } });
+25 -3
View File
@@ -56,6 +56,23 @@ const styles = stylex.create({
dash: {
color: colors.textMuted,
},
/**
* A learned name is runtime state, not something the operator typed, so it
* reads muted and carries an outlined "learned" tag. The tag is real text —
* a screen reader announces it — because colour alone is not an affordance.
*/
learnedTag: {
marginLeft: "0.5rem",
borderWidth: 1,
borderStyle: "solid",
borderColor: colors.border,
borderRadius: "0.25rem",
paddingInline: "0.375rem",
paddingBlock: "0.125rem",
fontSize: "0.75rem",
lineHeight: "1rem",
color: colors.textMuted,
},
badge: {
marginLeft: "0.5rem",
borderRadius: "0.25rem",
@@ -133,10 +150,15 @@ export default function ClientsPage() {
<tr key={client.id} {...stylex.props(styles.bodyRow)}>
<td {...stylex.props(styles.cell, shared.mono)}>{client.ip}</td>
<td {...stylex.props(styles.cell)}>
{client.name === "" ? (
<span {...stylex.props(styles.dash)}></span>
) : (
{client.name !== "" ? (
client.name
) : client.learned_name !== "" ? (
<span {...stylex.props(styles.dash)}>
{client.learned_name}
<span {...stylex.props(styles.learnedTag)}>learned</span>
</span>
) : (
<span {...stylex.props(styles.dash)}></span>
)}
{client.hand_edited && <span {...stylex.props(styles.badge)}>edited</span>}
</td>
+2
View File
@@ -259,6 +259,7 @@ export const sample_list_clients: { clients: Client[] } = {
id: 0,
ip: "192.168.1.50",
last_seen: 0,
learned_name: "",
name: "laptop",
},
],
@@ -272,6 +273,7 @@ export const sample_update_client: Client = {
id: 0,
ip: "192.168.1.50",
last_seen: 0,
learned_name: "",
name: "laptop-renamed",
};
+2
View File
@@ -256,6 +256,8 @@ export interface Client {
id: number;
ip: string;
name: string;
/** Learned over reverse DNS. `name` wins whenever it is non-empty. */
learned_name: string;
group_id: number;
group: string;
hand_edited: boolean;