From f7e61446bdd8cc867b0d0c5af02400dda79164d1 Mon Sep 17 00:00:00 2001
From: rcourtman <8825017+rcourtman@users.noreply.github.com>
Date: Mon, 5 Oct 2026 23:01:46 +0100
Subject: [PATCH] Move three forms onto shared controls and draw the select
arrow
The shared template audit listed Settings, Alerts and Discovery forms
still building their own controls. The Thresholds CPU averaging selects
and the Discovery availability proposal now compose FormSelect (the
proposal's name field takes the shared field classes beside them), and
an availability check's request body composes FormTextarea, which links
its help text and, with sm:min-h-24, finally gets the height its class
asked for on desktop.
FormSelect's default chrome set appearance-none and drew no arrow, so
every default select in Settings read as a text field. A
form-select-arrow utility draws the caret in the padding the class
already reserved, and hands the native arrow back in forced-colors mode.
Two registry entries were wrong: AlertEscalationSection has used
checkboxes instead of a select since its destinations became
per-escalation, and the inline detail row rule matched a test file;
test files are now excluded from it.
---
.../v6/internal/subsystems/alerts.md | 3 +-
.../v6/internal/subsystems/api-contracts.md | 4 ++
.../subsystems/frontend-primitives.md | 6 ++
.../internal/subsystems/unified-resources.md | 4 +-
.../07daa1c32e41a709.json | 48 ++++++++++++++
.../scripts/shared-template-registry.json | 6 +-
.../src/__tests__/App.architecture.test.ts | 12 ++++
.../Discovery/AvailabilityProposalCard.tsx | 62 ++++++++-----------
.../AvailabilityTargetSlot.tsx | 23 ++++---
.../__tests__/AvailabilityTargetSlot.test.tsx | 4 ++
frontend-modern/src/components/shared/Form.ts | 4 +-
.../src/components/shared/FormSelect.test.tsx | 2 +
.../SharedPrimitives.guardrails.test.ts | 8 +--
.../alerts/__tests__/ThresholdsTab.test.tsx | 6 ++
.../features/alerts/tabs/ThresholdsTab.tsx | 48 +++++++-------
frontend-modern/src/index.css | 22 +++++++
16 files changed, 177 insertions(+), 85 deletions(-)
create mode 100644 frontend-modern/browser-verification/07daa1c32e41a709.json
diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md
index d5f84143cd..cf47b6f445 100644
--- a/docs/release-control/v6/internal/subsystems/alerts.md
+++ b/docs/release-control/v6/internal/subsystems/alerts.md
@@ -2928,7 +2928,8 @@ hours colour the dashboard and never alert. A group switched off as a whole
shows its Disabled badge and no defaults line. The CPU averaging line counts
every configured profile override, including ones only the API sets. CPU averaging is tuning, not the page's
answer. It is one collapsed disclosure stating the effective window and the
-number of platform overrides, with the same per-profile selects inside. Docker
+number of platform overrides, with the same per-profile selects inside, each
+a compact shared `FormSelect` with its profile name as a visible label. Docker
and Machines list their resource groups before Docker-only rules and the
SMART card. Threshold values, inheritance, metric evaluation windows and
persistence are unchanged.
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index 12452c53b1..d2d49ec6b7 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -1822,6 +1822,10 @@ payload shape change when the portal presents compact client rows.
arbitrary remote content. Contract tests must pin redaction, unchanged-
secret preservation, bounded evaluation, and the reachable-but-incorrect
application case.
+ The settings client edits the request body in the shared `FormTextarea`,
+ whose size note (`Up to 8 KiB`) is the field's accessible description;
+ removing a stored body stays an explicit action beside it, and leaving the
+ field blank keeps the stored body as before.
Availability target protocol vocabulary is canonicalized at the API/config
boundary: clients may submit `ping` as a user-facing alias for the ICMP
check, but saved targets, API reads, probe status, connections rows, and
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 38a433deaf..9bfb47ec5c 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -2464,6 +2464,12 @@ not a replacement status card, CTA band, or page-local nested card.
apply controlled `value` props after options are mounted so settings panels
such as Discovery show the persisted option instead of falling back
to the first option while the collapsed summary shows a different value.
+ Its default select chrome (`formSelect` / `formSelectCompact` in
+ `Form.ts`) sets `appearance-none` to match the text inputs, so it also
+ carries the `form-select-arrow` utility, which draws the dropdown caret in
+ the reserved right padding (and hands the native arrow back in forced-colors
+ mode); without it every default `FormSelect` read as a text field. Callers
+ that pass their own `selectBaseClass` keep their own arrow.
The Assistant runtime controls in
`frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx` — e.g. the
service context scan `Toggle` — are settings-shell chrome bound to the canonical
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index 7269935c42..21b335013f 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -2907,7 +2907,9 @@ Discovery-led availability proposals present their evidence and canonical
attachment boundary as short, complete sentences in both the inline card and
machine-wide review. The colocated component test pins that operator-facing
copy so the shared copy-style audit cannot regress it back to semicolon-joined
-instructions.
+instructions. The card's editable check name, interval and observation
+location use the shared form label and control chrome (`FormSelect` for the
+two choices), so they match the availability settings form they hand off to.
### Provider workflows hydrate only their active route inventory
diff --git a/frontend-modern/browser-verification/07daa1c32e41a709.json b/frontend-modern/browser-verification/07daa1c32e41a709.json
new file mode 100644
index 0000000000..2d108625f2
--- /dev/null
+++ b/frontend-modern/browser-verification/07daa1c32e41a709.json
@@ -0,0 +1,48 @@
+{
+ "version": 1,
+ "base_sha": "979466013876ce54a67494b0c4cda9b404ea45e8",
+ "verified_at": "2026-10-05T22:01:46Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/components/Discovery/AvailabilityProposalCard.tsx",
+ "frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx",
+ "frontend-modern/src/components/shared/Form.ts",
+ "frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx",
+ "frontend-modern/src/index.css"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/components/Discovery/AvailabilityProposalCard.tsx": "bfa50b13f521b9a91b57495eec3a827a71a413a0eed0832be0d58444e8ca3ee7",
+ "frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx": "af7a1d322045c33747600466a580ae6148f752c8d847404a6f5b04fa49fd53e7",
+ "frontend-modern/src/components/shared/Form.ts": "e47d06ebbbba68ac0d19769acac83a0ca930b1e2d5a7258c7c1fcf43e82ce44a",
+ "frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx": "8142c12c2e6930a505e1f063aa8a396bfa45c6c55a8ea7c7d1085198a9f3915f",
+ "frontend-modern/src/index.css": "b8ae1a40f0915ff034e51a16a94e0c2159ac726ef0afdfdd8fb321f5d5f7dc5e"
+ },
+ "routes": [
+ "/alerts/thresholds",
+ "/settings/monitoring/availability",
+ "/proxmox/overview"
+ ],
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 1100
+ },
+ {
+ "width": 390,
+ "height": 1100
+ }
+ ],
+ "states": [
+ "Alerts > Thresholds > CPU averaging opened: all 10 per-platform selects render through FormSelect with their visible label tied by for/id, values intact (5-minute average, Inherit), 32px desktop / 44px phone, and a dropdown caret; main showed native selects",
+ "Settings > Availability checks > Add service/device check with an HTTP POST probe: Request body (optional) is a FormTextarea with its label tied and the 8 KiB help linked by aria-describedby, 96px tall on desktop and phone (main: 54px on desktop because formControl's sm:min-h-9 overrode min-h-24; no help link)",
+ "Existing default FormSelects in that dialog (Preset, Target type, Probe) now show a caret; on main they had appearance-none and no arrow, reading as text fields",
+ "Proxmox guest drawer Discovery tab with discovery_enabled and an availability proposal served by route interception: Check name input and the Interval / Observation location FormSelects share the shared form label and control chrome at matched heights, with carets",
+ "Dark mode at 1440: caret drawn in the muted text colour and readable; forced-colors emulation: select falls back to appearance auto with no background image",
+ "Shared template audit: only the three findings in BackupTransferDialogs.tsx and AlertDeadManDestinationSection.tsx remain (held by another session's open bg-base change)",
+ "Re-checked after rebasing onto 9794660138 (#2535, shared toggle): CPU averaging selects unchanged, labelled, with the caret"
+ ],
+ "interactions": [
+ "Opened the CPU averaging disclosure, the availability check dialog (HTTP, POST) and a guest Discovery tab, on main (reverse-applied patch) and on the change",
+ "Emulated forced-colors: active"
+ ]
+}
diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json
index 04ad2b7184..68cc8cdb44 100644
--- a/frontend-modern/scripts/shared-template-registry.json
+++ b/frontend-modern/scripts/shared-template-registry.json
@@ -1709,7 +1709,6 @@
{ "path": "src/components/shared/FilterToolbar.tsx" },
{ "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" },
{ "path": "src/features/alerts/AlertDeliveryRoutingSection.tsx" },
- { "path": "src/features/alerts/AlertEscalationSection.tsx" },
{ "path": "src/features/alerts/AlertQuietHoursSection.tsx" },
{ "path": "src/features/patrol/PatrolAttentionWorkbench.tsx" }
],
@@ -6664,12 +6663,11 @@
"src/features/vmware"
],
"extensions": [".tsx"],
+ "pathExcludes": [".test.", "__tests__"],
"triggerPatterns": ["data-inline", "TableRow"],
"requiredPatterns": ["InlineDetailTableRow"],
"allowedPaths": [],
- "ignoredPaths": [
- "src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx"
- ],
+ "ignoredPaths": [],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"scripts/shared-template-audit.mjs"
diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts
index e9d9cc865b..e02a726334 100644
--- a/frontend-modern/src/__tests__/App.architecture.test.ts
+++ b/frontend-modern/src/__tests__/App.architecture.test.ts
@@ -304,6 +304,18 @@ describe('App architecture', () => {
);
});
+ it('draws the shared select arrow that appearance-none removes', () => {
+ // FormSelect's default chrome sets appearance-none to match text inputs;
+ // without this utility every default select read as a text field.
+ expect(appStylesSource).toMatch(
+ /@utility form-select-arrow \{\s*background-image:\s*linear-gradient\(45deg, transparent 50%, var\(--color-text-muted\) 50%\)/,
+ );
+ // Forced-colors mode drops background images, so the native arrow returns.
+ expect(appStylesSource).toMatch(
+ /@media \(forced-colors: active\) \{\s*appearance: auto !important;\s*background-image: none;/,
+ );
+ });
+
it('keeps compact phone settings spacing scoped to the settings content shell', () => {
expect(appStylesSource).toContain('@media (max-width: 39.999rem)');
expect(appStylesSource).toContain(
diff --git a/frontend-modern/src/components/Discovery/AvailabilityProposalCard.tsx b/frontend-modern/src/components/Discovery/AvailabilityProposalCard.tsx
index 74c325d42a..4f5d36c575 100644
--- a/frontend-modern/src/components/Discovery/AvailabilityProposalCard.tsx
+++ b/frontend-modern/src/components/Discovery/AvailabilityProposalCard.tsx
@@ -22,6 +22,8 @@ import {
} from '@/api/discovery';
import { ActionIconButton, Button } from '@/components/shared/Button';
import { Dialog } from '@/components/shared/Dialog';
+import { formControl, formField, formLabel } from '@/components/shared/Form';
+import { FormSelect } from '@/components/shared/FormSelect';
import { LoadingSpinner } from '@/components/shared/LoadingSpinner';
import type { DiscoverySummary, ResourceDiscovery, ResourceType } from '@/types/discovery';
import {
@@ -298,47 +300,35 @@ export const AvailabilityProposalCard: Component