diff --git a/DESIGN.md b/DESIGN.md index 3408a98..be91bd3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -311,8 +311,10 @@ below is the working backlog; expand as we go. ### Page shell - [x] ~~Adopt `PageHeader` everywhere~~ — `/dashboard`, `/status`, all `/admin/*` sub-pages, `/addigy-devices`, `/analyzer/ticket/[…]`, - `/analyzer/analysis/[…]` now use it. A handful of pages still need - it (kiosk, settings, sentinelone/* — low priority). + `/analyzer/analysis/[…]`, `/settings`, `/settings/security`, + `/sentinelone/coverage`, `/sentinelone/mappings` now use it. Kiosk + uses its own shell; the only stragglers are very small/internal + pages. - [ ] Standardize container + padding (`container mx-auto px-6 py-6`). Pages using `px-4`, `py-8`, `max-w-2xl` etc. should justify the deviation. - [ ] Standardize section spacing (`space-y-6` between cards; pick `gap-6` @@ -363,9 +365,11 @@ below is the working backlog; expand as we go. scheduler heartbeats via `/api/status/workers` and `WorkerPulse`. ### Tokens & theming -- [ ] Audit places that hard-code Tailwind palette colors (`text-orange-600`, - `bg-blue-500/15`) and either keep them as semantic status colors or - move them behind a token. +- [-] Hard-coded Tailwind palette colors are extensive (~770 references) + but most are **semantic** (status reds, info blues, warning ambers + via the `bg-{hue}-500/15 text-{hue}-700` recipe documented above). + Surveyed and deprioritized — case-by-case cleanup as new work + touches a page. - [ ] Verify dark-mode contrast on status badges and chart legends; the 10%- opacity borders in dark mode are subtle and may need lifting. diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 2c8d90a..c811076 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -16,7 +16,7 @@ import { useEffect, useState } from 'react'; import { PageHeader } from '@/components/navigation/page-header'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Skeleton } from '@/components/ui/skeleton'; +import { SkeletonRows, SkeletonChart } from '@/components/ui/skeleton-helpers'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { EmptyState } from '@/components/ui/empty-state'; import { KpiCard } from '@/components/dashboard/kpi-card'; @@ -275,7 +275,7 @@ export default function DashboardPage() { {!trends ? ( - + ) : ( )} @@ -290,7 +290,7 @@ export default function DashboardPage() { {!trends ? ( - + ) : ( )} @@ -309,7 +309,7 @@ export default function DashboardPage() { {!trends ? ( - + ) : ( )} @@ -324,7 +324,7 @@ export default function DashboardPage() { {!trends ? ( - + ) : ( )} @@ -343,7 +343,7 @@ export default function DashboardPage() { {!data ? ( - + ) : data.observations.length === 0 ? ( {!data ? ( - + ) : data.audits.length === 0 ? ( - - - - - ); -} diff --git a/app/sentinelone/coverage/page.tsx b/app/sentinelone/coverage/page.tsx index 720469d..8b8e3d1 100644 --- a/app/sentinelone/coverage/page.tsx +++ b/app/sentinelone/coverage/page.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; +import { PageHeader } from '@/components/navigation/page-header'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; @@ -64,19 +65,20 @@ export default function S1CoveragePage() { }); return ( -
-
-
-

- - SentinelOne Coverage -

-

AV agent coverage and threat status per site

