mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Eliminate remaining frontend template drift
This commit is contained in:
+15
-12
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user