Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ jobs:
- name: Typecheck
run: npx tsc --noEmit

# Errors fail outright. Warnings are ratcheted: the cap is the current
# count of accepted warnings (React-Compiler hooks-rule findings demoted
# in eslint.config.mjs + exhaustive-deps) — when you fix warnings, lower
# the cap to match; never raise it.
- name: Lint
run: npm run lint -- --max-warnings 163

- name: Tests
run: npm test

Expand Down
41 changes: 16 additions & 25 deletions src/Web/autopilot-monitor-web/app/admin/AdminPageSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,15 @@
import { useMemo } from "react";
import { usePageSections } from "../../hooks/usePageSections";
import { PageSectionItem } from "../../contexts/SidebarContext";
import { route } from "../../lib/routes";
import {
GearIcon,
NoSymbolIcon,
DocumentTextIcon,
ChartBarIcon,
BuildingOfficeIcon,
ShieldCheckIcon,
} from "../../lib/sidebarIcons";

// Inline icon (same as MetricsSidebar had)
function TrendingUpIcon({ className = "w-5 h-5" }: { className?: string }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18L9 11.25l4.306 4.307a11.95 11.95 0 015.814-5.519l2.74-1.22m0 0l-5.94-2.28m5.94 2.28l-2.28 5.941" />
</svg>
);
}

