milestone 18: collapse duplicated infrastructure into shared listener core, crud list helper, resource shells, transport race, name and line helpers, ui modules
CI / test (push) Successful in 1m22s
CI / test-aarch64 (push) Successful in 5m6s
CI / frontend (push) Successful in 45s
CI / cross (push) Successful in 7m53s
CI / docker (push) Failing after 1h10m57s

This commit is contained in:
2026-08-07 18:20:30 +02:00
parent c50c6d285a
commit 6f67940995
82 changed files with 3167 additions and 3114 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
import { useState } from "react";
import RecordsTab from "@/features/local/RecordsTab";
import ZonesTab from "@/features/local/ZonesTab";
import { focusRing } from "@/ui/classes";
type Tab = "records" | "zones";
@@ -25,7 +26,7 @@ function TabButton({
aria-controls={controls}
aria-selected={selected}
onClick={onClick}
className={`-mb-px border-b-2 px-3 py-2 font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 ${
className={`-mb-px border-b-2 px-3 py-2 font-medium ${focusRing} ${
selected
? "border-blue-600 text-blue-600 dark:text-blue-400"
: "border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
+28 -54
View File
@@ -1,5 +1,5 @@
import { useId, useState, type FormEvent } from "react";
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { useSuspenseQuery } from "@tanstack/react-query";
import {
localRecordCreateMutation,
localRecordDeleteMutation,
@@ -8,20 +8,18 @@ import {
} from "@/lib/queries";
import type { LocalRecord, LocalRecordInput, LocalRecordType } from "@/lib/types";
import InlineError from "@/lib/InlineError";
import { useCrudForm } from "@/ui/useCrudForm";
import {
formCardClass,
inputClass,
largeButtonClass,
largePrimaryButtonClass,
rowButtonClass,
tableWrapClass,
} from "@/ui/classes";
const RTYPES: readonly LocalRecordType[] = ["A", "AAAA", "CNAME"];
const inputClass =
"mt-1 w-full rounded border border-zinc-300 bg-white px-3 py-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700 dark:bg-zinc-900";
const primaryButtonClass =
"rounded bg-blue-600 px-3 py-2 font-medium text-white disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600";
const secondaryButtonClass =
"rounded border border-zinc-300 px-3 py-2 font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700";
const rowButtonClass =
"rounded px-2 py-1 text-sm text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:text-blue-400";
type FormState = { mode: "create" } | { mode: "edit"; record: LocalRecord };
function RecordForm({
initial,
busy,
@@ -49,10 +47,7 @@ function RecordForm({
}
return (
<form
onSubmit={submit}
className="mt-4 max-w-lg space-y-3 rounded border border-zinc-200 p-4 dark:border-zinc-800"
>
<form onSubmit={submit} className={formCardClass}>
<h3 className="font-medium">{initial === undefined ? "New record" : `Edit ${initial.name}`}</h3>
<div>
<label htmlFor={`${id}-name`} className="block text-sm font-medium">
@@ -114,10 +109,10 @@ function RecordForm({
/>
</div>
<div className="flex gap-2">
<button type="submit" disabled={busy} className={primaryButtonClass}>
<button type="submit" disabled={busy} className={largePrimaryButtonClass}>
{busy ? "Saving…" : "Save"}
</button>
<button type="button" onClick={onCancel} className={secondaryButtonClass}>
<button type="button" onClick={onCancel} className={largeButtonClass}>
Cancel
</button>
</div>
@@ -128,50 +123,29 @@ function RecordForm({
export default function RecordsTab() {
const records = useSuspenseQuery(localRecordsQuery()).data;
const queryClient = useQueryClient();
const create = useMutation(localRecordCreateMutation(queryClient));
const update = useMutation(localRecordUpdateMutation(queryClient));
const remove = useMutation(localRecordDeleteMutation(queryClient));
const [form, setForm] = useState<FormState | null>(null);
function openForm(next: FormState) {
create.reset();
update.reset();
setForm(next);
}
function onSubmit(input: LocalRecordInput) {
if (form === null) return;
if (form.mode === "create") {
create.mutate(input, { onSuccess: () => setForm(null) });
} else {
update.mutate({ id: form.record.id, input }, { onSuccess: () => setForm(null) });
}
}
function onDelete(record: LocalRecord) {
if (!window.confirm(`Delete record "${record.name}"?`)) return;
remove.mutate(record.id);
}
const { create, update, remove, form, openForm, closeForm, onSubmit, onDelete } = useCrudForm<
LocalRecord,
LocalRecordInput
>({
create: localRecordCreateMutation,
update: localRecordUpdateMutation,
remove: localRecordDeleteMutation,
confirmDelete: (record) => `Delete record "${record.name}"?`,
});
return (
<div>
<div className="mt-4 flex items-center justify-between">
<p className="text-sm text-zinc-500">Answers served directly for LAN names. Changes apply live.</p>
<button type="button" onClick={() => openForm({ mode: "create" })} className={primaryButtonClass}>
<button type="button" onClick={() => openForm({ mode: "create" })} className={largePrimaryButtonClass}>
Add record
</button>
</div>
<InlineError error={remove.error} />
{form?.mode === "create" && (
<RecordForm
busy={create.isPending}
error={create.error}
onSubmit={onSubmit}
onCancel={() => setForm(null)}
/>
<RecordForm busy={create.isPending} error={create.error} onSubmit={onSubmit} onCancel={closeForm} />
)}
<div className="mt-4 overflow-x-auto">
<div className={tableWrapClass}>
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-zinc-200 text-zinc-500 dark:border-zinc-800">
@@ -202,14 +176,14 @@ export default function RecordsTab() {
)}
{records.map((record) => (
<tr key={record.id} className="border-b border-zinc-100 dark:border-zinc-900">
{form?.mode === "edit" && form.record.id === record.id ? (
{form?.mode === "edit" && form.entity.id === record.id ? (
<td colSpan={5}>
<RecordForm
initial={record}
busy={update.isPending}
error={update.error}
onSubmit={onSubmit}
onCancel={() => setForm(null)}
onCancel={closeForm}
/>
</td>
) : (
@@ -221,7 +195,7 @@ export default function RecordsTab() {
<td className="py-2 text-right whitespace-nowrap">
<button
type="button"
onClick={() => openForm({ mode: "edit", record })}
onClick={() => openForm({ mode: "edit", entity: record })}
className={rowButtonClass}
>
Edit
+28 -54
View File
@@ -1,5 +1,5 @@
import { useId, useState, type FormEvent } from "react";
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { useSuspenseQuery } from "@tanstack/react-query";
import {
forwardZoneCreateMutation,
forwardZoneDeleteMutation,
@@ -8,17 +8,15 @@ import {
} from "@/lib/queries";
import type { ForwardZone, ForwardZoneInput } from "@/lib/types";
import InlineError from "@/lib/InlineError";
const inputClass =
"mt-1 w-full rounded border border-zinc-300 bg-white px-3 py-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700 dark:bg-zinc-900";
const primaryButtonClass =
"rounded bg-blue-600 px-3 py-2 font-medium text-white disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600";
const secondaryButtonClass =
"rounded border border-zinc-300 px-3 py-2 font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700";
const rowButtonClass =
"rounded px-2 py-1 text-sm text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:text-blue-400";
type FormState = { mode: "create" } | { mode: "edit"; zone: ForwardZone };
import { useCrudForm } from "@/ui/useCrudForm";
import {
formCardClass,
inputClass,
largeButtonClass,
largePrimaryButtonClass,
rowButtonClass,
tableWrapClass,
} from "@/ui/classes";
function ZoneForm({
initial,
@@ -43,10 +41,7 @@ function ZoneForm({
}
return (
<form
onSubmit={submit}
className="mt-4 max-w-lg space-y-3 rounded border border-zinc-200 p-4 dark:border-zinc-800"
>
<form onSubmit={submit} className={formCardClass}>
<h3 className="font-medium">{initial === undefined ? "New forward zone" : `Edit ${initial.zone}`}</h3>
<div>
<label htmlFor={`${id}-zone`} className="block text-sm font-medium">
@@ -75,10 +70,10 @@ function ZoneForm({
/>
</div>
<div className="flex gap-2">
<button type="submit" disabled={busy} className={primaryButtonClass}>
<button type="submit" disabled={busy} className={largePrimaryButtonClass}>
{busy ? "Saving…" : "Save"}
</button>
<button type="button" onClick={onCancel} className={secondaryButtonClass}>
<button type="button" onClick={onCancel} className={largeButtonClass}>
Cancel
</button>
</div>
@@ -89,31 +84,15 @@ function ZoneForm({
export default function ZonesTab() {
const zones = useSuspenseQuery(forwardZonesQuery()).data;
const queryClient = useQueryClient();
const create = useMutation(forwardZoneCreateMutation(queryClient));
const update = useMutation(forwardZoneUpdateMutation(queryClient));
const remove = useMutation(forwardZoneDeleteMutation(queryClient));
const [form, setForm] = useState<FormState | null>(null);
function openForm(next: FormState) {
create.reset();
update.reset();
setForm(next);
}
function onSubmit(input: ForwardZoneInput) {
if (form === null) return;
if (form.mode === "create") {
create.mutate(input, { onSuccess: () => setForm(null) });
} else {
update.mutate({ id: form.zone.id, input }, { onSuccess: () => setForm(null) });
}
}
function onDelete(zone: ForwardZone) {
if (!window.confirm(`Delete forward zone "${zone.zone}"?`)) return;
remove.mutate(zone.id);
}
const { create, update, remove, form, openForm, closeForm, onSubmit, onDelete } = useCrudForm<
ForwardZone,
ForwardZoneInput
>({
create: forwardZoneCreateMutation,
update: forwardZoneUpdateMutation,
remove: forwardZoneDeleteMutation,
confirmDelete: (zone) => `Delete forward zone "${zone.zone}"?`,
});
return (
<div>
@@ -121,20 +100,15 @@ export default function ZonesTab() {
<p className="text-sm text-zinc-500">
Names under these zones go to their own resolver. Changes apply live.
</p>
<button type="button" onClick={() => openForm({ mode: "create" })} className={primaryButtonClass}>
<button type="button" onClick={() => openForm({ mode: "create" })} className={largePrimaryButtonClass}>
Add zone
</button>
</div>
<InlineError error={remove.error} />
{form?.mode === "create" && (
<ZoneForm
busy={create.isPending}
error={create.error}
onSubmit={onSubmit}
onCancel={() => setForm(null)}
/>
<ZoneForm busy={create.isPending} error={create.error} onSubmit={onSubmit} onCancel={closeForm} />
)}
<div className="mt-4 overflow-x-auto">
<div className={tableWrapClass}>
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-zinc-200 text-zinc-500 dark:border-zinc-800">
@@ -159,14 +133,14 @@ export default function ZonesTab() {
)}
{zones.map((zone) => (
<tr key={zone.id} className="border-b border-zinc-100 dark:border-zinc-900">
{form?.mode === "edit" && form.zone.id === zone.id ? (
{form?.mode === "edit" && form.entity.id === zone.id ? (
<td colSpan={3}>
<ZoneForm
initial={zone}
busy={update.isPending}
error={update.error}
onSubmit={onSubmit}
onCancel={() => setForm(null)}
onCancel={closeForm}
/>
</td>
) : (
@@ -176,7 +150,7 @@ export default function ZonesTab() {
<td className="py-2 text-right whitespace-nowrap">
<button
type="button"
onClick={() => openForm({ mode: "edit", zone })}
onClick={() => openForm({ mode: "edit", entity: zone })}
className={rowButtonClass}
>
Edit