Eliminate remaining frontend template drift

This commit is contained in:
rcourtman
2026-08-24 21:52:47 +01:00
parent a3d7a90b1b
commit 0a58e77bc6
10 changed files with 142 additions and 174 deletions
@@ -146,14 +146,18 @@ were not artificially inferred from healthy pixels.
excludes tests from runtime chevron/native-control rules, and retains the
canonical-platform lint gate.
### P3 — unrelated pre-existing shared-template findings
### P3 — remaining shared-template findings resolved in follow-up
The complete shared-template audit still reports four consumers outside this
platform canonicalization slice: `ApprovalBanner`/`LoadingSpinner`,
`UpdatesSettingsPanel`/`FormSelect`, `RelayPairingSection`/`ExternalTextLink`,
and `PatrolAttentionWorkbench`/`FormSelect`. None is rendered by or newly
introduced in the audited platform inventory/drawer contract. They remain
explicit exceptions rather than being hidden by this slice.
The follow-up complete shared-template audit now passes without exceptions.
Two reported consumers were inaccurate ownership declarations:
`ApprovalBanner` has no loading state and `UpdatesSettingsPanel` has no native
select, so those paths were removed from the respective required-consumer
inventories. The two real findings were migrated: `RelayPairingSection` now
composes `ExternalTextLink`, while the Patrol suppression reason and duration
compose `FormTextarea` and `FormSelect`. A Patrol textarea guard now protects
that additional form-control boundary and identified the same latent fork in
`PatrolObjectivesPanel`; both objective textareas now compose `FormTextarea` as
well.
## Decisions for previously ambiguous contracts
@@ -203,8 +207,7 @@ explicit exceptions rather than being hidden by this slice.
## Remaining exceptions
There are no known platform-page exceptions to the canonical table or summary
row interaction contracts after this slice. The four unrelated static-audit
findings listed above remain outside this task. Route-specific cells, metrics,
drawer sections, and technical details are intentional object content inside
the shared contract, not forks.
There are no known static-audit or platform-page exceptions to the canonical
table, summary-row, external-link, or labelled native-control contracts after
the follow-up. Route-specific cells, metrics, drawer sections, and technical
details are intentional object content inside the shared contract, not forks.
@@ -2832,6 +2832,11 @@ for desktop and mobile accessible navigation counts. The label remains
buttons, visible focus, selected-state semantics, focus restoration, narrow
viewport ordering, and reduced-motion-safe behavior. Detail deep links use
`frontend-modern/src/routing/resourceLinks.ts` and remain stable across reload.
The temporary-suppression reason and duration are labelled native controls
owned by `FormTextarea` and `FormSelect`; Patrol must not recreate their label,
focus, responsive touch-target, or controlled-value shells locally.
The objective brief and optional-context fields in `PatrolObjectivesPanel`
share the same `FormTextarea` ownership contract.
The default queue shows severity, lifecycle state, plain-language consequence,
resource, the required review posture, and observation age. It sorts severity
+23 -85
View File
@@ -1,76 +1,23 @@
{
"version": 1,
"base_sha": "164e03ad3c77a50574df782409ee7a417efe8955",
"verified_at": "2026-08-24T17:44:49Z",
"base_sha": "a3d7a90b1bfb14e60432a2a23b0cbd73d6911101",
"verified_at": "2026-08-24T20:51:46Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Docker/SwarmServicesDrawer.tsx",
"frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx",
"frontend-modern/src/components/Kubernetes/K8sDeploymentsDrawer.tsx",
"frontend-modern/src/components/Kubernetes/K8sNamespacesDrawer.tsx",
"frontend-modern/src/components/shared/Form.ts",
"frontend-modern/src/components/shared/FormSelect.tsx",
"frontend-modern/src/components/shared/FormTextarea.tsx",
"frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx",
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx",
"frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx",
"frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx",
"frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx",
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx",
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx"
"frontend-modern/src/components/Settings/RelayPairingSection.tsx",
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx",
"frontend-modern/src/features/patrol/PatrolObjectivesPanel.tsx",
"frontend-modern/src/utils/relayPresentation.ts"
],
"content_sha256": {
"frontend-modern/src/components/Docker/SwarmServicesDrawer.tsx": "cfe0382ab9ac0b73ac3d1c0a5c7fdd61e43d1851164cf38071b0d8d99a48fb7e",
"frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx": "80334e63a0a5560063cf8e3b26e0fd5316989f146f6f014242193c66c51d20a0",
"frontend-modern/src/components/Kubernetes/K8sDeploymentsDrawer.tsx": "1806e0ca765a2fe73f456760f1e4a6506befcbba9e56bbd5249c7ee685b4b225",
"frontend-modern/src/components/Kubernetes/K8sNamespacesDrawer.tsx": "d5d09d77eb54ba91e5e2539c31b5104473ecc73a7e891c50cca4e662975cd294",
"frontend-modern/src/components/shared/Form.ts": "0186bf1fe8d10378d8d603dec8f6b9796302b418e02444a8d18044692815c2b5",
"frontend-modern/src/components/shared/FormSelect.tsx": "38b64e18d93bf7f374e18e29c2f633028c52c4e8b1207a63578f22e0e65c3ec2",
"frontend-modern/src/components/shared/FormTextarea.tsx": "cccbd6e027aabf61eddebab7c9d153737e3b607f64568751cd03c7c138b483f6",
"frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx": "3c07555f296ab58dec752bd2ce8f8ac058257d0f1efd4d3823c1b5220525fd1b",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "97a23102f8aff06e4e271248244d9bc63d9fd61ef8d6afe7705353189a153671",
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "b9a2da5b1191687ffcbbb1ebf95a766c303fbe5717dc30a43f80908481d6b02e",
"frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx": "aa8df0cef4a602ab81b8d53251d3c1f7b9b4334f18a2caaa0ba71c70f079c73c",
"frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx": "663ffc8917075275617d1b0ca40e5739429553a855c0e130aa162b2421dca89b",
"frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx": "276ee693542e06b116cda0b8b6b9771fba1e416bc11f3645a4d265883d6353f2",
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx": "b9119d1611e87d8115c4f8e378b169b0db5750a077fc433f57969877bd6d4b8e",
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx": "f9e3185ca23b59bdfc7f77a2738c2cd9e7d5d768ad10846b5c9b3680750bc17a"
"frontend-modern/src/components/Settings/RelayPairingSection.tsx": "85c907c138a6d8693cff5c596236ea09b6188242a7454c42ce341ae02c59371a",
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "280ee62dc69a1d165da6dcb6db1dff2716c4905d43be8fa50ec5aa9628a4934d",
"frontend-modern/src/features/patrol/PatrolObjectivesPanel.tsx": "f5512db67c12c8a353680eb11af98d61493d942e24c2822d65d7e57a303cdf6c",
"frontend-modern/src/utils/relayPresentation.ts": "930d00c8191a34d6ce6b50197b1dbe94856056c8abeb1de91db312b6cd8966d7"
},
"routes": [
"/proxmox/overview",
"/proxmox/storage",
"/proxmox/replication",
"/proxmox/backups/date",
"/proxmox/backups/coverage",
"/proxmox/ceph",
"/proxmox/mail",
"/docker/overview",
"/docker/images",
"/docker/storage",
"/docker/networks",
"/docker/swarm",
"/kubernetes/overview",
"/kubernetes/nodes",
"/kubernetes/workloads",
"/kubernetes/services",
"/kubernetes/storage",
"/kubernetes/configuration",
"/kubernetes/events",
"/truenas/overview",
"/truenas/storage",
"/truenas/services",
"/truenas/apps",
"/truenas/vms",
"/truenas/shares",
"/truenas/protection",
"/vmware/overview",
"/vmware/storage",
"/vmware/networks",
"/vmware/health",
"/vmware/activity",
"/machines",
"/availability"
"/patrol",
"/settings/system-relay"
],
"viewports": [
{
@@ -83,27 +30,18 @@
}
],
"states": [
"default populated inventories for every Proxmox, Docker, Kubernetes, TrueNAS, vSphere, Machines, and availability route",
"live loading state while platform resources connected",
"healthy, attention, alert, stale, and disconnected resource presentations",
"grouped and flat inventory views where available",
"expanded Proxmox backup server, backup coverage evidence, Ceph cluster, and Mail Gateway rows",
"node, guest, storage, host, cluster, system, machine, and availability drawers",
"drawer Overview, Manage, Discovery when enabled, and History with range controls",
"cardless Docker Swarm, Kubernetes namespace and deployment, Ceph, Mail Gateway, and restore-evidence tables",
"rebased Proxmox Storage table with the newer mainline plain-header contract"
"populated Patrol decision inbox with critical and warning attention items",
"Patrol Protection view with the add-objective dialog open",
"Patrol attention detail with the temporary-suppression form expanded",
"Relay settings capability-gated state for the current runtime"
],
"interactions": [
"activated legacy Proxmox rows by whole-row pointer click and confirmed aria-expanded and aria-controls",
"activated Mail Gateway detail with Enter and Space and confirmed both keyboard paths toggle the same row",
"confirmed embedded controls do not trigger the owning summary row",
"confirmed desktop disclosure controls remain available and phone disclosure buttons have no visible footprint",
"opened backup coverage restore evidence and confirmed the nested canonical table header and body",
"opened representative platform drawers, switched to History, and inspected chart range placement",
"inspected Manage controls using the compact shared select and textarea presentation",
"confirmed no document-level horizontal overflow at 1280 by 800 or 390 by 844",
"independently inspected the fully rendered post-change backup coverage route with Computer Use in Safari",
"rechecked the rebased Storage route at exactly 1280 by 800 and backup coverage at exactly 390 by 844",
"confirmed phone disclosure controls remain accessibility-exposed while their clipped visual footprint leaves the whole row as the activation affordance"
"opened the Patrol Protection tab and Add objective dialog at 1280 by 800",
"confirmed both objective textareas have generated ids and explicit label associations",
"opened an Inbox decision and expanded temporary suppression at 390 by 844",
"confirmed the shared suppression textarea and select preserve 80 pixel and 44 pixel phone targets",
"confirmed no document-level horizontal overflow at either exact viewport",
"inspected the Relay route without mutating its capability-gated live configuration",
"independently inspected the populated Patrol interface and accessibility hierarchy with Computer Use in Safari"
]
}
@@ -822,7 +822,6 @@
{ "path": "src/components/shared/DiscoveryLoadingFallback.tsx" },
{ "path": "src/components/shared/HistoryChartOverlay.tsx" },
{ "path": "src/components/shared/PulseDataGrid.tsx" },
{ "path": "src/components/patrol/ApprovalBanner.tsx" },
{ "path": "src/components/patrol/ApprovalSection.tsx" },
{ "path": "src/components/patrol/InvestigationMessages.tsx" },
{ "path": "src/components/patrol/InvestigationSection.tsx" },
@@ -1701,7 +1700,6 @@
{ "path": "src/components/Settings/RecoverySettingsPanel.tsx" },
{ "path": "src/components/Settings/RolesEditorDialog.tsx" },
{ "path": "src/components/Settings/SystemLogsPanel.tsx" },
{ "path": "src/components/Settings/UpdatesSettingsPanel.tsx" },
{ "path": "src/components/Storage/DiskDetail.tsx" },
{ "path": "src/components/Storage/StoragePageControls.tsx" },
{ "path": "src/components/Storage/StoragePoolDetail.tsx" },
@@ -1711,7 +1709,8 @@
{ "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/alerts/AlertQuietHoursSection.tsx" },
{ "path": "src/features/patrol/PatrolAttentionWorkbench.tsx" }
],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
@@ -1736,7 +1735,9 @@
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
{ "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx" },
{ "path": "src/components/Settings/SuggestProfileModal.tsx" },
{ "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" }
{ "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" },
{ "path": "src/features/patrol/PatrolAttentionWorkbench.tsx" },
{ "path": "src/features/patrol/PatrolObjectivesPanel.tsx" }
],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
@@ -2009,7 +2010,6 @@
"requiredConsumers": [
{ "path": "src/components/shared/OrganizationBadges.tsx" },
{ "path": "src/components/AI/FindingsPanel.tsx" },
{ "path": "src/components/patrol/ApprovalBanner.tsx" },
{ "path": "src/components/patrol/ApprovalSection.tsx" },
{ "path": "src/components/patrol/InvestigationSection.tsx" },
{ "path": "src/components/patrol/RunHistoryEntry.tsx" },
@@ -5359,6 +5359,26 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "form-textarea-local-patrol-fields",
"category": "form-control",
"summary": "Patrol decision forms must compose FormTextarea instead of recreating labelled native textarea wrappers locally.",
"canonical": {
"path": "src/components/shared/FormTextarea.tsx",
"export": "FormTextarea"
},
"scopes": ["src/features/patrol"],
"extensions": [".tsx"],
"allPatterns": ["<textarea"],
"legacyReason": "Retired migration debt. Patrol decision fields use FormTextarea for shared label, focus, responsive sizing, and controlled-value behavior.",
"allowedPaths": [],
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "toggle-local-role-switch",
"category": "toggle-control",
@@ -1,6 +1,7 @@
import { Component, For, Show } from 'solid-js';
import type { OnboardingQRResponse } from '@/api/onboarding';
import { Card } from '@/components/shared/Card';
import { ExternalTextLink } from '@/components/shared/ExternalTextLink';
import { formField, formHelpText, labelClass } from '@/components/shared/Form';
import { PULSE_PRO_DOWNLOAD_URL } from '@/utils/licensePresentation';
import {
@@ -9,7 +10,6 @@ import {
RELAY_DIAGNOSTICS_TITLE_CLASS,
RELAY_DIAGNOSTICS_WRAP_CLASS,
RELAY_PAIRING_APP_AVAILABILITY_TEXT,
RELAY_PAIRING_APP_DOWNLOAD_LINK_CLASS,
RELAY_PAIRING_APP_DOWNLOAD_LINK_LABEL,
RELAY_PRIMARY_BUTTON_CLASS,
RELAY_QR_IMAGE_CLASS,
@@ -68,14 +68,12 @@ export const RelayPairingSection: Component<RelayPairingSectionProps> = (props)
<p class={formHelpText}>
Generate a QR code to securely link the Pulse Mobile app on your phone to this Pulse
instance. {RELAY_PAIRING_APP_AVAILABILITY_TEXT}{' '}
<a
<ExternalTextLink
href={PULSE_PRO_DOWNLOAD_URL}
target="_blank"
rel="noreferrer"
class={RELAY_PAIRING_APP_DOWNLOAD_LINK_CLASS}
class="font-medium"
>
{RELAY_PAIRING_APP_DOWNLOAD_LINK_LABEL}
</a>
</ExternalTextLink>
.
</p>
@@ -59,5 +59,7 @@ describe('Onboarding QR payload contract', () => {
it('points pairing users at the download page for the Pulse Mobile app', () => {
expect(relayPairingSectionSource).toContain('PULSE_PRO_DOWNLOAD_URL');
expect(relayPairingSectionSource).toContain('RELAY_PAIRING_APP_AVAILABILITY_TEXT');
expect(relayPairingSectionSource).toContain('ExternalTextLink');
expect(relayPairingSectionSource).not.toContain('<a');
});
});
@@ -38,6 +38,8 @@ import filterToolbarSource from '@/components/shared/FilterToolbar.tsx?raw';
import filterOptionPresentationSource from '@/components/shared/filterOptionPresentation.ts?raw';
import formSelectSource from '@/components/shared/FormSelect.tsx?raw';
import formTextareaSource from '@/components/shared/FormTextarea.tsx?raw';
import patrolAttentionWorkbenchSource from '@/features/patrol/PatrolAttentionWorkbench.tsx?raw';
import patrolObjectivesPanelSource from '@/features/patrol/PatrolObjectivesPanel.tsx?raw';
import helpIconSource from '@/components/shared/HelpIcon.tsx?raw';
import helpIconModelSource from '@/components/shared/helpIconModel.ts?raw';
import historyChartHeaderSource from '@/components/shared/HistoryChartHeader.tsx?raw';
@@ -686,7 +688,6 @@ describe('shared primitive guardrails', () => {
'src/components/Settings/RecoverySettingsPanel.tsx',
'src/components/Settings/RolesEditorDialog.tsx',
'src/components/Settings/SystemLogsPanel.tsx',
'src/components/Settings/UpdatesSettingsPanel.tsx',
'src/components/Storage/DiskDetail.tsx',
'src/components/Storage/StoragePageControls.tsx',
'src/components/Storage/StoragePoolDetail.tsx',
@@ -697,6 +698,7 @@ describe('shared primitive guardrails', () => {
'src/features/alerts/AlertDeliveryRoutingSection.tsx',
'src/features/alerts/AlertEscalationSection.tsx',
'src/features/alerts/AlertQuietHoursSection.tsx',
'src/features/patrol/PatrolAttentionWorkbench.tsx',
]);
expect(registeredGuard?.canonical?.path).toBe('src/components/shared/FormSelect.tsx');
expect(registeredGuard?.canonical?.export).toBe('FormSelect');
@@ -774,6 +776,9 @@ describe('shared primitive guardrails', () => {
const infrastructureGuard = registry.patternGuards?.find(
(guard) => guard.id === 'form-textarea-local-infrastructure-fields',
);
const patrolGuard = registry.patternGuards?.find(
(guard) => guard.id === 'form-textarea-local-patrol-fields',
);
expect(registeredRule?.canonical?.path).toBe('src/components/shared/FormTextarea.tsx');
expect(registeredRule?.canonical?.export).toBe('FormTextarea');
@@ -787,6 +792,8 @@ describe('shared primitive guardrails', () => {
'src/components/Settings/SelfHostedCommercialRecoverySection.tsx',
'src/components/Settings/SuggestProfileModal.tsx',
'src/features/alerts/AlertAppriseDestinationsSection.tsx',
'src/features/patrol/PatrolAttentionWorkbench.tsx',
'src/features/patrol/PatrolObjectivesPanel.tsx',
]);
expect(alertGuard?.canonical?.path).toBe('src/components/shared/FormTextarea.tsx');
expect(alertGuard?.canonical?.export).toBe('FormTextarea');
@@ -803,6 +810,11 @@ describe('shared primitive guardrails', () => {
expect(infrastructureGuard?.allPatterns).toEqual(['<textarea']);
expect(infrastructureGuard?.scopes).toEqual(['src/components/Infrastructure']);
expect(infrastructureGuard?.allowedPaths ?? []).toHaveLength(0);
expect(patrolGuard?.canonical?.path).toBe('src/components/shared/FormTextarea.tsx');
expect(patrolGuard?.canonical?.export).toBe('FormTextarea');
expect(patrolGuard?.allPatterns).toEqual(['<textarea']);
expect(patrolGuard?.scopes).toEqual(['src/features/patrol']);
expect(patrolGuard?.allowedPaths ?? []).toHaveLength(0);
expect(formTextareaSource).toContain("from '@/components/shared/Form'");
expect(formTextareaSource).toContain('createUniqueId');
@@ -834,6 +846,8 @@ describe('shared primitive guardrails', () => {
suggestProfileModalSource,
ssoProvidersPanelSource,
alertAppriseDestinationsSectionSource,
patrolAttentionWorkbenchSource,
patrolObjectivesPanelSource,
];
for (const source of migratedConsumers) {
expect(source).toContain('FormTextarea');
@@ -2511,7 +2525,6 @@ describe('shared primitive guardrails', () => {
expect(metadataRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
'src/components/shared/OrganizationBadges.tsx',
'src/components/AI/FindingsPanel.tsx',
'src/components/patrol/ApprovalBanner.tsx',
'src/components/patrol/ApprovalSection.tsx',
'src/components/patrol/InvestigationSection.tsx',
'src/components/patrol/RunHistoryEntry.tsx',
@@ -2798,7 +2811,6 @@ describe('shared primitive guardrails', () => {
'src/components/shared/DiscoveryLoadingFallback.tsx',
'src/components/shared/HistoryChartOverlay.tsx',
'src/components/shared/PulseDataGrid.tsx',
'src/components/patrol/ApprovalBanner.tsx',
'src/components/patrol/ApprovalSection.tsx',
'src/components/patrol/InvestigationMessages.tsx',
'src/components/patrol/InvestigationSection.tsx',
@@ -37,6 +37,8 @@ import {
} from '@/api/patrolAttention';
import { ResourceActionsAPI } from '@/api/resourceActions';
import { Button, ButtonLink, CopyValueButton } from '@/components/shared/Button';
import { FormSelect } from '@/components/shared/FormSelect';
import { FormTextarea } from '@/components/shared/FormTextarea';
import { LoadingSpinner } from '@/components/shared/LoadingSpinner';
import { MetadataBadge, type MetadataBadgeTone } from '@/components/shared/MetadataBadge';
import { ActionReviewDialog } from '@/features/actions/ActionReviewDialog';
@@ -1346,42 +1348,33 @@ function AttentionLifecycleControls(props: {
class="mt-3 space-y-3 border-t border-border-subtle pt-3"
onSubmit={submitSuppression}
>
<div>
<label
for={`attention-suppression-reason-${props.detail.item.id}`}
class="text-xs font-medium text-base-content"
>
Why is this safe to hide from active attention?
</label>
<textarea
id={`attention-suppression-reason-${props.detail.item.id}`}
class="mt-1 min-h-20 w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500"
value={reason()}
maxlength={240}
required
disabled={props.busy}
onInput={(event) => setReason(event.currentTarget.value)}
/>
</div>
<div>
<label
for={`attention-suppression-duration-${props.detail.item.id}`}
class="text-xs font-medium text-base-content"
>
Return it to active attention after
</label>
<select
id={`attention-suppression-duration-${props.detail.item.id}`}
class="mt-1 block min-h-11 rounded-md border border-border bg-surface px-3 py-1.5 text-sm text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500 sm:min-h-9"
value={String(durationMs())}
disabled={props.busy}
onChange={(event) => setDurationMs(Number(event.currentTarget.value))}
>
<For each={SUPPRESSION_DURATIONS}>
{(duration) => <option value={duration.value}>{duration.label}</option>}
</For>
</select>
</div>
<FormTextarea
id={`attention-suppression-reason-${props.detail.item.id}`}
label="Why is this safe to hide from active attention?"
density="compact"
fieldBaseClass="block"
labelClass="text-xs"
textareaClass="mt-1 min-h-20"
value={reason()}
maxlength={240}
required
disabled={props.busy}
onInput={(event) => setReason(event.currentTarget.value)}
/>
<FormSelect
id={`attention-suppression-duration-${props.detail.item.id}`}
label="Return it to active attention after"
fieldBaseClass="block"
labelClass="text-xs"
selectClass="mt-1 w-auto"
value={String(durationMs())}
disabled={props.busy}
onChange={(event) => setDurationMs(Number(event.currentTarget.value))}
>
<For each={SUPPRESSION_DURATIONS}>
{(duration) => <option value={duration.value}>{duration.label}</option>}
</For>
</FormSelect>
<div class="flex flex-wrap gap-2">
<Button
variant="warning"
@@ -7,6 +7,7 @@ import TrashIcon from 'lucide-solid/icons/trash-2';
import XIcon from 'lucide-solid/icons/x';
import { Button } from '@/components/shared/Button';
import { Dialog } from '@/components/shared/Dialog';
import { FormTextarea } from '@/components/shared/FormTextarea';
import { MetadataBadge } from '@/components/shared/MetadataBadge';
import { ResourcePicker, type SelectedResource } from '@/components/Settings/ResourcePicker';
import {
@@ -391,31 +392,29 @@ export const PatrolObjectivesPanel: Component = () => {
</Button>
</header>
<div class="space-y-5 overflow-y-auto px-5 py-4">
<label class="block">
<span class="text-sm font-medium text-base-content">
What should Patrol keep true?
</span>
<textarea
autofocus
rows="3"
maxlength="2048"
value={brief()}
onInput={(event) => setBrief(event.currentTarget.value)}
placeholder="Keep Jellyfin playback from buffering for users"
class="mt-2 w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-base-content outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30"
/>
</label>
<label class="block">
<span class="text-sm font-medium text-base-content">Useful context (optional)</span>
<textarea
rows="2"
maxlength="4096"
value={context()}
onInput={(event) => setContext(event.currentTarget.value)}
placeholder="For example: prefer local event evidence and avoid interrupting active playback"
class="mt-2 w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-base-content outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30"
/>
</label>
<FormTextarea
label="What should Patrol keep true?"
autofocus
rows="3"
maxlength="2048"
density="compact"
fieldBaseClass="block"
textareaClass="mt-2"
value={brief()}
onInput={(event) => setBrief(event.currentTarget.value)}
placeholder="Keep Jellyfin playback from buffering for users"
/>
<FormTextarea
label="Useful context (optional)"
rows="2"
maxlength="4096"
density="compact"
fieldBaseClass="block"
textareaClass="mt-2"
value={context()}
onInput={(event) => setContext(event.currentTarget.value)}
placeholder="For example: prefer local event evidence and avoid interrupting active playback"
/>
<div class="rounded-lg border border-border">
<button
type="button"
@@ -44,8 +44,6 @@ export const RELAY_ACTIVATION_REQUIRED_MESSAGE =
export const RELAY_PAIRING_APP_AVAILABILITY_TEXT =
'Pulse Mobile is in early access for iOS and Android. To get the install links, enter your activation key on';
export const RELAY_PAIRING_APP_DOWNLOAD_LINK_LABEL = 'your download page';
export const RELAY_PAIRING_APP_DOWNLOAD_LINK_CLASS =
'font-medium text-blue-600 underline-offset-2 hover:underline dark:text-blue-400';
export function getRelayDiagnosticClass(severity: 'warning' | 'error'): string {
return severity === 'error'