// Inline icon for Ops (wrench)
function WrenchIcon({ className = "w-5 h-5" }: { className?: string }) {
return (
Expand All @@ -47,29 +38,29 @@ function GlobeIcon({ className = "w-5 h-5" }: { className?: string }) {
export function AdminPageSections() {
const items: PageSectionItem[] = useMemo(() => [
// Tenants
{ id: "management", label: "Tenant Management", href: "/admin/tenants/management", group: "Tenants", groupIcon: <BuildingOfficeIcon /> },
{ id: "config-report", label: "Config Report", href: "/admin/tenants/config-report", group: "Tenants" },
{ id: "management", label: "Tenant Management", href: route("/admin/tenants/management"), group: "Tenants", groupIcon: <BuildingOfficeIcon /> },
{ id: "config-report", label: "Config Report", href: route("/admin/tenants/config-report"), group: "Tenants" },

// Metrics
{ id: "platform-metrics", label: "Platform Metrics", href: "/admin/metrics/platform-metrics", group: "Metrics", groupIcon: <ChartBarIcon /> },
{ id: "usage", label: "Platform Usage", href: "/admin/metrics/usage", group: "Metrics" },
{ id: "mcp-usage", label: "MCP Usage", href: "/admin/metrics/mcp-usage", group: "Metrics" },
{ id: "platform-metrics", label: "Platform Metrics", href: route("/admin/metrics/platform-metrics"), group: "Metrics", groupIcon: <ChartBarIcon /> },
{ id: "usage", label: "Platform Usage", href: route("/admin/metrics/usage"), group: "Metrics" },
{ id: "mcp-usage", label: "MCP Usage", href: route("/admin/metrics/mcp-usage"), group: "Metrics" },

// Reports
{ id: "session-reports", label: "Session Reports", href: "/admin/reports/session-reports", group: "Reports", groupIcon: <DocumentTextIcon /> },
{ id: "user-feedback", label: "User Feedback", href: "/admin/reports/user-feedback", group: "Reports" },
{ id: "session-export", label: "Session Export", href: "/admin/reports/session-export", group: "Reports" },
{ id: "session-reports", label: "Session Reports", href: route("/admin/reports/session-reports"), group: "Reports", groupIcon: <DocumentTextIcon /> },
{ id: "user-feedback", label: "User Feedback", href: route("/admin/reports/user-feedback"), group: "Reports" },
{ id: "session-export", label: "Session Export", href: route("/admin/reports/session-export"), group: "Reports" },

// Security
{ id: "device-block", label: "Device Block", href: "/admin/security/device-block", group: "Security", groupIcon: <ShieldCheckIcon /> },
{ id: "version-block", label: "Version Block", href: "/admin/security/version-block", group: "Security" },
{ id: "vulnerability-data", label: "Vulnerability Data", href: "/admin/security/vulnerability-data", group: "Security" },
{ id: "device-block", label: "Device Block", href: route("/admin/security/device-block"), group: "Security", groupIcon: <ShieldCheckIcon /> },
{ id: "version-block", label: "Version Block", href: route("/admin/security/version-block"), group: "Security" },
{ id: "vulnerability-data", label: "Vulnerability Data", href: route("/admin/security/vulnerability-data"), group: "Security" },

// Settings
{ id: "global", label: "Global Settings", href: "/admin/settings/global", group: "Settings", groupIcon: <GearIcon /> },
{ id: "diagnostics-log-paths", label: "Diagnostics Log Paths", href: "/admin/settings/diagnostics-log-paths", group: "Settings" },
{ id: "config-reseed", label: "Config Reseed", href: "/admin/settings/config-reseed", group: "Settings" },
{ id: "usage-plans", label: "Usage Plans", href: "/admin/settings/usage-plans", group: "Settings" },
{ id: "global", label: "Global Settings", href: route("/admin/settings/global"), group: "Settings", groupIcon: <GearIcon /> },
{ id: "diagnostics-log-paths", label: "Diagnostics Log Paths", href: route("/admin/settings/diagnostics-log-paths"), group: "Settings" },
{ id: "config-reseed", label: "Config Reseed", href: route("/admin/settings/config-reseed"), group: "Settings" },
{ id: "usage-plans", label: "Usage Plans", href: route("/admin/settings/usage-plans"), group: "Settings" },

// Ops (single page)
{ id: "ops", label: "Maintenance", href: "/admin/ops", group: "Ops", groupIcon: <WrenchIcon /> },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function RestoreRowDiffModal({
This is an <strong>authentication / authorization</strong> table
(<code className="font-mono">GlobalAdmins</code>, <code className="font-mono">TenantAdmins</code>,{" "}
<code className="font-mono">McpUsers</code>). Restoring this row will overwrite the live{" "}
<code className="font-mono">IsEnabled</code> flag — confirm that the backup row's enable/disable
<code className="font-mono">IsEnabled</code> flag — confirm that the backup row&apos;s enable/disable
state is what you intend.
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,6 @@ export function OpsAlertRulesSection({
const enabledRulesCount = rules.filter(r => r.enabled).length;
const enabledProviders = [telegramEnabled, teamsEnabled, slackEnabled].filter(Boolean).length;

// Get category for a given event type
const getCategoryForEvent = (eventType: string): string => {
for (const [cat, types] of Object.entries(OPS_EVENT_TYPES)) {
if (types.includes(eventType)) return cat;
}
return "Unknown";
};

return (
<div className="space-y-6">
{/* Alert Rules */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { sessionUrl } from "@/lib/routes";
import { sessionUrl, deviceBlockUrl } from "@/lib/routes";
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { api } from "@/lib/api";
Expand Down Expand Up @@ -558,7 +558,6 @@ export function OpsEventsSection({
const sessionId = extractSessionId(selectedEvent.details);
if (!sessionId) return null;
const reason = buildAutoReason(selectedEvent.eventType, sessionId);
const baseHref = `/admin/security/device-block?sessionId=${encodeURIComponent(sessionId)}&reason=${encodeURIComponent(reason)}`;
return (
<div className="mt-5 pt-4 border-t border-gray-200 dark:border-gray-700">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
Expand All @@ -574,14 +573,14 @@ export function OpsEventsSection({
View session
</Link>
<Link
href={`${baseHref}&action=Block`}
href={deviceBlockUrl(sessionId, reason, "Block")}
onClick={() => setSelectedEvent(null)}
className="inline-flex items-center px-3 py-1.5 rounded-md text-xs font-medium bg-orange-100 text-orange-800 hover:bg-orange-200 dark:bg-orange-900/40 dark:text-orange-200 dark:hover:bg-orange-900/60 border border-orange-300 dark:border-orange-700"
>
Block this device
</Link>
<Link
href={`${baseHref}&action=Kill`}
href={deviceBlockUrl(sessionId, reason, "Kill")}
onClick={() => setSelectedEvent(null)}
className="inline-flex items-center px-3 py-1.5 rounded-md text-xs font-medium bg-red-700 text-white hover:bg-red-800 dark:bg-red-700 dark:hover:bg-red-800"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { Suspense, useCallback, useEffect, useRef, useState } from "react";
import { Suspense, useEffect, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { api } from "@/lib/api";
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import { useEffect, useState, useMemo, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { api } from '@/lib/api';
import TruncatedLabel from '@/components/TruncatedLabel';
import { useAuth } from '../../../../contexts/AuthContext';
Expand Down Expand Up @@ -138,9 +137,7 @@ function pN(values: number[], percentile: number): number {
// ── Component ──────────────────────────────────────────────────────────────────

export function SectionAgentMetrics() {
const router = useRouter();

const { getAccessToken, user } = useAuth();
const { getAccessToken } = useAuth();
const { addNotification } = useNotifications();

const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -845,8 +842,6 @@ function StatCard({ label, value, detail, color }: { label: string; value: strin
function FootprintBadge({
value,
thresholds,
unit,
formatFn,
}: {
value: number;
thresholds: [number, number, number];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { api } from "@/lib/api";
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
import { useAdminConfig } from "../../../AdminConfigContext";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ function TabButton({
function InFlightTab({
getAccessToken,
setError,
setSuccessMessage,
}: {
getAccessToken: (forceRefresh?: boolean) => Promise<string | null>;
setError: (error: string | null) => void;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { notFound } from "next/navigation";
import { REPORTS_NAV_SECTIONS, type ReportsSectionId } from "../reportsNavSections";
import { type ReportsSectionId } from "../reportsNavSections";
import { SectionSessionReports } from "../sections/SectionSessionReports";
import { SectionDistressReports } from "../sections/SectionDistressReports";
import { SectionUserFeedback } from "../sections/SectionUserFeedback";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { notFound } from "next/navigation";
import { SECURITY_NAV_SECTIONS, type SecuritySectionId } from "../securityNavSections";
import { type SecuritySectionId } from "../securityNavSections";
import { SectionDeviceBlock } from "../sections/SectionDeviceBlock";
import { SectionVersionBlock } from "../sections/SectionVersionBlock";
import { SectionVulnerabilityData } from "../sections/SectionVulnerabilityData";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,8 +426,6 @@ export function SectionTenantConfigReport() {
})()
: [];

const selectedTenant = tenants.find((t) => t.tenantId === selectedTenantId);

return (
<div className="max-w-5xl mx-auto px-4 py-8">
{/* Header */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState, useEffect, useRef } from "react";
import { AnalyzeRule, TemplateVariable } from "../types";
import { AnalyzeRule } from "../types";

interface TemplateConfigModalProps {
rule: AnalyzeRule;
Expand Down
4 changes: 0 additions & 4 deletions src/Web/autopilot-monitor-web/app/analyze-rules/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"use client";

import { useEffect, useState, useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { ProtectedRoute } from "../../components/ProtectedRoute";
import { useAuth } from "../../contexts/AuthContext";
import { api } from "@/lib/api";
Expand All @@ -25,8 +23,6 @@ import TemplateConfigModal from "./components/TemplateConfigModal";
import { DOCS_URL } from "@/utils/config";

export default function AnalyzeRulesPage() {
const router = useRouter();

const { user, getAccessToken } = useAuth();

const { successMessage, error, showSuccess, showError } = useNotificationMessages();
Expand Down
5 changes: 0 additions & 5 deletions src/Web/autopilot-monitor-web/app/apps/detail/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,11 +376,6 @@ function AppDetailContent() {
return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;
}

const failureCodeBarColor = (row: Record<string, unknown>): string => {
const c = Number(row.count);
return c >= 5 ? chartColors.danger : c >= 2 ? chartColors.warning : chartColors.muted;
};

return (
<ProtectedRoute>
<div className="min-h-screen bg-gray-50">
Expand Down
3 changes: 0 additions & 3 deletions src/Web/autopilot-monitor-web/app/audit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { Fragment, useCallback, useEffect, useState } from 'react';
import { TableSkeleton } from '@/components/skeletons/TableSkeleton';
import { useRouter } from 'next/navigation';
import { useAuth } from '../../contexts/AuthContext';
import { useNotifications } from '../../contexts/NotificationContext';
import { ProtectedRoute } from '../../components/ProtectedRoute';
Expand Down Expand Up @@ -63,8 +62,6 @@ function dateInputToIsoEnd(value: string): string {
}

export default function AuditPage() {
const router = useRouter();

const { getAccessToken } = useAuth();
const { addNotification } = useNotifications();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { sessionUrl } from "@/lib/routes";
import { useRouter } from "next/navigation";
import type { Route } from "next";
import { useState, useEffect, useRef, useMemo, useDeferredValue } from "react";
import { Session } from "../types";
import { trackEvent } from "@/lib/appInsights";
Expand Down Expand Up @@ -103,7 +104,7 @@ interface SessionTableProps {
/** Builds the row's navigation target. Defaults to `/sessions/{id}`; a cross-tenant viewer overrides it to
* append `?tenantId=` so a delegated viewer opens the session in the managed tenant's (read-only) context.
* Receives the whole session because the target tenant is per-row (session.tenantId), not derivable from id. */
sessionLinkTarget?: (session: Session) => string;
sessionLinkTarget?: (session: Session) => Route;
}

export function SessionTable({
Expand Down Expand Up @@ -760,7 +761,6 @@ export function SessionTable({
function SessionCell({
columnKey,
session,
adminMode,
globalAdminMode,
blockedDevicesSet,
user,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import Link from "next/link";
import { DOCS_URL } from "@/utils/config";

export function WelcomeMessage() {
Expand Down
5 changes: 2 additions & 3 deletions src/Web/autopilot-monitor-web/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,8 @@
const mainClassName = fullWidth
? "w-full px-4 sm:px-6 lg:px-8 py-4"
: "max-w-7xl mx-auto py-4 sm:px-6 lg:px-8";
const { user, logout, getAccessToken, isPreviewBlocked, hasGlobalScope } = useAuth();
const { user, getAccessToken, isPreviewBlocked, hasGlobalScope } = useAuth();
const { addNotification } = useNotifications();
const [apiStatus, setApiStatus] = useState<"unchecked" | "checking" | "healthy" | "error">("unchecked");
// `?tenant=<id>` deep-links a cross-tenant view onto one tenant — used by the /fleet card grid to drill
// a managed tenant into this dashboard. Ignored for non-cross-tenant users (the filter is unused there).
const initialTenantFilter = searchParams?.get("tenant") ?? "";
Expand All @@ -109,7 +108,7 @@
// Drives the stats refetch — server-side stats follow the submitted scope so
// typing into the filter input doesn't trigger a backend round-trip per keystroke.
const [submittedTenantIdFilter, setSubmittedTenantIdFilter] = useState(initialTenantFilter);
const { adminMode, setAdminMode, globalAdminMode, setGlobalAdminMode } = useAdminMode();
const { adminMode, globalAdminMode, setGlobalAdminMode } = useAdminMode();

const signalR = useSignalR();
const { tenantId } = useTenant();
Expand Down Expand Up @@ -207,7 +206,7 @@
console.log('[Home] User has no platform scope, disabling global mode');
setGlobalAdminMode(false);
}
}, [user, globalAdminMode]);

Check warning on line 209 in src/Web/autopilot-monitor-web/app/dashboard/page.tsx

View workflow job for this annotation

GitHub Actions / Web (typecheck + tests)

React Hook useEffect has a missing dependency: 'setGlobalAdminMode'. Either include it or remove the dependency array

// Clear the tenant filter when cross-tenant mode turns off (refetch is owned by useDashboardSessions).
// Keyed on crossTenant (not raw globalAdminMode) so a delegated ("MSP") admin — whose crossTenant is
Expand Down
6 changes: 4 additions & 2 deletions src/Web/autopilot-monitor-web/app/diagnosis/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -802,12 +802,14 @@ function DiagnosisContent() {
);
}

function EvidenceEventLinks({ matchedConditions, sessionId }: { matchedConditions: Record<string, any>; sessionId: string }) {
function EvidenceEventLinks({ matchedConditions, sessionId }: { matchedConditions: Record<string, unknown>; sessionId: string }) {
const eventLinks: { signal: string; eventId: string; eventType?: string }[] = [];
const seenEventIds = new Set<string>();

for (const [signal, evidence] of Object.entries(matchedConditions)) {
for (const [signal, evidenceRaw] of Object.entries(matchedConditions)) {
if (signal.startsWith("factor_")) continue;
// Evidence values are rule-engine JSON; only object-shaped entries carry event links.
const evidence = evidenceRaw as { eventId?: string; eventType?: string } | string | null;
if (evidence && typeof evidence === "object" && evidence.eventId) {
// Multiple matched conditions can extract different fields from the
// same event — one chip per distinct event, not per condition.
Expand Down
3 changes: 2 additions & 1 deletion src/Web/autopilot-monitor-web/app/fleet-health/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useState, useRef, useMemo } from "react";
import Link from "next/link";
import type { Route } from "next";
import { ProtectedRoute } from "../../components/ProtectedRoute";
import { useSignalR } from "../../contexts/SignalRContext";
import { useTenant } from "../../contexts/TenantContext";
Expand Down Expand Up @@ -192,7 +193,7 @@ export default function FleetHealthPage() {
// model. The model key is "{Manufacturer} {Model}", which the dashboard search matches
// against its combined manufacturer+model text. Carry the selected tenant so a global
// admin scoped to one tenant lands on that tenant's list rather than their default scope.
const dashboardModelHref = (model: string) => {
const dashboardModelHref = (model: string): Route => {
const params = new URLSearchParams({ status: "Failed", search: model });
if (isGlobalAdmin && selectedTenantId) params.set("tenant", selectedTenantId);
return `/dashboard?${params.toString()}`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useMemo, useState } from "react";
import { GatherRule, NewRuleForm, CATEGORY_COLORS, COLLECTOR_TYPE_LABELS, EMPTY_FORM, formatTrigger, formatGatherPhase, withDerivedScopeMode } from "../types";
import { GatherRule, NewRuleForm, CATEGORY_COLORS, COLLECTOR_TYPE_LABELS, formatTrigger, formatGatherPhase, withDerivedScopeMode } from "../types";
import { GatherRuleFormFields } from "./GatherRuleFormFields";
import { FormJsonToggle, JsonModeToggleButtons, ReadOnlyJsonView } from "@/components/rules/FormJsonToggle";
import { validateGatherRuleTarget } from "@/utils/guardValidation";
Expand Down Expand Up @@ -455,7 +455,7 @@ export function GatherRuleCard({
try {
const parsed = JSON.parse(jsonText) as NewRuleForm;
onSaveEdit(rule, withDerivedScopeMode({ ...editForm, ...parsed }));
} catch (e) {
} catch {
// jsonError is handled by parent
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
/>
<p className="text-xs text-gray-400 mt-1">
XPath query to extract values. Examples: <code className="bg-gray-100 px-1 rounded">/root/element</code> (path),{" "}
<code className="bg-gray-100 px-1 rounded">//element</code> (anywhere),{" "}
<code className="bg-gray-100 px-1 rounded">{"//element"}</code> (anywhere),{" "}
<code className="bg-gray-100 px-1 rounded">/root/item[@attr=&apos;value&apos;]</code> (filter),{" "}
<code className="bg-gray-100 px-1 rounded">/root/element/text()</code> (text content).
</p>
Expand Down
Loading
Loading