From 78a043db0802c4515ef7a5a4733cf86f6dfb82ec Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 2 Sep 2026 11:02:45 +0200 Subject: [PATCH] feat(web): shared SectionCardHeader with docs links in every settings section - SectionCardHeader replaces 33 hand-copied card headers across settings, rules and admin pages - DocsLink + lib/docsPaths: every tenant-facing section links its documentation page (new tab) - docsPaths guard test encodes the GitBook URL rule (group slug, not folder name) - delete dead MaintenanceSection duplicate Co-Authored-By: Claude Fable 5.1 --- .../components/AdminConfigSettingsSection.tsx | 18 +- .../admin/components/ConfigReseedSection.tsx | 18 +- .../admin/components/DeviceBlockSection.tsx | 18 +- .../components/DiagnosticsLogPathsSection.tsx | 20 +- .../admin/components/MaintenanceSection.tsx | 456 ------------------ .../components/MaintenanceTriggerSection.tsx | 18 +- .../admin/components/OpsAlertRulesSection.tsx | 39 +- .../app/admin/components/OpsEventsSection.tsx | 28 +- .../admin/components/SessionExportSection.tsx | 18 +- .../components/SessionReportsSection.tsx | 22 +- .../admin/components/VersionBlockSection.tsx | 18 +- .../components/VulnerabilityDataSection.tsx | 18 +- .../sections/SectionDelegatedAdmins.tsx | 20 +- .../settings/sections/SectionTenantGroups.tsx | 20 +- .../app/analyze-rules/page.tsx | 24 +- .../app/gather-rules/page.tsx | 25 +- .../components/AdminManagementSection.tsx | 20 +- .../components/AgentAnalyzersSection.tsx | 20 +- .../components/AgentSettingsSection.tsx | 38 +- .../components/AutopilotValidationSection.tsx | 28 +- .../components/BootstrapSessionsSection.tsx | 41 +- .../components/DataManagementSection.tsx | 20 +- .../components/DiagnosticsSection.tsx | 20 +- .../components/HardwareWhitelistSection.tsx | 25 +- .../settings/components/McpUsersSection.tsx | 21 +- .../components/NotificationsSection.tsx | 22 +- .../components/OffboardingSection.tsx | 20 +- .../components/UnrestrictedModeSection.tsx | 20 +- .../reporting/sections/SectionMcpUsage.tsx | 3 + .../tenant/sections/SectionContact.tsx | 20 +- .../settings/tenant/sections/SectionPlan.tsx | 20 +- .../tenant/sections/SectionSlaTargets.tsx | 24 +- .../components/DocsLink.tsx | 29 ++ .../components/SectionCardHeader.tsx | 189 ++++++++ .../__tests__/SectionCardHeader.test.ts | 75 +++ .../lib/__tests__/docsPaths.test.ts | 33 ++ .../autopilot-monitor-web/lib/docsPaths.ts | 47 ++ 37 files changed, 645 insertions(+), 870 deletions(-) delete mode 100644 src/Web/autopilot-monitor-web/app/admin/components/MaintenanceSection.tsx create mode 100644 src/Web/autopilot-monitor-web/components/DocsLink.tsx create mode 100644 src/Web/autopilot-monitor-web/components/SectionCardHeader.tsx create mode 100644 src/Web/autopilot-monitor-web/components/__tests__/SectionCardHeader.test.ts create mode 100644 src/Web/autopilot-monitor-web/lib/__tests__/docsPaths.test.ts create mode 100644 src/Web/autopilot-monitor-web/lib/docsPaths.ts diff --git a/src/Web/autopilot-monitor-web/app/admin/components/AdminConfigSettingsSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/AdminConfigSettingsSection.tsx index d9045c0e..6bfe365a 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/AdminConfigSettingsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/AdminConfigSettingsSection.tsx @@ -1,6 +1,7 @@ "use client"; import type { AdminConfiguration } from "@/types/adminConfig"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; interface AdminConfigSettingsSectionProps { loadingConfig: boolean; @@ -87,17 +88,12 @@ export function AdminConfigSettingsSection({ }: AdminConfigSettingsSectionProps) { return (
-
-
- - - -
-

Global Settings

-

Configure global settings for all tenants

-
-
-
+
{loadingConfig ? (
diff --git a/src/Web/autopilot-monitor-web/app/admin/components/ConfigReseedSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/ConfigReseedSection.tsx index b727f7fb..72af0c03 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/ConfigReseedSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/ConfigReseedSection.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { api } from "@/lib/api"; import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; interface ConfigReseedSectionProps { getAccessToken: () => Promise; @@ -148,17 +149,12 @@ export function ConfigReseedSection({
{/* Fetch & Reseed from GitHub */}
-
-
- - - -
-

Fetch & Reseed from GitHub

-

Fetch the latest rules from the GitHub repository and reseed all rule types

-
-
-
+

This operation fetches all rule definitions from the GitHub repository and writes them to Azure Table Storage: diff --git a/src/Web/autopilot-monitor-web/app/admin/components/DeviceBlockSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/DeviceBlockSection.tsx index 5f4c2c6d..2359b33f 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/DeviceBlockSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/DeviceBlockSection.tsx @@ -11,6 +11,7 @@ import { TenantConfiguration } from "./TenantManagementSection"; import { TenantSearchSelect } from "./TenantSearchSelect"; import { useCanMutatePlatform } from "@/hooks/useCanMutatePlatform"; import { firstBlockedSessionId, blockedSessionCount } from "./blockedDeviceHelpers"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; interface ResolvedDevice { sessionId: string; @@ -309,17 +310,12 @@ function DeviceBlockSectionInner({ return (

-
-
- - - -
-

Device Block Management

-

Temporarily block a rogue device from sending data. The agent will stop uploading when it receives the block signal.

-
-
-
+
{/* Block / Kill a device form */}
diff --git a/src/Web/autopilot-monitor-web/app/admin/components/DiagnosticsLogPathsSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/DiagnosticsLogPathsSection.tsx index 96655ca7..1c71e34f 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/DiagnosticsLogPathsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/DiagnosticsLogPathsSection.tsx @@ -6,6 +6,7 @@ import { ValidationIndicator } from "@/components/ValidationIndicator"; import { BuiltInSectionsList } from "@/components/diagnostics/BuiltInSectionsList"; import { DiagnosticsPathRow } from "@/components/diagnostics/DiagnosticsPathRow"; import type { DiagnosticsBuiltInSection, DiagnosticsLogPath } from "@/types/diagnostics"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; // Re-export so existing consumers importing from this component keep working export type { DiagnosticsLogPath }; @@ -52,19 +53,12 @@ export function DiagnosticsLogPathsSection({ return (
-
-
- - - -
-

Diagnostics Log Paths

-

- What every diagnostics package collects: the built-in sections compiled into the agent, plus the global paths below sent to all tenants. Tenants may add their own paths in Settings. -

-
-
-
+
{/* Info box */}
diff --git a/src/Web/autopilot-monitor-web/app/admin/components/MaintenanceSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/MaintenanceSection.tsx deleted file mode 100644 index ff8d98bd..00000000 --- a/src/Web/autopilot-monitor-web/app/admin/components/MaintenanceSection.tsx +++ /dev/null @@ -1,456 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { api } from "@/lib/api"; -import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; - -interface MaintenanceSectionProps { - getAccessToken: () => Promise; - setError: (error: string | null) => void; - setSuccessMessage: (message: string | null) => void; -} - -export function MaintenanceSection({ - getAccessToken, - setError, - setSuccessMessage, -}: MaintenanceSectionProps) { - const [triggeringMaintenance, setTriggeringMaintenance] = useState(false); - const [maintenanceDate, setMaintenanceDate] = useState(""); - // Latest selectable day (yesterday), fixed at mount — render must stay pure - // (react-hooks/purity) and the cap doesn't need a live midnight rollover. - const [maxMaintenanceDate] = useState(() => new Date(Date.now() - 86400000).toISOString().split('T')[0]); - const [reseedingRules, setReseedingRules] = useState(false); - const [reseedingGatherRules, setReseedingGatherRules] = useState(false); - const [reseedingImePatterns, setReseedingImePatterns] = useState(false); - const [fetchingFromGitHub, setFetchingFromGitHub] = useState(false); - - const handleTriggerMaintenance = async () => { - try { - setTriggeringMaintenance(true); - setError(null); - setSuccessMessage(null); - - const response = await authenticatedFetch(api.maintenance.trigger(maintenanceDate || undefined), getAccessToken, { - method: "POST", - }); - - if (!response.ok) { - throw new Error(`Failed to trigger maintenance: ${response.statusText}`); - } - - const dateInfo = maintenanceDate ? ` for ${maintenanceDate}` : ''; - setSuccessMessage(`Maintenance job completed successfully${dateInfo}!`); - - // Auto-hide success message after 5 seconds - setTimeout(() => setSuccessMessage(null), 5000); - } catch (err) { - if (err instanceof TokenExpiredError) { - console.error("Session expired while triggering maintenance"); - } else { - console.error("Error triggering maintenance:", err); - } - setError(err instanceof Error ? err.message : "Failed to trigger maintenance job"); - } finally { - setTriggeringMaintenance(false); - } - }; - - const handleReseedAnalyzeRules = async () => { - try { - setReseedingRules(true); - setError(null); - setSuccessMessage(null); - - const response = await authenticatedFetch(api.rules.reseedFromGitHub("analyze"), getAccessToken, { - method: "POST", - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `Failed to reseed analyze rules: ${response.statusText}`); - } - - const result = await response.json(); - setSuccessMessage(`Analyze rules reseeded from GitHub: ${result.analyze?.deleted ?? 0} deleted, ${result.analyze?.written ?? 0} written`); - setTimeout(() => setSuccessMessage(null), 5000); - } catch (err) { - if (err instanceof TokenExpiredError) { - console.error("Session expired while reseeding analyze rules"); - } else { - console.error("Error reseeding analyze rules:", err); - } - setError(err instanceof Error ? err.message : "Failed to reseed analyze rules"); - } finally { - setReseedingRules(false); - } - }; - - const handleReseedGatherRules = async () => { - try { - setReseedingGatherRules(true); - setError(null); - setSuccessMessage(null); - - const response = await authenticatedFetch(api.rules.reseedFromGitHub("gather"), getAccessToken, { - method: "POST", - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `Failed to reseed gather rules: ${response.statusText}`); - } - - const result = await response.json(); - setSuccessMessage(`Gather rules reseeded from GitHub: ${result.gather?.deleted ?? 0} deleted, ${result.gather?.written ?? 0} written`); - setTimeout(() => setSuccessMessage(null), 5000); - } catch (err) { - if (err instanceof TokenExpiredError) { - console.error("Session expired while reseeding gather rules"); - } else { - console.error("Error reseeding gather rules:", err); - } - setError(err instanceof Error ? err.message : "Failed to reseed gather rules"); - } finally { - setReseedingGatherRules(false); - } - }; - - const handleReseedImePatterns = async () => { - try { - setReseedingImePatterns(true); - setError(null); - setSuccessMessage(null); - - const response = await authenticatedFetch(api.rules.reseedFromGitHub("ime"), getAccessToken, { - method: "POST", - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `Failed to reseed IME log patterns: ${response.statusText}`); - } - - const result = await response.json(); - setSuccessMessage(`IME log patterns reseeded from GitHub: ${result.ime?.deleted ?? 0} deleted, ${result.ime?.written ?? 0} written`); - setTimeout(() => setSuccessMessage(null), 5000); - } catch (err) { - if (err instanceof TokenExpiredError) { - console.error("Session expired while reseeding IME log patterns"); - } else { - console.error("Error reseeding IME log patterns:", err); - } - setError(err instanceof Error ? err.message : "Failed to reseed IME log patterns"); - } finally { - setReseedingImePatterns(false); - } - }; - - const handleFetchFromGitHub = async () => { - try { - setFetchingFromGitHub(true); - setError(null); - setSuccessMessage(null); - - const response = await authenticatedFetch(api.rules.reseedFromGitHub("all"), getAccessToken, { - method: "POST", - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `Failed to reseed from GitHub: ${response.statusText}`); - } - - const result = await response.json(); - setSuccessMessage( - `GitHub Reseed complete: Gather (${result.gather?.deleted ?? 0} deleted, ${result.gather?.written ?? 0} written), ` + - `Analyze (${result.analyze?.deleted ?? 0} deleted, ${result.analyze?.written ?? 0} written), ` + - `IME (${result.ime?.deleted ?? 0} deleted, ${result.ime?.written ?? 0} written)` - ); - setTimeout(() => setSuccessMessage(null), 8000); - } catch (err) { - if (err instanceof TokenExpiredError) { - console.error("Session expired while reseeding from GitHub"); - } else { - console.error("Error reseeding from GitHub:", err); - } - setError(err instanceof Error ? err.message : "Failed to reseed from GitHub"); - } finally { - setFetchingFromGitHub(false); - } - }; - - return ( - <> - {/* Manual Maintenance Trigger */} -
-
-
- - - -
-

Manual Maintenance Trigger

-

Execute platform-wide maintenance operations

-
-
-
-
-

- Runs all timer-triggered tasks plus additional backfill & repair operations that only run on manual trigger. -

-
-
-

Standard tasks (also run by 2h timer)

-
    -
  • - • - Mark stalled sessions as timed out -
  • -
  • - • - Aggregate metrics into historical snapshots -
  • -
  • - • - Clean up old data based on retention policies -
  • -
  • - • - Recompute platform-wide statistics -
  • -
-
-
-

Manual-only tasks (backfill & repair)

-
    -
  • - • - Backfill sessions missing from SessionsIndex -
  • -
  • - • - Clean up ghost SessionsIndex entries (remove after 2026-06) -
  • -
  • - • - Backfill tenant OnboardedAt dates (remove when complete) -
  • -
-
-
-
- - setMaintenanceDate(e.target.value)} - max={maxMaintenanceDate} - className="w-full max-w-xs px-3 py-2 border border-purple-300 dark:border-purple-600 rounded-lg text-sm bg-white dark:bg-gray-600 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-purple-500 focus:border-purple-500" - /> -

- Leave empty to run the standard maintenance with automatic catch-up (aggregates any missed days within the last 7 days). - Select a specific date to manually aggregate metrics for that day, e.g. to backfill data older than 7 days. -

-
-
-
- - - -

- Warning: This operation runs across all tenants and may take several minutes to complete. - Use this only for testing or when immediate cleanup is needed. -

-
-
-
- -
-
-
- - {/* Fetch & Reseed from GitHub */} -
-
-
- - - -
-

Fetch & Reseed from GitHub

-

Fetch the latest rules from the GitHub repository and reseed all rule types

-
-
-
-
-

- This operation fetches all rule definitions from the GitHub repository and writes them to Azure Table Storage: -

-
    -
  • - • - Fetches Gather Rules, Analyze Rules, and IME Log Patterns from GitHub -
  • -
  • - • - Deletes all existing global built-in entries and writes the latest versions -
  • -
  • - • - Tenant-specific custom rules and overrides are not affected -
  • -
-
-
- - - -

- Rules are loaded from raw.githubusercontent.com. After a merge to the repository, it may take up to 5 minutes{" "}for GitHub's CDN cache to update. If you see stale rules after reseeding, wait a few minutes and try again. -

-
-
-
- -
-
-
- - {/* Individual Reseed Sections */} -
- {/* Reseed Analyze Rules */} -
-
-

Reseed Analyze Rules

-

Fetch and reseed only analyze rules from GitHub

-
-
-
- -
-
-
- - {/* Reseed Gather Rules */} -
-
-

Reseed Gather Rules

-

Fetch and reseed only gather rules from GitHub

-
-
-
- -
-
-
- - {/* Reseed IME Log Patterns */} -
-
-

Reseed IME Log Patterns

-

Fetch and reseed only IME log patterns from GitHub

-
-
-
- -
-
-
-
- - ); -} diff --git a/src/Web/autopilot-monitor-web/app/admin/components/MaintenanceTriggerSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/MaintenanceTriggerSection.tsx index 8d20f208..18dd4f9e 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/MaintenanceTriggerSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/MaintenanceTriggerSection.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { api } from "@/lib/api"; import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; interface MaintenanceTriggerSectionProps { getAccessToken: () => Promise; @@ -87,17 +88,12 @@ export function MaintenanceTriggerSection({ return (
-
-
- - - -
-

Manual Maintenance Trigger

-

Execute platform-wide maintenance operations

-
-
-
+

Runs all timer-triggered tasks plus additional backfill & repair operations that only run on manual trigger. diff --git a/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx index 7e07cf6a..228d5325 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx @@ -6,6 +6,7 @@ import { ChannelEditor } from "@/components/notifications/ChannelEditor"; import { MAX_NOTIFICATION_CHANNELS, type NotificationChannel } from "@/app/settings/types"; import { toggleChannelBinding } from "./opsChannelRouting"; import { AUTO_ACTION_MODES, describeAutoActionWarning, type AutoActionMode } from "./excessiveEventAutoAction"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; // All known ops event types grouped by category const OPS_EVENT_TYPES: Record = { @@ -351,19 +352,12 @@ export function OpsAlertRulesSection({

{/* Alert Rules */}
-
-
- - - -
-

Alert Rules

-

- Select which ops events trigger alerts. {enabledRulesCount} rule(s) active, {enabledProviders} provider(s) enabled. -

-
-
-
+
{loadingConfig ? (
@@ -526,19 +520,12 @@ export function OpsAlertRulesSection({ {/* Channels */}
-
-
- - - -
-

Alert Channels

-

- Named destinations for platform alerts. Each rule above chooses which of these it notifies. -

-
-
-
+
{channels.length === 0 && (

diff --git a/src/Web/autopilot-monitor-web/app/admin/components/OpsEventsSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/OpsEventsSection.tsx index 947c4dec..a14d70b9 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/OpsEventsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/OpsEventsSection.tsx @@ -8,6 +8,7 @@ import TruncatedLabel from "@/components/TruncatedLabel"; import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { extractContinuation } from "@/lib/paginationLink"; import { extractSessionId, buildAutoReason } from "./opsEventSessionHelpers"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; interface OpsEvent { id: string; @@ -242,20 +243,13 @@ export function OpsEventsSection({ return (

{/* Header */} -
-
-
- - - -
-

Operational Events

-

- Vital infrastructure events across consent, maintenance, security, and tenant lifecycle. -

-
-
-
+ {loading ? "Loading..." : "Refresh"} -
-
-
+ + } + /> {/* Summary Cards */} {!loading && filteredEvents.length > 0 && ( diff --git a/src/Web/autopilot-monitor-web/app/admin/components/SessionExportSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/SessionExportSection.tsx index ce4dd220..03cb5fa4 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/SessionExportSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/SessionExportSection.tsx @@ -7,6 +7,7 @@ import { TenantConfiguration } from "./TenantManagementSection"; import { TenantSearchSelect } from "./TenantSearchSelect"; import { SessionExportEvent, generateCsvExport, generateUiExport } from "@/utils/sessionExportUtils"; import { trackEvent } from "@/lib/appInsights"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; function downloadFile(content: string, filename: string, mimeType: string) { const blob = new Blob([content], { type: mimeType }); @@ -66,17 +67,12 @@ export function SessionExportSection({ return (
-
-
- - - -
-

Session Event Export

-

Fetch and export all events directly from storage — use to analyze timeline phase grouping and ordering

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/admin/components/SessionReportsSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/SessionReportsSection.tsx index 438a8915..77d5cd5b 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/SessionReportsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/SessionReportsSection.tsx @@ -10,6 +10,7 @@ import { isGuid } from "@/utils/inputValidation"; import { trackEvent } from "@/lib/appInsights"; import { useCanMutatePlatform } from "@/hooks/useCanMutatePlatform"; import { TableSkeleton } from "@/components/skeletons/TableSkeleton"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; const PAGE_SIZE = 20; @@ -367,21 +368,12 @@ function SessionReportsSectionInner({ return (
{/* Section Header */} -
-
- - - -
-
-

Session Reports

-
-

- Sessions reported by Tenant Admins for analysis -

-
-
-
+ {/* Tenant filter + refresh */}
diff --git a/src/Web/autopilot-monitor-web/app/admin/components/VersionBlockSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/VersionBlockSection.tsx index 07e316ce..22fe5dc4 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/VersionBlockSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/VersionBlockSection.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { api } from "@/lib/api"; import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { useCanMutatePlatform } from "@/hooks/useCanMutatePlatform"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; interface BlockedVersion { versionPattern: string; @@ -126,17 +127,12 @@ export function VersionBlockSection({ return (
-
-
- - - -
-

Version Block Management

-

Block or kill agents by version pattern. Useful for catching old agents that are still running.

-
-
-
+
{/* Pattern help */}
diff --git a/src/Web/autopilot-monitor-web/app/admin/components/VulnerabilityDataSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/VulnerabilityDataSection.tsx index f0a7dcf6..b5e54376 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/VulnerabilityDataSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/VulnerabilityDataSection.tsx @@ -7,6 +7,7 @@ import SaveResetBar from "../../settings/components/SaveResetBar"; import VulnerabilityExposurePanel from "@/components/VulnerabilityExposurePanel"; import type { AdminConfiguration } from "@/types/adminConfig"; import { useCanMutatePlatform } from "@/hooks/useCanMutatePlatform"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; interface VulnerabilityTopCve { cveId: string; @@ -422,17 +423,12 @@ export function VulnerabilityDataSection({ return (
{/* Header */} -
-
- - - -
-

Vulnerability Data Configuration

-

Configure vulnerability correlation data sources and API keys.

-
-
-
+ {/* Content */}
diff --git a/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionDelegatedAdmins.tsx b/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionDelegatedAdmins.tsx index 4846ee55..ba4e635e 100644 --- a/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionDelegatedAdmins.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionDelegatedAdmins.tsx @@ -7,6 +7,7 @@ import { useTenantList } from "@/hooks/useTenantList"; import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { api } from "@/lib/api"; import { HOME_TENANT_UNRESOLVED } from "@/lib/identityBinding"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; /** One delegated-admin assignment as returned by /api/global/delegated-admins (camelCase JSON). */ interface DelegatedAssignment { @@ -191,19 +192,12 @@ export function SectionDelegatedAdmins() { return (
{/* Header */} -
-
- - - -

Delegated Admins (MSP Mode)

-
-

- Grant a person read access to a subset of tenants (a “fleet”), without making them a - full platform admin. They sign in to their own tenant and see only the tenants you grant here. -

-
+
{/* Notifications */} diff --git a/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionTenantGroups.tsx b/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionTenantGroups.tsx index 497c07a8..4508c6d3 100644 --- a/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionTenantGroups.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionTenantGroups.tsx @@ -7,6 +7,7 @@ import { useTenantList } from "@/hooks/useTenantList"; import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { api } from "@/lib/api"; import { HOME_TENANT_UNRESOLVED } from "@/lib/identityBinding"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; /** One UPN assigned to a group (camelCase JSON from the backend). */ interface GroupAssignee { @@ -245,19 +246,12 @@ export function SectionTenantGroups() { return (
{/* Header */} -
-
- - - -

Tenant Groups (MSP Mode)

-
-

- Bundle tenants into a named group, then assign people to the group instead of to each tenant. - Add a tenant once and every assignee gets it; manage your managed-service team by who is assigned here. -

-
+
{/* Notifications */} diff --git a/src/Web/autopilot-monitor-web/app/analyze-rules/page.tsx b/src/Web/autopilot-monitor-web/app/analyze-rules/page.tsx index 8e9e4ccb..bae064fc 100644 --- a/src/Web/autopilot-monitor-web/app/analyze-rules/page.tsx +++ b/src/Web/autopilot-monitor-web/app/analyze-rules/page.tsx @@ -31,7 +31,8 @@ function validateEvaluateOnForm(form: RuleForm): string | null { import AnalyzeRuleFormFields from "./components/AnalyzeRuleFormFields"; import AnalyzeRuleCard, { RuleTrendPoint, RuleRegressionInfo } from "./components/AnalyzeRuleCard"; import TemplateConfigModal from "./components/TemplateConfigModal"; -import { DOCS_URL } from "@/utils/config"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; export default function AnalyzeRulesPage() { const { user, getAccessToken } = useAuth(); @@ -768,16 +769,13 @@ export default function AnalyzeRulesPage() { {/* Create Custom Rule Form */} {showCreateForm && (
-
-
-
- -
-

Create Custom Analyze Rule

-

Define conditions and confidence scoring for issue detection — see documentation

-
-
- {/* JSON Mode Toggle */} + { @@ -786,8 +784,8 @@ export default function AnalyzeRulesPage() { setJsonError(null); }} /> -
-
+ } + />
-
-
-
- - - -
-

Create Custom Rule

-

Define a new data collection rule for enrolled devices — see documentation

-
-
+ { @@ -503,8 +500,8 @@ export default function GatherRulesPage() { setJsonError(null); }} /> -
-
+ } + />
-
-
- - - -
-

Access Management

-

Manage team members and their roles for this tenant

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/AgentAnalyzersSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/AgentAnalyzersSection.tsx index 9a0c91fe..c0c2f88a 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/AgentAnalyzersSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/AgentAnalyzersSection.tsx @@ -2,6 +2,8 @@ import SaveResetBar from "./SaveResetBar"; import ReadOnlyFieldset from "./ReadOnlyFieldset"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface AgentAnalyzersSectionProps { enableLocalAdminAnalyzer: boolean; @@ -73,17 +75,13 @@ export default function AgentAnalyzersSection({ return (
-
-
- - - -
-

Agent Analyzers

-

Security analyzers that run on enrolled devices to detect configuration anomalies.

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/AgentSettingsSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/AgentSettingsSection.tsx index 802ce3b0..9efb759a 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/AgentSettingsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/AgentSettingsSection.tsx @@ -2,6 +2,8 @@ import SaveResetBar from "./SaveResetBar"; import ReadOnlyFieldset from "./ReadOnlyFieldset"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface AgentSettingsSectionProps { enablePerformanceCollector: boolean; @@ -101,17 +103,13 @@ export default function AgentSettingsSection({ <> {/* Agent Parameters */}
-
-
- - - -
-

Agent Parameters

-

Control agent behavior on enrolled devices. Changes take effect on the next agent config refresh.

-
-
-
+
@@ -361,17 +359,13 @@ export default function AgentSettingsSection({ {/* Agent Collectors */}
-
-
- - - -
-

Agent Collectors

-

Enable optional data collectors on enrolled devices. These generate additional telemetry traffic.

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/AutopilotValidationSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/AutopilotValidationSection.tsx index 50b6da5c..5fedfd7d 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/AutopilotValidationSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/AutopilotValidationSection.tsx @@ -1,6 +1,8 @@ "use client"; import { useState } from "react"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface AutopilotValidationSectionProps { validateAutopilotDevice: boolean; @@ -120,26 +122,18 @@ export default function AutopilotValidationSection({ return (
-
-
-
- - - -
-

- Enrollment Device Validation -

-

- Validate devices against Intune registrations before accepting agent data (mandatory for agent ingestion) -

-
-
+ {anyValidationEnabled ? "Enabled" : "Disabled"} -
-
+ } + />

diff --git a/src/Web/autopilot-monitor-web/app/settings/components/BootstrapSessionsSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/BootstrapSessionsSection.tsx index f19e1eaf..c6d420cf 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/BootstrapSessionsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/BootstrapSessionsSection.tsx @@ -2,6 +2,8 @@ import { useState } from "react"; import { BOOTSTRAP_GO_URL } from "@/utils/config"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; // Display-only: the authoritative URL comes from the backend's bootstrapUrl // (Constants.BootstrapGoBaseUrl); this host renders the sessions list rows. @@ -131,34 +133,13 @@ export default function BootstrapSessionsSection({ return (

{/* Header */} -
-
-
-
- - - -
-
-

- OOBE Bootstrap -

-

- Generate time-limited URLs to install the agent before Intune - enrollment -

-
-
+ -
-
+ } + />
{/* Info banner */} diff --git a/src/Web/autopilot-monitor-web/app/settings/components/DataManagementSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/DataManagementSection.tsx index 0078c539..7a54e449 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/DataManagementSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/DataManagementSection.tsx @@ -2,6 +2,8 @@ import SaveResetBar from "./SaveResetBar"; import ReadOnlyFieldset from "./ReadOnlyFieldset"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface DataManagementSectionProps { dataRetentionDays: number; @@ -35,17 +37,13 @@ export default function DataManagementSection({ return (
-
-
- - - -
-

Data Management

-

Configure data retention and session timeout policies

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/DiagnosticsSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/DiagnosticsSection.tsx index f477e3cf..7d274e1f 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/DiagnosticsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/DiagnosticsSection.tsx @@ -10,6 +10,8 @@ import { ContextPill, DiagnosticsPathRow } from "@/components/diagnostics/Diagno import SaveResetBar from "./SaveResetBar"; import ReadOnlyFieldset from "./ReadOnlyFieldset"; import { parseSasExpiry } from "./diagnosticsSasExpiry"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; // Re-export for callers that still import from the section. Implementation lives // in `diagnosticsSasExpiry.ts` so vitest can test it without pulling in JSX. @@ -89,17 +91,13 @@ export default function DiagnosticsSection({ return (
-
-
- - - -
-

Diagnostics Package

-

Upload diagnostic files as a ZIP package to your Azure Blob Storage after enrollment.

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/HardwareWhitelistSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/HardwareWhitelistSection.tsx index 3ef7c0a4..7f44cb46 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/HardwareWhitelistSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/HardwareWhitelistSection.tsx @@ -4,6 +4,8 @@ import { useState } from "react"; import SaveResetBar from "./SaveResetBar"; import ReadOnlyFieldset from "./ReadOnlyFieldset"; import { joinList, parseList } from "../lib/hardwareWhitelist"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface HardwareWhitelistSectionProps { manufacturerWhitelist: string; @@ -136,22 +138,13 @@ export default function HardwareWhitelistSection({ return (
-
-
- - - -
-

- Hardware Whitelist -

-

- Control which device manufacturers and models the backend accepts - monitoring data from -

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/McpUsersSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/McpUsersSection.tsx index edccdabd..8baac8e9 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/McpUsersSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/McpUsersSection.tsx @@ -7,6 +7,8 @@ import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch" import { api } from "@/lib/api"; import { useTenantList } from "@/hooks/useTenantList"; import { HOME_TENANT_UNRESOLVED } from "@/lib/identityBinding"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface McpUser { upn: string; @@ -286,18 +288,13 @@ export default function McpUsersSection() { return (
{/* Header */} -
-
- - - -

MCP User Management

-
-

- Control who can access the AI agent interface (Model Context Protocol) and assign individual usage plans. -

-
+
{/* Notifications */} diff --git a/src/Web/autopilot-monitor-web/app/settings/components/NotificationsSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/NotificationsSection.tsx index 336f57d7..88cfb3ec 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/NotificationsSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/NotificationsSection.tsx @@ -4,6 +4,8 @@ import SaveResetBar from "./SaveResetBar"; import ReadOnlyFieldset from "./ReadOnlyFieldset"; import { MAX_NOTIFICATION_CHANNELS, NotificationChannel } from "../types"; import { ChannelEditor } from "@/components/notifications/ChannelEditor"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface NotificationsSectionProps { channels: NotificationChannel[]; @@ -51,19 +53,13 @@ export default function NotificationsSection({ return (
-
-
- - - -
-

Notifications

-

- Send notifications to one or more channels — Teams, Slack, Discord, or any JSON webhook (ticketing, automation, bots). Each channel picks which events it receives. -

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/OffboardingSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/OffboardingSection.tsx index 163c128f..d58a382b 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/OffboardingSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/OffboardingSection.tsx @@ -3,6 +3,8 @@ import { useEffect, useRef, useState } from "react"; import { api } from "@/lib/api"; import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface OffboardingInProgressInfo { status: string; // "Queued" | "Initiated" | "InProgress" | "Completed" | "Failed" @@ -118,17 +120,13 @@ export default function OffboardingSection({ <> {/* Danger Zone: Tenant Offboarding */}
-
-
- - - -
-

Danger Zone

-

Irreversible and destructive actions

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/UnrestrictedModeSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/UnrestrictedModeSection.tsx index d44f4935..586eb3f7 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/UnrestrictedModeSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/UnrestrictedModeSection.tsx @@ -1,6 +1,8 @@ "use client"; import { useState } from "react"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface UnrestrictedModeSectionProps { unrestrictedMode: boolean; @@ -34,17 +36,13 @@ export default function UnrestrictedModeSection({ return (
-
-
- - - -

Unrestricted Mode

-
-

- Disable agent guardrails for advanced data collection -

-
+
{/* Active warning banner */} diff --git a/src/Web/autopilot-monitor-web/app/settings/reporting/sections/SectionMcpUsage.tsx b/src/Web/autopilot-monitor-web/app/settings/reporting/sections/SectionMcpUsage.tsx index 88facea3..aecd3a3d 100644 --- a/src/Web/autopilot-monitor-web/app/settings/reporting/sections/SectionMcpUsage.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/reporting/sections/SectionMcpUsage.tsx @@ -5,6 +5,8 @@ import { useCallback, useEffect, useState } from "react"; import { useAuth } from "../../../../contexts/AuthContext"; import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"; import { api } from "@/lib/api"; +import { DocsLink } from "@/components/DocsLink"; +import { DOCS_PATHS } from "@/lib/docsPaths"; interface UsageRecord { userId: string; @@ -177,6 +179,7 @@ export function SectionMcpUsage() { > {loading ? "Loading..." : "Refresh"} +
diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionContact.tsx b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionContact.tsx index 389fbbc0..155c444b 100644 --- a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionContact.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionContact.tsx @@ -3,6 +3,8 @@ import { useTenantConfig } from "../../TenantConfigContext"; import SaveResetBar from "../../components/SaveResetBar"; import ReadOnlyFieldset from "../../components/ReadOnlyFieldset"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; export function SectionContact() { const { @@ -17,17 +19,13 @@ export function SectionContact() { return (
-
-
- - - -
-

Contact

-

Where we reach you about the service

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionPlan.tsx b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionPlan.tsx index 6c414657..cbeb8eb4 100644 --- a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionPlan.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionPlan.tsx @@ -6,6 +6,8 @@ import { useTenantConfig } from "../../TenantConfigContext"; import { trialDaysLeft } from "@/lib/edition"; import { PlanCards } from "@/components/plans/PlanCards"; import { SITE_URL } from "@/utils/config"; +import { SectionCardHeader } from "@/components/SectionCardHeader"; +import { DOCS_PATHS } from "@/lib/docsPaths"; /** * Self-service Pro trial switch. The Pro feature set is not finalized yet, so the @@ -135,17 +137,13 @@ export function SectionPlan() { return (
-
-
- - - -
-

Plan

-

Your current plan and what Pro adds

-
-
-
+
-
-
- - - -
-

- SLA Targets -

-

- SLA targets and breach notification settings -

-
-
-
+
diff --git a/src/Web/autopilot-monitor-web/components/DocsLink.tsx b/src/Web/autopilot-monitor-web/components/DocsLink.tsx new file mode 100644 index 00000000..4d5c6d04 --- /dev/null +++ b/src/Web/autopilot-monitor-web/components/DocsLink.tsx @@ -0,0 +1,29 @@ +import { DOCS_URL } from "@/utils/config"; + +interface DocsLinkProps { + /** Path below the published docs root, with a leading slash (e.g. "/reference/settings#notifications"). */ + path: string; + label?: string; + className?: string; +} + +/** + * Small "Read the docs" link that opens the matching customer documentation page in a new tab. + * Neutral colouring so it sits on every section-header tone, including the red danger zone. + */ +export function DocsLink({ path, label = "Read the docs", className = "" }: DocsLinkProps) { + return ( + + {label} + + + ); +} diff --git a/src/Web/autopilot-monitor-web/components/SectionCardHeader.tsx b/src/Web/autopilot-monitor-web/components/SectionCardHeader.tsx new file mode 100644 index 00000000..8d6f0040 --- /dev/null +++ b/src/Web/autopilot-monitor-web/components/SectionCardHeader.tsx @@ -0,0 +1,189 @@ +import type { ReactNode } from "react"; +import { DocsLink } from "./DocsLink"; + +/** + * Colour tones for section card headers. Every class string is literal so Tailwind can see it — + * never assemble these from fragments. Tenant tones use the pastel 50-level gradient with grey + * text; admin tones use the 100-level gradient, tinted text, and dark-mode variants. + */ +const TONES = { + amber: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-orange-50", + icon: "w-6 h-6 text-amber-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + violet: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-violet-50 to-purple-50", + icon: "w-6 h-6 text-violet-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + emerald: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-emerald-50 to-teal-50", + icon: "w-6 h-6 text-emerald-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + rose: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-rose-50 to-red-50", + icon: "w-6 h-6 text-rose-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + indigo: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-blue-50", + icon: "w-6 h-6 text-indigo-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + indigoPurple: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-purple-50", + icon: "w-6 h-6 text-indigo-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + sky: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-sky-50 to-blue-50", + icon: "w-6 h-6 text-sky-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + skyIndigo: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-sky-50 to-indigo-50", + icon: "w-6 h-6 text-sky-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + purple: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-purple-50 to-indigo-50", + icon: "w-6 h-6 text-purple-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + green: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-green-50 to-emerald-50", + icon: "w-6 h-6 text-green-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + cyan: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-cyan-50 to-teal-50", + icon: "w-6 h-6 text-cyan-600", + title: "text-xl font-semibold text-gray-900", + subtitle: "text-sm text-gray-500 mt-1", + }, + /** Amber gradient with amber text — guardrail-relaxing settings (Unrestricted Mode). */ + warning: { + wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-orange-50", + icon: "w-6 h-6 text-amber-600", + title: "text-xl font-semibold text-amber-900", + subtitle: "text-sm text-amber-700 mt-1", + }, + /** Flat red — irreversible actions (Danger Zone). */ + danger: { + wrapper: "p-6 border-b border-red-100 bg-red-50", + icon: "w-6 h-6 text-red-600", + title: "text-xl font-semibold text-red-900", + subtitle: "text-sm text-red-600 mt-1", + }, + adminIndigo: { + wrapper: "p-6 border-b border-indigo-200 dark:border-indigo-700 bg-gradient-to-r from-indigo-100 to-blue-100 dark:from-indigo-900/40 dark:to-blue-900/40", + icon: "w-6 h-6 text-indigo-600 dark:text-indigo-400", + title: "text-xl font-semibold text-indigo-900 dark:text-indigo-100", + subtitle: "text-sm text-indigo-600 dark:text-indigo-300 mt-1", + }, + adminIndigoPurple: { + wrapper: "p-6 border-b border-indigo-200 dark:border-indigo-700 bg-gradient-to-r from-indigo-100 to-purple-100 dark:from-indigo-900/40 dark:to-purple-900/40", + icon: "w-6 h-6 text-indigo-600 dark:text-indigo-400", + title: "text-xl font-semibold text-indigo-900 dark:text-indigo-100", + subtitle: "text-sm text-indigo-600 dark:text-indigo-300 mt-1", + }, + adminEmerald: { + wrapper: "p-6 border-b border-emerald-200 dark:border-emerald-700 bg-gradient-to-r from-emerald-100 to-teal-100 dark:from-emerald-900/40 dark:to-teal-900/40", + icon: "w-6 h-6 text-emerald-600 dark:text-emerald-400", + title: "text-xl font-semibold text-emerald-900 dark:text-emerald-100", + subtitle: "text-sm text-emerald-600 dark:text-emerald-300 mt-1", + }, + adminAmber: { + wrapper: "p-6 border-b border-amber-200 dark:border-amber-700 bg-gradient-to-r from-amber-100 to-orange-100 dark:from-amber-900/40 dark:to-orange-900/40", + icon: "w-6 h-6 text-amber-600 dark:text-amber-400", + title: "text-xl font-semibold text-amber-900 dark:text-amber-100", + subtitle: "text-sm text-amber-600 dark:text-amber-300 mt-1", + }, + adminRed: { + wrapper: "p-6 border-b border-red-200 dark:border-red-700 bg-gradient-to-r from-red-100 to-orange-100 dark:from-red-900/40 dark:to-orange-900/40", + icon: "w-6 h-6 text-red-600 dark:text-red-400", + title: "text-xl font-semibold text-red-900 dark:text-red-100", + subtitle: "text-sm text-red-600 dark:text-red-300 mt-1", + }, + adminTeal: { + wrapper: "p-6 border-b border-teal-200 dark:border-teal-700 bg-gradient-to-r from-teal-100 to-cyan-100 dark:from-teal-900/40 dark:to-cyan-900/40", + icon: "w-6 h-6 text-teal-600 dark:text-teal-400", + title: "text-xl font-semibold text-teal-900 dark:text-teal-100", + subtitle: "text-sm text-teal-600 dark:text-teal-300 mt-1", + }, + adminPurple: { + wrapper: "p-6 border-b border-purple-200 dark:border-purple-700 bg-gradient-to-r from-purple-100 to-violet-100 dark:from-purple-900/40 dark:to-violet-900/40", + icon: "w-6 h-6 text-purple-600 dark:text-purple-400", + title: "text-xl font-semibold text-purple-900 dark:text-purple-100", + subtitle: "text-sm text-purple-600 dark:text-purple-300 mt-1", + }, + adminSky: { + wrapper: "p-6 border-b border-sky-200 dark:border-sky-700 bg-gradient-to-r from-sky-100 to-cyan-100 dark:from-sky-900/40 dark:to-cyan-900/40", + icon: "w-6 h-6 text-sky-600 dark:text-sky-400", + title: "text-xl font-semibold text-sky-900 dark:text-sky-100", + subtitle: "text-sm text-sky-600 dark:text-sky-300 mt-1", + }, + adminSlate: { + wrapper: "p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-slate-100 to-gray-100 dark:from-slate-900/40 dark:to-gray-900/40", + icon: "w-6 h-6 text-slate-600 dark:text-slate-400", + title: "text-xl font-semibold text-slate-900 dark:text-slate-100", + subtitle: "text-sm text-slate-600 dark:text-slate-300 mt-1", + }, +} as const; + +export type SectionCardTone = keyof typeof TONES; + +export interface SectionCardHeaderProps { + tone: SectionCardTone; + /** The `d` attribute of a single 24x24 Heroicons outline path. */ + iconPath: string; + title: string; + subtitle?: ReactNode; + /** Docs path below the published docs root; renders a "Read the docs" link on the right. */ + docsPath?: string; + /** Badge, button, or toggle group rendered on the right, before the docs link. */ + trailing?: ReactNode; +} + +/** + * Header strip of a settings/admin card: icon, title, subtitle on the left; optional trailing + * controls and docs link on the right. Renders only the header — the card wrapper stays with the + * caller so ids, refs, and borders remain where they are. + */ +export function SectionCardHeader({ tone, iconPath, title, subtitle, docsPath, trailing }: SectionCardHeaderProps) { + const t = TONES[tone]; + const hasRight = trailing !== undefined || docsPath !== undefined; + return ( +
+
+
+ +
+

{title}

+ {subtitle !== undefined &&

{subtitle}

} +
+
+ {hasRight && ( +
+ {trailing} + {docsPath !== undefined && } +
+ )} +
+
+ ); +} diff --git a/src/Web/autopilot-monitor-web/components/__tests__/SectionCardHeader.test.ts b/src/Web/autopilot-monitor-web/components/__tests__/SectionCardHeader.test.ts new file mode 100644 index 00000000..f34a1d5c --- /dev/null +++ b/src/Web/autopilot-monitor-web/components/__tests__/SectionCardHeader.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from "vitest"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { SectionCardHeader } from "../SectionCardHeader"; +import { DocsLink } from "../DocsLink"; +import { DOCS_URL } from "@/utils/config"; + +const ICON = "M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"; + +describe("SectionCardHeader", () => { + it("renders tone classes, icon path, title and subtitle", () => { + const html = renderToStaticMarkup( + createElement(SectionCardHeader, { + tone: "amber", + iconPath: ICON, + title: "Diagnostics Package", + subtitle: "Upload diagnostic files after enrollment.", + }), + ); + expect(html).toContain("from-amber-50 to-orange-50"); + expect(html).toContain("text-amber-600"); + expect(html).toContain(`d="${ICON}"`); + expect(html).toContain("

Diagnostics Package

"); + expect(html).toContain("Upload diagnostic files after enrollment."); + // No right-hand group when neither docsPath nor trailing is given. + expect(html).not.toContain("gap-3"); + }); + + it("renders the docs link as a new-tab link below DOCS_URL", () => { + const html = renderToStaticMarkup( + createElement(SectionCardHeader, { + tone: "sky", + iconPath: ICON, + title: "Notifications", + docsPath: "/integrations/notifications", + }), + ); + expect(html).toContain(`href="${DOCS_URL}/integrations/notifications"`); + expect(html).toContain('target="_blank"'); + expect(html).toContain('rel="noopener noreferrer"'); + expect(html).toContain("Read the docs"); + }); + + it("renders trailing content before the docs link", () => { + const html = renderToStaticMarkup( + createElement(SectionCardHeader, { + tone: "danger", + iconPath: ICON, + title: "Danger Zone", + docsPath: "/reference/settings#danger-zone", + trailing: createElement("span", { id: "badge" }, "Enabled"), + }), + ); + expect(html).toContain("bg-red-50"); + expect(html).toContain("text-red-900"); + expect(html.indexOf('id="badge"')).toBeGreaterThan(-1); + expect(html.indexOf('id="badge"')).toBeLessThan(html.indexOf("Read the docs")); + }); + + it("admin tones carry dark-mode variants", () => { + const html = renderToStaticMarkup( + createElement(SectionCardHeader, { tone: "adminAmber", iconPath: ICON, title: "Alert Rules" }), + ); + expect(html).toContain("dark:from-amber-900/40"); + expect(html).toContain("dark:text-amber-100"); + }); +}); + +describe("DocsLink", () => { + it("labels the new-tab behaviour for assistive tech", () => { + const html = renderToStaticMarkup(createElement(DocsLink, { path: "/plans", label: "See plans" })); + expect(html).toContain(`href="${DOCS_URL}/plans"`); + expect(html).toContain('aria-label="See plans (opens in a new tab)"'); + }); +}); diff --git a/src/Web/autopilot-monitor-web/lib/__tests__/docsPaths.test.ts b/src/Web/autopilot-monitor-web/lib/__tests__/docsPaths.test.ts new file mode 100644 index 00000000..4824725d --- /dev/null +++ b/src/Web/autopilot-monitor-web/lib/__tests__/docsPaths.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { DOCS_PATHS, DOCS_TOP_LEVEL_SEGMENTS } from "../docsPaths"; + +/** + * GitBook publishes the first URL segment from the SUMMARY.md group heading, not the folder + * name (`troubleshooting/` on disk is `/troubleshooting-and-support/` on the web). A path written + * from the repo layout 404s silently in production — this test makes that a CI failure. + */ +describe("DOCS_PATHS", () => { + const entries = Object.entries(DOCS_PATHS); + + it("every path is root-relative, lowercase, with an optional single anchor", () => { + for (const [key, path] of entries) { + expect(path, key).toMatch(/^\/[a-z0-9-]+(\/[a-z0-9-]+)*(#[a-z0-9-]+)?$/); + expect(path, key).not.toMatch(/\.md/); + expect(path, key).not.toMatch(/--/); + } + }); + + it("every path starts with a published top-level segment", () => { + const allowed = new Set(DOCS_TOP_LEVEL_SEGMENTS); + for (const [key, path] of entries) { + const first = path.slice(1).split(/[/#]/)[0]; + expect(allowed.has(first), `${key}: "${first}" is not a published docs section`).toBe(true); + } + }); + + it("does not use on-disk folder names that GitBook renames", () => { + for (const [key, path] of entries) { + expect(path, key).not.toMatch(/^\/(troubleshooting|trust)\//); + } + }); +}); diff --git a/src/Web/autopilot-monitor-web/lib/docsPaths.ts b/src/Web/autopilot-monitor-web/lib/docsPaths.ts new file mode 100644 index 00000000..31010ce7 --- /dev/null +++ b/src/Web/autopilot-monitor-web/lib/docsPaths.ts @@ -0,0 +1,47 @@ +/** + * Documentation page for each portal section that links to the customer docs, as a path below DOCS_URL. + * + * GitBook publishes the first path segment as the slugified SUMMARY.md group heading, not the + * folder name: `troubleshooting/` → `/troubleshooting-and-support/`, `trust/` → `/trust-and-security/`. + * Anchors are GitBook heading slugs (single hyphens). `docsPaths.test.ts` guards the shape. + */ +export const DOCS_PATHS = { + // Settings → Tenant + enrollmentDeviceValidation: "/reference/settings#enrollment-device-validation", + hardwareWhitelist: "/reference/settings#hardware-whitelist", + notifications: "/integrations/notifications", + accessManagement: "/concepts/roles-and-permissions", + slaTargets: "/portal-guide/sla-compliance#configuring-targets", + plan: "/plans", + contact: "/reference/settings#contact", + bootstrapSessions: "/reference/bootstrap-script-and-tokens", + mcpUsers: "/integrations/ai-integration-mcp", + // Settings → Agent + agentParameters: "/reference/settings#agent-parameters", + agentCollectors: "/reference/settings#agent-collectors", + agentAnalyzers: "/reference/settings#agent-analyzers", + diagnosticsPackage: "/troubleshooting-and-support/diagnostics-and-log-collection", + unrestrictedMode: "/reference/settings#unrestricted-mode-pro-plan-on-request", + // Settings → Maintenance + dataManagement: "/reference/settings#data-management", + dangerZone: "/reference/settings#danger-zone", + // Settings → Reporting + mcpUsage: "/integrations/ai-integration-mcp#rate-limits-and-usage-plans", + // Rules + analyzeRules: "/rules/analyze-rules", + gatherRules: "/rules/gather-rules", +} as const; + +/** Top-level URL segments GitBook publishes for docs.autopilotmonitor.com (SUMMARY.md groups + root pages). */ +export const DOCS_TOP_LEVEL_SEGMENTS = [ + "getting-started", + "concepts", + "portal-guide", + "rules", + "integrations", + "reference", + "trust-and-security", + "troubleshooting-and-support", + "changelog", + "plans", +] as const;