Feature components
Every user-facing screen in the Lockstep VM frontend is an Astro route that mounts exactly one feature component as a React island (client:load). Feature components live directly in src/components/*.tsx; the shared design-system primitives they compose live in src/components/ui/ (see UI primitives). This page catalogs the feature components. For the route wiring pattern, see Pages & routes; for data/client conventions, see Conventions.
Conventions shared by feature components
Follow these when adding or editing a feature component — they are near-universal in this codebase:
| Convention | Detail |
|---|---|
| Own QueryClient | Almost every feature component creates its own new QueryClient(...) and wraps its content in <QueryClientProvider>. There is no app-wide provider. The exported component is a thin wrapper; the real UI is an inner Content / *Content function. Default options are usually { queries: { refetchOnWindowFocus: false, retry: 1 } }. |
| Two API access styles | Newer/refactored components import typed helpers from @/lib/api (inventoryApi, devicesApi, authApi, dashboardApi, …) which use the shared axios api instance (baseURL = ${PUBLIC_API_URL}/api/v1, withCredentials: true). Older components call fetch('http://localhost:8000/api/v1/...', { credentials: 'include' }) directly against a hardcoded const API. Both rely on the Better Auth session cookie; no bearer header is sent. |
| Named + default export | Components export both a named export (export function Foo) and export default Foo. |
| Props are minimal | List/hub components take no props. Detail components take a single identifier prop (cveId, deviceId, appName, upn, sourceKey, scanId, projectId) passed from the Astro route’s dynamic segment. |
| Navigation | Row/link clicks navigate with window.location.assign(...) or <a href> — full page loads, not client routing. |
Layout & chrome
These wrap every authenticated page. See Pages & routes for how they nest: Layout.astro → AuthCheck → AppLayout → <FeaturePage>.
AuthCheck
Client-side route guard. Rendered as the outermost island on every protected page.
| Aspect | Detail |
|---|---|
| Props | { children: React.ReactNode } |
| Renders | A centered spinner while state === 'pending'; null when denied; children when ok. |
| Behavior | On mount, fetch('/api/auth/get-session', { credentials: 'include' }) (the Astro Better Auth endpoint, not the FastAPI backend). If body.user exists → ok; otherwise sets denied and redirects to /login. Uses a plain effect (not authClient.useSession()) to keep SSR and first client render identical and avoid React #418/#423 hydration mismatches. |
| react-query | None. |
| Used by | Every protected page (index, devices, applications, users, all integration pages, etc.). |
AppLayout
Thin alias — AppLayout just renders <AppShell>{children}</AppShell>. Kept so existing Astro pages don’t need editing. Props: { children: React.ReactNode }.
AppShell
Root v2 layout: ThemeProvider + Sidebar + TopBar + <main> + footer + <Toaster>.
| Aspect | Detail |
|---|---|
| Props | { children, currentPath?, requireAuth = true } |
| Behavior | When requireAuth, on mount fetches /api/auth/get-session; redirects to /login if no user. Seeds user from the session (email, name→full_name, role), then best-effort enriches via authApi.getCurrentUser() (GET /auth/me) — a backend hiccup never logs anyone out. Renders children only once ready. currentPath defaults to window.location.pathname. |
| react-query | None (raw fetch + authApi). |
Sidebar
Static left navigation. Props: { currentPath?: string }. Renders the “Lockstep VM” brand and grouped nav links defined in a local groups array: Overview (Dashboard), Inventory (Assets, Software, Users, Vulnerabilities, Version Tracking), Intelligence (Security News, Advisories, Threat Intel, Weaknesses, Vendors & Products), Sources (SentinelOne, Rapid7, Intune, Entra ID, ConnectWise, CW Automate, Upload Data), Work (Projects, Reports, Statistics, Subscriptions), Administration (Organizations, Tags), plus footer links Profile & Usage/Billing. isActive(current, href) highlights the active item. No data fetching.
TopBar
Sticky header inside the content column. Props: { user?: { email?, full_name? } }. Renders a global-search button (⌘K placeholder, non-functional), notifications bell, ThemeToggle, and a user Menu (Profile → /profile, Settings → /organizations, Sign out). logout() calls authClient.signOut(), clears access_token, redirects to /login. Detects Mac to show ⌘K vs Ctrl K.
ThemeProvider
Exports ThemeProvider (applies .v2 + dark/light class to <html>, persists choice) and ThemeToggle (button used by TopBar). Wraps the whole app via AppShell.
NavBar
Legacy top nav (not used by the v2 AppShell chrome). Renders brand + links and a sign-out that calls authClient.signOut(). Retained but effectively superseded by Sidebar/TopBar.
Inventory
The core asset/software/vulnerability browsing surfaces.
DevicesTable
The Assets list (/devices). A structured, server-paginated, query-buildable device table joined across Intune, SentinelOne, and Rapid7.
| Aspect | Detail |
|---|---|
| Props | None. |
| Renders | PageHeader (Export / Integrate menus + Scan stub), a 5-tile KPI strip (Total / Intune / SentinelOne / Rapid7 / Matched), a search bar + Query builder drawer trigger, results toolbar (range, page size), a DataTable, and pagination. Rows link to /device/{device_id}. SourceBadges renders per-source chips (intune/sentinelone/rapid7/connectwise_automate/autoelevate) with a Link2 “matched across sources” marker. |
| State | URL-synced search, ps (pageSize), p (page); sort; query-builder Group state (query, qbDraft, qbOpen) built from the DEVICE_FIELDS schema (General / Microsoft Intune intune.* / SentinelOne s1.* / Rapid7 r7.*). copyQueryUrl() serializes state (incl. base64 query) to the clipboard. |
| API | devicesApi.searchDevices({ query, search, skip, limit, sort }) → POST /devices/search; devicesApi.getStats() → GET /devices/stats. |
| react-query | ['devices-search', search, toServerPayload(query), skip, pageSize, sort], ['device-stats'] |
ApplicationsTable
The Software inventory list (/applications). Same architecture as DevicesTable but for software groups.
| Aspect | Detail |
|---|---|
| Props | None. |
| Renders | PageHeader (Export / Import → /inventory / Integrate / Scan stub), search bar + Query builder drawer, results toolbar, DataTable (Name → /application/{name}, Asset Count, Vendor, Product, Version with +N roll-up, Site), pagination. |
| State | URL-synced search/ps/p, sort, query builder over APP_FIELDS (General + Microsoft Intune metadata: publisher, platform, size_bytes, category, install_status). Field ids must match the backend APPLICATION_FIELDS allowlist. |
| API | inventoryApi.searchApplications({ query, search, skip, limit, sort }) → POST /inventory/applications/search. |
| react-query | ['applications-search', search, toServerPayload(query), skip, pageSize, sort] |
ApplicationDetail
Software detail page (/application/[appName]). Props: { appName: string }.
| Aspect | Detail |
|---|---|
| Renders | Header with “Discovered via” source chips (SOURCE_LABELS), 4 summary cards (Total Versions, Devices, Most Common version, Vulnerabilities), Recent Vulnerability Scans (link to /vulnerabilities/scans/{scan_id}), Installed-on-Devices table, a Version Distribution table with per-version checkboxes, and Platforms. |
| State | selectedVersions (checkbox multi-select), isScanning. The “Scan selected versions” button POSTs /vulnerabilities/scan with { application_name, vendor, versions, enrich: true }. Uses staleTime: 0, gcTime: 0 (always fresh). Uses raw fetch against hardcoded localhost:8000. |
| API | GET /inventory/applications?search=^{name}$&limit=100 (exact match), GET /vulnerabilities/scans?application_name={name}, GET /devices/by-application/{name}?limit=100, POST /vulnerabilities/scan. |
| react-query | ['application-detail', appName], ['application-vulnerabilities', appName], ['application-devices', appName] |
AssetDetail
The primary device detail page (/device/[deviceName], prop is actually the device id). Props: { deviceId: string }. Large multi-source aggregation view.
| Aspect | Detail |
|---|---|
| Renders | Header with SourceChips, KPI cards, source Tabs/SourceTabs (Inventory, SentinelOne, Rapid7, ConnectWise Automate, AutoElevate) with AttributeTable/KeyValueGrid per source, a vulnerabilities/findings table, export-JSON action, and per-CVE research action. NotInSource/EmptyNoIssues handle empty states. Flattens nested backend objects into dot-path attribute rows via flattenAttributes. |
| State | activeSource tab, researchingCve, researchedCves (Set), loadedAt. |
| Mutations | scanMutation → POST /vulnerabilities/scan-device ({ device_name, applications }), invalidates ['asset-vulns']. researchFinding(vulnId) → POST /rapid7/research-finding. |
| API | devicesApi.getDeviceById(deviceId); GET /inventory/items?device_name=; GET /vulnerabilities/?device_name=; GET /sentinelone/device-by-name/{name}; GET /rapid7/device-by-name/{name}; GET /connectwise-automate/device-by-name/{name}; GET /csv-sources/autoelevate/device-by-name/{name}; GET /rapid7/findings/{assetId}?limit=500. |
| react-query | ['asset', deviceId], ['asset-apps', name], ['asset-vulns', name], ['asset-s1', name], ['asset-r7', name], ['asset-cwa', name], ['asset-ae', name], ['asset-r7-findings', rapid7AssetId] |
DeviceDetail
Older/parallel device detail component (prop deviceId). Same shape as AssetDetail (multi-source device view with apps, vulns, Rapid7 findings, SentinelOne data, scan-device + research-finding actions) using raw fetch. Present in the tree; AssetDetail is the one wired to the /device/[deviceName] route.
| Aspect | Detail |
|---|---|
| Props | { deviceId: string } |
| API | GET /inventory/items?device_name=, GET /vulnerabilities/?device_name=, GET /rapid7/findings/{assetId}, POST /rapid7/research-finding, POST /vulnerabilities/scan-device. |
| react-query | ['device-detail', deviceId], ['device-apps', device?.device_name], ['device-vulns', device?.device_name], ['device-rapid7', device?.device_name], ['device-rapid7-findings', rapid7AssetId], ['device-sentinel', device?.device_name] |
VulnerabilitiesTable
The Vulnerabilities list (/vulnerabilities). Props: none.
| Aspect | Detail |
|---|---|
| Renders | Vulnerability stats, a scans list, and a searchable CVE table. |
| State | searchTerm. |
| API | GET /vulnerabilities/, GET /vulnerabilities/scans, GET /vulnerabilities/stats. |
| react-query | ['vulnerability-scans'], ['vulnerabilities', searchTerm], ['vulnerability-stats'] |
VulnerabilityDetail
CVE detail page (/vulnerabilities/[cve] and /vulnerabilities/cve/[cveId]). Props: { cveId: string }.
| Aspect | Detail |
|---|---|
| Renders | CVE metadata, affected-devices list, CVE tags, and subscription-status checks for the affected vendor/product. |
| API | vulnerability detail, affected devices, CVE tags, all-tags, subscription checks by product & vendor. |
| react-query | ['vulnerability', cveId], ['cve-tags', cveId], ['all-tags'], ['sub-check-product', vulnData?.application_name], ['sub-check-vendor', vulnData?.vendor], ['cve-devices', cveId] |
ScanDetail
Single scan result page (/vulnerabilities/scans/[scanId]). Props: { scanId: string }. Fetches GET /vulnerabilities/scans/{scanId} and its vulnerabilities.
| react-query | ['scan', scanId], ['scan-vulnerabilities', scanId] |
|---|
WeaknessesPage
CWE weaknesses browser (/weaknesses). Props: none. Search + category filter over a weaknesses list.
| react-query | ['weaknesses', search, categoryFilter] |
|---|
VendorsProducts
Vendors & Products explorer (/vendors). Props: none. Combines local inventory vendors, subscriptions, projects, and live NVD lookups (CVE keyword search + CPE search).
| react-query | ['inventory-vendors'], ['subscriptions'], ['projects'], ['nvd-cves', nvdKeyword], ['nvd-cpe', cpeSubmitted, cpeStartIndex] |
|---|
Users
UsersPage
The Users list (/users). Props: none. People synced from Microsoft Entra ID, linked to owned assets.
| Aspect | Detail |
|---|---|
| Renders | Header, search bar with “Enabled only” toggle, a paginated table (Name/UPN, Department, Title, Groups count, Status). Row click → /user/{upn}. Includes a UserDrawer slide-over (used for inline detail) that fetches a single user. Page size 25. |
| State | search/q (submitted), enabledOnly, page, selected. |
| API | GET /entra/users?skip&limit&search&enabled_only; drawer: GET /entra/user/{upn}. Raw fetch, const E = ${API}/entra. |
| react-query | ['users', q, enabledOnly, page], ['user', upn] (drawer) |
UserDetail
User detail page (/user/[upn]). Props: { upn: string }.
| Aspect | Detail |
|---|---|
| Renders | Header (avatar, door-admin/enabled badges), KPI cards (Assets, Groups, Status, Department), a source-tabbed AttributeTable (entra.id + enrichment sources like badge_access/hr_roster), a Profile card, an Assets list (links to /device/{device_id}), and filterable Group memberships. Reuses the same SourceTabs/AttributeTable UI as AssetDetail. |
| State | groupSearch, activeSource. |
| API | GET /entra/user/{upn}. |
| react-query | ['user-detail', upn] |
Integrations
Source-connector admin pages. Each shows connection config, a sync trigger, stats, and browsable synced data. They follow a common pattern: config query, stats query, sync-status query (polled while running), plus test-connection / save / sync / delete-config mutations.
SentinelOnePage
/sentinelone. Props: none. Tabs: overview / apps / devices / threats (plus a config setup view).
| Aspect | Detail |
|---|---|
| Renders | Connection/config panel, stats KPIs, sync controls, and tabbed tables for applications, devices, and threats (each with its own search box). Uses a local apiFetch(path, opts) helper over const API. |
| Mutations | POST /sentinelone/test-connection, POST /sentinelone/config (save), POST /sentinelone/sync, DELETE /sentinelone/config. |
| API (queries) | GET /sentinelone/config, /stats, /sync-status, /apps, /devices, /threats. |
| react-query | ['s1-config'], ['s1-stats'], ['s1-sync-status'] (polls while syncing), ['s1-apps', searchApps, minDevices], ['s1-devices', searchDevices], ['s1-threats', searchThreats] |
Rapid7Page
/rapid7. Props: none. Tabs: overview / apps / assets (plus config).
| Aspect | Detail |
|---|---|
| Renders | Same shape as SentinelOnePage — config, stats, sync, and tabbed apps/assets tables via apiFetch. |
| Mutations | POST /rapid7/test-connection, POST /rapid7/config, POST /rapid7/sync, DELETE /rapid7/config. |
| API (queries) | GET /rapid7/config, /stats, /sync-status, /apps, /assets. |
| react-query | ['r7-config'], ['r7-stats'], ['r7-sync-status'], ['r7-apps', searchApps, minDevices], ['r7-assets', searchAssets] |
IntunePage
/intune. Props: none. Includes an inline SetupWizard, plus DevicesTab and AppsTab sub-components.
| Aspect | Detail |
|---|---|
| Renders | If unconfigured, a multi-step SetupWizard (POST /intune/config, POST /intune/test-connection). Once configured: stats KPIs, sync control, a Devices tab (GET /intune/devices?limit=200) and an Apps tab (GET /intune/apps with search + minDevices filter). |
| Mutations | save config, test-connection, POST /intune/sync, DELETE /intune/config. |
| API (queries) | GET /intune/config, /stats, /devices, /apps. |
| react-query | ['intune-config'], ['intune-stats'], ['intune-devices'], ['intune-apps', search, minDevices] |
EntraPage
/entra — Microsoft Entra ID (Azure AD) connector. Props: none. Reuses the Intune app registration.
| Aspect | Detail |
|---|---|
| Renders | Connected badge, stats (Users / Groups / Last sync), App-registration card (tenant/client id, cert vs secret), and a Sync panel with “Sync now” + “Test connection” and a link to /users. If not configured, an instructional card. |
| Mutations (react-query) | testMutation → POST /entra/test-connection; syncMutation → POST /entra/sync (invalidates ['entra-status']). |
| API (queries) | GET /entra/config, GET /entra/sync-status (enabled only when configured; polls every 3 s while sync_status === 'running'). |
| react-query | ['entra-config'], ['entra-status'] |
ConnectWiseAutomatePage
/connectwise-automate — CW Automate (RMM) connector. Props: none. Inner Content, with a Field input helper.
| Aspect | Detail |
|---|---|
| Renders | Config form, connection test, sync control, and a clients list. const CWA = ${API}/connectwise-automate. |
| Mutations | saveScopeMutation, saveMutation (POST /connectwise-automate/config), testMutation (POST /connectwise-automate/test-connection), syncMutation (POST /connectwise-automate/sync), deleteMutation (DELETE /connectwise-automate/config). |
| API (queries) | GET /connectwise-automate/config, /clients, /sync-status. |
| react-query | ['cwa-config'], ['cwa-clients'], ['cwa-status'] |
ConnectWisePage
/connectwise — the largest feature component (~2,100 lines): the ConnectWise PSA ticketing workspace. Props: none. Many sub-components.
| Aspect | Detail |
|---|---|
| Renders | Connection status, board list, a recent-tickets table, and a rich TicketDetail modal with tabs overview / notes / assets / time / context. Additional modals: BulkPullModal (bulk-import tickets by board), VulnTicketModal (create a ticket from a vuln, with CVE/app/company search), AdvisoryComposeModal. QuickActions patches status/priority. |
| Sub-components | ConnectWiseContent, BulkPullModal, VulnTicketModal, TicketDetail, QuickActions, TicketOverviewTab, TicketNotesTab, TicketAssetsTab, TicketTimeTab, TicketContextTab, AdvisoryComposeModal. |
| Mutations | autoConfigMutation (POST /connectwise/auto-config), syncMutation (POST /connectwise/sync), bulkPullMutation (POST /connectwise/bulk-pull?board=), createMutation (POST /connectwise/tickets), createVulnMutation (POST /connectwise/tickets/from-vuln), patchMutation (PATCH /connectwise/service/tickets/{id}), assignMutation, addNoteMutation (POST .../notes), config link/unlink mutations. |
| Key queries | status, boards, service tickets, local tickets, bulk-pull status, projects, ticket notes/configs/time-entries/context, plus modal searches (cve-search-modal, app-search-modal, cw-company-search, cw-config-search). |
| react-query | ['cw-status'], ['cw-boards'], ['cw-tickets', filterBoard], ['cw-local-tickets'], ['cw-bulk-status'], ['projects'], ['cw-notes', ticketId], ['cw-ticket-configs', ticketId], ['cw-time-entries', ticketId], ['cw-ticket-context', ticketId], ['cve-search-modal', cveQ], ['app-search-modal', appQ], ['cw-company-search', companyQ], ['cw-config-search', configSearchDebounced, companyId] |
CWBulkTicketsPage
/connectwise-bulk — bulk ticket operations. Props: none. Filterable/paginated ticket list with per-ticket notes.
| API | GET /connectwise/bulk-pull tickets with q, status/company filters, pagination; per-row notes. |
|---|---|
| react-query | ['cw-bulk-tickets', q, statusFilter, companyFilter, page], ['cw-bulk-notes', ticket.ticket_id] |
ThreatIntelPage
/threat-intel. Props: none. EPSS / exploit-in-the-wild threat intelligence.
| Aspect | Detail |
|---|---|
| Renders | Config panel and a high-risk CVE list (EPSS top-50), with per-CVE enrichment. |
| API | GET /threat-intel/config, GET /threat-intel/epss/high-risk?limit=50, GET /threat-intel/enrich/{cveId}. |
| react-query | ['threat-intel-high-risk'] |
Data sources (CSV / no-API)
CsvSourcesHub
The Data Sources / Upload Data hub (/inventory). Props: none.
| Aspect | Detail |
|---|---|
| Renders | A card grid of CSV source connectors (each: icon, category pill software/identity/access, record count, StatusPill of last import status). Cards link to /csv-source/{key}. |
| API | GET /csv-sources/. Raw fetch, const API. |
| react-query | ['csv-sources'] |
CsvSourceDetail
Per-source upload page (/csv-source/[key]). Props: { sourceKey: string }.
| Aspect | Detail |
|---|---|
| Renders | Header + category, stats (record count / last upload / total uploads), a react-dropzone upload area (accepts .csv/.xls/.xlsx), expected-columns hint, and an upload-history table. Clicking a history row opens UploadModal showing parse metrics (row/imported/in-db counts, detected shape, warnings, per-field fill-rate) and a raw sample table. A “Purge data” button DELETEs all imported data. |
| State | uploading, msg, openUpload (upload id for modal). |
| API | GET /csv-sources/{key} (polls every 2.5 s while any upload is processing); POST /csv-sources/{key}/upload (multipart); DELETE /csv-sources/{key}/data; modal: GET /csv-sources/{key}/uploads/{uploadId}. |
| react-query | ['csv-source', sourceKey], ['csv-upload', sourceKey, uploadId] |
FileUpload
Reusable CSV drop-zone card (not a page). Props: { onUploadComplete?: () => void }. Uses react-dropzone (single .csv), calls inventoryApi.uploadFile(file), shows success (with upload_id) / error state. No react-query. Standalone helper used where a simple upload widget is needed.
Dashboard
DashboardPage
The home dashboard (/, wired via src/components/dashboard/DashboardPage.tsx). Props: none. A configurable widget-grid dashboard, not the legacy Dashboard.tsx.
| Aspect | Detail |
|---|---|
| Renders | A dashboard selector, an edit-mode toggle, and a DashboardCanvas of widgets. In edit mode: add widgets (WidgetPicker/WidgetEditor), drag layout, delete. System dashboards are read-only. Empty state prompts adding the first widget. Widgets render via the @/widgets registry / WidgetRenderer / WidgetShell; data via useWidgetData. |
| State | selectedId, editMode, draftLayout, editing (widget being edited). |
| API | dashboardApi (list, default, get by id, addWidget, update/delete). |
| react-query | ['dashboards-list'], ['dashboard-default'], ['dashboard', selectedId], ['widget-data'] |
| Supporting files | dashboard/DashboardCanvas.tsx, WidgetShell.tsx, WidgetRenderer.tsx, WidgetPicker.tsx, WidgetEditor.tsx, useWidgetData.ts, fieldCatalogs.ts. |
Dashboard (legacy)
Dashboard.tsx — an earlier fixed-layout dashboard (inventory stats, vulnerability stats, subscription activity, recent scans, subscriptions). Props: none. Superseded by dashboard/DashboardPage on the / route but still in the tree.
| API | GET /subscriptions/activity?limit=15, GET /vulnerabilities/?limit=15, GET /vulnerabilities/scans, GET /subscriptions/. |
|---|---|
| react-query | ['inventory-stats'], ['vulnerability-stats'], ['dashboard-sub-activity'], ['dashboard-scans'], ['dashboard-subs'] |
DashboardDemo
Static demo/sample dashboard component (no live data). Not mounted on a production route.
Work (projects, reports, statistics, subscriptions)
ProjectsPage
/projects. Props: none. Lists projects with create/edit/delete.
| API | GET/POST/PUT/DELETE /projects/. |
|---|---|
| react-query | ['projects'] |
ProjectDetail
/project/[id]. Props: { projectId: string }. Project workspace: vendors, apps, tracked vulnerabilities, and device assignment.
| Aspect | Detail |
|---|---|
| Renders | Project header, vendor/app pickers (searchable), a project-vulnerabilities list, and a device selector. Mutations refetch ['project', projectId] and ['project-vulns', projectId]. |
| react-query | ['project', projectId], ['project-vulns', projectId], ['vendors-list', vendorSearch], ['apps-list', appSearch], ['devices-all'] |
StatisticsPage
/statistics. Props: none. Aggregate charts/tiles.
| API | GET /vulnerabilities/stats, GET /devices/stats, GET /vulnerabilities/scans. |
|---|---|
| react-query | ['stats-vulns'], ['stats-devices'], ['stats-scans'] |
SubscriptionsPage
/subscriptions. Props: none. Vendor/product notification subscriptions + activity feed, with tag-based subscribe.
| react-query | ['subscriptions'], ['subscription-activity'], ['tags'], ['vendor-tags'], ['product-tags'], ['projects'] |
|---|
ReportsPage
/reports. Props: none. Generate / list / manage reports.
| API | GET/POST/DELETE /reports. |
|---|---|
| react-query | ['reports'] |
Intelligence
NewsFeedPage
/news — Security News aggregator. Props: none. The most heavily-filtered list in the app.
| Aspect | Detail |
|---|---|
| Renders | News-stats header, a feed sidebar/manager, and a filterable, infinite/skip-paginated news-items list. Filters: severity, category, feed, search, exploited, Patch Tuesday. Can link items into projects. |
| react-query | ['news-feeds'], ['news-stats'], ['news-items', filterSev, filterCat, filterFeed, filterSearch, filterExploited, filterPatchTuesday, skip], ['projects'] |
AdvisoryEmailPage
/advisories — compose & send security advisory emails. Props: none.
| Aspect | Detail |
|---|---|
| Renders | Advisory composer with CVE search and app search, affected-user resolution, draft preview, and a sent/advisories history list. Uses a local apiFetch. |
| API | POST /advisories/affected-users, POST /advisories/draft, POST /advisories/send, GET /advisories?limit=100, CVE/app search endpoints. |
| react-query | ['advisories'], ['advisory', previewId], ['cve-search', q], ['app-search', q] |
Administration
OrganizationsPage
/organizations — tenant members & invites. Props: none. Uses raw fetch(${API}/api/v1{path}).
| Aspect | Detail |
|---|---|
| Renders | Members list and invites list. Actions: change member role (PATCH), create invite (POST), revoke invite (DELETE), remove member (DELETE .../members/{id}). Role select state `useState<‘user’ |
| API | GET /organizations/members, GET /organizations/invites, PATCH member role, POST /organizations/invites, DELETE invite, DELETE /organizations/members/{id}. |
TagsPage
/tags. Props: none. CRUD over tags.
| API | GET/POST/DELETE /tags. |
|---|---|
| react-query | ['tags'] |
BillingPage
/billing — Usage & Billing. Props: none. Currently static/mock (hardcoded usage object, “Professional Plan”). No API calls or react-query.
ProfilePage
/profile. Props: none. Currently static/mock — local useState for name/email/notification toggles; no backend wiring yet.
Authentication & account
These render outside the app chrome (no AuthCheck/AppLayout wrapper) and use Better Auth via authClient (@/lib/auth-client) rather than the FastAPI backend. See Conventions for the auth model.
| Component | Route | Props | Key calls |
|---|---|---|---|
Login | /login | none | authClient.signIn.email(...) |
Signup | /signup | none | authClient.signUp.email(...); GET /invites/{token} to resolve an invite, POST /invites/{token}/accept |
ForgotPassword | /forgot-password | none | Better Auth password-reset request |
ResetPassword | /reset-password | none | Better Auth password reset |
VerifyEmail | /verify-email | none | fetch('/api/auth/verify-email?token=...') |
TwoFactor | /two-factor | none | authClient.twoFactor.verifyTotp / verifyOtp / sendOtp / verifyBackupCode |
MfaSettings | /settings/mfa | none | authClient.useSession(), authClient.twoFactor.enable / disable / verifyTotp |
None of the auth components use react-query; they drive Better Auth directly and redirect on success.
Quick index (component → route → props)
| Component | Route | Props |
|---|---|---|
AuthCheck | (all protected) | children |
AppLayout / AppShell | (all protected) | children, (currentPath?, requireAuth?) |
Sidebar | (chrome) | currentPath? |
TopBar | (chrome) | user? |
DashboardPage | / | — |
DevicesTable | /devices | — |
AssetDetail | /device/[deviceName] | deviceId |
ApplicationsTable | /applications | — |
ApplicationDetail | /application/[appName] | appName |
VulnerabilitiesTable | /vulnerabilities | — |
VulnerabilityDetail | /vulnerabilities/[cve], /vulnerabilities/cve/[cveId] | cveId |
ScanDetail | /vulnerabilities/scans/[scanId] | scanId |
WeaknessesPage | /weaknesses | — |
VendorsProducts | /vendors | — |
UsersPage | /users | — |
UserDetail | /user/[upn] | upn |
SentinelOnePage | /sentinelone | — |
Rapid7Page | /rapid7 | — |
IntunePage | /intune | — |
EntraPage | /entra | — |
ConnectWisePage | /connectwise | — |
CWBulkTicketsPage | /connectwise-bulk | — |
ConnectWiseAutomatePage | /connectwise-automate | — |
ThreatIntelPage | /threat-intel | — |
CsvSourcesHub | /inventory | — |
CsvSourceDetail | /csv-source/[key] | sourceKey |
NewsFeedPage | /news | — |
AdvisoryEmailPage | /advisories | — |
ProjectsPage | /projects | — |
ProjectDetail | /project/[id] | projectId |
StatisticsPage | /statistics | — |
SubscriptionsPage | /subscriptions | — |
ReportsPage | /reports | — |
OrganizationsPage | /organizations | — |
TagsPage | /tags | — |
BillingPage | /billing | — (mock) |
ProfilePage | /profile | — (mock) |
Login / Signup / ForgotPassword / ResetPassword / VerifyEmail / TwoFactor | auth routes | — |
MfaSettings | /settings/mfa | — |
FileUpload | (helper) | onUploadComplete? |