-
- -
+ <> + + + Refresh + + } + /> +
{/* Summary cards */} {summary && ( @@ -182,6 +184,7 @@ export default function S1CoveragePage() { -
+
+ ); } diff --git a/app/sentinelone/mappings/page.tsx b/app/sentinelone/mappings/page.tsx index 4c99b54..3f492ed 100644 --- a/app/sentinelone/mappings/page.tsx +++ b/app/sentinelone/mappings/page.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; +import { PageHeader } from '@/components/navigation/page-header'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { @@ -95,26 +96,26 @@ export default function S1MappingsPage() { }; return ( -
-
-
-

- - SentinelOne Site Mappings -

-

Map SentinelOne sites to Autotask companies

-
-
- - -
-
+ <> + + + + + } + /> +
{/* Stats */}
@@ -182,7 +183,8 @@ export default function S1MappingsPage() {
-
+
+ ); } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 9b3aac1..2d6a30a 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,7 +1,14 @@ import { redirect } from "next/navigation"; import { getSession } from "@/lib/auth-utils"; import { ProfileForm } from "@/components/settings/profile-form"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { PageHeader } from "@/components/navigation/page-header"; export default async function SettingsPage() { const session = await getSession(); @@ -11,25 +18,24 @@ export default async function SettingsPage() { } return ( -
-
-

Settings

-

- Manage your account settings -

+ <> + +
+ + + Profile + Update your personal information + + + + +
- - - - Profile - - Update your personal information - - - - - - -
+ ); } diff --git a/app/settings/security/page.tsx b/app/settings/security/page.tsx index e35227c..89606b9 100644 --- a/app/settings/security/page.tsx +++ b/app/settings/security/page.tsx @@ -2,7 +2,14 @@ import { redirect } from "next/navigation"; import { getSession } from "@/lib/auth-utils"; import { TwoFactorSetup } from "@/components/settings/two-factor-setup"; import { ActiveSessions } from "@/components/settings/active-sessions"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { PageHeader } from "@/components/navigation/page-header"; export default async function SecuritySettingsPage() { const session = await getSession(); @@ -12,21 +19,21 @@ export default async function SecuritySettingsPage() { } return ( -
-
-

Security Settings

-

- Manage your security preferences -

-
- -
+ <> + +
- Two-Factor Authentication - - Add an extra layer of security to your account - + Two-factor authentication + Add an extra layer of security to your account. @@ -35,16 +42,14 @@ export default async function SecuritySettingsPage() { - Active Sessions - - Manage your active sessions across devices - + Active sessions + Manage your active sessions across devices.
-
+ ); } diff --git a/app/status/page.tsx b/app/status/page.tsx index 02941c2..b45b7b8 100644 --- a/app/status/page.tsx +++ b/app/status/page.tsx @@ -15,6 +15,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; +import { SkeletonTable } from '@/components/ui/skeleton-helpers'; import { EmptyState } from '@/components/ui/empty-state'; import { StatusLight, type StatusLightState } from '@/components/ui/status-light'; import { StatusBadge } from '@/components/ui/status-badge'; @@ -409,9 +410,7 @@ export default function StatusPage() { {!overview ? ( -
- -
+ ) : overview.syncHealth.length === 0 ? (
void; -}) { - const [open, setOpen] = useState(false); - const catCfg = CATEGORY_CFG[t.problem_category]; - - return ( - <> - setOpen(o => !o)} +const TICKET_COLUMNS: Column[] = [ + { + key: 'ticket_number', + label: 'Ticket', + render: (v) => {v}, + }, + { + key: 'company_name', + label: 'Client', + render: (v) => {v ?? '—'}, + }, + { + key: 'device_hostname', + label: 'Device', + render: (v) => {v ?? '—'}, + }, + { + key: 'problem_category', + label: 'Category', + render: (v) => { + const cfg = CATEGORY_CFG[v as string]; + return ( + + {catLabel(v as string)} + + ); + }, + }, + { + key: 'resolution_type', + label: 'Resolution', + render: (v) => {resLabel(v as string)}, + }, + { + key: 'same_day_close', + label: 'Same-day', + render: (v) => + v ? ( + + ) : ( + + ), + }, + { + key: 'hours_worked', + label: 'Hours', + render: (v) => {parseFloat(v as string).toFixed(2)}h, + }, + { + key: 'complexity', + label: 'Complexity', + render: (v) => ( + - - {open - ? - : } - - {t.ticket_number} - {t.company_name ?? '—'} - {t.device_hostname ?? '—'} - - - {catLabel(t.problem_category)} - - - {resLabel(t.resolution_type)} - - {t.same_day_close - ? - : } - - {parseFloat(t.hours_worked).toFixed(2)}h - - - {t.complexity} - - - {timeAgo(t.ticket_created_at)} - - {open && ( - - -
-
- {t.work_summary && ( -
- Summary -

{t.work_summary}

-
- )} -
- {t.device_was_offline != null && ( - - - {t.device_was_offline ? 'Device was offline' : 'Device was online'} - - )} - {t.backup_completed_before_tech != null && ( - - {t.backup_completed_before_tech - ? <> Backup auto-completed - : <> Tech action required} - - )} - {t.preventable != null && ( - - {t.preventable ? '⚠ Preventable' : '✓ Not preventable'} - - )} -
-
- {t.recommended_procedure && ( -
- Recommended SOP -

{t.recommended_procedure}

-
+ {v} + + ), + }, + { + key: 'ticket_created_at', + label: 'Age', + render: (v) => {timeAgo(v as string)}, + }, +]; + +function renderTicketSubRow(t: TicketRow) { + return ( +
+
+ {t.work_summary && ( +
+ Summary +

{t.work_summary}

+
+ )} +
+ {t.device_was_offline != null && ( + + + {t.device_was_offline ? 'Device was offline' : 'Device was online'} + + )} + {t.backup_completed_before_tech != null && ( + + {t.backup_completed_before_tech ? ( + <> + Backup auto-completed + + ) : ( + <> + Tech action required + )} -
- - + + )} + {t.preventable != null && ( + + {t.preventable ? '⚠ Preventable' : '✓ Not preventable'} + + )} +
+
+ {t.recommended_procedure && ( +
+ Recommended SOP +

{t.recommended_procedure}

+
)} - +
); } @@ -675,60 +695,22 @@ export default function VeeamAnalysisPage() { - - - - - - Ticket - Client - Device - Category - Resolution - Same-day - Hours - Complexity - Age - - - - {data.tickets.length > 0 - ? data.tickets.map(t => ( - - )) - : ( - - - No analyzed tickets yet — run the analysis above. - - - )} - -
- - {/* Pagination */} - {totalPages > 1 && ( -
- Page {page} of {totalPages} -
- - -
-
- )} + + + columns={TICKET_COLUMNS} + data={data.tickets} + totalCount={data.total} + page={page} + pageSize={data.limit ?? 50} + onPageChange={(next) => { + setPage(next); + fetchData(catFilter, next); + }} + emptyTitle="No analyzed tickets yet" + emptyDescription="Run the analysis above to populate this list." + getRowCanExpand={() => true} + renderSubRow={renderTicketSubRow} + /> )}