Landing page v2 — scroll story, portal-first hero, Autopilot Monitor rebrand (#113)

* feat(web): landing page v2 — scroll story, green brand, Autopilot Monitor wordmark

Complete rebuild of the landing page as a scroll story of one enrollment:
- Hero shows the product live (animated event-stream timeline, real event vocabulary)
- Story acts: 09:02 power-on -> 09:41 ANALYZE-ESP-001 hit (most-firing rule, ~22% hit rate)
  -> 09:43 MCP terminal demo (full session report via get_session_summary +
  get_time_attribution) -> fleet zoom-out
- Floating pill navbar, capabilities strip, restyled comparison, shared SiteFooter
  (now also mounted on /about, /terms, /privacy which had no footer at all)
- Green mark (#33B161) unified as the only brand mark via BrandMark.tsx;
  blue-indigo gradient marketing mark removed
- Wordmark is now "Autopilot Monitor" (two words) across all user-visible text,
  metadata and JSON-LD; technical identifiers (script names, ProgramData paths) untouched
- Theming via lp-* CSS variables on :root/.dark — new components never collide with
  the global .dark !important overrides; old .landing-page block kept for /preview
- StatsBand replaces PlatformStats: adds success rate (successfulEnrollments payload
  field, previously unused) and a loading skeleton instead of fake defaults
- New opengraph/twitter/apple images in the new design (green mark on dark slate)
- All animations hand-rolled (rAF/CSS/IntersectionObserver), CSP-safe, zero new
  dependencies, prefers-reduced-motion respected; auth path (AuthGate/LoginButton/
  hostRouting) and staticwebapp.config.json unchanged

Checks: tsc clean, vitest 611/611, static export build clean, no horizontal
overflow at 375px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): landing feedback round — linear story timeline, honest examples, dev stats

- Story is now one vertical timeline with a strict top-down reading order
  (time rail left, text left, visual right) instead of the zigzag grid;
  act 1 gets a "session appeared" card so no column is left empty
- Hero live session decoupled from the story device (CONTOSO-0912/Surface
  Laptop 7 completes; the story's CONTOSO-4711 is the one that gets stuck)
- Defender for Endpoint removed from the demo stream (it is onboarded, not
  installed during ESP) — replaced with Microsoft 365 Apps
- StatsBand: sample numbers in local dev only; in production the band shows
  real figures or hides entirely on fetch failure (never fake numbers,
  never an endless skeleton)
- Capabilities copy: "Pull a diagnostics package" instead of the incorrect
  "Pull ETL and event logs"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): hero v3 — mission-control cockpit instead of single-session card

The first screen now shows the product as a live fleet cockpit: KPI strip
(active-now count ticks), five enrollments running concurrently with
advancing progress bars, a new session popping in, two completing during
the ~23s loop, and the analysis engine visibly flagging the stuck device
inline (HIGH ANALYZE-ESP-001 with the culprit app). Event stream column on
desktop. Headline block is left-aligned and more compact to break the
centered-template look. Deterministic single-tick choreography,
IntersectionObserver start, prefers-reduced-motion gets a rich static
frame. Replaces LiveTimeline.tsx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): hero v4 — real portal screenshots instead of fabricated cockpit

Hero visual is now the actual product: the live portal dashboard (captured
via Playwright from portal.autopilotmonitor.com, demo fleet staged in the
DOM — no customer data, preview banner removed, wordmark as it ships) with
a real analysis finding card (ANALYZE-APP-002 incl. remediation + evidence
JSON, from the docs assets) overlapping bottom-right. Bonus asset
time-attribution.png captured for later use. Removes HeroCockpit.tsx.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): hero v5 — clean dashboard, real-analysis terminal, refined header

- Analysis-finding overlay removed: the dashboard shot stands alone (was
  colliding with the stats band and not catching)
- Terminal demo moved into the hero as its own "Then just ask" section and
  rescripted from a real production analysis (anonymized): failed attempt
  with 0x8007EA61 decoded to unmapped installer exit code 60001 (PSADT),
  transient proof via clean retry, actionable recommendation
- Headline block refined: uppercase eyebrow instead of pill badge, tighter
  type and buttons
- Wordmark "Monitor" back to ink color (green variant parked for compare)
- Story act 3 now shows the real time-attribution screenshot instead of
  duplicating the terminal

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): story act 3 — notification + one-click diagnostics instead of cropped screenshot

"See where the minutes went" (half-cut time-attribution raster) replaced
with the admin's real next move: the Teams/webhook alert that fired with
the rule, and the on-demand diagnostics flow (requested -> collected
mid-enrollment -> ZIP ready). Crafted JSX cards consistent with the other
acts; unused time-attribution.png removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): hero v6 — portal-first layout from approved design draft

- Full-width sticky navbar (brand, Product/Capabilities/Compare/Docs,
  Sign in + Get started) replaces the floating pill
- Hero per draft: gray eyebrow, "See every Autopilot enrollment. Live.",
  sub left / CTA group right with trust line, portal shot nearly full
  width (max-w-7xl), green background glow removed
- Stats as a full-bleed band with dividers under the shot; "issues
  detected" replaces success rate (underlines automated analysis),
  events compact-formatted (8.3M)
- "Then just ask" terminal extracted into its own JustAsk section after
  the band

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): get-started flow page, honest Teams card, calm buttons, light-only landing

- New public /get-started page: five-step schematic (sign in -> request
  access -> consent -> deploy bootstrapper -> watch live); "Get started"
  in navbar and hero now lead here, "Sign in" keeps the login handoff —
  resolving the two-identical-CTAs problem. Wired into hostRouting
  PUBLIC_PATH_PREFIXES, sitemap, robots allow-list.
- StoryAction Teams card corrected against the backend renderer: the real
  adaptive card has exactly one action ("Open session") — the invented
  "Collect diagnostics" button is gone (NotificationAlertBuilder.cs:60).
- Buttons no longer jump (translate-y removed); hover is brightness +
  shadow only. Final CTA rebuilt as a calm two-column band per draft
  (no dark box, no glow).
- Landing/marketing surface is light-only for now: lp-* tokens re-declared
  under .dark .landing-v2, theme toggle removed from LandingNavbar
  (screenshots are light; portal keeps full dark support).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): about/terms/privacy on the landing shell — no more sidebar flash

The three public pages now render exactly like /get-started: LandingNavbar
+ landing-v2 tokens + eyebrow/headline header + SiteFooter. GlobalSidebar
no longer treats them as sidebar pages for anonymous visitors — that was
the "weird loading": app sidebar + PublicSiteNavbar popping in after
hydration on marketing pages. PublicSiteNavbar and PublicPageHeader are
dead and deleted; terms/privacy links switched from indigo to the accent
green. Authenticated users keep the portal chrome on these pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): landing navbar anchors root-anchored so they work from subpages

#story/#features/#comparison -> /#story/... — clicking Product/Capabilities/
Compare on /get-started (or any public subpage) now navigates back to the
landing section instead of dead-ending on a missing local anchor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): calmer CTA green, about de-blued, public-page typography aligned

- All real CTAs (navbar, hero, final CTA, get-started, story mock button)
  switch from bright #33b161 to the deep accent-ink green — matches the
  dark-green family in the portal screenshot, less loud
- About page fully on the new design language: rainbow colorMap collapsed
  to the single green accent, audience cards/step bubbles/links/quick-link
  hovers de-blued
- About/terms/privacy typography scaled to landing sizes: 15px body,
  smaller page h1s and section headings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): terms callouts on lp tokens — no more stray blue/amber banners

Info callout ("Who can use it") now uses the green accent-soft style;
the AS-IS warning uses the palette warn tones. Verified terms/about carry
no remaining raw blue/amber/indigo utility colors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): about tech stack — drop version numbers, fix stale SSR claim

Versions age silently on marketing pages (.NET 10 / Next.js 15 / React 18
removed, technologies stay). Also corrects "server-rendered React app" —
the portal has been a static export since the RSC-prefetch incident fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): terminal demo app renamed to Contoso CRM App

Neutral fictional app instead of a vendor-adjacent print driver name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): analysis card confidence score, 40+ rules claim, final CTA to /get-started

- StoryAnalysis card gets the portal-style confidence bar (80%)
- Act 2 copy: "40+ built-in rules" (exactly 40 in rules/analyze today)
- "Start monitoring now" leads to /get-started like the other Get-started
  CTAs; sign-in stays in the navbar and on /get-started itself

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): portal brand aligned to landing, subtle Enterprise anchoring

- Portal navbar (both variants) now uses BrandMark + the landing wordmark
  style (w-6 mark, 15px bold tracking-tight, strong ink in light/dark) —
  completes the brand consolidation follow-up
- Enterprise plan anchored softly, free-first tone preserved: final-CTA
  subline + /get-started closing note ("same service, nothing to
  migrate") + "Plans" link in the footer Product column, all pointing to
  the docs plans page

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): final CTA right-aligned, get-started enterprise note "and more"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(web): social images refreshed to current hero claim and tagline

"See every Autopilot enrollment. Live." + the site tagline (real-time
monitoring, automated analysis, AI-ready telemetry) on the green-mark
dark card; opengraph + twitter regenerated at 1200x630.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(web): social card wordmark single-color, matching the site

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): portal sidebar selection states green — brand-aligned like the docs

Active nav items, expandable group actives, page-section title/divider
switch from blue to the green brand accent (light + dark variants).
Purple Global Admin mode stays purple (semantic distinction); broader
portal blues (status pills, buttons, KPI tiles) deliberately untouched —
that is a separate re-theme pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): preview feedback round — dark-mode leak, alignment, renamed repo links

- About/terms/privacy no longer render half dark for dark-mode visitors:
  the light-only marketing scope now neutralizes the global .dark utility
  overrides (bg-white / text-gray-* / border-gray-* / gray hovers) inside
  .landing-v2
- All landing sections aligned to one container width (max-w-7xl): story,
  just-ask, capabilities, comparison, how-it-works now share the same
  left edge as hero, stats band and final CTA
- GitHub links updated to the renamed repository okieselbach/AutopilotMonitor
  (old hyphenated URLs redirected, now correct at the source)
- /get-started wording: "Preview" instead of "Private Preview" (legal pages
  and portal status texts intentionally unchanged)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): preview page on the new design + ?demo=1 review mode; footer width aligned

- /preview rebuilt on lp tokens: BrandMark, single wordmark, calm card,
  accent notify box, neutral next-steps, ghost actions — "Preview"
  wording instead of "Private Preview"
- ?demo=1 skips the auth redirects and renders placeholder identity so
  the page can be reviewed unauthenticated (locally and on SWA previews)
- SiteFooter container widened to max-w-7xl to match navbar and sections

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): preview page — prominent approval-pending badge, honest status copy

- Amber "Approval pending" status pill above the headline
- Next-steps wording per review ("sign out and wait a bit or contact
  me…", capacity clause dropped)
- Corrected the false "approval status on your dashboard" claim: a
  blocked tenant never reaches a dashboard — signing in again lands back
  on this page while pending, and goes straight into the portal once
  approved (verified against AuthContext PrivatePreview handling)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): about/terms/privacy cards flattened to the landing pinstripe style

All raised cards (shadow / shadow-sm) become flat lp-surface panels with
the thin lp-line-soft border; inner plan boxes and quick-link tiles
switch from gray-200 borders to the same pinstripe line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): portal primary actions green — buttons, avatar, Progress Portal icon

- Primary buttons (bg-blue-600/indigo-600 + hover pairs) switch to
  green-600/700: admin backups, customs archive, vulnerability data,
  session reports, and the settings SaveResetBar
- Navbar user avatar circles green instead of blue (all four variants)
- Progress Portal sidebar MonitorIcon gets a deliberate green accent
  (light + dark) instead of inheriting gray
- Semantic blues (status pills, info banners, KPI tiles) untouched

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): portal links and focus rings on brand green

- All focus states unified: focus:ring/border blue-500 and indigo-500
  (207 occurrences) -> green-500 across inputs, selects, buttons
- Link patterns text-blue-600/indigo-600 with hover 800, hover:underline
  variants (36) -> text-green-700 hover:text-green-800; icon hovers
  hover:text-blue-600 (12) -> green
- New dark override .dark .text-green-700 -> green-400, mirroring the
  existing blue-700 convention
- Semantic blues untouched: info banners, status pills, KPI tiles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): local dev sign-in repaired; progress portal brand accents green

Local sign-in appeared broken (always landed on /progress as a role-less
user) — two stacked regressions in the dev API proxy:
1. trailingSlash:true (static-export change) made next dev 308-redirect
   /api/* to /api/*/ BEFORE the proxy rewrite; the Functions backend
   rejects trailing-slash routes, so /me silently failed.
   -> skipTrailingSlashRedirect: true (no effect on the static export;
   SWA handles trailing slashes itself).
2. .env.local DEV_API_PROXY_TARGET still pointed at the pre-EU-cutover
   autopilotmonitor-api (dead since 07-18) -> now -api-eu; proxy verified
   healthy (v1.5.1027).

Progress portal: header icon, icon circle, "Change device" link and
"Check Status" button on brand green; the blue activity indicators
(downloading/installing) stay blue — that is the portal-wide in-progress
semantic, not branding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): loading screens and remaining primary buttons on brand green

- All four loading spinners (AuthGate, ProtectedRoute, CalculatingCard,
  UnmappedSoftwareTab) green instead of blue
- Blue-indigo gradient loading/error backgrounds (AuthGate overlay,
  ProtectedRoute states, error page, CalculatingCard, SectionPlatformUsage)
  replaced with the neutral lp background (theme-aware in the portal)
- 19 non-contiguous primary buttons (bg-blue-600 text-white ...
  hover:bg-blue-700) the first sweep missed -> green, incl. ProtectedRoute
  retry/back buttons, error page, audit, diagnosis, session table

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): last blue interactive states green; monitoring headers wrap on mobile

- Time-range toggles (fleet health, software, usage, dashboard, platform
  usage), software lens tabs, MCP-usage segmented controls, SLA report
  button, gather/analyze rule save buttons, and the page-header icons on
  fleet health / geographic performance -> brand green. Zero remaining
  bg-blue-600/indigo-600 interactive patterns.
- Monitoring page headers (fleet health, software, SLA, usage metrics,
  geographic performance, audit): title row gets flex-wrap + gap-y-3 and
  the control group (tenant scope + range buttons) wraps as a compact
  second line on narrow screens instead of pushing off the right edge;
  range buttons slightly compacted (px-3 py-1.5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): geo map dev crash — per-mount container key for Leaflet

"Map container is already initialized": dev-mode remounts (StrictMode
re-runs, Fast Refresh over the dynamic() wrapper) hand Leaflet the same
container div twice and it refuses. A per-mount key on MapContainer
forces a virgin div for every (re)mount; MapCleanup keeps handling
instance teardown. No production behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): form control accents (radios/checkboxes) green portal-wide

Checked-state colors for radios and checkboxes (indigo/blue/amber/teal/
sky variants, 19 controls incl. the diagnostics destination radios and
subfolder checkboxes) unified on green, plus the diagnostics "Add" button.
Deliberately kept: red danger-confirmation checkboxes and the purple
global-admin control.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): pin native form-control accent-color to brand green

Root cause of the "red" radios/checkboxes: there is no @tailwindcss/forms
plugin, so text-* utilities never styled native controls — their check
color silently followed the user's OS accent color. A global accent-color
rule (green-600, green-500 in dark) pins all radios/checkboxes to the
brand; the danger-red and global-admin-purple controls override via
accent-* utilities.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): progress portal icon inherits item color again

Permanent green stood out against the gray siblings — the sidebar icon
now follows the item state like all others: gray at rest, green when
active.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): geo map root fix (react-leaflet 5) + readable dark muted text

- Root cause of "Map container is already initialized": Next 15 app
  router runs its bundled React 19 regardless of the installed react@18,
  and react-leaflet 4.x is incompatible with React 19 StrictMode
  double-invoked effects. Upgraded react-leaflet 4.2.1 -> 5.0.0
  (@react-leaflet/core 3) and removed our now-counterproductive
  workarounds (MapCleanup manual remove, per-mount container key) — v5
  owns the map lifecycle including unmount teardown. Verified via a
  throwaway repro page: map renders, remount toggle survives, console
  clean (repro page deleted).
- Dark mode: .dark .text-gray-400/500 lightened (#64748b -> #7b8ba3 /
  #8896ad) — notification timestamps and other muted text were nearly
  invisible on raised dark surfaces (unread bg, hover rows).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): subtler dark hover surfaces — hovered rows stay readable

.dark hover:bg-gray-50/100 dropped from #334155 to #283548/#2e3c52 (one
step above the card surface): the bright hover wash made notification
timestamps and other muted text illegible on highlighted rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): GA/tenant notification rows dark-aware — opacity variants escaped the overrides

The global-admin and tenant notification rows use bg-purple-50/30 and
bg-blue-50/30 (+ /50 hovers) — opacity-variant class tokens the global
.dark .bg-*-50 overrides never match, leaving light rows with light text
on dark mode. Explicit dark: variants (purple/blue-900 at low opacity)
fix the readability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): notification row hover clearly distinguishable in dark mode

GA (purple) and tenant (blue) rows: dark hover raised from /30 to /45
opacity so the highlighted row visibly differs from its resting tint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(web): one SegmentedControl for all range/group toggles

New shared components/SegmentedControl.tsx (the geo-style connected
pills on a gray track) replaces the three mixed patterns: loose button
trios on fleet health and software, the months <select> on SLA, and
geo's two inline toggles (time range + group-by) now all render the
same control with shared 7/30/90-day labels. Dark mode: active pill
uses arbitrary-value colors because the global .dark .bg-white override
would blend it into the track.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): MCP usage sections use the shared SegmentedControl too

The last remaining variant (joined bordered buttons with raw 7d/30d/90d
labels in admin metrics + reporting MCP usage) replaced by the shared
control with the unified "7/30/90 Days" labels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: clickable notification deep links in the bell dropdown

GA notifications now carry hrefs (session/diag-files report -> report
modal, preview signup -> tenant management filtered), GA rows render the
View link, and every bell row with an href navigates on row click and
closes the dropdown. Target sections accept ?reportId= / ?tenantId= via
the proven Suspense+useSearchParams pattern from DeviceBlockSection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(web): npm overrides pin react-leaflet peer deps to installed react

react-leaflet 5 peer-depends on react 19; the app router runs Next's
bundled React 19 at runtime while the installed package stays react 18.
Without the overrides a plain npm ci (SWA/Oryx build) fails on the peer
conflict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-07-30 22:21:18 +02:00
committed by GitHub
parent 9c50d98792
commit e4c06a9564
131 changed files with 2520 additions and 1827 deletions
@@ -131,7 +131,7 @@ public class AuthFunctionSideEffectTests
_globalNotificationMock.Verify(x => x.CreateNotificationAsync(
"preview_signup", "New Preview Signup",
It.Is<string>(m => m.Contains(TenantId) && m.Contains("contoso.com") && m.Contains(Upn)),
null), Times.Once);
$"/admin/tenants/management?tenantId={TenantId}"), Times.Once);
}
[Fact]
@@ -326,7 +326,8 @@ public class AuthFunction
_ = _globalNotificationService.CreateNotificationAsync(
"preview_signup",
"New Preview Signup",
$"Tenant {tenantId} ({domain}), UPN: {upn}");
$"Tenant {tenantId} ({domain}), UPN: {upn}",
href: $"/admin/tenants/management?tenantId={Uri.EscapeDataString(tenantId)}");
}
/// <summary>
@@ -137,7 +137,8 @@ namespace AutopilotMonitor.Functions.Functions.Reports
_ = _globalNotificationService.CreateNotificationAsync(
"diag_files_report",
"New Diag Files Report",
$"{userIdentifier} (Tenant: {request.TenantId})");
$"{userIdentifier} (Tenant: {request.TenantId})",
href: $"/admin/reports/session-reports?reportId={Uri.EscapeDataString(metadata.ReportId)}");
_logger.LogInformation("Diag-files report submitted: ReportId={ReportId}, By={User}",
metadata.ReportId, userIdentifier);
@@ -140,7 +140,8 @@ namespace AutopilotMonitor.Functions.Functions.Reports
_ = _globalNotificationService.CreateNotificationAsync(
"session_report",
"New Session Report",
$"{userIdentifier} — Session {sessionId} (Tenant: {request.TenantId})");
$"{userIdentifier} — Session {sessionId} (Tenant: {request.TenantId})",
href: $"/admin/reports/session-reports?reportId={Uri.EscapeDataString(metadata.ReportId)}");
_logger.LogInformation("Session report submitted: ReportId={ReportId}, Session={SessionId}, By={User}",
metadata.ReportId, sessionId, userIdentifier);
@@ -26,13 +26,13 @@ export const metadata: Metadata = {
"Autopilot Monitor overview",
],
openGraph: {
title: "About AutopilotMonitor – Real-Time Windows Autopilot Monitoring",
title: "About Autopilot Monitor – Real-Time Windows Autopilot Monitoring",
description:
"Free, open-source real-time monitoring and troubleshooting for Windows Autopilot enrollments. Track every phase, run analyze rules, and resolve issues faster — built by Oliver Kieselbach.",
url: `${SITE_URL}/about`,
},
twitter: {
title: "About AutopilotMonitor – Real-Time Windows Autopilot Monitoring",
title: "About Autopilot Monitor – Real-Time Windows Autopilot Monitoring",
description:
"Free, open-source real-time monitoring and troubleshooting for Windows Autopilot enrollments. Track every phase, run analyze rules, and resolve issues faster.",
},
@@ -1,5 +1,6 @@
import Link from "next/link";
import { PublicPageHeader } from "../../components/PublicPageHeader";
import { LandingNavbar } from "../../components/landing/LandingNavbar";
import { SiteFooter } from "../../components/SiteFooter";
import { DOCS_URL } from "@/utils/config";
const FEATURES = [
@@ -47,25 +48,40 @@ const FEATURES = [
},
];
// Landing v2: one accent, no rainbow — every feature card uses the
// green brand accent regardless of its legacy color key.
const ACCENT_STYLE = {
bg: "bg-[var(--lp-accent-soft)]",
text: "text-[var(--lp-ink)]",
dot: "bg-[var(--lp-accent)]",
};
const colorMap: Record<string, { bg: string; text: string; dot: string }> = {
blue: { bg: "bg-blue-100", text: "text-blue-600", dot: "bg-blue-400" },
indigo: { bg: "bg-indigo-100", text: "text-indigo-600", dot: "bg-indigo-400" },
purple: { bg: "bg-purple-100", text: "text-purple-600", dot: "bg-purple-400" },
green: { bg: "bg-green-100", text: "text-green-600", dot: "bg-green-400" },
orange: { bg: "bg-orange-100", text: "text-orange-600", dot: "bg-orange-400" },
red: { bg: "bg-red-100", text: "text-red-600", dot: "bg-red-400" },
blue: ACCENT_STYLE,
indigo: ACCENT_STYLE,
purple: ACCENT_STYLE,
green: ACCENT_STYLE,
orange: ACCENT_STYLE,
red: ACCENT_STYLE,
};
export default function AboutPage() {
return (
<div className="min-h-screen bg-gray-50">
<PublicPageHeader title="About Autopilot Monitor" />
<div className="landing-v2 min-h-screen bg-[var(--lp-bg)]">
<LandingNavbar />
<header className="px-4 sm:px-6 lg:px-8 pt-14 sm:pt-16">
<div className="max-w-7xl mx-auto">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[var(--lp-ink-faint)]">About</p>
<h1 className="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-[var(--lp-ink)]">
About Autopilot Monitor
</h1>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-10">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-10 text-[15px]">
{/* What is Autopilot Monitor */}
<section className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">What is Autopilot Monitor?</h2>
<section className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">What is Autopilot Monitor?</h2>
<p className="text-gray-700 leading-relaxed mb-4">
<strong>Autopilot Monitor</strong> is a free, open-source, real-time monitoring and troubleshooting
platform for <strong>Windows Autopilot enrollments</strong> managed through{" "}
@@ -100,7 +116,7 @@ export default function AboutPage() {
{FEATURES.map((f) => {
const c = colorMap[f.color];
return (
<div key={f.title} className="bg-white rounded-xl border border-gray-100 shadow-sm p-6">
<div key={f.title} className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6">
<h3 className={`text-base font-semibold mb-2 ${c.text}`}>{f.title}</h3>
<p className="text-sm text-gray-600 leading-relaxed mb-3">{f.description}</p>
<ul className="space-y-1">
@@ -118,8 +134,8 @@ export default function AboutPage() {
</section>
{/* Who It's For */}
<section className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Who Is It For?</h2>
<section className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">Who Is It For?</h2>
<p className="text-gray-700 leading-relaxed mb-5">
Autopilot Monitor is built for anyone responsible for deploying or supporting
Windows devices through Microsoft Intune and Autopilot.
@@ -142,17 +158,17 @@ export default function AboutPage() {
"Autopilot Monitor is a multi-tenant service with strict per-tenant data isolation — telemetry, configuration, and diagnostics are partitioned and access-scoped to each tenant. MSPs can be granted delegated read access across several customer tenants from a single login.",
},
].map((item) => (
<div key={item.title} className="rounded-lg bg-blue-50 border border-blue-100 p-4">
<h3 className="text-sm font-semibold text-blue-900 mb-2">{item.title}</h3>
<p className="text-sm text-blue-800 leading-relaxed">{item.description}</p>
<div key={item.title} className="rounded-lg bg-[var(--lp-accent-soft)] border border-[var(--lp-accent-line)] p-4">
<h3 className="text-sm font-semibold text-[var(--lp-ink)] mb-2">{item.title}</h3>
<p className="text-sm text-[var(--lp-ink-soft)] leading-relaxed">{item.description}</p>
</div>
))}
</div>
</section>
{/* How It Works */}
<section className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">How It Works</h2>
<section className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">How It Works</h2>
<p className="text-gray-700 leading-relaxed mb-5">
Autopilot Monitor uses a lightweight .NET agent deployed to devices via an Intune bootstrapper
script. The agent monitors the enrollment process in real time and streams telemetry events
@@ -169,7 +185,7 @@ export default function AboutPage() {
{ step: "5", text: "On completion, the agent uploads a diagnostics bundle if configured, then removes itself." },
].map((item) => (
<li key={item.step} className="flex items-start gap-3 text-sm text-gray-700">
<span className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-blue-600 text-white text-xs font-bold">
<span className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--lp-accent)] text-white text-xs font-bold">
{item.step}
</span>
{item.text}
@@ -179,8 +195,8 @@ export default function AboutPage() {
</section>
{/* Tech Stack & Platform */}
<section className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Technology &amp; Platform</h2>
<section className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">Technology &amp; Platform</h2>
<p className="text-gray-700 leading-relaxed mb-5">
Autopilot Monitor is built on modern, enterprise-grade technology designed to scale with large
device fleets and multi-tenant deployments.
@@ -190,7 +206,7 @@ export default function AboutPage() {
{
title: "Backend",
items: [
"Azure Functions (.NET 10 Isolated, Flex Consumption) — serverless, scalable API",
"Azure Functions (.NET Isolated, Flex Consumption) — serverless, scalable API",
"Azure Table Storage — high-throughput event ingestion",
"Azure Blob Storage — diagnostics and log bundle storage",
"Azure SignalR Service — real-time push to the portal",
@@ -199,7 +215,7 @@ export default function AboutPage() {
{
title: "Portal (Web Frontend)",
items: [
"Next.js 15 (React 18) + TypeScript — fast, server-rendered React app",
"Next.js (React) + TypeScript — fast, statically delivered web app",
"Microsoft Entra ID (MSAL) — secure authentication",
"Role-based access control (Admin / Operator / Viewer)",
"Multi-tenant architecture with delegated MSP access",
@@ -241,8 +257,8 @@ export default function AboutPage() {
</section>
{/* Open Source */}
<section className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Open Source &amp; Free to Use</h2>
<section className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">Open Source &amp; Free to Use</h2>
<p className="text-gray-700 leading-relaxed mb-4">
Autopilot Monitor is fully open source and free to use. The complete source code is available on
GitHub under an open license. Contributions, bug reports, and feature requests from the
@@ -257,16 +273,16 @@ export default function AboutPage() {
</p>
<p className="text-gray-700 leading-relaxed mb-4">
The hosted service is operated by <strong>glueckkanja AG</strong> — see the{" "}
<a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800 underline">Imprint</a> for company
details and the <Link href="/terms" className="text-blue-600 hover:text-blue-800 underline">Terms of Use</Link>{" "}
<a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Imprint</a> for company
details and the <Link href="/terms" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Terms of Use</Link>{" "}
for what each plan does and does not commit to.
</p>
<div className="flex flex-wrap gap-3 mt-5">
<a
href="https://github.com/okieselbach/Autopilot-Monitor"
href="https://github.com/okieselbach/AutopilotMonitor"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-2.5 text-sm font-medium text-gray-700 hover:border-gray-300 hover:bg-gray-100 transition-colors"
className="inline-flex items-center gap-2 rounded-lg border border-[var(--lp-line-soft)] bg-gray-50 px-4 py-2.5 text-sm font-medium text-gray-700 hover:border-gray-300 hover:bg-gray-100 transition-colors"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
@@ -274,10 +290,10 @@ export default function AboutPage() {
View on GitHub
</a>
<a
href="https://github.com/okieselbach/Autopilot-Monitor/issues"
href="https://github.com/okieselbach/AutopilotMonitor/issues"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-2.5 text-sm font-medium text-gray-700 hover:border-gray-300 hover:bg-gray-100 transition-colors"
className="inline-flex items-center gap-2 rounded-lg border border-[var(--lp-line-soft)] bg-gray-50 px-4 py-2.5 text-sm font-medium text-gray-700 hover:border-gray-300 hover:bg-gray-100 transition-colors"
>
Submit Feedback
</a>
@@ -285,7 +301,7 @@ export default function AboutPage() {
href="https://www.linkedin.com/in/oliver-kieselbach/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-4 py-2.5 text-sm font-medium text-gray-700 hover:border-gray-300 hover:bg-gray-100 transition-colors"
className="inline-flex items-center gap-2 rounded-lg border border-[var(--lp-line-soft)] bg-gray-50 px-4 py-2.5 text-sm font-medium text-gray-700 hover:border-gray-300 hover:bg-gray-100 transition-colors"
>
Oliver Kieselbach on LinkedIn
</a>
@@ -293,22 +309,23 @@ export default function AboutPage() {
</section>
{/* Quick Links */}
<section className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Explore Further</h2>
<section className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-8">
<h2 className="text-xl font-bold text-gray-900 mb-4">Explore Further</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<a href={DOCS_URL} target="_blank" rel="noopener noreferrer" className="rounded-lg border border-gray-200 px-4 py-3 hover:border-blue-300 hover:bg-blue-50/50 transition-colors text-gray-700 font-medium">
<a href={DOCS_URL} target="_blank" rel="noopener noreferrer" className="rounded-lg border border-[var(--lp-line-soft)] px-4 py-3 hover:border-[var(--lp-accent-line)] hover:bg-[var(--lp-accent-soft)] transition-colors text-gray-700 font-medium">
Documentation →
</a>
<a href={`${DOCS_URL}/changelog/platform-changelog`} target="_blank" rel="noopener noreferrer" className="rounded-lg border border-gray-200 px-4 py-3 hover:border-blue-300 hover:bg-blue-50/50 transition-colors text-gray-700 font-medium">
<a href={`${DOCS_URL}/changelog/platform-changelog`} target="_blank" rel="noopener noreferrer" className="rounded-lg border border-[var(--lp-line-soft)] px-4 py-3 hover:border-[var(--lp-accent-line)] hover:bg-[var(--lp-accent-soft)] transition-colors text-gray-700 font-medium">
Changelog →
</a>
<Link href="/privacy" className="rounded-lg border border-gray-200 px-4 py-3 hover:border-blue-300 hover:bg-blue-50/50 transition-colors text-gray-700 font-medium">
<Link href="/privacy" className="rounded-lg border border-[var(--lp-line-soft)] px-4 py-3 hover:border-[var(--lp-accent-line)] hover:bg-[var(--lp-accent-soft)] transition-colors text-gray-700 font-medium">
Privacy Policy →
</Link>
</div>
</section>
</main>
<SiteFooter />
</div>
);
}
@@ -159,7 +159,7 @@ export function RestoreRowDiffModal({
type="button"
onClick={commit}
disabled={committing}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
className="px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
>
{committing ? "Restoring…" : "Restore row"}
</button>
@@ -276,7 +276,7 @@ function RestoreInputRow({
// — only null is rejected. Match that here so an Azure Table row with
// a literal empty key (legal per spec) can be previewed/restored.
disabled={disabled || !tableName}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
className="px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
>
Preview restore
</button>
@@ -134,7 +134,7 @@ export default function BackupsListPage() {
type="button"
onClick={triggerBackup}
disabled={triggering || activeJob !== null}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
className="px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
>
{triggering ? "Triggering…" : "Run backup now"}
</button>
@@ -104,7 +104,7 @@ export function AdminConfigSettingsSection({
max="1000"
value={globalRateLimit}
onChange={(e) => setGlobalRateLimit(parseInt(e.target.value) || 100)}
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
</label>
</div>
@@ -121,7 +121,7 @@ export function AdminConfigSettingsSection({
max="10000"
value={userRateLimit}
onChange={(e) => setUserRateLimit(parseInt(e.target.value) || 120)}
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
</label>
</div>
@@ -138,7 +138,7 @@ export function AdminConfigSettingsSection({
max="10000"
value={globalAdminRateLimit}
onChange={(e) => setGlobalAdminRateLimit(parseInt(e.target.value) || 600)}
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
</label>
</div>
@@ -158,7 +158,7 @@ export function AdminConfigSettingsSection({
value={platformStatsBlobSasUrl}
onChange={(e) => setPlatformStatsBlobSasUrl(e.target.value)}
placeholder="https://storageaccount.blob.core.windows.net/publicstats?sv=...&sig=..."
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors font-mono text-sm"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors font-mono text-sm"
/>
</label>
</div>
@@ -177,7 +177,7 @@ export function AdminConfigSettingsSection({
value={agentMigrateApiBaseUrl}
onChange={(e) => setAgentMigrateApiBaseUrl(e.target.value)}
placeholder="https://autopilotmonitor-api-us.azurewebsites.net"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors font-mono text-sm"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors font-mono text-sm"
/>
</label>
</div>
@@ -195,7 +195,7 @@ export function AdminConfigSettingsSection({
onChange={(e) => setAgentMigrateTenantOverridesJson(e.target.value)}
placeholder='{"11111111-2222-3333-4444-555555555555": "https://autopilotmonitor-api-us.azurewebsites.net", "66666666-7777-8888-9999-000000000000": ""}'
rows={3}
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors font-mono text-sm"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors font-mono text-sm"
/>
</label>
</div>
@@ -214,7 +214,7 @@ export function AdminConfigSettingsSection({
max="120"
value={collectorIdleTimeoutMinutes}
onChange={(e) => setCollectorIdleTimeoutMinutes(parseInt(e.target.value) || 0)}
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
</label>
</div>
@@ -233,7 +233,7 @@ export function AdminConfigSettingsSection({
max="60"
value={desktopDetectorNoCandidateTimeoutMinutes}
onChange={(e) => setDesktopDetectorNoCandidateTimeoutMinutes(parseInt(e.target.value) || 0)}
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
</label>
</div>
@@ -253,7 +253,7 @@ export function AdminConfigSettingsSection({
max="168"
value={slaNotificationCooldownHours}
onChange={(e) => setSlaNotificationCooldownHours(parseInt(e.target.value) || 24)}
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
</label>
</div>
@@ -264,7 +264,7 @@ export function AdminConfigSettingsSection({
type="checkbox"
checked={allowAgentDowngrade}
onChange={(e) => setAllowAgentDowngrade(e.target.checked)}
className="mt-1 h-5 w-5 rounded border-indigo-300 dark:border-indigo-600 text-indigo-600 focus:ring-indigo-500"
className="mt-1 h-5 w-5 rounded border-indigo-300 dark:border-indigo-600 text-green-600 focus:ring-green-500"
/>
<span>
<span className="text-indigo-900 dark:text-indigo-100 font-medium">Allow agent downgrade</span>
@@ -289,7 +289,7 @@ export function AdminConfigSettingsSection({
value={modernDeploymentHarmlessEventIds}
onChange={(e) => setModernDeploymentHarmlessEventIds(e.target.value)}
placeholder="100, 1005, 1010"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors font-mono text-sm"
className="mt-1 block w-full px-4 py-2 border border-indigo-300 dark:border-indigo-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors font-mono text-sm"
/>
</label>
</div>
@@ -300,7 +300,7 @@ export function AdminConfigSettingsSection({
type="checkbox"
checked={enableIndexDualWrite}
onChange={(e) => setEnableIndexDualWrite(e.target.checked)}
className="mt-1 h-5 w-5 rounded border-indigo-300 dark:border-indigo-600 text-indigo-600 focus:ring-indigo-500"
className="mt-1 h-5 w-5 rounded border-indigo-300 dark:border-indigo-600 text-green-600 focus:ring-green-500"
/>
<span>
<span className="text-indigo-900 dark:text-indigo-100 font-medium">V2 Decision Engine: Enable index-table dual-write</span>
@@ -328,7 +328,7 @@ export function AdminConfigSettingsSection({
type="checkbox"
checked={sessionDeletionKillSwitch}
onChange={(e) => setSessionDeletionKillSwitch(e.target.checked)}
className="mt-1 h-5 w-5 rounded border-red-300 dark:border-red-600 text-red-600 focus:ring-red-500"
className="mt-1 h-5 w-5 rounded border-red-300 dark:border-red-600 text-red-600 accent-red-600 focus:ring-red-500"
/>
<span>
<span className="text-red-900 dark:text-red-100 font-medium">Cascade-Delete Kill-Switch (emergency)</span>
@@ -542,7 +542,7 @@ function DeviceBlockSectionInner({
<td className="px-3 py-2">
<button
onClick={() => navigator.clipboard.writeText(d.tenantId)}
className="text-xs font-mono text-gray-500 hover:text-blue-600 transition-colors"
className="text-xs font-mono text-gray-500 hover:text-green-600 transition-colors"
title={d.tenantId}
>
{d.tenantId.split("-").slice(0, 2).join("-")}...
@@ -90,7 +90,7 @@ export function DiagnosticsLogPathsSection({
updated[idx] = { ...entry, includeSubfolders: !entry.includeSubfolders };
setGlobalDiagPaths(updated);
}}
className="w-3.5 h-3.5 rounded border-teal-400 text-teal-600 focus:ring-teal-500"
className="w-3.5 h-3.5 rounded border-teal-400 text-green-600 focus:ring-green-500"
/>
<span className="text-xs text-teal-600 dark:text-teal-400">Include subfolders</span>
</label>
@@ -148,7 +148,7 @@ export function DiagnosticsLogPathsSection({
type="checkbox"
checked={newDiagSubfolders}
onChange={() => setNewDiagSubfolders(!newDiagSubfolders)}
className="w-3.5 h-3.5 rounded border-gray-400 text-teal-600 focus:ring-teal-500"
className="w-3.5 h-3.5 rounded border-gray-400 text-green-600 focus:ring-green-500"
/>
<span className="text-xs text-gray-600 dark:text-gray-400">Include subfolders</span>
</label>
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { api } from "@/lib/api";
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
import TruncatedLabel from "@/components/TruncatedLabel";
@@ -76,7 +77,18 @@ function CopyButton({ value }: { value: string }) {
);
}
export function SessionReportsSection({
export function SessionReportsSection(props: SessionReportsSectionProps) {
// useSearchParams() in the inner component requires a Suspense boundary for
// static prerender (this section renders inside the prerendered
// /admin/reports/session-reports page).
return (
<Suspense fallback={null}>
<SessionReportsSectionInner {...props} />
</Suspense>
);
}
function SessionReportsSectionInner({
getAccessToken,
setError,
}: SessionReportsSectionProps) {
@@ -101,6 +113,25 @@ export function SessionReportsSection({
const [continuationStack, setContinuationStack] = useState<Array<string | null>>([]);
const [pageNumber, setPageNumber] = useState(1);
// Deep link from GA notifications: ?reportId=… auto-opens the report modal once
// the first page has loaded. Reports are listed newest-first, so a report reached
// via a fresh notification is on page 1; older ones get a hint instead.
const searchParams = useSearchParams();
const autoOpenedRef = useRef(false);
useEffect(() => {
if (autoOpenedRef.current || loading) return;
const reportIdParam = searchParams?.get("reportId");
if (!reportIdParam) return;
autoOpenedRef.current = true;
const match = reports.find((r) => r.reportId === reportIdParam);
if (match) {
setSelectedReport(match);
} else {
setError(`Report ${reportIdParam} is not on the first page — it may be older. Page through or filter by tenant to find it.`);
}
}, [loading, reports, searchParams, setError]);
useEffect(() => {
if (selectedReport) {
setAdminNoteValue(selectedReport.adminNote ?? "");
@@ -279,7 +310,7 @@ export function SessionReportsSection({
onKeyDown={(e) => {
if (e.key === "Enter") handleApplyTenantFilter();
}}
className="w-72 px-2 py-1 text-xs font-mono border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
className="w-72 px-2 py-1 text-xs font-mono border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-green-500 focus:border-green-500"
/>
<button
onClick={handleApplyTenantFilter}
@@ -514,7 +545,7 @@ export function SessionReportsSection({
readOnly={!canMutate}
rows={3}
placeholder={canMutate ? "Add an internal note about this report..." : "No note"}
className="w-full text-sm rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 resize-none placeholder:text-gray-400 read-only:opacity-60 read-only:cursor-not-allowed"
className="w-full text-sm rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-500 resize-none placeholder:text-gray-400 read-only:opacity-60 read-only:cursor-not-allowed"
/>
<div className="flex items-center justify-between mt-1.5">
<div className="text-xs">
@@ -533,7 +564,7 @@ export function SessionReportsSection({
<button
onClick={handleSaveAdminNote}
disabled={!canMutate || savingNote}
className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white rounded-md transition-colors text-xs font-medium disabled:cursor-not-allowed"
className="flex items-center gap-1.5 px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:bg-indigo-400 text-white rounded-md transition-colors text-xs font-medium disabled:cursor-not-allowed"
>
{savingNote ? (
<>
@@ -555,7 +586,7 @@ export function SessionReportsSection({
<button
onClick={() => handleDownload(selectedReport.blobName)}
disabled={downloadingBlob === selectedReport.blobName}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white rounded-md transition-colors text-sm font-medium"
className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-indigo-400 text-white rounded-md transition-colors text-sm font-medium"
>
{downloadingBlob === selectedReport.blobName ? (
<>
@@ -423,7 +423,7 @@ export function TenantAdminSection({
onChange={(e) => setNewAdminEmail(e.target.value)}
placeholder="admin@tenant.com"
autoComplete="off"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { api } from "@/lib/api";
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
import { TenantAdminSection } from "./TenantAdminSection";
@@ -48,7 +49,18 @@ export interface TenantManagementSectionProps {
setSuccessMessage: (message: string | null) => void;
}
export function TenantManagementSection({
export function TenantManagementSection(props: TenantManagementSectionProps) {
// useSearchParams() in the inner component requires a Suspense boundary for
// static prerender (this section renders inside the prerendered
// /admin/tenants/management page).
return (
<Suspense fallback={null}>
<TenantManagementSectionInner {...props} />
</Suspense>
);
}
function TenantManagementSectionInner({
tenants,
loadingTenants,
fetchTenants,
@@ -62,6 +74,18 @@ export function TenantManagementSection({
// Read-only Global Readers may view tenants (incl. config report) but not edit them.
const canMutate = useCanMutatePlatform();
const [searchQuery, setSearchQuery] = useState("");
// Deep link from GA notifications: ?tenantId=… seeds the search box once, so the
// list opens filtered to the tenant the notification is about.
const searchParams = useSearchParams();
const seededSearchRef = useRef(false);
useEffect(() => {
if (seededSearchRef.current) return;
const tenantIdParam = searchParams?.get("tenantId");
if (!tenantIdParam) return;
seededSearchRef.current = true;
setSearchQuery(tenantIdParam);
}, [searchParams]);
const [showOnlyWaitlist, setShowOnlyWaitlist] = useState(false);
const [showOnlyReady, setShowOnlyReady] = useState(false);
const [tenantSectionExpanded, setTenantSectionExpanded] = useState(false);
@@ -331,7 +355,7 @@ export function TenantManagementSection({
placeholder={'Search by domain or tenant ID (use "..." for exact match)'}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-10 py-2 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
className="w-full pl-10 pr-10 py-2 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
{searchQuery && (
<button
@@ -349,7 +373,7 @@ export function TenantManagementSection({
onClick={() => { setShowOnlyReady(v => !v); setCurrentPage(0); }}
className={`flex items-center space-x-1 px-3 py-2 text-sm rounded-lg border transition-colors whitespace-nowrap ${
showOnlyReady
? 'bg-blue-600 text-white border-blue-600'
? 'bg-green-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
}`}
>
@@ -432,7 +456,7 @@ export function TenantManagementSection({
className={`px-3 py-2 text-sm rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
previewApproved.has(tenant.tenantId)
? 'bg-amber-500 text-white hover:bg-amber-600'
: 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-green-600 text-white hover:bg-green-700'
}`}
>
{togglingPreviewTenant === tenant.tenantId
@@ -670,7 +694,7 @@ export function TenantManagementSection({
value={notificationEmail}
onChange={(e) => setNotificationEmail(e.target.value)}
placeholder="user@example.com"
className="flex-1 min-w-0 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="flex-1 min-w-0 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<button
onClick={() => handleSendWelcomeEmail(editingTenant.tenantId, notificationEmail)}
@@ -718,7 +742,7 @@ export function TenantManagementSection({
const v = e.target.value.trim();
setEditingTenant({ ...editingTenant, customRateLimitRequestsPerMinute: v === "" ? null : (parseInt(v) || null) });
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="mt-1 text-xs text-gray-500">Per-device (agent/cert) limit. Leave blank to inherit the global default.</p>
</div>
@@ -736,7 +760,7 @@ export function TenantManagementSection({
const v = e.target.value.trim();
setEditingTenant({ ...editingTenant, customUserRateLimitRequestsPerMinute: v === "" ? null : (parseInt(v) || null) });
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="mt-1 text-xs text-gray-500">Per-user (portal) limit for standard users. Leave blank to inherit the global default. Does not apply to Global Admins.</p>
</div>
@@ -787,7 +811,7 @@ export function TenantManagementSection({
const val = parseInt(e.target.value);
setEditingTenant({ ...editingTenant, dataRetentionDays: isNaN(val) ? 90 : val });
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
{editingTenant.dataRetentionDays === 0 ? (
<p className="text-xs text-amber-600 mt-1 font-medium">⚠ Infinite retention — data will never be automatically deleted</p>
@@ -16,7 +16,7 @@ export function TenantSearchSelect({
value,
onChange,
placeholder = "Search tenant by name or ID\u2026",
focusRingClass = "focus:ring-blue-500 focus:border-blue-500",
focusRingClass = "focus:ring-green-500 focus:border-green-500",
}: TenantSearchSelectProps) {
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
@@ -472,7 +472,7 @@ export function UnmappedSoftwareTab({
{autoResolving && autoResolveProgress && (
<div className="mb-3 px-4 py-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md text-sm">
<div className="flex items-center gap-2 text-blue-700 dark:text-blue-300">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600" />
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600" />
<span>
Auto-resolving... Batch {autoResolveProgress.batchIndex}/{autoResolveProgress.totalBatches}
{autoResolveProgress.resolvedSoFar > 0 && <> &middot; Resolved: {autoResolveProgress.resolvedSoFar}</>}
@@ -532,7 +532,7 @@ export function UnmappedSoftwareTab({
checked={allOnPageSelected && entries.length > 0}
onChange={allOnPageSelected ? deselectAll : selectAllOnPage}
disabled={autoResolving}
className="h-4 w-4 rounded border-gray-300 text-amber-600 focus:ring-amber-500 disabled:opacity-40"
className="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500 disabled:opacity-40"
/>
</th>
<th className="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Software Name</th>
@@ -561,7 +561,7 @@ export function UnmappedSoftwareTab({
checked={selectedEntries.has(key)}
onChange={() => toggleRowSelection(entry)}
disabled={autoResolving}
className="h-4 w-4 rounded border-gray-300 text-amber-600 focus:ring-amber-500 disabled:opacity-40"
className="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500 disabled:opacity-40"
/>
)}
</div>
@@ -517,7 +517,7 @@ export function VulnerabilityDataSection({
<button
onClick={handleSyncMsrc}
disabled={!canMutate || syncingMsrc || syncing || reseedingSeed || reseedingCommunity}
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shadow-sm"
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shadow-sm"
title="Refreshes the MSRC CVRF index (last 6 Patch Tuesdays from api.msrc.microsoft.com)"
>
{syncingMsrc ? (
@@ -143,7 +143,7 @@ function CustomsArchiveDetailContent() {
type="button"
onClick={load}
disabled={loading}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
className="px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
>
{loading ? "Loading…" : "Refresh"}
</button>
@@ -102,7 +102,7 @@ export default function CustomsArchivePage() {
type="button"
onClick={load}
disabled={loading}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
className="px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:opacity-50 text-white text-sm rounded-md transition-colors"
>
{loading ? "Loading…" : "Refresh"}
</button>
@@ -1,5 +1,6 @@
"use client";
import { SegmentedControl, TIME_RANGE_OPTIONS } from "@/components/SegmentedControl";
import { useCallback, useEffect, useState } from "react";
import { useAuth } from "../../../../contexts/AuthContext";
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
@@ -134,21 +135,11 @@ export function SectionMcpUsage() {
<p className="text-sm text-gray-500">API usage across all MCP-enabled users</p>
</div>
<div className="flex items-center gap-3">
<div className="flex rounded-md shadow-sm">
{(["7d", "30d", "90d"] as DateRange[]).map((range) => (
<button
key={range}
onClick={() => setDateRange(range)}
className={`px-3 py-1.5 text-sm font-medium border ${
dateRange === range
? "bg-indigo-600 text-white border-indigo-600 z-10"
: "bg-white text-gray-700 border-gray-300 hover:bg-gray-50"
} ${range === "7d" ? "rounded-l-md" : ""} ${range === "90d" ? "rounded-r-md" : ""} -ml-px first:ml-0`}
>
{range}
</button>
))}
</div>
<SegmentedControl
options={TIME_RANGE_OPTIONS}
value={dateRange}
onChange={(v) => setDateRange(v as DateRange)}
/>
<button
onClick={() => fetchData(dateRange)}
disabled={loading}
@@ -186,7 +186,7 @@ export function SectionPlatformUsage() {
if (error) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="min-h-screen bg-[var(--lp-bg)] flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md w-full">
<div className="text-center">
<svg className="h-12 w-12 text-red-500 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -197,7 +197,7 @@ export function SectionPlatformUsage() {
<div className="mt-6 space-y-3">
<button
onClick={() => fetchMetrics()}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
className="w-full px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Retry
</button>
@@ -276,7 +276,7 @@ export function RestoreBrowserTab({ getAccessToken, setError, setSuccessMessage
setSelectedTenantId("");
}
}}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500"
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-purple-600 accent-purple-600 focus:ring-purple-500"
/>
<label
htmlFor="restore-browser-only-with-data"
@@ -136,7 +136,7 @@ export function RestoreConfirmDialog({
placeholder="e.g. customer support — accidental delete"
maxLength={200}
disabled={submitting}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded text-sm text-gray-900 dark:text-white bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded text-sm text-gray-900 dark:text-white bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-60"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Stored on the <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded">deletion_restored</code> audit row when the real restore runs. Dry-runs are intentionally audit-free.
@@ -174,7 +174,7 @@ export function RestoreConfirmDialog({
<button
onClick={() => submit(true)}
disabled={submitting}
className="px-3 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
className="px-3 py-2 text-sm bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
>
{submitting ? "Running…" : dryRunResult ? "Run dry-run again" : "Run dry-run"}
</button>
@@ -191,7 +191,7 @@ function InFlightTab({
renderRowActions={(row) => (
<button
onClick={() => setPreviewing(row)}
className="px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
className="px-3 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700"
>
Preview
</button>
@@ -368,7 +368,7 @@ function StrandedTab({
renderRowActions={(row) => (
<button
onClick={() => setPreviewing(row)}
className="px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700"
className="px-3 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700"
>
Preview
</button>
@@ -144,7 +144,7 @@ export function SectionUsagePlans() {
<button
onClick={handleSave}
disabled={saving || !hasChanges}
className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-md hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="px-4 py-2 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{saving ? "Saving..." : "Save Changes"}
</button>
@@ -182,7 +182,7 @@ export function SectionUsagePlans() {
<p className="text-gray-500 mb-4">No plan tiers defined yet.</p>
<button
onClick={addTier}
className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition-colors"
className="px-4 py-2 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
>
Create First Plan Tier
</button>
@@ -203,7 +203,7 @@ export function SectionUsagePlans() {
value={tier.name}
onChange={(e) => updateTier(index, "name", e.target.value)}
placeholder="e.g., free, pro, enterprise"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
{/* Daily Limit */}
@@ -214,7 +214,7 @@ export function SectionUsagePlans() {
min={0}
value={tier.dailyRequestLimit}
onChange={(e) => updateTier(index, "dailyRequestLimit", parseInt(e.target.value) || 0)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
{/* Monthly Limit */}
@@ -225,7 +225,7 @@ export function SectionUsagePlans() {
min={0}
value={tier.monthlyRequestLimit}
onChange={(e) => updateTier(index, "monthlyRequestLimit", parseInt(e.target.value) || 0)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
{/* Description */}
@@ -236,7 +236,7 @@ export function SectionUsagePlans() {
value={tier.description}
onChange={(e) => updateTier(index, "description", e.target.value)}
placeholder="Brief description"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
@@ -578,7 +578,7 @@ export default function AnalyzeRuleCard({
<span>Export</span>
</button>
{canEdit && (
<button onClick={() => onStartEditing(rule)} className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors flex items-center space-x-2" title="Edit rule">
<button onClick={() => onStartEditing(rule)} className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center space-x-2" title="Edit rule">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
<span>Edit</span>
</button>
@@ -655,7 +655,7 @@ export default function AnalyzeRuleCard({
} else {
onSaveEdit(rule);
}
}} disabled={saving} className="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2 text-sm font-medium">
}} disabled={saving} className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2 text-sm font-medium">
{saving ? (<><div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div><span>Saving...</span></>) : (<span>Save Changes</span>)}
</button>
</div>
@@ -25,7 +25,7 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
{showRuleId && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Rule ID <span className="text-red-500">*</span></label>
<input type="text" value={form.ruleId} onChange={(e) => setForm({ ...form, ruleId: e.target.value })} placeholder="e.g., ANALYZE-CUSTOM-001" autoComplete="off" className={`w-full px-4 py-2 border rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 ${isDuplicate ? "border-red-400 focus:ring-red-300 focus:border-red-400" : "border-gray-300 focus:ring-indigo-500 focus:border-indigo-500"}`} />
<input type="text" value={form.ruleId} onChange={(e) => setForm({ ...form, ruleId: e.target.value })} placeholder="e.g., ANALYZE-CUSTOM-001" autoComplete="off" className={`w-full px-4 py-2 border rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 ${isDuplicate ? "border-red-400 focus:ring-red-300 focus:border-red-400" : "border-gray-300 focus:ring-green-500 focus:border-green-500"}`} />
{isDuplicate && (
<p className="mt-1 text-xs text-red-600">A rule with this ID already exists. Please choose a unique Rule ID.</p>
)}
@@ -33,31 +33,31 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Title <span className="text-red-500">*</span></label>
<input type="text" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} placeholder="e.g., Proxy Authentication Failure" autoComplete="off" className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" />
<input type="text" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} placeholder="e.g., Proxy Authentication Failure" autoComplete="off" className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Describe what this rule detects..." rows={2} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-none" />
<textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Describe what this rule detects..." rows={2} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 resize-none" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Severity</label>
<select value={form.severity} onChange={(e) => setForm({ ...form, severity: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
<select value={form.severity} onChange={(e) => setForm({ ...form, severity: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500">
{SEVERITIES.map((s) => (<option key={s} value={s}>{s.charAt(0).toUpperCase() + s.slice(1)}</option>))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
<select value={form.category} onChange={(e) => setForm({ ...form, category: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
<select value={form.category} onChange={(e) => setForm({ ...form, category: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500">
{CATEGORIES.map((cat) => (<option key={cat} value={cat}>{cat.charAt(0).toUpperCase() + cat.slice(1)}</option>))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Trigger Type</label>
<select value={form.trigger} onChange={(e) => setForm({ ...form, trigger: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
<select value={form.trigger} onChange={(e) => setForm({ ...form, trigger: e.target.value })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500">
{TRIGGERS.map((t) => (<option key={t} value={t}>{t.charAt(0).toUpperCase() + t.slice(1)}</option>))}
</select>
</div>
@@ -70,7 +70,7 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
<label className="block text-sm font-semibold text-gray-700">Preconditions</label>
<p className="text-xs text-gray-500">Optional. Rule is silently skipped (no result) when ANY precondition fails. Useful e.g. to skip on virtual machines.</p>
</div>
<button type="button" onClick={() => setForm({ ...form, preconditions: [...form.preconditions, { ...EMPTY_PRECONDITION }] })} className="text-xs text-indigo-600 hover:text-indigo-800 font-medium">+ Add Precondition</button>
<button type="button" onClick={() => setForm({ ...form, preconditions: [...form.preconditions, { ...EMPTY_PRECONDITION }] })} className="text-xs text-green-700 hover:text-green-800 font-medium">+ Add Precondition</button>
</div>
{form.preconditions.length === 0 ? (
<p className="text-xs text-gray-400 italic">No preconditions. Rule applies to every device.</p>
@@ -108,7 +108,7 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-semibold text-gray-700">Conditions</label>
<button type="button" onClick={() => setForm({ ...form, conditions: [...form.conditions, { ...EMPTY_CONDITION }] })} className="text-xs text-indigo-600 hover:text-indigo-800 font-medium">+ Add Condition</button>
<button type="button" onClick={() => setForm({ ...form, conditions: [...form.conditions, { ...EMPTY_CONDITION }] })} className="text-xs text-green-700 hover:text-green-800 font-medium">+ Add Condition</button>
</div>
<div className="space-y-3">
{form.conditions.map((cond, idx) => {
@@ -134,22 +134,22 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
{/* Row 1: Signal, Source, Event Type A */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<input type="text" value={cond.signal} onChange={(e) => updateCond({ signal: e.target.value })} placeholder="Signal name" autoComplete="off" className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<select value={cond.source} onChange={(e) => updateCond({ source: e.target.value })} className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<input type="text" value={cond.signal} onChange={(e) => updateCond({ signal: e.target.value })} placeholder="Signal name" autoComplete="off" className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
<select value={cond.source} onChange={(e) => updateCond({ source: e.target.value })} className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 bg-white focus:outline-none focus:ring-1 focus:ring-green-500">
{SOURCES.map((s) => (<option key={s} value={s}>{s}</option>))}
</select>
<input type="text" value={cond.eventType} onChange={(e) => updateCond({ eventType: e.target.value })} placeholder={isCorrelation ? "Event A type (e.g. app_install_completed)" : "Event type"} autoComplete="off" className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="text" value={cond.eventType} onChange={(e) => updateCond({ eventType: e.target.value })} placeholder={isCorrelation ? "Event A type (e.g. app_install_completed)" : "Event type"} autoComplete="off" className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
</div>
{/* Row 2: Data field / Operator / Value / Required */}
<div className="grid grid-cols-1 sm:grid-cols-4 gap-2">
<input type="text" value={cond.dataField} onChange={(e) => updateCond({ dataField: e.target.value })} placeholder={isCorrelation ? "Filter field on Event B" : isArray ? "Array field (e.g. artifacts)" : "Data field"} autoComplete="off" className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<select value={cond.operator} onChange={(e) => updateCond({ operator: e.target.value })} className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<input type="text" value={cond.dataField} onChange={(e) => updateCond({ dataField: e.target.value })} placeholder={isCorrelation ? "Filter field on Event B" : isArray ? "Array field (e.g. artifacts)" : "Data field"} autoComplete="off" className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
<select value={cond.operator} onChange={(e) => updateCond({ operator: e.target.value })} className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 bg-white focus:outline-none focus:ring-1 focus:ring-green-500">
{OPERATORS.map((o) => (<option key={o} value={o}>{o}</option>))}
</select>
<input type="text" value={cond.value} onChange={(e) => updateCond({ value: e.target.value })} placeholder={isCorrelation ? "Filter value on Event B" : isArray ? "Value / allow-list regex" : "Value"} autoComplete="off" className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="text" value={cond.value} onChange={(e) => updateCond({ value: e.target.value })} placeholder={isCorrelation ? "Filter value on Event B" : isArray ? "Value / allow-list regex" : "Value"} autoComplete="off" className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
<label className="flex items-center space-x-2 text-sm text-gray-700">
<input type="checkbox" checked={cond.required} onChange={(e) => updateCond({ required: e.target.checked })} className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
<input type="checkbox" checked={cond.required} onChange={(e) => updateCond({ required: e.target.checked })} className="rounded border-gray-300 text-green-600 focus:ring-green-500" />
<span>Required</span>
</label>
</div>
@@ -163,15 +163,15 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div>
<label className="block text-xs text-gray-500 mb-1">Event B type <span className="text-red-500">*</span></label>
<input type="text" value={cond.correlateEventType ?? ""} onChange={(e) => updateCond({ correlateEventType: e.target.value })} placeholder="e.g. app_install_failed" autoComplete="off" className="w-full px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="text" value={cond.correlateEventType ?? ""} onChange={(e) => updateCond({ correlateEventType: e.target.value })} placeholder="e.g. app_install_failed" autoComplete="off" className="w-full px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Join field <span className="text-red-500">*</span></label>
<input type="text" value={cond.joinField ?? ""} onChange={(e) => updateCond({ joinField: e.target.value })} placeholder="e.g. appId" autoComplete="off" className="w-full px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="text" value={cond.joinField ?? ""} onChange={(e) => updateCond({ joinField: e.target.value })} placeholder="e.g. appId" autoComplete="off" className="w-full px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Time window (seconds, optional)</label>
<input type="number" min={0} value={cond.timeWindowSeconds ?? ""} onChange={(e) => updateCond({ timeWindowSeconds: e.target.value === "" ? null : parseInt(e.target.value) || 0 })} placeholder="e.g. 300" className="w-full px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="number" min={0} value={cond.timeWindowSeconds ?? ""} onChange={(e) => updateCond({ timeWindowSeconds: e.target.value === "" ? null : parseInt(e.target.value) || 0 })} placeholder="e.g. 300" className="w-full px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
</div>
</div>
@@ -179,11 +179,11 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
<div>
<label className="block text-xs text-gray-500 mb-1">Event A filter (optional)</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<input type="text" value={cond.eventAFilterField ?? ""} onChange={(e) => updateCond({ eventAFilterField: e.target.value })} placeholder="Filter field on Event A" autoComplete="off" className="px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<select value={cond.eventAFilterOperator ?? "equals"} onChange={(e) => updateCond({ eventAFilterOperator: e.target.value })} className="px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 bg-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<input type="text" value={cond.eventAFilterField ?? ""} onChange={(e) => updateCond({ eventAFilterField: e.target.value })} placeholder="Filter field on Event A" autoComplete="off" className="px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
<select value={cond.eventAFilterOperator ?? "equals"} onChange={(e) => updateCond({ eventAFilterOperator: e.target.value })} className="px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 bg-white focus:outline-none focus:ring-1 focus:ring-green-500">
{OPERATORS.map((o) => (<option key={o} value={o}>{o}</option>))}
</select>
<input type="text" value={cond.eventAFilterValue ?? ""} onChange={(e) => updateCond({ eventAFilterValue: e.target.value })} placeholder="Filter value on Event A" autoComplete="off" className="px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="text" value={cond.eventAFilterValue ?? ""} onChange={(e) => updateCond({ eventAFilterValue: e.target.value })} placeholder="Filter value on Event A" autoComplete="off" className="px-3 py-1.5 border border-indigo-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
</div>
</div>
</div>
@@ -212,11 +212,11 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Base Confidence (%)</label>
<input type="number" min={0} max={100} value={form.baseConfidence} onChange={(e) => setForm({ ...form, baseConfidence: parseInt(e.target.value) || 0 })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" />
<input type="number" min={0} max={100} value={form.baseConfidence} onChange={(e) => setForm({ ...form, baseConfidence: parseInt(e.target.value) || 0 })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Confidence Threshold (%)</label>
<input type="number" min={0} max={100} value={form.confidenceThreshold} onChange={(e) => setForm({ ...form, confidenceThreshold: parseInt(e.target.value) || 0 })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" />
<input type="number" min={0} max={100} value={form.confidenceThreshold} onChange={(e) => setForm({ ...form, confidenceThreshold: parseInt(e.target.value) || 0 })} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500" />
</div>
</div>
@@ -224,7 +224,7 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-semibold text-gray-700">Confidence Factors</label>
<button type="button" onClick={() => setForm({ ...form, confidenceFactors: [...form.confidenceFactors, { ...EMPTY_FACTOR }] })} className="text-xs text-indigo-600 hover:text-indigo-800 font-medium">+ Add Factor</button>
<button type="button" onClick={() => setForm({ ...form, confidenceFactors: [...form.confidenceFactors, { ...EMPTY_FACTOR }] })} className="text-xs text-green-700 hover:text-green-800 font-medium">+ Add Factor</button>
</div>
{form.confidenceFactors.length === 0 ? (
<p className="text-xs text-gray-400 italic">No confidence factors. Click &quot;+ Add Factor&quot; to add one.</p>
@@ -232,9 +232,9 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
<div className="space-y-2">
{form.confidenceFactors.map((factor, idx) => (
<div key={idx} className="flex items-center gap-2">
<input type="text" value={factor.signal} onChange={(e) => { const f = [...form.confidenceFactors]; f[idx] = { ...f[idx], signal: e.target.value }; setForm({ ...form, confidenceFactors: f }); }} placeholder="Signal" autoComplete="off" className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="text" value={factor.condition} onChange={(e) => { const f = [...form.confidenceFactors]; f[idx] = { ...f[idx], condition: e.target.value }; setForm({ ...form, confidenceFactors: f }); }} placeholder="Condition" autoComplete="off" className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="number" value={factor.weight} onChange={(e) => { const f = [...form.confidenceFactors]; f[idx] = { ...f[idx], weight: parseInt(e.target.value) || 0 }; setForm({ ...form, confidenceFactors: f }); }} className="w-20 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="text" value={factor.signal} onChange={(e) => { const f = [...form.confidenceFactors]; f[idx] = { ...f[idx], signal: e.target.value }; setForm({ ...form, confidenceFactors: f }); }} placeholder="Signal" autoComplete="off" className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
<input type="text" value={factor.condition} onChange={(e) => { const f = [...form.confidenceFactors]; f[idx] = { ...f[idx], condition: e.target.value }; setForm({ ...form, confidenceFactors: f }); }} placeholder="Condition" autoComplete="off" className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
<input type="number" value={factor.weight} onChange={(e) => { const f = [...form.confidenceFactors]; f[idx] = { ...f[idx], weight: parseInt(e.target.value) || 0 }; setForm({ ...form, confidenceFactors: f }); }} className="w-20 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 focus:outline-none focus:ring-1 focus:ring-green-500" />
<span className="text-xs text-gray-500">%</span>
<button type="button" onClick={() => setForm({ ...form, confidenceFactors: form.confidenceFactors.filter((_, i) => i !== idx) })} className="text-red-400 hover:text-red-600">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
@@ -248,14 +248,14 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
{/* Explanation */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Explanation</label>
<textarea value={form.explanation} onChange={(e) => setForm({ ...form, explanation: e.target.value })} placeholder="Detailed explanation shown when this rule fires..." rows={3} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-none" />
<textarea value={form.explanation} onChange={(e) => setForm({ ...form, explanation: e.target.value })} placeholder="Detailed explanation shown when this rule fires..." rows={3} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 resize-none" />
</div>
{/* Remediation Steps */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-semibold text-gray-700">Remediation Steps</label>
<button type="button" onClick={() => setForm({ ...form, remediation: [...form.remediation, { title: "", steps: [""] }] })} className="text-xs text-indigo-600 hover:text-indigo-800 font-medium">+ Add Section</button>
<button type="button" onClick={() => setForm({ ...form, remediation: [...form.remediation, { title: "", steps: [""] }] })} className="text-xs text-green-700 hover:text-green-800 font-medium">+ Add Section</button>
</div>
{form.remediation.length === 0 ? (
<p className="text-xs text-gray-400 italic">No remediation steps. Click &quot;+ Add Section&quot; to add one.</p>
@@ -289,7 +289,7 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-semibold text-gray-700">Related Documentation</label>
<button type="button" onClick={() => setForm({ ...form, relatedDocs: [...form.relatedDocs, { title: "", url: "" }] })} className="text-xs text-indigo-600 hover:text-indigo-800 font-medium">+ Add Link</button>
<button type="button" onClick={() => setForm({ ...form, relatedDocs: [...form.relatedDocs, { title: "", url: "" }] })} className="text-xs text-green-700 hover:text-green-800 font-medium">+ Add Link</button>
</div>
{form.relatedDocs.length === 0 ? (
<p className="text-xs text-gray-400 italic">No related docs. Click &quot;+ Add Link&quot; to add one.</p>
@@ -297,8 +297,8 @@ export default function AnalyzeRuleFormFields({ form, setForm, showRuleId, exist
<div className="space-y-2">
{form.relatedDocs.map((doc, idx) => (
<div key={idx} className="flex items-center gap-2">
<input type="text" value={doc.title} onChange={(e) => { const d = [...form.relatedDocs]; d[idx] = { ...d[idx], title: e.target.value }; setForm({ ...form, relatedDocs: d }); }} placeholder="Link title" autoComplete="off" className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="text" value={doc.url} onChange={(e) => { const d = [...form.relatedDocs]; d[idx] = { ...d[idx], url: e.target.value }; setForm({ ...form, relatedDocs: d }); }} placeholder="https://..." autoComplete="off" className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500" />
<input type="text" value={doc.title} onChange={(e) => { const d = [...form.relatedDocs]; d[idx] = { ...d[idx], title: e.target.value }; setForm({ ...form, relatedDocs: d }); }} placeholder="Link title" autoComplete="off" className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
<input type="text" value={doc.url} onChange={(e) => { const d = [...form.relatedDocs]; d[idx] = { ...d[idx], url: e.target.value }; setForm({ ...form, relatedDocs: d }); }} placeholder="https://..." autoComplete="off" className="flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500" />
<button type="button" onClick={() => setForm({ ...form, relatedDocs: form.relatedDocs.filter((_, i) => i !== idx) })} className="text-red-400 hover:text-red-600">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
@@ -563,7 +563,7 @@ export default function AnalyzeRulesPage() {
</svg>
<p className="text-sm text-blue-800">
Missing a rule, found a bug, or have an improvement idea? Help make Autopilot Monitor better —{" "}
<a href="https://github.com/okieselbach/Autopilot-Monitor/issues" target="_blank" rel="noopener noreferrer" className="font-medium underline hover:text-blue-900">
<a href="https://github.com/okieselbach/AutopilotMonitor/issues" target="_blank" rel="noopener noreferrer" className="font-medium underline hover:text-blue-900">
open a GitHub issue
</a>.
</p>
@@ -749,7 +749,7 @@ export default function AnalyzeRulesPage() {
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /></svg>
<div>
<h2 className="text-xl font-semibold text-gray-900">Create Custom Analyze Rule</h2>
<p className="text-sm text-gray-500 mt-1">Define conditions and confidence scoring for issue detection — <a href={`${DOCS_URL}/rules/analyze-rules`} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">see documentation</a></p>
<p className="text-sm text-gray-500 mt-1">Define conditions and confidence scoring for issue detection — <a href={`${DOCS_URL}/rules/analyze-rules`} target="_blank" rel="noopener noreferrer" className="text-green-700 hover:text-green-800 underline">see documentation</a></p>
</div>
</div>
{/* JSON Mode Toggle */}
@@ -804,7 +804,7 @@ export default function AnalyzeRulesPage() {
} else {
handleCreateRule();
}
}} disabled={creating} className="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2 text-sm font-medium">
}} disabled={creating} className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2 text-sm font-medium">
{creating ? (<><div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div><span>Creating...</span></>) : (<span>Save Rule</span>)}
</button>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

@@ -254,7 +254,7 @@ export default function InstallsTab({ scope, timeRange }: InstallsTabProps) {
placeholder="Search apps by name…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent text-sm"
/>
</div>
@@ -138,7 +138,7 @@ export default function InventoryTab({ scope }: { scope: SoftwareTabScope }) {
placeholder="Search software by name or publisher…"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent text-sm"
/>
</div>
@@ -392,7 +392,7 @@ function AppDetailContent() {
// Tenant scope is carried in sessionStorage, so the list restores it on its own.
router.push("/apps");
}}
className="text-sm text-blue-600 hover:underline mb-2 inline-flex items-center"
className="text-sm text-green-700 hover:underline mb-2 inline-flex items-center"
>
← Back to all apps
</button>
@@ -420,7 +420,7 @@ function AppDetailContent() {
onClick={() => setDays(d)}
className={`px-4 py-2 text-sm rounded-md transition-colors ${
days === d
? "bg-blue-600 text-white"
? "bg-green-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
@@ -693,7 +693,7 @@ function AppDetailContent() {
}}
className={`px-3 py-1 text-xs rounded-md ${
statusFilter === s
? "bg-blue-600 text-white"
? "bg-green-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
@@ -738,7 +738,7 @@ function AppDetailContent() {
type="checkbox"
checked={visibleSessionColumns.has(col.key)}
onChange={() => toggleSessionColumn(col.key)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 h-3.5 w-3.5"
className="rounded border-gray-300 text-green-600 focus:ring-green-500 h-3.5 w-3.5"
/>
<span className="flex-1">{col.label}</span>
{col.globalOnly && (
@@ -7,6 +7,7 @@ import { ProtectedRoute } from "../../components/ProtectedRoute";
import { useAggregatedAdminScope } from "@/hooks";
import { GlobalAdminBanner, globalAdminSubtitle } from "@/components/GlobalAdminBanner";
import { TenantScopeSelector } from "@/components/TenantScopeSelector";
import { SegmentedControl, TIME_RANGE_OPTIONS } from "@/components/SegmentedControl";
import InstallsTab from "./components/InstallsTab";
import InventoryTab from "./components/InventoryTab";
import VulnerabilitiesTab from "./components/VulnerabilitiesTab";
@@ -39,26 +40,20 @@ function SoftwareHub() {
<GlobalAdminBanner show={scope.isGlobalAdmin} delegated={scope.isDelegatedScope} subtitle={globalAdminSubtitle(scope)} />
<header className="bg-white shadow">
<div className="max-w-7xl mx-auto pt-6 px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-center justify-between gap-y-3">
<div>
<h1 className="text-2xl font-normal text-gray-900">Software</h1>
<p className="text-sm text-gray-500 mt-1">
App installs, the installed-software inventory, and vulnerability exposure across enrollments.
</p>
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-2">
<TenantScopeSelector scope={scope} allowAggregated />
{(["7d", "30d", "90d"] as const).map((range) => (
<button
key={range}
onClick={() => setTimeRange(range)}
className={`px-4 py-2 text-sm rounded-md transition-colors ${
timeRange === range ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{range === "7d" ? "7 Days" : range === "30d" ? "30 Days" : "90 Days"}
</button>
))}
<SegmentedControl
options={TIME_RANGE_OPTIONS}
value={timeRange}
onChange={(v) => setTimeRange(v as typeof timeRange)}
/>
</div>
</div>
@@ -70,7 +65,7 @@ function SoftwareHub() {
onClick={() => selectTab(t.id)}
className={`-mb-px border-b-2 px-1 py-3 text-sm font-medium transition-colors ${
activeTab === t.id
? "border-blue-600 text-blue-700"
? "border-green-600 text-green-700"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
@@ -248,7 +248,7 @@ export default function AuditPage() {
/>
<header className="bg-white shadow">
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-center justify-between gap-y-3">
<div>
<div>
<h1 className="text-2xl font-normal text-gray-900">Audit Log</h1>
@@ -262,7 +262,7 @@ export default function AuditPage() {
</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-2">
<TenantScopeSelector scope={scope} allowAggregated />
<button
onClick={handleRefresh}
@@ -289,14 +289,14 @@ export default function AuditPage() {
type="date"
value={isoToDateInputValue(dateFromIso)}
onChange={(e) => setDateFromIso(dateInputToIsoStart(e.target.value))}
className="px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-green-500"
/>
<label className="text-sm text-gray-600 dark:text-gray-400">To:</label>
<input
type="date"
value={isoToDateInputValue(dateToIso)}
onChange={(e) => setDateToIso(dateInputToIsoEnd(e.target.value))}
className="px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
@@ -310,7 +310,7 @@ export default function AuditPage() {
placeholder="Search by user, entity, action or details (this page only)..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="w-full pl-10 pr-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
@@ -318,7 +318,7 @@ export default function AuditPage() {
<select
value={actionFilter}
onChange={(e) => setActionFilter(e.target.value as ActionFilter)}
className="px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-green-500"
>
<option value="DEFAULT">All (excl. deletions)</option>
<option value="ALL">All Actions</option>
@@ -331,7 +331,7 @@ export default function AuditPage() {
<select
value={entityTypeFilter}
onChange={(e) => setEntityTypeFilter(e.target.value)}
className="px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-green-500"
>
{entityTypes.map(type => (
<option key={type} value={type}>
@@ -465,7 +465,7 @@ export default function AuditPage() {
<button
onClick={handleNextPage}
disabled={!nextLink || refreshing}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Next &rarr;
</button>
@@ -333,7 +333,7 @@ export function SessionTable({
<select
value={sessionsPerPage}
onChange={(e) => onSessionsPerPageChange(Number(e.target.value))}
className="px-2 py-1.5 rounded-lg text-xs font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 border border-gray-200 transition-colors cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500"
className="px-2 py-1.5 rounded-lg text-xs font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 border border-gray-200 transition-colors cursor-pointer focus:outline-none focus:ring-2 focus:ring-green-500"
title="Sessions per page"
>
{[10, 15, 20, 50, 100].map((n) => (
@@ -399,7 +399,7 @@ export function SessionTable({
type="checkbox"
checked={visibleColumns.has(col.key)}
onChange={() => toggleColumn(col.key)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 h-3.5 w-3.5"
className="rounded border-gray-300 text-green-600 focus:ring-green-500 h-3.5 w-3.5"
/>
{col.label}
</label>
@@ -462,7 +462,7 @@ export function SessionTable({
}
}
}}
className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
{searchQuery && (
<button
@@ -557,7 +557,7 @@ export function SessionTable({
setShowSearchSuggestions(false);
onLoadAll();
}}
className="text-xs font-medium text-blue-600 hover:text-blue-800 transition-colors"
className="text-xs font-medium text-green-700 hover:text-green-800 transition-colors"
>
Search all sessions
</button>
@@ -574,7 +574,7 @@ export function SessionTable({
{(["Succeeded", "InProgress", "Pending", "Stalled", "AwaitingUser", "Failed", "Incomplete"] as const).map((status) => {
const config: Record<string, { bg: string; bgActive: string; text: string; label: string }> = {
Succeeded: { bg: "bg-green-50 text-green-700 border-green-200 hover:bg-green-100", bgActive: "bg-green-600 text-white border-green-600", text: "text-green-600", label: "Succeeded" },
InProgress: { bg: "bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100", bgActive: "bg-blue-600 text-white border-blue-600", text: "text-blue-600", label: "In Progress" },
InProgress: { bg: "bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100", bgActive: "bg-green-600 text-white border-blue-600", text: "text-blue-600", label: "In Progress" },
Pending: { bg: "bg-amber-50 text-amber-700 border-amber-200 hover:bg-amber-100", bgActive: "bg-amber-500 text-white border-amber-500", text: "text-amber-600", label: "Pending" },
Stalled: { bg: "bg-orange-50 text-orange-700 border-orange-200 hover:bg-orange-100", bgActive: "bg-orange-600 text-white border-orange-600", text: "text-orange-600", label: "Stalled" },
// Non-terminal reclassification state (Device Setup done, waiting on user) — sky, mirrors SessionStatusBadge.
@@ -740,7 +740,7 @@ export function SessionTable({
<button
onClick={onNextPage}
disabled={(currentPage >= totalPages && !hasMore) || loadingMore}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2"
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2"
>
{loadingMore && (
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
@@ -803,7 +803,7 @@ function SessionCell({
e.stopPropagation();
navigator.clipboard.writeText(session.tenantId);
}}
className="group flex items-center gap-1 text-xs font-mono text-gray-600 hover:text-blue-600 transition-colors"
className="group flex items-center gap-1 text-xs font-mono text-gray-600 hover:text-green-600 transition-colors"
title={session.tenantId}
>
<span>{session.tenantId.split('-').slice(0, 2).join('-')}...</span>
@@ -1098,7 +1098,7 @@ function SortableHeader({
e.stopPropagation();
onFilterToggle();
}}
className={`p-0.5 rounded transition-colors ${hasActiveFilter ? "text-blue-600 hover:text-blue-800" : "text-gray-300 hover:text-gray-500"}`}
className={`p-0.5 rounded transition-colors ${hasActiveFilter ? "text-green-700 hover:text-green-800" : "text-gray-300 hover:text-gray-500"}`}
title={hasActiveFilter ? `Filtered (${activeFilter!.size} selected)` : "Filter this column"}
>
<svg className="w-3 h-3" fill={hasActiveFilter ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
@@ -1123,7 +1123,7 @@ function SortableHeader({
placeholder="Search..."
value={filterSearch}
onChange={(e) => setFilterSearch(e.target.value)}
className="w-full px-2 py-1 text-xs border border-gray-200 rounded text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500"
className="w-full px-2 py-1 text-xs border border-gray-200 rounded text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-green-500"
onClick={(e) => e.stopPropagation()}
/>
</div>
@@ -1148,7 +1148,7 @@ function SortableHeader({
}
onFilterChange(filterKey, next);
}}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 h-3 w-3"
className="rounded border-gray-300 text-green-600 focus:ring-green-500 h-3 w-3"
/>
<span className="truncate">{value}</span>
</label>
@@ -68,7 +68,7 @@ export function UserMenu({
<div className="text-xs text-gray-500 truncate">
{email}
</div>
<button className="text-xs text-blue-600 hover:text-blue-800 mt-1">
<button className="text-xs text-green-700 hover:text-green-800 mt-1">
View account
</button>
</div>
@@ -25,23 +25,23 @@ export function WelcomeMessage() {
{/* Steps */}
<ol className="space-y-3 mb-7">
<li className="flex items-start gap-3">
<span className="shrink-0 mt-0.5 w-5 h-5 rounded-full bg-blue-600 text-white text-[10px] font-bold flex items-center justify-center">1</span>
<span className="shrink-0 mt-0.5 w-5 h-5 rounded-full bg-green-600 text-white text-[10px] font-bold flex items-center justify-center">1</span>
<p className="text-sm text-gray-700">
Follow the{" "}
<a href={`${DOCS_URL}/getting-started/portal-setup`} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline font-medium">setup guide</a>
<a href={`${DOCS_URL}/getting-started/portal-setup`} target="_blank" rel="noopener noreferrer" className="text-green-700 hover:underline font-medium">setup guide</a>
{" "}to configure your tenant and grant the required permissions.
</p>
</li>
<li className="flex items-start gap-3">
<span className="shrink-0 mt-0.5 w-5 h-5 rounded-full bg-blue-600 text-white text-[10px] font-bold flex items-center justify-center">2</span>
<span className="shrink-0 mt-0.5 w-5 h-5 rounded-full bg-green-600 text-white text-[10px] font-bold flex items-center justify-center">2</span>
<p className="text-sm text-gray-700">
Deploy the{" "}
<a href={`${DOCS_URL}/getting-started/deploy-the-agent`} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline font-medium">Intune bootstrapper</a>
<a href={`${DOCS_URL}/getting-started/deploy-the-agent`} target="_blank" rel="noopener noreferrer" className="text-green-700 hover:underline font-medium">Intune bootstrapper</a>
{" "}to your Autopilot device groups — the agent installs automatically on first boot.
</p>
</li>
<li className="flex items-start gap-3">
<span className="shrink-0 mt-0.5 w-5 h-5 rounded-full bg-blue-600 text-white text-[10px] font-bold flex items-center justify-center">3</span>
<span className="shrink-0 mt-0.5 w-5 h-5 rounded-full bg-green-600 text-white text-[10px] font-bold flex items-center justify-center">3</span>
<p className="text-sm text-gray-700">Enrollment sessions will appear here in real time as devices go through the enrollment process.</p>
</li>
</ol>
@@ -66,7 +66,7 @@ export function WelcomeMessage() {
</svg>
</a>
<a
href="https://github.com/okieselbach/Autopilot-Monitor"
href="https://github.com/okieselbach/AutopilotMonitor"
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 hover:border-gray-400 hover:bg-gray-100 transition-colors"
@@ -299,7 +299,7 @@ function HomeContent() {
href={`${DOCS_URL}/changelog/platform-changelog`}
target="_blank"
rel="noopener noreferrer"
className="underline font-medium hover:text-blue-600 dark:hover:text-blue-200"
className="underline font-medium hover:text-green-600 dark:hover:text-blue-200"
>
Private Preview Changelog
</a>{" "}
@@ -308,17 +308,17 @@ function HomeContent() {
href={`${DOCS_URL}/troubleshooting/service-announcements`}
target="_blank"
rel="noopener noreferrer"
className="underline font-medium hover:text-blue-600 dark:hover:text-blue-200"
className="underline font-medium hover:text-green-600 dark:hover:text-blue-200"
>
Service Announcements
</a>
.{" "}
Feedback or bug report?{" "}
<a
href="https://github.com/okieselbach/Autopilot-Monitor/issues"
href="https://github.com/okieselbach/AutopilotMonitor/issues"
target="_blank"
rel="noopener noreferrer"
className="underline font-medium hover:text-blue-600 dark:hover:text-blue-200"
className="underline font-medium hover:text-green-600 dark:hover:text-blue-200"
>
Open a GitHub issue
</a>
@@ -327,7 +327,7 @@ function HomeContent() {
href="https://www.linkedin.com/in/oliver-kieselbach/"
target="_blank"
rel="noopener noreferrer"
className="underline font-medium hover:text-blue-600 dark:hover:text-blue-200"
className="underline font-medium hover:text-green-600 dark:hover:text-blue-200"
>
LinkedIn
</a>
@@ -343,7 +343,7 @@ function DiagnosisContent() {
onClick={() =>
router.push(sessionUrl(sessionId))
}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
className="px-4 py-2 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
>
Full Details
</button>
@@ -521,7 +521,7 @@ function DiagnosisContent() {
href={primaryResult.relatedDocs[0].url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center space-x-1 text-sm text-blue-600 hover:text-blue-800"
className="inline-flex items-center space-x-1 text-sm text-green-700 hover:text-green-800"
>
<svg
className="w-4 h-4"
@@ -654,7 +654,7 @@ function DiagnosisContent() {
e.stopPropagation();
copyRemediation(result);
}}
className="text-xs text-blue-600 hover:text-blue-800"
className="text-xs text-green-700 hover:text-green-800"
>
{copiedId === result.ruleId
? "Copied!"
@@ -719,7 +719,7 @@ function DiagnosisContent() {
href={doc.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 hover:text-blue-800 underline"
className="text-xs text-green-700 hover:text-green-800 underline"
>
{doc.title}
</a>
+2 -2
View File
@@ -29,7 +29,7 @@ export default function Error({
}, [error]);
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="min-h-screen bg-[var(--lp-bg)] flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md w-full text-center">
<div className="text-4xl mb-3">!</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">Something went wrong</h2>
@@ -39,7 +39,7 @@ export default function Error({
<div className="flex gap-3 justify-center flex-wrap">
<button
onClick={() => router.push("/")}
className="px-5 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm"
className="px-5 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium text-sm"
>
Back to Home
</button>
@@ -217,7 +217,7 @@ export default function DeviceJourneySection({
<div className="flex items-center justify-between gap-2">
<Link
href={sessionHref(d.lastSessionId)}
className="text-gray-700 hover:text-blue-600 font-mono text-xs truncate"
className="text-gray-700 hover:text-green-600 font-mono text-xs truncate"
title={`Open the latest enrollment of ${d.serialNumber}`}
>
{d.serialNumber}
@@ -17,6 +17,7 @@ import { useFleetHealth } from "./hooks/useFleetHealth";
import { useAggregatedAdminScope } from "@/hooks";
import { GlobalAdminBanner, globalAdminSubtitle } from "@/components/GlobalAdminBanner";
import { TenantScopeSelector } from "@/components/TenantScopeSelector";
import { SegmentedControl, TIME_RANGE_OPTIONS } from "@/components/SegmentedControl";
import { CardSkeleton } from "@/components/skeletons/PageSkeleton";
import { TableSkeleton } from "@/components/skeletons/TableSkeleton";
@@ -238,11 +239,11 @@ export default function FleetHealthPage() {
<GlobalAdminBanner show={scope.isGlobalAdmin} delegated={scope.isDelegatedScope} subtitle={globalAdminSubtitle(scope)} />
<header className="bg-white shadow">
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-center justify-between gap-y-3">
<div>
<div className="flex items-center space-x-3">
<svg
className="w-8 h-8 text-blue-600"
className="w-8 h-8 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -259,25 +260,13 @@ export default function FleetHealthPage() {
</h1>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-2">
<TenantScopeSelector scope={scope} allowAggregated />
{(["7d", "30d", "90d"] as const).map((range) => (
<button
key={range}
onClick={() => setTimeRange(range)}
className={`px-4 py-2 text-sm rounded-md transition-colors ${
timeRange === range
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{range === "7d"
? "Last 7 Days"
: range === "30d"
? "Last 30 Days"
: "Last 90 Days"}
</button>
))}
<SegmentedControl
options={TIME_RANGE_OPTIONS}
value={timeRange}
onChange={(v) => setTimeRange(v as typeof timeRange)}
/>
</div>
</div>
</div>
@@ -733,7 +722,7 @@ export default function FleetHealthPage() {
title={`Show failed enrollments for ${m.model}`}
>
<div className="flex items-baseline justify-between mb-1">
<span className="text-sm text-gray-700 group-hover:text-blue-600 break-words leading-snug">{m.model}</span>
<span className="text-sm text-gray-700 group-hover:text-green-600 break-words leading-snug">{m.model}</span>
<span className="ml-3 flex-shrink-0 text-sm font-medium text-gray-900">
{successRate !== null ? `${successRate}%` : "—"} <span className="text-xs font-normal text-gray-400">({m.total} devices)</span>
</span>
@@ -373,7 +373,7 @@ export function GatherRuleCard({
{canEdit && (
<button
onClick={(e) => { e.stopPropagation(); onStartEditing(); }}
className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors flex items-center space-x-2"
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center space-x-2"
title="Edit rule"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -463,7 +463,7 @@ export function GatherRuleCard({
}
}}
disabled={saving}
className="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2 text-sm font-medium"
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2 text-sm font-medium"
>
{saving ? (
<>
@@ -55,7 +55,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, ruleId: e.target.value })}
placeholder="e.g., custom-network-check"
autoComplete="off"
className={`w-full px-4 py-2 border rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 transition-colors ${isDuplicate ? "border-red-400 focus:ring-red-300 focus:border-red-400" : "border-gray-300 focus:ring-indigo-500 focus:border-indigo-500"}`}
className={`w-full px-4 py-2 border rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 transition-colors ${isDuplicate ? "border-red-400 focus:ring-red-300 focus:border-red-400" : "border-gray-300 focus:ring-green-500 focus:border-green-500"}`}
/>
{isDuplicate && (
<p className="mt-1 text-xs text-red-600">A rule with this ID already exists. Please choose a unique Rule ID.</p>
@@ -72,7 +72,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, title: e.target.value })}
placeholder="e.g., Custom Network Check"
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
</div>
</div>
@@ -85,7 +85,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, description: e.target.value })}
placeholder="Describe what this rule collects and why..."
rows={2}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors resize-none"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors resize-none"
/>
</div>
@@ -96,7 +96,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
<select
value={form.category}
onChange={(e) => setForm({ ...form, category: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
>
{CATEGORIES.map((cat) => (
<option key={cat} value={cat}>
@@ -110,7 +110,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
<select
value={form.collectorType}
onChange={(e) => setForm({ ...form, collectorType: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
>
{COLLECTOR_TYPES.map((ct) => (
<option key={ct} value={ct}>
@@ -132,7 +132,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, target: e.target.value })}
placeholder={TARGET_PLACEHOLDERS[form.collectorType] || "Target for data collection"}
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<div className="flex items-center gap-2 mt-1">
<p className="text-xs text-gray-400">{TARGET_HINTS[form.collectorType] || "Registry path, WMI class, event log name, file path, or command depending on collector type"}</p>
@@ -152,7 +152,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
placeholder="e.g., IsRecoveryAllowed (leave empty to read all values)"
autoComplete="off"
disabled={form.listSubkeys}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors disabled:opacity-50 disabled:bg-gray-50"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors disabled:opacity-50 disabled:bg-gray-50"
/>
<p className="text-xs text-gray-400 mt-1">Specific registry value to read. Leave empty to read all values in the key.</p>
</div>
@@ -161,7 +161,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
type="checkbox"
checked={form.listSubkeys}
onChange={(e) => setForm({ ...form, listSubkeys: e.target.checked, valueName: e.target.checked ? "" : form.valueName })}
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<span>List subkeys instead of values</span>
</label>
@@ -180,7 +180,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, eventId: e.target.value })}
placeholder="e.g., 62407 (leave empty for all events)"
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">Filter by specific Event ID. Leave empty to collect all events.</p>
</div>
@@ -193,7 +193,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
value={form.maxEntries}
onChange={(e) => setForm({ ...form, maxEntries: e.target.value })}
placeholder="10"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">Maximum number of events to return (1-50, default: 10).</p>
</div>
@@ -207,7 +207,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, source: e.target.value })}
placeholder="e.g., Microsoft-Windows-Kernel-General"
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">Filter by event provider/source name. Leave empty for all sources.</p>
</div>
@@ -219,7 +219,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, messageFilter: e.target.value })}
placeholder="e.g., *ESPProgress* (leave empty for no filter)"
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">Filter by message text. Use * as wildcard prefix/suffix.</p>
</div>
@@ -235,7 +235,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
type="checkbox"
checked={form.readContent}
onChange={(e) => setForm({ ...form, readContent: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
className="w-4 h-4 rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<span className="text-sm font-medium text-gray-700">Read file content</span>
</label>
@@ -251,7 +251,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
<select
value={form.logFormat}
onChange={(e) => setForm({ ...form, logFormat: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
>
<option value="cmtrace">CMTrace (default)</option>
<option value="text">Plain Text</option>
@@ -274,7 +274,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
? `e.g., (?<timestamp>\\d{4}-\\d{2}-\\d{2}).*(?<level>ERROR|WARN|INFO).*(?<message>.*)`
: `e.g., (?<action>Install|Uninstall).*(?<appName>[A-Za-z0-9_-]+)`}
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 font-mono focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">Regex with named capture groups. Each match emits a separate event. Named groups become event data fields.</p>
</div>
@@ -288,7 +288,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
value={form.maxLines}
onChange={(e) => setForm({ ...form, maxLines: e.target.value })}
placeholder="1000"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">Max lines to parse per file per execution (default: 1000).</p>
</div>
@@ -298,7 +298,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
type="checkbox"
checked={form.trackPosition}
onChange={(e) => setForm({ ...form, trackPosition: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
className="w-4 h-4 rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<span className="text-sm font-medium text-gray-700">Track position</span>
</label>
@@ -321,7 +321,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, jsonPath: e.target.value })}
placeholder="e.g., $.settings.tenantId or $..errorCode"
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 font-mono focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">
JSONPath query to extract values. Examples: <code className="bg-gray-100 px-1 rounded">$.key</code> (root property),{" "}
@@ -339,7 +339,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
value={form.maxResults}
onChange={(e) => setForm({ ...form, maxResults: e.target.value })}
placeholder="20"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">Maximum number of matches to return (1-100, default: 20).</p>
</div>
@@ -359,7 +359,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, xpath: e.target.value })}
placeholder="e.g., /configuration/appSettings/add[@key='Setting1']/@value"
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 font-mono focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<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),{" "}
@@ -377,7 +377,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, xmlNamespaces: e.target.value })}
placeholder="e.g., ns=http://schemas.example.com/config"
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">
Optional. Format: <code className="bg-gray-100 px-1 rounded">prefix=uri;prefix2=uri2</code>
@@ -392,7 +392,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
value={form.maxResults}
onChange={(e) => setForm({ ...form, maxResults: e.target.value })}
placeholder="20"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">Max matches to return (1-100, default: 20).</p>
</div>
@@ -406,7 +406,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
<select
value={form.trigger}
onChange={(e) => setForm({ ...form, trigger: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
>
{TRIGGERS.map((t) => (
<option key={t} value={t}>{formatTrigger(t)}</option>
@@ -424,7 +424,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
max={3600}
value={form.intervalSeconds}
onChange={(e) => setForm({ ...form, intervalSeconds: parseInt(e.target.value) || 60 })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">How often to run this rule (5 - 3600 seconds)</p>
</div>
@@ -436,7 +436,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
<select
value={form.triggerPhase}
onChange={(e) => setForm({ ...form, triggerPhase: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
>
<option value="">Any phase</option>
{form.triggerPhase && !GATHER_PHASES.some((p) => p.value === form.triggerPhase) && (
@@ -469,7 +469,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, triggerEventType: e.target.value })}
placeholder="e.g., session_stalled, enrollment_failed, modern_deployment_error"
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<datalist id="gather-rule-event-types">
{KNOWN_EVENT_TYPES.map((et) => (
@@ -528,7 +528,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
activeFromPhase: scopeMode === "from" ? form.activeFromPhase : "",
});
}}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
>
<option value="always">All phases (always)</option>
<option value="during">Only during specific phases</option>
@@ -547,7 +547,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
<select
value={form.emitMode}
onChange={(e) => setForm({ ...form, emitMode: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
>
{EMIT_MODES.map((m) => (
<option key={m.value} value={m.value}>{m.label}</option>
@@ -576,7 +576,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
: form.activePhases.filter((x) => x !== p.value),
})
}
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<span>{p.label}</span>
</label>
@@ -596,7 +596,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
<select
value={form.activeFromPhase}
onChange={(e) => setForm({ ...form, activeFromPhase: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
>
<option value="">Select a phase…</option>
{GATHER_PHASES.map((p) => (
@@ -623,7 +623,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
onChange={(e) => setForm({ ...form, outputEventType: e.target.value })}
placeholder="e.g., CustomNetworkStatus"
autoComplete="off"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
</div>
<div>
@@ -631,7 +631,7 @@ export function GatherRuleFormFields({ form, setForm, showRuleId, unrestrictedMo
<select
value={form.outputSeverity}
onChange={(e) => setForm({ ...form, outputSeverity: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
>
{SEVERITIES.map((s) => (
<option key={s} value={s}>
@@ -449,7 +449,7 @@ export default function GatherRulesPage() {
</svg>
<p className="text-sm text-blue-800">
Missing a rule, found a bug, or have an improvement idea? Help make Autopilot Monitor better —{" "}
<a href="https://github.com/okieselbach/Autopilot-Monitor/issues" target="_blank" rel="noopener noreferrer" className="font-medium underline hover:text-blue-900">
<a href="https://github.com/okieselbach/AutopilotMonitor/issues" target="_blank" rel="noopener noreferrer" className="font-medium underline hover:text-blue-900">
open a GitHub issue
</a>.
</p>
@@ -512,7 +512,7 @@ export default function GatherRulesPage() {
</svg>
<div>
<h2 className="text-xl font-semibold text-gray-900">Create Custom Rule</h2>
<p className="text-sm text-gray-500 mt-1">Define a new data collection rule for enrolled devices — <a href={`${DOCS_URL}/rules/gather-rules`} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">see documentation</a></p>
<p className="text-sm text-gray-500 mt-1">Define a new data collection rule for enrolled devices — <a href={`${DOCS_URL}/rules/gather-rules`} target="_blank" rel="noopener noreferrer" className="text-green-700 hover:text-green-800 underline">see documentation</a></p>
</div>
</div>
<JsonModeToggleButtons
@@ -584,7 +584,7 @@ export default function GatherRulesPage() {
}
}}
disabled={creating}
className="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2 text-sm font-medium"
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2 text-sm font-medium"
>
{creating ? (
<>
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useMemo, useRef } from "react";
import { useEffect, useMemo } from "react";
import { MapContainer, TileLayer, CircleMarker, Tooltip, useMap } from "react-leaflet";
import "leaflet/dist/leaflet.css";
@@ -61,22 +61,6 @@ function formatThroughput(bytesPerSec: number): string {
return `${bytesPerSec.toFixed(0)} B/s`;
}
// Clean up Leaflet map on unmount to prevent _leaflet_pos errors during client-side navigation.
// map.remove() fully destroys the instance, preventing stale transitionend callbacks.
function MapCleanup() {
const map = useMap();
const mapRef = useRef(map);
mapRef.current = map;
useEffect(() => {
return () => {
try { mapRef.current.remove(); } catch { /* already removed by react-leaflet */ }
};
}, []);
return null;
}
// Component to auto-fit bounds when locations change
function FitBounds({ locations }: { locations: { coords: [number, number] }[] }) {
const map = useMap();
@@ -131,7 +115,6 @@ export default function GeoMap({ locations, globalAvgDuration, selectedLocation,
style={{ height: "100%", width: "100%", borderRadius: "0.5rem", zIndex: 0 }}
scrollWheelZoom={true}
>
<MapCleanup />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
@@ -13,6 +13,7 @@ import { useAggregatedAdminScope } from "@/hooks";
import { useFetchProgress } from "@/hooks/useFetchProgress";
import { GlobalAdminBanner, globalAdminSubtitle } from "@/components/GlobalAdminBanner";
import { TenantScopeSelector } from "@/components/TenantScopeSelector";
import { SegmentedControl, TIME_RANGE_OPTIONS } from "@/components/SegmentedControl";
import { CalculatingCard } from "@/components/CalculatingCard";
// A cross-tenant geo aggregation can take tens of seconds server-side; the default 30s fetch
@@ -245,49 +246,33 @@ export default function GeographicPerformancePage() {
/>
<header className="bg-white shadow">
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-center justify-between gap-y-3">
<div>
<div className="flex items-center space-x-3">
<svg className="w-8 h-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="w-8 h-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h1 className="text-2xl font-normal text-gray-900">Geographic Performance</h1>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex flex-wrap items-center gap-2">
<TenantScopeSelector scope={scope} allowAggregated />
{/* Time Range Toggle */}
<div className="flex bg-gray-100 rounded-lg p-1">
{(["7d", "30d", "90d"] as TimeRange[]).map((range) => (
<button
key={range}
onClick={() => setTimeRange(range)}
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
timeRange === range
? "bg-white text-gray-900 shadow-sm"
: "text-gray-500 hover:text-gray-700"
}`}
>
{range === "7d" ? "7 Days" : range === "30d" ? "30 Days" : "90 Days"}
</button>
))}
</div>
<SegmentedControl
options={TIME_RANGE_OPTIONS}
value={timeRange}
onChange={(v) => setTimeRange(v as TimeRange)}
/>
{/* Group By Toggle */}
<div className="flex bg-gray-100 rounded-lg p-1">
{(["city", "region", "country"] as GroupBy[]).map((g) => (
<button
key={g}
onClick={() => setGroupBy(g)}
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors capitalize ${
groupBy === g
? "bg-white text-gray-900 shadow-sm"
: "text-gray-500 hover:text-gray-700"
}`}
>
{g}
</button>
))}
</div>
<SegmentedControl
options={[
{ value: "city", label: "City" },
{ value: "region", label: "Region" },
{ value: "country", label: "Country" },
]}
value={groupBy}
onChange={(v) => setGroupBy(v as GroupBy)}
/>
</div>
</div>
</div>
@@ -231,7 +231,7 @@ function LocationSessionsContent() {
&larr; Back to Geographic Performance
</button>
<div className="flex items-center space-x-3">
<svg className="w-8 h-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="w-8 h-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
@@ -0,0 +1,122 @@
import type { Metadata } from "next";
import { LandingNavbar } from "../../components/landing/LandingNavbar";
import { LoginButton } from "../../components/landing/LoginButton";
import { SiteFooter } from "../../components/SiteFooter";
import { DOCS_URL, SITE_URL } from "@/utils/config";
export const metadata: Metadata = {
title: "Get started",
description:
"From sign-in to your first live Autopilot enrollment in five steps: sign in, request access, grant consent, deploy the bootstrapper via Intune, watch live.",
alternates: {
canonical: `${SITE_URL}/get-started`,
},
};
const STEPS = [
{
title: "Sign in with Microsoft",
description:
"Use your work account — there is no signup form. The first sign-in creates nothing in your tenant.",
note: "30 seconds",
},
{
title: "Request access",
description:
"Autopilot Monitor is in preview: your tenant is queued and approved — you'll see the status right after signing in.",
note: "Preview approval",
},
{
title: "Grant consent once",
description:
"Approve the Entra ID application for your tenant so the portal can operate. One admin consent, fully revocable.",
note: "One-time admin consent",
},
{
title: "Deploy the bootstrapper via Intune",
description:
"Assign one PowerShell script to your Autopilot scope. That is the whole rollout — no servers, no gateways, no infrastructure on your side.",
note: "One script assignment",
},
{
title: "Watch your first enrollment live",
description:
"The next device that enrolls streams in minutes later: phases, app installs, analysis results, diagnostics on demand.",
note: "Minutes later",
},
];
export default function GetStartedPage() {
return (
<div className="landing-v2 min-h-screen bg-[var(--lp-bg)] flex flex-col">
<LandingNavbar />
<main className="flex-1 px-6 pt-16 sm:pt-20 pb-20">
<div className="max-w-3xl mx-auto">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[var(--lp-ink-faint)]">
Get started
</p>
<h1 className="mt-4 text-4xl sm:text-5xl font-bold tracking-tight text-[var(--lp-ink)] leading-[1.08] text-balance">
From sign-in to your first live enrollment.
</h1>
<p className="mt-4 text-lg text-[var(--lp-ink-soft)] leading-relaxed max-w-xl">
Five steps, no infrastructure on your side. Most teams see their first live session
the same day.
</p>
{/* Step sequence */}
<div className="relative mt-12">
<div className="absolute left-[15px] top-3 bottom-3 w-px bg-[var(--lp-line)]" aria-hidden="true" />
<ol className="space-y-8">
{STEPS.map((step, i) => (
<li key={step.title} className="relative pl-14">
<span className="absolute left-0 top-0 w-8 h-8 rounded-full bg-[var(--lp-accent)] text-white text-sm font-bold flex items-center justify-center">
{i + 1}
</span>
<div className="flex items-baseline gap-3 flex-wrap">
<h2 className="text-lg font-semibold text-[var(--lp-ink)]">{step.title}</h2>
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-[var(--lp-accent-soft)] text-[var(--lp-accent-ink)]">
{step.note}
</span>
</div>
<p className="mt-1.5 text-[15px] text-[var(--lp-ink-soft)] leading-relaxed">{step.description}</p>
</li>
))}
</ol>
</div>
{/* CTA */}
<div className="mt-14 flex flex-wrap items-center gap-3">
<LoginButton className="px-7 py-3 rounded-lg bg-[var(--lp-accent-ink)] hover:brightness-110 hover:shadow-lg text-white font-semibold shadow-md transition-all">
Sign in to get started
</LoginButton>
<a
href={DOCS_URL}
target="_blank"
rel="noopener noreferrer"
className="px-7 py-3 rounded-lg border border-[var(--lp-line)] bg-[var(--lp-surface)] text-[var(--lp-ink)] font-semibold hover:border-[var(--lp-ink-faint)] transition-colors"
>
Read the docs
</a>
</div>
<p className="mt-6 text-sm text-[var(--lp-ink-faint)] leading-relaxed max-w-xl">
When you need more later: the{" "}
<a
href={`${DOCS_URL}/plans`}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--lp-accent-ink)] hover:opacity-80 underline"
>
Enterprise plan
</a>{" "}
adds SLAs, support commitments, MSP delegation, higher operating limits and more —
same service, nothing to migrate.
</p>
</div>
</main>
<SiteFooter />
</div>
);
}
+194 -5
View File
@@ -27,12 +27,71 @@ html {
:root {
--background: #f9fafb;
--foreground: #171717;
/* ===== Landing v2 design tokens (lp-*) =====
Semantic tokens for the marketing surface (landing + shared footer).
Components reference these via arbitrary-value utilities
(bg-[var(--lp-surface)]) so the global `.dark .bg-white` override
block never fights them — dark mode only redefines the variables. */
--lp-bg: #fafafa;
--lp-surface: #ffffff;
--lp-surface-2: #f4f4f5;
--lp-nav: rgba(255, 255, 255, 0.72);
--lp-ink: #18181b;
--lp-ink-soft: #52525b;
--lp-ink-faint: #9ca3af;
--lp-line: #e4e4e7;
--lp-line-soft: #ededf0;
--lp-accent: #33b161;
--lp-accent-ink: #1e8a4c;
--lp-accent-soft: rgba(51, 177, 97, 0.1);
--lp-accent-line: rgba(51, 177, 97, 0.35);
--lp-warn: #d97706;
--lp-warn-soft: rgba(217, 119, 6, 0.1);
--lp-danger: #dc2626;
/* Terminal is dark in both themes */
--lp-term-bg: #0d1424;
--lp-term-line: #22304a;
--lp-term-ink: #d6e1f0;
--lp-term-faint: #7b8ba3;
}
.dark {
--lp-bg: #0d1424;
--lp-surface: #131d31;
--lp-surface-2: #1a2740;
--lp-nav: rgba(13, 20, 36, 0.72);
--lp-ink: #e7edf6;
--lp-ink-soft: #9fb0c5;
--lp-ink-faint: #64748b;
--lp-line: #23304a;
--lp-line-soft: #1c2740;
--lp-accent: #3bc06c;
--lp-accent-ink: #55c57f;
--lp-accent-soft: rgba(59, 192, 108, 0.12);
--lp-accent-line: rgba(59, 192, 108, 0.4);
--lp-warn: #f5a623;
--lp-warn-soft: rgba(245, 166, 35, 0.12);
--lp-danger: #f87171;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
/* Native radios/checkboxes: without @tailwindcss/forms the text-* utilities
do nothing — the check color follows the OS accent color. Pin it to the
brand green; semantic exceptions (danger red, global-admin purple)
override per control via Tailwind accent-* utilities. */
input[type="checkbox"],
input[type="radio"] {
accent-color: #16a34a; /* green-600 */
}
.dark input[type="checkbox"],
.dark input[type="radio"] {
accent-color: #22c55e; /* green-500 */
}
@layer utilities {
@@ -80,10 +139,13 @@ body {
color: #94a3b8 !important;
}
.dark .text-gray-500 {
color: #64748b !important;
/* was #64748b — too dark on raised dark surfaces (notification items,
hover rows); lightened for readable muted text, hierarchy kept. */
color: #8896ad !important;
}
.dark .text-gray-400 {
color: #64748b !important;
/* was #64748b — see text-gray-500 note; one step more muted than 500. */
color: #7b8ba3 !important;
}
/* -- Borders -- */
@@ -118,11 +180,14 @@ body {
}
/* -- Hover states -- */
/* Subtle, DARK hover surfaces: the former #334155 washed out muted text
(notification timestamps, table metadata) on hovered rows. One step
above the card surface (#1e293b) is enough as a hover signal. */
.dark .hover\:bg-gray-50:hover {
background-color: #334155 !important;
background-color: #283548 !important;
}
.dark .hover\:bg-gray-100:hover {
background-color: #334155 !important;
background-color: #2e3c52 !important;
}
.dark .hover\:bg-gray-200:hover {
background-color: #334155 !important;
@@ -559,6 +624,9 @@ html:hover ::-webkit-scrollbar-thumb {
.dark .text-blue-700 {
color: #60a5fa !important; /* blue-400 — step icons etc. on dark surfaces */
}
.dark .text-green-700 {
color: #4ade80 !important; /* green-400 — brand links/accents on dark surfaces */
}
.dark .text-indigo-700 {
color: #a5b4fc !important; /* indigo-300 — category badge, correlation labels, doc links */
}
@@ -729,6 +797,127 @@ html:hover ::-webkit-scrollbar-thumb {
animation: dotPulse 12.6s linear infinite;
}
/* ===== LANDING V2 (scroll reveals, event stream, terminal) ===== */
/* The marketing surface is light-only for now: product screenshots are
light, and a half-dark landing reads unfinished. Re-declare the light
tokens inside .landing-v2 so a dark OS/portal preference does not
flip the landing. Remove this block to re-enable dark marketing. */
.dark .landing-v2 {
--lp-bg: #fafafa;
--lp-surface: #ffffff;
--lp-surface-2: #f4f4f5;
--lp-nav: rgba(255, 255, 255, 0.72);
--lp-ink: #18181b;
--lp-ink-soft: #52525b;
--lp-ink-faint: #9ca3af;
--lp-line: #e4e4e7;
--lp-line-soft: #ededf0;
--lp-accent: #33b161;
--lp-accent-ink: #1e8a4c;
--lp-accent-soft: rgba(51, 177, 97, 0.1);
--lp-accent-line: rgba(51, 177, 97, 0.35);
--lp-warn: #d97706;
--lp-warn-soft: rgba(217, 119, 6, 0.1);
--lp-danger: #dc2626;
}
/* Neutralize the global .dark utility overrides inside the light-only
marketing surface: about/terms/privacy still use plain bg-white /
text-gray-* cards, which the app-wide dark overrides would otherwise
flip and leave those pages half dark. Dies together with the
light-only decision above. */
.dark .landing-v2 .bg-white { background-color: #ffffff !important; }
.dark .landing-v2 .bg-gray-50 { background-color: #f9fafb !important; }
.dark .landing-v2 .bg-gray-100 { background-color: #f3f4f6 !important; }
.dark .landing-v2 .text-gray-900 { color: #111827 !important; }
.dark .landing-v2 .text-gray-800 { color: #1f2937 !important; }
.dark .landing-v2 .text-gray-700 { color: #374151 !important; }
.dark .landing-v2 .text-gray-600 { color: #4b5563 !important; }
.dark .landing-v2 .text-gray-500 { color: #6b7280 !important; }
.dark .landing-v2 .text-gray-400 { color: #9ca3af !important; }
.dark .landing-v2 .border-gray-200 { border-color: #e5e7eb !important; }
.dark .landing-v2 .border-gray-300 { border-color: #d1d5db !important; }
.dark .landing-v2 .hover\:bg-gray-50:hover { background-color: #f9fafb !important; }
.dark .landing-v2 .hover\:bg-gray-100:hover { background-color: #f3f4f6 !important; }
/* Scroll-triggered reveal: Reveal.tsx adds .lp-in when the element
enters the viewport. Reduced motion renders everything visible. */
.lp-reveal {
opacity: 0;
transform: translateY(18px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.lp-reveal.lp-in {
opacity: 1;
transform: none;
}
@keyframes lpFadeUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: none;
}
}
.lp-event-in {
animation: lpFadeUp 0.4s ease-out both;
}
@keyframes lpBlink {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
.lp-cursor::after {
content: "";
display: inline-block;
width: 8px;
height: 1.05em;
margin-left: 2px;
vertical-align: text-bottom;
background: var(--lp-accent);
animation: lpBlink 1s steps(1) infinite;
}
@keyframes lpPulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(51, 177, 97, 0.35);
}
60% {
box-shadow: 0 0 0 6px rgba(51, 177, 97, 0);
}
}
.lp-live-dot {
animation: lpPulse 2s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.lp-reveal {
opacity: 1;
transform: none;
transition: none;
}
.lp-event-in {
animation: none;
}
.lp-cursor::after {
animation: none;
}
.lp-live-dot {
animation: none;
}
}
/* ===== FEEDBACK BUBBLE ANIMATION ===== */
@keyframes fadeInUp {
from {
@@ -183,7 +183,7 @@ export default function HealthCheckPage() {
<button
onClick={() => { performHealthCheck(); performMcpCheck(); }}
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm font-medium flex items-center gap-2"
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm font-medium flex items-center gap-2"
>
{loading ? (
<>
@@ -250,7 +250,7 @@ export default function HealthCheckPage() {
<dd className="mt-1 font-mono text-gray-900 dark:text-gray-100">
{healthResult.commitHash ? (
<a
href={`https://github.com/okieselbach/Autopilot-Monitor/commit/${healthResult.commitHash}`}
href={`https://github.com/okieselbach/AutopilotMonitor/commit/${healthResult.commitHash}`}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-600 dark:text-indigo-400 hover:underline"
@@ -334,7 +334,7 @@ export default function HealthCheckPage() {
<button
onClick={testGroupJoin}
disabled={groupJoinTest === 'testing'}
className="mt-3 px-3 py-1.5 bg-blue-600 text-white rounded text-xs font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors"
className="mt-3 px-3 py-1.5 bg-green-600 text-white rounded text-xs font-medium hover:bg-green-700 disabled:opacity-50 transition-colors"
>
{groupJoinTest === 'testing' ? 'Testing...' : groupJoinTest === 'failed' ? 'Retry Join Test' : 'Test Group Join'}
</button>
@@ -251,7 +251,7 @@ export default function ImePatternCard({
{canEdit && (
<button
onClick={() => onStartEditing(pattern)}
className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors flex items-center space-x-2"
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center space-x-2"
title="Edit pattern (Global Admin)"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -330,7 +330,7 @@ export default function ImePatternCard({
<select
value={editForm.category}
onChange={(e) => setEditForm({ ...editForm, category: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:ring-2 focus:ring-green-500 focus:border-green-500"
>
{CATEGORIES.map((c) => (
<option key={c} value={c}>{CATEGORY_LABELS[c]}</option>
@@ -344,7 +344,7 @@ export default function ImePatternCard({
value={editForm.pattern}
onChange={(e) => setEditForm({ ...editForm, pattern: e.target.value })}
rows={3}
className="w-full font-mono text-sm px-3 py-2 border border-gray-300 rounded-lg text-gray-900 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
className="w-full font-mono text-sm px-3 py-2 border border-gray-300 rounded-lg text-gray-900 focus:ring-2 focus:ring-green-500 focus:border-green-500"
spellCheck={false}
placeholder="e.g., EMS Agent Started"
/>
@@ -356,7 +356,7 @@ export default function ImePatternCard({
<select
value={editForm.action}
onChange={(e) => setEditForm({ ...editForm, action: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:ring-2 focus:ring-green-500 focus:border-green-500"
>
{ACTIONS.map((a) => (
<option key={a} value={a}>{ACTION_LABELS[a]} ({a})</option>
@@ -371,7 +371,7 @@ export default function ImePatternCard({
value={editForm.description}
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
rows={2}
className="w-full text-sm px-3 py-2 border border-gray-300 rounded-lg text-gray-900 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
className="w-full text-sm px-3 py-2 border border-gray-300 rounded-lg text-gray-900 focus:ring-2 focus:ring-green-500 focus:border-green-500"
placeholder="What this pattern detects and why it matters..."
/>
</div>
@@ -382,7 +382,7 @@ export default function ImePatternCard({
id="edit-enabled"
checked={editForm.enabled}
onChange={(e) => setEditForm({ ...editForm, enabled: e.target.checked })}
className="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"
className="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-green-500"
/>
<label htmlFor="edit-enabled" className="text-sm font-medium text-gray-700">Enabled</label>
</div>
@@ -403,7 +403,7 @@ export default function ImePatternCard({
updated[key] = e.target.value;
setEditForm({ ...editForm, parameters: updated });
}}
className="flex-1 text-sm px-2 py-1 border border-gray-300 rounded text-gray-900 focus:ring-1 focus:ring-indigo-500"
className="flex-1 text-sm px-2 py-1 border border-gray-300 rounded text-gray-900 focus:ring-1 focus:ring-green-500"
/>
<button
onClick={() => {
@@ -428,14 +428,14 @@ export default function ImePatternCard({
value={newParamKey}
onChange={(e) => onSetNewParamKey(e.target.value)}
placeholder="Key"
className="w-32 text-sm px-2 py-1 border border-gray-300 rounded text-gray-900 focus:ring-1 focus:ring-indigo-500"
className="w-32 text-sm px-2 py-1 border border-gray-300 rounded text-gray-900 focus:ring-1 focus:ring-green-500"
/>
<input
type="text"
value={newParamValue}
onChange={(e) => onSetNewParamValue(e.target.value)}
placeholder="Value"
className="flex-1 text-sm px-2 py-1 border border-gray-300 rounded text-gray-900 focus:ring-1 focus:ring-indigo-500"
className="flex-1 text-sm px-2 py-1 border border-gray-300 rounded text-gray-900 focus:ring-1 focus:ring-green-500"
/>
<button
onClick={() => {
@@ -449,7 +449,7 @@ export default function ImePatternCard({
}
}}
disabled={!newParamKey.trim()}
className="px-3 py-1 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="px-3 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Add
</button>
@@ -468,7 +468,7 @@ export default function ImePatternCard({
<button
onClick={() => onSaveEdit(pattern.patternId)}
disabled={saving}
className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2"
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2"
>
{saving ? (
<>
@@ -256,7 +256,7 @@ export default function ImeLogPatternsPage() {
</svg>
<p className="text-sm text-blue-800">
Missing a pattern, found a bug, or have an improvement idea? Help make Autopilot Monitor better —{" "}
<a href="https://github.com/okieselbach/Autopilot-Monitor/issues" target="_blank" rel="noopener noreferrer" className="font-medium underline hover:text-blue-900">
<a href="https://github.com/okieselbach/AutopilotMonitor/issues" target="_blank" rel="noopener noreferrer" className="font-medium underline hover:text-blue-900">
open a GitHub issue
</a>.
</p>
+8 -8
View File
@@ -24,9 +24,9 @@ const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
title: {
absolute: "AutopilotMonitor – Real-Time Windows Enrollment Monitoring",
default: "AutopilotMonitor",
template: "%s | AutopilotMonitor",
absolute: "Autopilot Monitor – Real-Time Windows Enrollment Monitoring",
default: "Autopilot Monitor",
template: "%s | Autopilot Monitor",
},
description:
"Real-time monitoring and troubleshooting for Windows deployments. Track every enrollment phase, detect issues automatically with Analyze Rules, and resolve failures faster.",
@@ -52,14 +52,14 @@ export const metadata: Metadata = {
type: "website",
locale: "en_US",
url: SITE_URL,
siteName: "AutopilotMonitor",
title: "AutopilotMonitor – Real-Time Windows Enrollment Monitoring",
siteName: "Autopilot Monitor",
title: "Autopilot Monitor – Real-Time Windows Enrollment Monitoring",
description:
"Real-time monitoring and troubleshooting for Windows Autopilot deployments. Track every enrollment phase, detect issues automatically, and resolve failures faster.",
},
twitter: {
card: "summary_large_image",
title: "AutopilotMonitor – Real-Time Windows Enrollment Monitoring",
title: "Autopilot Monitor – Real-Time Windows Enrollment Monitoring",
description:
"Real-time monitoring and troubleshooting for Windows Autopilot deployments. Track every enrollment phase, detect issues automatically, and resolve failures faster.",
},
@@ -87,7 +87,7 @@ export const metadata: Metadata = {
const jsonLd = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: "AutopilotMonitor",
name: "Autopilot Monitor",
description:
"Real-time monitoring and troubleshooting platform for Windows deployments. Gives IT teams full visibility into enrollment phases, app progress, errors, and timelines.",
applicationCategory: "BusinessApplication",
@@ -103,7 +103,7 @@ const jsonLd = {
url: "https://www.linkedin.com/in/oliver-kieselbach/",
},
url: SITE_URL,
codeRepository: "https://github.com/okieselbach/Autopilot-Monitor",
codeRepository: "https://github.com/okieselbach/AutopilotMonitor",
keywords:
"Windows Autopilot, Intune, enrollment monitoring, autopilot troubleshooting, Windows deployment",
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 150 KiB

+27 -854
View File
@@ -1,863 +1,36 @@
import { PublicSiteNavbar } from "../components/PublicSiteNavbar";
import { AuthGate } from "../components/landing/AuthGate";
import { LoginButton } from "../components/landing/LoginButton";
import { PlatformStats } from "../components/landing/PlatformStats";
import { DOCS_URL } from "@/utils/config";
const QUICK_START = [
{
title: "Sign in and grant access",
description: "Authenticate with Microsoft and approve tenant access once.",
},
{
title: "Deploy bootstrapper in Intune",
description: "Assign the bootstrap script to your Autopilot scope.",
},
{
title: "Watch live telemetry",
description: "Track phases, apps, failures, and actions in real time.",
},
];
const FLOW_STEPS = [
{
title: "Intune assignment",
description: "Target the bootstrapper script to your selected Autopilot device groups.",
icon: "users",
},
{
title: "Bootstrapper execution",
description: "Device runs the bootstrapper script and installs Autopilot Monitor Agent.",
icon: "code",
},
{
title: "Live monitoring",
description: "Phase transitions, app installs, and progress are captured continuously.",
icon: "monitor",
},
{
title: "Event upload",
description: "Predefined and custom events are uploaded to the backend pipeline.",
icon: "cloud",
},
{
title: "Rule analysis",
description: "Backend correlates phases and runs analyze rules for instant insights.",
icon: "rules",
},
{
title: "Completion and notifications",
description: "Session status is finalized and Teams notifications are triggered.",
icon: "bell",
},
{
title: "Diagnostics download",
description: "Grab diagnostic bundles quickly for fast root-cause validation.",
icon: "download",
},
];
function StepIcon({ icon }: { icon: string }) {
switch (icon) {
case "users":
return (
<svg className="w-4 h-4 text-blue-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-1a4 4 0 00-5-3.87M9 20H2v-1a4 4 0 015-3.87m9-5.13a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
);
case "code":
return (
<svg className="w-4 h-4 text-blue-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="m8 9-3 3 3 3m8-6 3 3-3 3M13 7l-2 10" />
</svg>
);
case "monitor":
return (
<svg className="w-4 h-4 text-blue-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 4h18v12H3zM8 20h8m-4-4v4m-3-8 2-2 2 3 3-4 2 3" />
</svg>
);
case "cloud":
return (
<svg className="w-4 h-4 text-blue-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M7 18a4 4 0 01-.3-8A5 5 0 1117 8h1a4 4 0 010 8h-3m-3 0v-7m0 7-3-3m3 3 3-3" />
</svg>
);
case "rules":
return (
<svg className="w-4 h-4 text-blue-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5h10M9 9h10M9 13h10M9 17h10M4 5h.01M4 9h.01M4 13h.01M4 17h.01" />
</svg>
);
case "bell":
return (
<svg className="w-4 h-4 text-blue-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 17h5l-1.4-1.4A2 2 0 0118 14.2V11a6 6 0 10-12 0v3.2c0 .5-.2 1-.6 1.4L4 17h5m6 0a3 3 0 11-6 0h6z" />
</svg>
);
default:
return (
<svg className="w-4 h-4 text-blue-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 5v10m0 0-3-3m3 3 3-3M5 19h14" />
</svg>
);
}
}
import { LandingNavbar } from "../components/landing/LandingNavbar";
import { Hero } from "../components/landing/Hero";
import { StatsBand } from "../components/landing/StatsBand";
import { JustAsk } from "../components/landing/JustAsk";
import { Story } from "../components/landing/Story";
import { CapabilitiesStrip } from "../components/landing/CapabilitiesStrip";
import { Comparison } from "../components/landing/Comparison";
import { HowItWorks } from "../components/landing/HowItWorks";
import { FinalCta } from "../components/landing/FinalCta";
import { SiteFooter } from "../components/SiteFooter";
/**
* Landing page v2 — a scroll story of one enrollment.
* All theming goes through the lp-* tokens (globals.css); sections live
* in components/landing/. Auth behavior (AuthGate redirect, LoginButton
* portal handoff) is unchanged from v1.
*/
export default function LandingPage() {
return (
<div className="landing-page min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50">
<div className="landing-v2 min-h-screen bg-[var(--lp-bg)]">
{/* Client component: handles auth redirect + loading overlay */}
<AuthGate />
<PublicSiteNavbar showSectionLinks={true} />
{/* Hero Section */}
<div className="pt-20 pb-20 px-6">
<div className="max-w-7xl mx-auto">
<div className="text-center max-w-4xl mx-auto">
<div className="mb-7 inline-flex items-center gap-3 rounded-2xl border border-blue-300/70 bg-gradient-to-r from-blue-50 via-indigo-50 to-blue-50 px-4 py-2.5 shadow-md ring-1 ring-blue-200/60">
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-blue-600 text-white shadow-sm">
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-1a4 4 0 00-5-3.87M9 20H2v-1a4 4 0 015-3.87m9-5.13a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
<p className="text-sm font-semibold text-blue-900">
Community-driven Analyze Rules support
<span className="font-normal text-blue-800">: discover, build, and share rules for everyone.</span>
</p>
</div>
<div className="relative inline-block mb-6">
<h1 className="text-6xl font-bold text-gray-900 leading-tight">
Advanced Monitoring for
<span className="bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent"> Windows Enrollments</span>
</h1>
<div className="pointer-events-none absolute -right-10 -top-1 rotate-[13deg] inline-flex items-center rounded-md border border-amber-300/80 bg-gradient-to-r from-amber-500 to-orange-500 px-3 py-1 shadow-md">
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-white whitespace-nowrap">
Private Preview Running
</span>
</div>
</div>
<p className="text-xl text-gray-600 mb-8 leading-relaxed">
Real-time insights, intelligent troubleshooting, and comprehensive analytics for your Autopilot deployments.
Monitor every phase, run customizable analyze rules, and resolve issues faster than ever before.
</p>
<div className="flex items-center justify-center space-x-4">
<LoginButton
className="px-8 py-4 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-lg font-semibold text-lg shadow-xl hover:shadow-2xl transform hover:-translate-y-0.5 transition-all flex items-center space-x-2"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
</svg>
<span>Get Started</span>
</LoginButton>
</div>
<p className="mt-4 text-sm text-gray-500">
Free to use • Open-Source • Sign in to request early access
</p>
{/* Platform Stats - client component fetches live data */}
<PlatformStats />
{/* Product Preview Showcase */}
<div className="mt-14 relative max-w-5xl mx-auto">
{/* Glow effects */}
<div className="absolute -top-10 left-1/2 -translate-x-1/2 w-[600px] h-[300px] bg-gradient-to-br from-blue-400/20 via-indigo-400/15 to-purple-400/10 blur-3xl rounded-full pointer-events-none" />
{/* Main screenshot — Fleet Health Dashboard */}
<div className="relative z-10 rounded-xl border border-gray-200/80 bg-white shadow-2xl overflow-hidden">
{/* Browser chrome */}
<div className="flex items-center gap-1.5 px-4 py-2.5 bg-gray-50 border-b border-gray-200">
<div className="w-2.5 h-2.5 rounded-full bg-red-400" />
<div className="w-2.5 h-2.5 rounded-full bg-amber-400" />
<div className="w-2.5 h-2.5 rounded-full bg-green-400" />
<div className="ml-3 flex-1 bg-gray-200/80 rounded-md px-3 py-1 text-[10px] text-gray-400 font-mono">autopilotmonitor.com/fleet-health</div>
</div>
{/* Fleet Health mock content */}
<div className="p-5 md:p-7">
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-2.5">
<div className="w-6 h-6 rounded bg-blue-100 flex items-center justify-center">
<svg className="w-3.5 h-3.5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6m6 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0h6m4-10v10" /></svg>
</div>
<h4 className="text-lg font-bold text-gray-900">Fleet Health</h4>
</div>
<div className="flex gap-1.5">
<span className="px-3 py-1 text-[10px] font-semibold bg-blue-600 text-white rounded-md">Last 7 Days</span>
<span className="px-3 py-1 text-[10px] font-medium text-gray-500 bg-gray-100 rounded-md">Last 30 Days</span>
<span className="px-3 py-1 text-[10px] font-medium text-gray-500 bg-gray-100 rounded-md hidden sm:block">Last 90 Days</span>
</div>
</div>
{/* KPI cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
<div className="rounded-lg border-l-4 border-l-green-500 border border-gray-100 bg-green-50/50 p-3">
<p className="text-[10px] text-gray-500 mb-0.5">Success Rate</p>
<p className="text-2xl font-bold text-green-600">80.0%</p>
<p className="text-[9px] text-gray-400">8 of 10 enrollments</p>
</div>
<div className="rounded-lg border-l-4 border-l-blue-500 border border-gray-100 bg-blue-50/50 p-3">
<p className="text-[10px] text-gray-500 mb-0.5">Avg. Enrollment Time</p>
<p className="text-2xl font-bold text-blue-600">18 min</p>
<p className="text-[9px] text-gray-400">Completed enrollments</p>
</div>
<div className="rounded-lg border-l-4 border-l-red-500 border border-gray-100 bg-red-50/50 p-3">
<p className="text-[10px] text-gray-500 mb-0.5">Failed</p>
<p className="text-2xl font-bold text-red-500">2</p>
<p className="text-[9px] text-gray-400">Needs attention</p>
</div>
<div className="rounded-lg border-l-4 border-l-indigo-500 border border-gray-100 bg-indigo-50/50 p-3">
<p className="text-[10px] text-gray-500 mb-0.5">Active Now</p>
<p className="text-2xl font-bold text-indigo-600">0</p>
<p className="text-[9px] text-gray-400">Currently enrolling</p>
</div>
</div>
{/* Chart area */}
<div className="rounded-lg border border-gray-100 bg-gray-50/60 p-4">
<p className="text-sm font-semibold text-gray-800 mb-3">Enrollments Timeline</p>
<div className="flex items-end gap-3 h-28 px-2">
{[0, 0, 0, { g: 65, r: 25 }, { g: 50, r: 30 }, 0, { g: 40, r: 20 }, 0].map((bar, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-0.5">
{typeof bar === 'object' ? (
<div className="w-full flex flex-col gap-px">
<div className="w-full rounded-t bg-green-400" style={{ height: `${bar.g}px` }} />
<div className="w-full rounded-b bg-red-300" style={{ height: `${bar.r}px` }} />
</div>
) : (
<div className="w-full rounded bg-gray-200/60" style={{ height: '2px' }} />
)}
<span className="text-[8px] text-gray-400 mt-1">{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon'][i]}</span>
</div>
))}
</div>
<div className="flex items-center gap-4 mt-2 ml-2">
<span className="flex items-center gap-1 text-[9px] text-gray-500"><span className="w-2 h-2 rounded-sm bg-green-400" />Success (8)</span>
<span className="flex items-center gap-1 text-[9px] text-gray-500"><span className="w-2 h-2 rounded-sm bg-red-300" />Failed (2)</span>
</div>
</div>
</div>
</div>
{/* Floating card — Session Details (overlapping bottom-left) */}
<div className="absolute -bottom-8 -left-4 md:-left-8 z-20 w-64 md:w-80 rounded-xl border border-gray-200/80 bg-white shadow-xl overflow-hidden transform rotate-[-2deg] hover:rotate-0 transition-transform duration-300">
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-50 border-b border-gray-100">
<div className="w-2 h-2 rounded-full bg-red-400" />
<div className="w-2 h-2 rounded-full bg-amber-400" />
<div className="w-2 h-2 rounded-full bg-green-400" />
<span className="ml-2 text-[8px] text-gray-400 font-mono">Session Details</span>
</div>
<div className="p-3.5">
<p className="text-xs font-bold text-gray-900 mb-2">Enrollment Progress</p>
{/* Progress steps */}
<div className="flex items-center gap-0">
{['Start', 'Prep', 'Setup', 'Apps', 'Account', 'Apps', 'Final', 'Done'].map((step, i) => (
<div key={i} className="flex items-center">
<div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center shrink-0">
<svg className="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
</div>
{i < 7 && <div className="w-2 md:w-3 h-0.5 bg-green-400" />}
</div>
))}
</div>
<div className="flex justify-between mt-1 px-0.5">
{['Start', '', '', '', '', '', '', 'Done'].map((label, i) => (
<span key={i} className="text-[7px] text-gray-400">{label}</span>
))}
</div>
{/* Analysis result */}
<div className="mt-3 rounded-lg border-l-3 border-l-orange-400 bg-orange-50 p-2">
<div className="flex items-center gap-1.5 mb-0.5">
<span className="px-1.5 py-0.5 text-[7px] font-bold bg-orange-500 text-white rounded">HIGH</span>
<span className="text-[8px] text-gray-500 font-mono">ANALYZE-APP-006</span>
</div>
<p className="text-[10px] font-semibold text-gray-800">Reboot Required Loop</p>
<div className="flex items-center gap-1 mt-1">
<span className="text-[8px] text-gray-400">Confidence:</span>
<div className="w-12 h-1.5 bg-gray-200 rounded-full overflow-hidden"><div className="w-[70%] h-full bg-orange-500 rounded-full" /></div>
<span className="text-[8px] text-gray-500">70%</span>
</div>
</div>
</div>
</div>
{/* Floating card — Performance Metrics (overlapping bottom-right) */}
<div className="absolute -bottom-6 -right-4 md:-right-6 z-20 w-60 md:w-72 rounded-xl border border-gray-200/80 bg-white shadow-xl overflow-hidden transform rotate-[2deg] hover:rotate-0 transition-transform duration-300">
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-50 border-b border-gray-100">
<div className="w-2 h-2 rounded-full bg-red-400" />
<div className="w-2 h-2 rounded-full bg-amber-400" />
<div className="w-2 h-2 rounded-full bg-green-400" />
<span className="ml-2 text-[8px] text-gray-400 font-mono">Performance Metrics</span>
</div>
<div className="p-3.5">
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg border border-gray-100 p-2">
<div className="flex items-center justify-between mb-1">
<span className="text-[9px] text-gray-500">CPU</span>
<span className="text-[10px] font-bold text-red-500">92%</span>
</div>
<div className="h-6 flex items-end gap-px">
{[40, 55, 70, 85, 92, 88, 75, 60, 80, 92].map((v, i) => (
<div key={i} className="flex-1 rounded-t bg-red-400/70" style={{ height: `${v * 0.24}px` }} />
))}
</div>
</div>
<div className="rounded-lg border border-gray-100 p-2">
<div className="flex items-center justify-between mb-1">
<span className="text-[9px] text-gray-500">Memory</span>
<span className="text-[10px] font-bold text-amber-600">77%</span>
</div>
<div className="h-6 flex items-end gap-px">
{[60, 65, 68, 70, 72, 75, 74, 76, 77, 77].map((v, i) => (
<div key={i} className="flex-1 rounded-t bg-amber-400/70" style={{ height: `${v * 0.24}px` }} />
))}
</div>
</div>
</div>
{/* Download progress */}
<div className="mt-2.5">
<p className="text-[9px] font-semibold text-gray-700 mb-1.5">Download Progress</p>
{['RealmJoin Setup Launcher', 'BGInfo46', 'CMTrace.exe'].map((app, i) => (
<div key={i} className="mb-1.5">
<div className="flex items-center justify-between">
<span className="text-[8px] text-gray-600 flex items-center gap-1">
<svg className="w-2.5 h-2.5 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
{app}
</span>
<span className="text-[8px] text-green-600 font-medium">100%</span>
</div>
<div className="w-full h-1 bg-gray-100 rounded-full mt-0.5"><div className="w-full h-full bg-green-500 rounded-full" /></div>
</div>
))}
</div>
</div>
</div>
</div>
{/* spacer for floating cards */}
<div className="h-14" />
</div>
</div>
</div>
{/* Eye-Catcher Workflow Section */}
<section id="how-it-works" className="py-20 px-6 scroll-mt-14">
<div className="max-w-7xl mx-auto">
<div className="text-center max-w-4xl mx-auto">
<p className="text-sm font-semibold text-blue-600 uppercase tracking-[0.22em] mb-3">
How It Works
</p>
<h2 className="text-4xl md:text-5xl font-bold text-gray-900 leading-tight">
From Intune rollout to deep insights in minutes
</h2>
<p className="mt-5 text-lg text-gray-600 leading-relaxed">
Simple onboarding, clear phase visibility, and automated analysis in one workflow.
Deploy once, monitor everything, and react faster when issues appear.
</p>
</div>
<div className="mt-10 grid grid-cols-1 md:grid-cols-3 gap-4">
{QUICK_START.map((item, index) => (
<div
key={item.title}
className="rounded-2xl border border-blue-100 bg-white/90 backdrop-blur-sm p-5 shadow-md"
>
<div className="inline-flex h-7 min-w-7 items-center justify-center rounded-full bg-blue-600 text-white text-xs font-bold px-2">
{index + 1}
</div>
<h3 className="mt-3 text-lg font-semibold text-gray-900">{item.title}</h3>
<p className="mt-2 text-sm text-gray-600 leading-relaxed">{item.description}</p>
</div>
))}
</div>
<div className="mt-8 md:mt-10 text-center">
<p className="text-sm md:text-base text-gray-600 max-w-3xl mx-auto leading-relaxed">
This is what happens after rollout: each phase is captured, correlated, and translated into clear, actionable insights.
</p>
</div>
<div className="mt-6 md:mt-7 rounded-3xl border border-blue-100 bg-gradient-to-br from-white via-blue-50/60 to-indigo-50/60 p-5 md:p-6 shadow-xl overflow-hidden relative">
<div className="absolute -top-16 -left-16 w-48 h-48 bg-blue-200/30 blur-3xl rounded-full pointer-events-none" />
<div className="absolute -bottom-20 -right-16 w-56 h-56 bg-indigo-200/25 blur-3xl rounded-full pointer-events-none" />
<div className="relative z-10">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<h3 className="text-gray-900 text-2xl md:text-3xl font-bold">
One pipeline from deployment to action
</h3>
</div>
<LoginButton
className="px-5 py-2.5 rounded-lg bg-gradient-to-r from-blue-600 to-indigo-600 text-white font-semibold shadow-lg hover:shadow-xl transition-all"
>
Start Now
</LoginButton>
</div>
<div className="mt-6 relative">
{/* Desktop: independent left/right stacks for true vertical overlap */}
<div className="hidden md:grid grid-cols-[minmax(0,1fr)_40px_minmax(0,1fr)] gap-4 items-start relative isolate">
<div className="space-y-20 relative z-30">
{FLOW_STEPS.filter((_, i) => i % 2 === 0).map((step, leftIndex) => {
const index = leftIndex * 2;
return (
<div
key={step.title}
className="step-card relative rounded-xl border border-blue-100 bg-white/90 backdrop-blur-sm p-3 md:p-3.5 shadow-md"
style={{ animationDelay: `${index * 0.08}s` }}
>
<div className="absolute right-[-36px] top-[calc(50%+1px)] -translate-y-1/2 h-px w-[36px] bg-blue-200 z-10" />
<span
className="dot-pulse absolute right-[-42px] top-[calc(50%-4px)] -translate-y-1/2 h-3 w-3 rounded-full bg-blue-500 ring-4 ring-white z-50"
style={{ animationDelay: `${index * 1.8}s` }}
/>
<p className="text-blue-600 text-xs font-semibold uppercase tracking-[0.18em]">
Step {index + 1}
</p>
<div className="mt-1.5 flex items-start justify-between gap-3">
<h4 className="text-gray-900 font-semibold">{step.title}</h4>
<span className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-blue-200 bg-blue-50">
<StepIcon icon={step.icon} />
</span>
</div>
<p className="mt-1 text-sm text-gray-600 leading-relaxed">{step.description}</p>
</div>
);
})}
</div>
<div className="relative self-stretch z-0">
<div className="absolute left-1/2 -translate-x-1/2 top-2 bottom-2 w-px bg-blue-200 z-0" />
</div>
<div className="space-y-20 pt-24 relative z-30">
{FLOW_STEPS.filter((_, i) => i % 2 === 1).map((step, rightIndex) => {
const index = rightIndex * 2 + 1;
return (
<div
key={step.title}
className="step-card relative rounded-xl border border-blue-100 bg-white/90 backdrop-blur-sm p-3 md:p-3.5 shadow-md"
style={{ animationDelay: `${index * 0.08}s` }}
>
<div className="absolute left-[-36px] top-[calc(50%+1px)] -translate-y-1/2 h-px w-[36px] bg-blue-200 z-10" />
<span
className="dot-pulse absolute left-[-42px] top-[calc(50%-4px)] -translate-y-1/2 h-3 w-3 rounded-full bg-blue-500 ring-4 ring-white z-50"
style={{ animationDelay: `${index * 1.8}s` }}
/>
<p className="text-blue-600 text-xs font-semibold uppercase tracking-[0.18em]">
Step {index + 1}
</p>
<div className="mt-1.5 flex items-start justify-between gap-3">
<h4 className="text-gray-900 font-semibold">{step.title}</h4>
<span className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-blue-200 bg-blue-50">
<StepIcon icon={step.icon} />
</span>
</div>
<p className="mt-1 text-sm text-gray-600 leading-relaxed">{step.description}</p>
</div>
);
})}
</div>
</div>
{/* Mobile: linear timeline */}
<div className="md:hidden relative isolate">
<div className="absolute left-4 top-3 bottom-3 w-px bg-blue-200" />
<div className="space-y-5">
{FLOW_STEPS.map((step, index) => (
<div key={step.title} className="relative">
<div className="absolute left-4 top-[calc(2rem+1px)] h-px w-6 bg-blue-200 z-10" />
<span
className="dot-pulse absolute left-4 top-[calc(1.75rem-4px)] h-3 w-3 rounded-full bg-blue-500 ring-4 ring-white z-50"
style={{ animationDelay: `${index * 1.8}s` }}
/>
<div
className="step-card ml-10 rounded-xl border border-blue-100 bg-white/90 backdrop-blur-sm p-3 shadow-md"
style={{ animationDelay: `${index * 0.08}s` }}
>
<p className="text-blue-600 text-xs font-semibold uppercase tracking-[0.18em]">
Step {index + 1}
</p>
<div className="mt-1.5 flex items-start justify-between gap-3">
<h4 className="text-gray-900 font-semibold">{step.title}</h4>
<span className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-blue-200 bg-blue-50">
<StepIcon icon={step.icon} />
</span>
</div>
<p className="mt-1 text-sm text-gray-600 leading-relaxed">{step.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
<div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-3">
{[
"Know exactly what happens at each enrollment phase",
"Detect app bottlenecks and policy issues immediately",
"Move from alert to diagnostics with minimal friction",
].map(item => (
<div key={item} className="rounded-xl border border-blue-200 bg-blue-50/80 p-3.5 text-sm text-blue-900">
{item}
</div>
))}
</div>
</div>
</div>
</div>
</section>
{/* Features Grid */}
<div id="features" className="py-20 px-6 bg-white/50 backdrop-blur-sm scroll-mt-14">
<div className="max-w-7xl mx-auto">
<p className="text-sm font-semibold text-center text-blue-600 uppercase tracking-[0.22em] mb-3">
Features
</p>
<h2 className="text-3xl font-bold text-center text-gray-900 mb-12">
Everything you need for full Autopilot visibility
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Feature 1 - Real-Time Monitoring */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Real-Time Monitoring</h3>
<p className="text-gray-600 mb-4">
Watch Autopilot deployments in near real-time with live event streaming. Track every phase from device registration to user login.
</p>
<ul className="space-y-2">
{["Live phase tracking", "Near realtime push updates", "Per-device event stream"].map(item => (
<li key={item} className="flex items-center text-sm text-gray-500">
<span className="w-1.5 h-1.5 rounded-full bg-blue-400 mr-2 shrink-0" />
{item}
</li>
))}
</ul>
</div>
{/* Feature 2 - Rich Analytics */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-indigo-100 rounded-xl flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Rich Analytics</h3>
<p className="text-gray-600 mb-4">
Comprehensive metrics on deployment success rates, performance trends, and hardware insights — powered by customizable analyze rules.
</p>
<ul className="space-y-2">
{["Customizable analyze rules", "Success & failure rates", "Hardware model insights"].map(item => (
<li key={item} className="flex items-center text-sm text-gray-500">
<span className="w-1.5 h-1.5 rounded-full bg-indigo-400 mr-2 shrink-0" />
{item}
</li>
))}
</ul>
</div>
{/* Feature 3 - Fleet Health */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Fleet Health</h3>
<p className="text-gray-600 mb-4">
Live overview of your entire device fleet. Spot unhealthy devices, track blocked enrollments, and monitor per-tenant health at a glance.
</p>
<ul className="space-y-2">
{["Device health scoring", "Blocked device detection", "Tenant-level overview"].map(item => (
<li key={item} className="flex items-center text-sm text-gray-500">
<span className="w-1.5 h-1.5 rounded-full bg-purple-400 mr-2 shrink-0" />
{item}
</li>
))}
</ul>
</div>
{/* Feature 4 - Diagnostics Collection */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Diagnostics Collection</h3>
<p className="text-gray-600 mb-4">
Trigger on-demand diagnostic uploads directly from the portal. Collect ETL logs, event logs, and system info from any enrolled device.
</p>
<ul className="space-y-2">
{["On-demand ZIP upload", "ETL & event log collection", "Per-device retrieval"].map(item => (
<li key={item} className="flex items-center text-sm text-gray-500">
<span className="w-1.5 h-1.5 rounded-full bg-green-400 mr-2 shrink-0" />
{item}
</li>
))}
</ul>
</div>
{/* Feature 5 - Event Timeline */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Event Timeline</h3>
<p className="text-gray-600 mb-4">
Detailed event timeline for every deployment session. Drill down into events, errors, and warnings to troubleshoot efficiently.
</p>
<ul className="space-y-2">
{["Phase-by-phase breakdown", "App install details", "Error & warning highlights"].map(item => (
<li key={item} className="flex items-center text-sm text-gray-500">
<span className="w-1.5 h-1.5 rounded-full bg-orange-400 mr-2 shrink-0" />
{item}
</li>
))}
</ul>
</div>
{/* Feature 6 - Audit Logging */}
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-shadow">
<div className="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">Audit Logging</h3>
<p className="text-gray-600 mb-4">
Complete audit trail of all actions and changes. Meet compliance requirements with detailed logging and data retention policies.
</p>
<ul className="space-y-2">
{["Admin action history", "Configurable retention", "Tamper-evident records"].map(item => (
<li key={item} className="flex items-center text-sm text-gray-500">
<span className="w-1.5 h-1.5 rounded-full bg-red-400 mr-2 shrink-0" />
{item}
</li>
))}
</ul>
</div>
</div>
</div>
</div>
{/* Comparison Table */}
<div id="comparison" className="py-20 px-6 bg-white/50 backdrop-blur-sm scroll-mt-14">
<div className="max-w-5xl mx-auto">
<p className="text-sm font-semibold text-center text-blue-600 uppercase tracking-widest mb-3">Comparison</p>
<h2 className="text-4xl font-bold text-center text-gray-900 mb-4">
Standard Autopilot vs. Monitored Autopilot
</h2>
<p className="text-center text-gray-500 mb-12 max-w-2xl mx-auto">
See what you're missing without Autopilot Monitor — and what you gain the moment you deploy it.
</p>
{/* Header */}
<div className="grid grid-cols-[1fr_1fr_1fr] gap-0 mb-1">
<div />
<div className="bg-gradient-to-br from-blue-600 to-indigo-600 text-white text-center py-4 px-6 rounded-t-2xl mx-1">
<div className="font-bold text-lg">Autopilot Monitor</div>
<div className="text-blue-200 text-sm mt-0.5">Fully Monitored</div>
</div>
<div className="bg-gray-100 text-center py-4 px-6 rounded-t-2xl mx-1">
<div className="font-semibold text-gray-700 text-lg">Standard Autopilot</div>
<div className="text-gray-400 text-sm mt-0.5">Out of the Box</div>
</div>
</div>
{/* Rows */}
{[
{
label: "Deployment Visibility",
monitor: "Real-time phase tracking with live push updates",
standard: "None — black box until it finishes or fails",
},
{
label: "Download Progress",
monitor: "Per-app download speed, bytes transferred, % complete",
standard: "No visibility into what's downloading or how long it takes",
},
{
label: "User-Facing Progress Page",
monitor: "Progress view with live app status & download info",
standard: "Generic ESP screen — no details for the end user",
},
{
label: "Fleet Health Dashboard",
monitor: "Success rates, failure trends, avg. duration across all devices",
standard: "Limited manual report extraction from Intune",
},
{
label: "Analyze Rules",
monitor: "Built-in + fully customizable rules for automated issue detection",
standard: "Manual log review required after every failed deployment",
},
{
label: "Extended Data Gathering",
monitor: "Custom gather rules to capture registry, files, or WMI on any event",
standard: "No automated data collection during enrollment",
},
{
label: "Geo & Network Context",
monitor: "Device location, and network info captured at enrollment start",
standard: "No location or network context in deployment records",
},
{
label: "Performance Monitoring",
monitor: "CPU, memory, disk, and network snapshots during deployment",
standard: "Not captured — no way to detect resource bottlenecks",
},
{
label: "Troubleshooting Speed",
monitor: "Drill into per-event timeline, IME log patterns, and analyze results",
standard: "Manual IME log hunting — slow and error-prone",
},
].map((row, i) => (
<div
key={row.label}
className={`grid grid-cols-[1fr_1fr_1fr] gap-0 border-b border-gray-100 ${i % 2 === 0 ? "bg-white" : "bg-gray-50/60"}`}
>
<div className="py-4 px-5 font-semibold text-gray-800 text-sm flex items-center">{row.label}</div>
<div className="py-4 px-5 mx-1 bg-blue-50/50 text-sm text-blue-900 flex items-start gap-2">
<svg className="w-4 h-4 text-blue-500 mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
{row.monitor}
</div>
<div className="py-4 px-5 mx-1 text-sm text-gray-400 flex items-start gap-2">
<svg className="w-4 h-4 text-gray-300 mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
{row.standard}
</div>
</div>
))}
{/* Bottom cap */}
<div className="grid grid-cols-[1fr_1fr_1fr] gap-0">
<div />
<div className="bg-gradient-to-br from-blue-600 to-indigo-600 rounded-b-2xl mx-1 py-3 text-center text-blue-100 text-xs font-medium">
Full observability from day one
</div>
<div className="bg-gray-100 rounded-b-2xl mx-1 py-3 text-center text-gray-400 text-xs">
Limited to what Intune reports after the fact
</div>
</div>
</div>
</div>
{/* CTA Section */}
<div className="py-20 px-6">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-4xl font-bold text-gray-900 mb-6">
Ready to transform your Autopilot monitoring?
</h2>
<p className="text-xl text-gray-600 mb-8">
Join organizations using Autopilot Monitor to react faster and monitor more reliably.
</p>
<LoginButton
className="px-8 py-4 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-lg font-semibold text-lg shadow-xl hover:shadow-2xl transform hover:-translate-y-0.5 transition-all"
>
Start Monitoring Now
</LoginButton>
</div>
</div>
{/* Footer */}
<footer className="border-t border-gray-200/80 bg-white/60 backdrop-blur-sm">
<div className="max-w-7xl mx-auto px-6 py-10">
<div className="flex flex-col md:flex-row md:items-start gap-10">
{/* Brand — left, fixed width with extra right margin */}
<div className="shrink-0 md:w-52 md:mr-32">
<div className="flex items-center space-x-2.5 mb-3">
<div className="w-7 h-7 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-white" viewBox="0 0 24 24" fill="none">
<rect x="5.0" y="12.2" width="2.8" height="7.8" rx="0.9" fill="currentColor" />
<rect x="10.6" y="10.9" width="2.8" height="9.1" rx="0.9" fill="currentColor" />
<rect x="16.2" y="8.6" width="2.8" height="11.4" rx="0.9" fill="currentColor" />
<path d="M4.4 8.9L8.6 6.8L12.0 7.4L15.4 5.5L18.8 4.9" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17.8 4.2L19.1 4.9L17.9 5.9" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<span className="text-sm font-bold text-gray-900">Autopilot Monitor</span>
</div>
<p className="text-xs text-gray-500 leading-relaxed mb-4">
Real-time monitoring and analytics for Windows enrollments.
</p>
<div className="flex items-center gap-3">
<a href="https://www.linkedin.com/in/oliver-kieselbach/" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-blue-600 transition-colors" title="LinkedIn">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
</a>
<a href="https://github.com/okieselbach/Autopilot-Monitor" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-gray-900 transition-colors" title="GitHub">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
</a>
</div>
</div>
{/* Link columns — spread across remaining space */}
<div className="flex-1 grid grid-cols-2 sm:grid-cols-4 gap-8">
{/* Product */}
<div>
<h4 className="text-[11px] font-semibold text-gray-900 uppercase tracking-wider mb-2.5">Product</h4>
<ul className="space-y-1.5">
<li><a href="#features" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">Features</a></li>
<li><a href="#how-it-works" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">How It Works</a></li>
<li><a href="#comparison" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">Comparison</a></li>
</ul>
</div>
{/* Resources */}
<div>
<h4 className="text-[11px] font-semibold text-gray-900 uppercase tracking-wider mb-2.5">Resources</h4>
<ul className="space-y-1.5">
<li><a href={DOCS_URL} target="_blank" rel="noopener noreferrer" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">Documentation</a></li>
<li><a href="https://github.com/okieselbach/Autopilot-Monitor/issues" target="_blank" rel="noopener noreferrer" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">Feedback</a></li>
<li><a href="https://github.com/okieselbach/Autopilot-Monitor" target="_blank" rel="noopener noreferrer" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">GitHub</a></li>
</ul>
</div>
{/* About */}
<div>
<h4 className="text-[11px] font-semibold text-gray-900 uppercase tracking-wider mb-2.5">About</h4>
<ul className="space-y-1.5">
<li><a href="/about" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">Overview</a></li>
</ul>
</div>
{/* Legal */}
<div>
<h4 className="text-[11px] font-semibold text-gray-900 uppercase tracking-wider mb-2.5">Legal</h4>
<ul className="space-y-1.5">
<li><a href="/privacy" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">Privacy Policy</a></li>
<li><a href="/terms" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">Terms of Use</a></li>
<li><a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-xs text-gray-500 hover:text-blue-600 transition-colors">Imprint</a></li>
</ul>
</div>
</div>
</div>
{/* Bottom bar */}
<div className="mt-8 pt-5 border-t border-gray-200 flex flex-col sm:flex-row items-center justify-between gap-2">
<p className="text-[11px] text-gray-400">
&copy; 2026 Autopilot Monitor. Built with ❤️ by Oliver Kieselbach.{" "}
<span className="inline-block">Hosted on Azure by glueckkanja AG.</span>
</p>
<p className="text-[11px] text-gray-400">
Open source. Star us on{' '}
<a href="https://github.com/okieselbach/Autopilot-Monitor" target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-blue-600 transition-colors">GitHub</a>
</p>
</div>
</div>
</footer>
<LandingNavbar />
<Hero />
<StatsBand />
<JustAsk />
<Story />
<CapabilitiesStrip />
<Comparison />
<HowItWorks />
<FinalCta />
<SiteFooter />
</div>
);
}
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { api } from "@/lib/api";
import { DOCS_URL } from "@/utils/config";
import { BrandMark } from "../../components/BrandMark";
export default function PreviewPage() {
const { isAuthenticated, isLoading, user, isPreviewBlocked, previewMessage, logout, getAccessToken } = useAuth();
@@ -14,8 +15,16 @@ export default function PreviewPage() {
const [emailStatus, setEmailStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
const [emailError, setEmailError] = useState("");
// ?demo=1 renders the page without auth/redirects (placeholder data) so
// the design can be reviewed locally and on SWA preview environments.
const [demo, setDemo] = useState<boolean | null>(null);
useEffect(() => {
setDemo(new URLSearchParams(window.location.search).has("demo"));
}, []);
// If not preview-blocked (e.g. approved tenant navigates here), redirect away
useEffect(() => {
if (demo === null || demo) return;
if (!isLoading && isAuthenticated && user && !isPreviewBlocked) {
if (user.isTenantAdmin || user.isGlobalAdmin) {
router.push("/dashboard");
@@ -26,7 +35,7 @@ export default function PreviewPage() {
if (!isLoading && !isAuthenticated) {
router.push("/");
}
}, [isAuthenticated, isLoading, user, isPreviewBlocked, router]);
}, [demo, isAuthenticated, isLoading, user, isPreviewBlocked, router]);
const handleSaveEmail = async () => {
const email = notificationEmail.trim();
@@ -67,63 +76,64 @@ export default function PreviewPage() {
}
};
if (isLoading) {
if (!demo && isLoading) {
return (
<div className="landing-page min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 flex items-center justify-center">
<div className="landing-v2 min-h-screen bg-[var(--lp-bg)] flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[var(--lp-accent)] mx-auto"></div>
<p className="mt-4 text-[var(--lp-ink-soft)]">Loading...</p>
</div>
</div>
);
}
const upn = user?.upn ?? "you@contoso.com";
const tenantId = user?.tenantId ?? "00000000-0000-0000-0000-000000000000";
return (
<div className="landing-page min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 flex items-center justify-center px-6">
<div className="landing-v2 min-h-screen bg-[var(--lp-bg)] flex items-center justify-center px-6 py-12">
<div className="max-w-lg w-full text-center">
{/* Logo */}
<div className="flex items-center justify-center space-x-3 mb-8">
<div className="w-12 h-12 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
<svg className="w-7 h-7 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent">
Autopilot Monitor
</span>
{/* Brand */}
<div className="flex items-center justify-center gap-2.5 mb-8">
<BrandMark className="w-7 h-7" />
<span className="text-xl font-bold tracking-tight text-[var(--lp-ink)]">Autopilot Monitor</span>
</div>
{/* Card */}
<div className="bg-white rounded-2xl shadow-xl p-10">
{/* Clock icon */}
<div className="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-8 h-8 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line)] rounded-2xl shadow-xl shadow-black/[0.06] p-8 sm:p-10">
<div className="w-14 h-14 bg-[var(--lp-warn-soft)] rounded-full flex items-center justify-center mx-auto mb-5">
<svg className="w-7 h-7 text-[var(--lp-warn)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h1 className="text-2xl font-normal text-gray-900 mb-3">
Private Preview
<div className="mb-3">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[var(--lp-warn-soft)] text-[var(--lp-warn)] text-xs font-bold uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-[var(--lp-warn)]" />
Approval pending
</span>
</div>
<h1 className="text-2xl font-bold tracking-tight text-[var(--lp-ink)] mb-2">
Almost there — you&apos;re on the list
</h1>
<p className="text-gray-600 mb-6 leading-relaxed">
{previewMessage || "Autopilot Monitor is currently in private preview. Your organization has been added to the waitlist."}
<p className="text-[15px] text-[var(--lp-ink-soft)] mb-6 leading-relaxed">
{previewMessage || "Autopilot Monitor is currently in preview. Your organization has been added to the waitlist."}
</p>
<div className="bg-blue-50 rounded-lg p-4 mb-6">
<p className="text-sm text-blue-700">
Signed in as <span className="font-semibold">{user?.upn}</span>
</p>
<p className="text-xs text-blue-500 mt-1">
Tenant: {user?.tenantId}
<div className="bg-[var(--lp-surface-2)] border border-[var(--lp-line-soft)] rounded-xl p-3.5 mb-5">
<p className="text-sm text-[var(--lp-ink-soft)]">
Signed in as <span className="font-semibold text-[var(--lp-ink)]">{upn}</span>
</p>
<p className="text-xs text-[var(--lp-ink-faint)] mt-0.5 font-mono">Tenant: {tenantId}</p>
</div>
{/* Notification Email */}
<div className="text-left bg-indigo-50 border border-indigo-200 rounded-lg p-4 mb-6">
<p className="text-sm font-semibold text-indigo-900 mb-1">Get notified when approved</p>
<p className="text-sm text-indigo-700 mb-3">
Enter your email and we'll notify you as soon as your Private Preview access is granted.
<div className="text-left bg-[var(--lp-accent-soft)] border border-[var(--lp-accent-line)] rounded-xl p-4 mb-5">
<p className="text-sm font-semibold text-[var(--lp-ink)] mb-1">Get notified when approved</p>
<p className="text-sm text-[var(--lp-ink-soft)] mb-3">
Enter your email and we&apos;ll notify you as soon as your preview access is granted.
</p>
<div className="flex gap-2">
<input
@@ -131,73 +141,72 @@ export default function PreviewPage() {
value={notificationEmail}
onChange={(e) => { setNotificationEmail(e.target.value); setEmailStatus("idle"); setEmailError(""); }}
placeholder="your@email.com"
className="flex-1 px-3 py-2 border border-indigo-300 rounded-lg text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
className="flex-1 px-3 py-2 border border-[var(--lp-line)] bg-[var(--lp-surface)] rounded-lg text-sm text-[var(--lp-ink)] placeholder-[var(--lp-ink-faint)] focus:outline-none focus:ring-2 focus:ring-[var(--lp-accent)] focus:border-[var(--lp-accent)] transition-colors"
onKeyDown={(e) => { if (e.key === "Enter") handleSaveEmail(); }}
/>
<button
onClick={handleSaveEmail}
disabled={emailStatus === "saving" || !notificationEmail.trim()}
className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
className="px-4 py-2 text-sm font-semibold text-white bg-[var(--lp-accent-ink)] rounded-lg hover:brightness-110 disabled:opacity-50 disabled:cursor-not-allowed transition-all whitespace-nowrap"
>
{emailStatus === "saving" ? "Saving..." : "Notify me"}
</button>
</div>
{emailStatus === "saved" && (
<p className="text-xs text-green-600 mt-2 font-medium">
Email saved! We'll send you a notification when your access is approved.
<p className="text-xs text-[var(--lp-accent-ink)] mt-2 font-medium">
Email saved! We&apos;ll send you a notification when your access is approved.
</p>
)}
{emailStatus === "error" && emailError && (
<p className="text-xs text-red-600 mt-2 font-medium">{emailError}</p>
<p className="text-xs text-[var(--lp-danger)] mt-2 font-medium">{emailError}</p>
)}
</div>
<div className="text-left bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
<p className="text-sm font-semibold text-amber-900 mb-1">Next steps</p>
<p className="text-sm text-amber-800">
Please sign out and contact me on LinkedIn or open a GitHub issue to request access to the Private Preview. I check incoming requests regularly and will approve them as quickly as possible if I have enough capacity left.
<div className="text-left bg-[var(--lp-surface-2)] border border-[var(--lp-line-soft)] rounded-xl p-4 mb-6">
<p className="text-sm font-semibold text-[var(--lp-ink)] mb-1">Next steps</p>
<p className="text-sm text-[var(--lp-ink-soft)]">
Please sign out and wait a bit or contact me on LinkedIn or open a GitHub issue to request
access to the preview. I check incoming requests regularly and will approve them as quickly
as possible.
</p>
<p className="text-sm text-amber-800 mt-2">
In the meantime, you can already review the setup and configuration in the <a href={DOCS_URL} target="_blank" rel="noopener noreferrer" className="underline hover:no-underline">documentation</a>.
<p className="text-sm text-[var(--lp-ink-soft)] mt-2">
In the meantime, you can already review the setup and configuration in the{" "}
<a href={DOCS_URL} target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">documentation</a>.
</p>
<p className="text-sm text-amber-800 mt-2">
When you signed-up, sign in again later to view the updated approval status on your dashboard.
<p className="text-sm text-[var(--lp-ink-soft)] mt-2">
Sign in again later to check: while your approval is pending you&apos;ll land back on this
page — once approved, you&apos;ll go straight into the portal.
</p>
<div className="flex flex-wrap gap-2 mt-3">
<a
href="https://www.linkedin.com/in/oliver-kieselbach/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-blue-700 bg-blue-100 rounded-md hover:bg-blue-200 transition-colors"
>
LinkedIn
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
<a
href="https://github.com/okieselbach/Autopilot-Monitor/issues"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-blue-700 bg-blue-100 rounded-md hover:bg-blue-200 transition-colors"
>
GitHub Issues
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
{[
{ label: "LinkedIn", href: "https://www.linkedin.com/in/oliver-kieselbach/" },
{ label: "GitHub Issues", href: "https://github.com/okieselbach/AutopilotMonitor/issues" },
].map(link => (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-[var(--lp-ink)] border border-[var(--lp-line)] bg-[var(--lp-surface)] rounded-lg hover:border-[var(--lp-ink-faint)] transition-colors"
>
{link.label}
<svg className="w-3.5 h-3.5 text-[var(--lp-ink-faint)]" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
))}
</div>
</div>
<button
onClick={logout}
className="px-6 py-3 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors"
className="px-6 py-2.5 border border-[var(--lp-line)] bg-[var(--lp-surface)] text-[var(--lp-ink)] rounded-lg font-semibold hover:border-[var(--lp-ink-faint)] transition-colors"
>
Sign Out
</button>
</div>
<p className="mt-6 text-sm text-gray-400">
<p className="mt-6 text-sm text-[var(--lp-ink-faint)]">
&copy; 2026 Autopilot Monitor. Powered by Azure and Microsoft Identity.
</p>
</div>
@@ -6,7 +6,7 @@ export const metadata: Metadata = {
description:
"Privacy Policy for Autopilot Monitor. Learn how we collect, store, and protect your data when using the Windows Autopilot monitoring platform.",
openGraph: {
title: "Privacy Policy | AutopilotMonitor",
title: "Privacy Policy | Autopilot Monitor",
description:
"Privacy Policy for Autopilot Monitor. Learn how we collect, store, and protect your data.",
url: `${SITE_URL}/privacy`,
@@ -1,4 +1,5 @@
import { PublicPageHeader } from "../../components/PublicPageHeader";
import { LandingNavbar } from "../../components/landing/LandingNavbar";
import { SiteFooter } from "../../components/SiteFooter";
import { DOCS_URL } from "@/utils/config";
const LAST_UPDATED = "29 July 2026";
@@ -11,7 +12,7 @@ function DocsLink({ href, children }: { href: string; children: React.ReactNode
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-600 hover:text-indigo-800 underline"
className="text-[var(--lp-accent-ink)] hover:opacity-80 underline"
>
{children}
</a>
@@ -20,10 +21,18 @@ function DocsLink({ href, children }: { href: string; children: React.ReactNode
export default function PrivacyPage() {
return (
<div className="min-h-screen bg-gray-50">
<PublicPageHeader title="Privacy Policy" />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<div className="landing-v2 min-h-screen bg-[var(--lp-bg)]">
<LandingNavbar />
<header className="px-4 sm:px-6 lg:px-8 pt-14 sm:pt-16">
<div className="max-w-7xl mx-auto">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[var(--lp-ink-faint)]">Legal</p>
<h1 className="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-[var(--lp-ink)]">
Privacy Policy
</h1>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6 text-[15px]">
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<p className="text-sm text-gray-500">Last updated: {LAST_UPDATED}</p>
<p className="text-gray-700">
Autopilot Monitor collects technical telemetry about Windows Autopilot enrollments so that IT teams can see
@@ -32,7 +41,7 @@ export default function PrivacyPage() {
published in the <DocsLink href={DOCS_SECURITY_FAQ}>Security &amp; Privacy FAQ</DocsLink>.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">Roles: Who Is Responsible for What</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">Roles: Who Is Responsible for What</h2>
<p className="text-gray-700">
For enrollment telemetry, <strong>your organization is the controller and Autopilot Monitor acts as your
processor</strong>. You decide which devices are monitored, what additional data your gather rules collect,
@@ -45,7 +54,7 @@ export default function PrivacyPage() {
<p className="text-gray-700">
glueckkanja AG, a German company certified to ISO/IEC 27001, operates Autopilot Monitor for{" "}
<strong>both plans</strong> — the same service, the same infrastructure, the same protection measures. Company
details are in the <a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">Imprint</a>.
details are in the <a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Imprint</a>.
The project is maintained by Oliver Kieselbach, who acts in that role on behalf of glueckkanja AG and is the
contact for the open-source project and the Community edition.
</p>
@@ -54,7 +63,7 @@ export default function PrivacyPage() {
glueckkanja AG. On the Enterprise plan it forms part of the written agreement.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">What We Collect</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">What We Collect</h2>
<h3 className="text-lg font-medium text-gray-800 mt-4">From enrolling devices</h3>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li><strong>Device identity</strong> — serial number, device name, manufacturer, model, and the Entra ID tenant the device enrolls into</li>
@@ -78,7 +87,7 @@ export default function PrivacyPage() {
notification address, that address is copied once as the initial contact and is yours to change from then on.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">What We Do Not Collect</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">What We Do Not Collect</h2>
<p className="text-gray-700">
During Autopilot provisioning the user signs in once to start the process and then does not interact with the
device while it provisions. The agent captures <strong>no browsing history, no file or document content, no
@@ -91,7 +100,7 @@ export default function PrivacyPage() {
and establishing persistence are hard-blocked and cannot be enabled by configuration.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">IP Addresses and Geolocation</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">IP Addresses and Geolocation</h2>
<p className="text-gray-700">
Geolocation is a tenant setting and is <strong>enabled by default</strong>. While it is enabled:
</p>
@@ -106,7 +115,7 @@ export default function PrivacyPage() {
tenant setting and also an agent command-line switch. Only the Geographic Performance view is affected.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">Where Your Data Is Stored</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">Where Your Data Is Stored</h2>
<p className="text-gray-700">
All customer data — sessions, events, configuration, audit logs, diagnostics, and backups — is stored in
Microsoft Azure in <strong>Germany West Central</strong>. The only component outside that region is the portal
@@ -114,7 +123,7 @@ export default function PrivacyPage() {
replication and no transfer of customer data outside the EU by the platform.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">Who Can Access Your Data</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">Who Can Access Your Data</h2>
<p className="text-gray-700">
Your data is not sold, and it is not shared with third parties for their own purposes. Access is limited to:
</p>
@@ -128,7 +137,7 @@ export default function PrivacyPage() {
isolation model is in the <DocsLink href={DOCS_SECURITY_FAQ}>Security &amp; Privacy FAQ</DocsLink>.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">External Services</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">External Services</h2>
<p className="text-gray-700">
<strong>Microsoft Azure is the only place your data is stored.</strong> Resend delivers the onboarding
approval email when a tenant is activated — it receives an administrator&apos;s email address and the tenant
@@ -144,7 +153,7 @@ export default function PrivacyPage() {
the parties engaged and the terms of their engagement.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">Diagnostics Uploads</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">Diagnostics Uploads</h2>
<p className="text-gray-700">
Diagnostics upload is <strong>off by default</strong>. When enabled, the default destination is{" "}
<strong>your own Azure storage account</strong> — the package never reaches our infrastructure. Hosted upload
@@ -163,7 +172,7 @@ export default function PrivacyPage() {
tenant that has no upload destination configured.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">How Long We Keep Data</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">How Long We Keep Data</h2>
<p className="text-gray-700">
Retention is <strong>configured by you per tenant, defaulting to 90 days</strong> — 7 to 90 days on the
Community plan and 7 to 365 days on Enterprise. Expired sessions are purged automatically. You additionally
@@ -185,7 +194,7 @@ export default function PrivacyPage() {
Either can be removed on request.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">How Your Data Is Protected</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">How Your Data Is Protected</h2>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li><strong>Device authentication by mutual TLS</strong> using the Intune MDM client certificate, validated against pinned Intune root CAs rather than the operating system trust store — and rejected if no trust anchor loads</li>
<li><strong>Autopilot device validation via Microsoft Graph</strong> — only devices registered in your tenant are accepted, with an optional hardware allow-list on top</li>
@@ -199,7 +208,7 @@ export default function PrivacyPage() {
<li><strong>Manifest-first deletion</strong> — what will be deleted is captured, restorably, before anything is removed</li>
</ul>
<h2 className="text-xl font-semibold text-gray-900 mt-6">Your Rights</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">Your Rights</h2>
<p className="text-gray-700">
Where the GDPR applies, you have the right to access, correction, deletion, restriction of processing,
portability, and to object to processing. We act on such requests within the statutory time limits. In
@@ -211,24 +220,25 @@ export default function PrivacyPage() {
organization; we support you in fulfilling them.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">Changes to This Policy</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">Changes to This Policy</h2>
<p className="text-gray-700">
Material changes are reflected in the &quot;last updated&quot; date above and announced through the service
announcements in the portal. If you hold a signed data processing agreement, notification follows the terms of
that agreement.
</p>
<h2 className="text-xl font-semibold text-gray-900 mt-6">Contact</h2>
<h2 className="text-lg font-semibold text-gray-900 mt-6">Contact</h2>
<p className="text-gray-700">
For privacy questions, a data processing agreement, or a data subject request, contact glueckkanja AG using
the details in the <a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">Imprint</a>.
the details in the <a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Imprint</a>.
For the open-source project and the Community edition you can also reach the maintainer via{" "}
<a href="https://www.linkedin.com/in/oliver-kieselbach" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">LinkedIn</a>{" "}
<a href="https://www.linkedin.com/in/oliver-kieselbach" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">LinkedIn</a>{" "}
or open a{" "}
<a href="https://github.com/okieselbach/Autopilot-Monitor/issues" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">GitHub issue</a>.
<a href="https://github.com/okieselbach/AutopilotMonitor/issues" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">GitHub issue</a>.
</p>
</div>
</main>
<SiteFooter />
</div>
);
}
@@ -89,7 +89,7 @@ export default function ProgressPortalPage() {
>
<div className="flex items-center space-x-2 min-w-0">
<svg
className="w-4 h-4 text-blue-600 flex-shrink-0"
className="w-4 h-4 text-green-600 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -106,9 +106,9 @@ export default function ProgressPortalPage() {
</span>
</div>
<div className="flex items-center space-x-1.5 flex-shrink-0 ml-3">
<span className="text-xs text-blue-600 font-medium">Change device</span>
<span className="text-xs text-green-700 font-medium">Change device</span>
<svg
className="w-3.5 h-3.5 text-blue-600"
className="w-3.5 h-3.5 text-green-700"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -147,9 +147,9 @@ export default function ProgressPortalPage() {
<span>Collapse</span>
</button>
)}
<div className="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4">
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4">
<svg
className="w-8 h-8 text-blue-600"
className="w-8 h-8 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -179,13 +179,13 @@ export default function ProgressPortalPage() {
onChange={(e) => setSerialInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter serial number or device name..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg"
className="w-full px-4 py-3 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 text-lg"
/>
</div>
<button
onClick={searchBySerial}
disabled={searching || !serialInput.trim()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-medium"
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{searching ? "Searching..." : "Check Status"}
</button>
+1 -1
View File
@@ -9,7 +9,7 @@ export default function robots(): MetadataRoute.Robots {
rules: [
{
userAgent: "*",
allow: ["/", "/about", "/privacy", "/terms"],
allow: ["/", "/about", "/get-started", "/privacy", "/terms"],
disallow: [
"/dashboard",
"/fleet-health",
@@ -271,7 +271,7 @@ function AnalysisResultCard({ result, hitRate }: { result: RuleResult; hitRate?:
href={doc.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 underline"
className="text-sm text-green-700 hover:text-green-800 underline"
>
{doc.title}
</a>
@@ -267,7 +267,7 @@ export default function CollectLogsButton({
</p>
<p className="text-sm text-gray-700">
<span className="font-semibold">Enable &amp; Collect</span> switches the upload destination to
{" "}<span className="font-semibold">hosted storage</span> (managed by AutopilotMonitor) with mode
{" "}<span className="font-semibold">hosted storage</span> (managed by Autopilot Monitor) with mode
{" "}<span className="font-semibold">On failure</span>, then collects the logs from this device right away.
</p>
{/* Data-boundary disclosure. This dialog is the SECOND opt-in path into hosted
@@ -277,7 +277,7 @@ export default function CollectLogsButton({
<div className="bg-sky-50 border border-sky-200 rounded-lg p-3 space-y-1">
<p className="text-sm text-sky-900">
<span className="font-semibold">Uploaded packages leave your own Azure tenant boundary.</span>{" "}
They are stored in the AutopilotMonitor backend&apos;s Azure Storage (Germany West Central),
They are stored in the Autopilot Monitor backend&apos;s Azure Storage (Germany West Central),
isolated per tenant, and each upload uses a fresh blob-scoped, write-only token.
</p>
<p className="text-xs text-sky-800">
@@ -231,7 +231,7 @@ export default function DeviceDetailsCard({ events, latestAgentVersion, session
<div className="mt-1">
<button
onClick={() => setShowIpv6(prev => ({ ...prev, [i]: !isIpv6Shown }))}
className="text-xs text-blue-600 hover:text-blue-800 flex items-center space-x-1"
className="text-xs text-green-700 hover:text-green-800 flex items-center space-x-1"
>
<svg className={`w-3 h-3 transition-transform duration-200 ${isIpv6Shown ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
@@ -462,7 +462,7 @@ export default function DeviceDetailsCard({ events, latestAgentVersion, session
<span className="text-xs text-gray-500 min-w-[120px]">OOBE Config</span>
<button
onClick={() => setShowOobeModal(true)}
className="text-xs text-right text-blue-600 hover:text-blue-800 hover:underline cursor-pointer font-mono transition-colors"
className="text-xs text-right text-green-700 hover:text-green-800 hover:underline cursor-pointer font-mono transition-colors"
title="Click to decode bitmask"
>
{`${autopilotProfile.CloudAssignedOobeConfig}`}
@@ -166,7 +166,7 @@ export default function DeviceHistoryBanner({
{isCurrent ? (
<span className="text-xs text-amber-800">this session</span>
) : (
<Link href={sessionHref(r.sessionId)} className="text-xs text-blue-600 hover:text-blue-800 underline underline-offset-2">
<Link href={sessionHref(r.sessionId)} className="text-xs text-green-700 hover:text-green-800 underline underline-offset-2">
open
</Link>
)}
@@ -91,7 +91,7 @@ export default function EventTimeline({
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search events..."
className="w-full pl-7 pr-7 py-1 text-xs border border-gray-300 rounded-full focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
className="w-full pl-7 pr-7 py-1 text-xs border border-gray-300 rounded-full focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-green-500"
/>
{searchQuery && (
<button
@@ -332,7 +332,7 @@ function PhaseSection({
onClick={onToggle}
className="flex items-center justify-between w-full text-left mb-3 group"
>
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-blue-600">
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-green-600">
{phaseName} ({events.length} events)
</h3>
<svg className={`w-5 h-5 text-gray-400 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -603,7 +603,7 @@ function EventRow({ event, showScriptOutput }: { event: EnrollmentEvent; showScr
{hasDetails && (
<button
onClick={() => setShowDetails(!showDetails)}
className="text-xs text-blue-600 hover:text-blue-800 ml-4 flex-shrink-0"
className="text-xs text-green-700 hover:text-green-800 ml-4 flex-shrink-0"
>
{showDetails ? 'Hide' : hasGatherOutput ? 'Output' : 'Details'}
</button>
@@ -622,7 +622,7 @@ function EventRow({ event, showScriptOutput }: { event: EnrollmentEvent; showScr
type="button"
onClick={copyEventId}
title={copied ? 'Copied!' : 'Copy EventId'}
className="absolute top-1.5 right-1.5 inline-flex items-center justify-center w-5 h-5 rounded border border-gray-200 bg-white text-gray-400 opacity-0 group-hover/meta:opacity-100 focus:opacity-100 hover:bg-gray-50 hover:text-blue-600 transition-opacity"
className="absolute top-1.5 right-1.5 inline-flex items-center justify-center w-5 h-5 rounded border border-gray-200 bg-white text-gray-400 opacity-0 group-hover/meta:opacity-100 focus:opacity-100 hover:bg-gray-50 hover:text-green-600 transition-opacity"
>
{copied ? (
<svg className="w-3 h-3 text-green-500" viewBox="0 0 20 20" fill="currentColor">
@@ -814,7 +814,7 @@ function RawEventRow({ event }: { event: EnrollmentEvent }) {
<span className="text-gray-900 font-medium flex-shrink-0">{event.eventType}</span>
<span className="text-gray-500 truncate flex-1 min-w-0" title={event.message || undefined}>{shortenBuildHashInMessage(event.message)}</span>
{hasDetails && (
<button onClick={() => setExpanded(!expanded)} className="text-gray-400 hover:text-blue-600 flex-shrink-0 ml-1">
<button onClick={() => setExpanded(!expanded)} className="text-gray-400 hover:text-green-600 flex-shrink-0 ml-1">
{expanded ? '−' : '+'}
</button>
)}
@@ -245,7 +245,7 @@ export default function ReportSessionModal({
value={comment}
onChange={e => setComment(e.target.value)}
placeholder="Describe what seems incorrect or unexpected..."
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
rows={3}
disabled={submitting}
/>
@@ -261,7 +261,7 @@ export default function ReportSessionModal({
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="your.email@company.com"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
disabled={submitting}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
@@ -365,7 +365,7 @@ export default function ReportSessionModal({
checked={hasDiagnostics && includeDiagnostics}
onChange={e => setIncludeDiagnostics(e.target.checked)}
disabled={submitting || !hasDiagnostics}
className="mt-0.5 h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
className="mt-0.5 h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-green-600 focus:ring-green-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">
Include uploaded diagnostics archive
@@ -424,7 +424,7 @@ export default function ReportSessionModal({
<button
onClick={handleSubmit}
disabled={submitting || !!agentLogError || !!screenshotError}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center gap-2"
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 flex items-center gap-2"
>
{submitting ? (
<>
@@ -562,7 +562,7 @@ export default function VulnerabilityReportSection({
placeholder="Search software, versions, CVE IDs..."
value={findingsSearch}
onChange={e => setFindingsSearch(e.target.value)}
className="w-full sm:w-80 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
className="w-full sm:w-80 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 outline-none"
/>
</div>
@@ -665,7 +665,7 @@ export default function VulnerabilityReportSection({
placeholder="Search name, version, publisher, source..."
value={inventorySearch}
onChange={e => setInventorySearch(e.target.value)}
className="w-full sm:w-80 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
className="w-full sm:w-80 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 outline-none"
/>
</div>
<div className="overflow-x-auto">
@@ -917,7 +917,7 @@ function FindingRow({ finding, isExpanded, onToggle }: {
href={`https://nvd.nist.gov/vuln/detail/${cve.cveId}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-600 hover:text-blue-800 underline"
className="text-sm font-medium text-green-700 hover:text-green-800 underline"
>
{cve.cveId}
</a>
@@ -167,7 +167,7 @@ function Tabs({
onClick={() => onChange(t.id)}
className={`border-b-2 px-1 py-2 text-sm font-medium transition-colors ${
activeTab === t.id
? "border-blue-600 text-blue-600"
? "border-green-600 text-green-700"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
}`}
>
@@ -167,9 +167,9 @@ function SessionDetailContent() {
// title. Falls back to the serial number, then to the bare app name while the session is still loading.
useEffect(() => {
const label = session?.deviceName || session?.serialNumber;
document.title = label ? `${label} | AutopilotMonitor` : "AutopilotMonitor";
document.title = label ? `${label} | Autopilot Monitor` : "Autopilot Monitor";
return () => {
document.title = "AutopilotMonitor";
document.title = "Autopilot Monitor";
};
}, [session?.deviceName, session?.serialNumber]);
@@ -63,7 +63,7 @@ export default function DataManagementSection({
value={dataRetentionDays}
disabled={isRetentionDisabled}
onChange={(e) => setDataRetentionDays(parseInt(e.target.value) || 90)}
className={`mt-1 block w-full px-4 py-2 border rounded-lg placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors ${
className={`mt-1 block w-full px-4 py-2 border rounded-lg placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors ${
isRetentionDisabled
? "bg-gray-100 text-gray-500 border-gray-200 cursor-not-allowed"
: "border-gray-300 text-gray-900"
@@ -106,7 +106,7 @@ export default function DataManagementSection({
max="12"
value={sessionTimeoutHours}
onChange={(e) => setSessionTimeoutHours(parseInt(e.target.value) || 5)}
className="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
className="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors"
/>
<p className="text-xs text-gray-400 mt-1">Default: 5 hours (ESP default). Minimum: 1 hour, Maximum: 12 hours</p>
</label>
@@ -101,7 +101,7 @@ export default function DiagnosticsSection({
value="CustomerSas"
checked={!isHosted}
onChange={() => setDiagnosticsUploadDestination("CustomerSas")}
className="mt-1 text-amber-600 focus:ring-amber-500"
className="mt-1 text-green-600 focus:ring-green-500"
/>
<div className="min-w-0">
<p className="font-medium text-gray-900 text-sm">Your own Azure Blob Storage</p>
@@ -122,7 +122,7 @@ export default function DiagnosticsSection({
value="Hosted"
checked={isHosted}
onChange={() => setDiagnosticsUploadDestination("Hosted")}
className="mt-1 text-sky-600 focus:ring-sky-500"
className="mt-1 text-green-600 focus:ring-green-500"
/>
<div className="min-w-0">
<p className="font-medium text-gray-900 text-sm flex items-center gap-1.5">
@@ -132,7 +132,7 @@ export default function DiagnosticsSection({
</span>
</p>
<p className="text-xs text-gray-500 mt-0.5">
Uploads go to the AutopilotMonitor backend's Azure Storage. Per-upload, blob-scoped tokens; 15-min TTL.
Uploads go to the Autopilot Monitor backend's Azure Storage. Per-upload, blob-scoped tokens; 15-min TTL.
</p>
</div>
</label>
@@ -215,7 +215,7 @@ export default function DiagnosticsSection({
Hosted storage
</p>
<p className="text-sm text-sky-900 mb-2">
Diagnostics packages are uploaded to the AutopilotMonitor backend's Azure Storage (operated by the Autopilot Monitor Team). Blobs are isolated per tenant via a <code className="font-mono text-xs bg-sky-100 px-1 rounded">&#123;tenantId&#125;/</code> prefix, and each upload uses a fresh blob-scoped, write-only token (15-min TTL).
Diagnostics packages are uploaded to the Autopilot Monitor backend's Azure Storage (operated by the Autopilot Monitor Team). Blobs are isolated per tenant via a <code className="font-mono text-xs bg-sky-100 px-1 rounded">&#123;tenantId&#125;/</code> prefix, and each upload uses a fresh blob-scoped, write-only token (15-min TTL).
</p>
<p className="text-xs text-sky-800">
Heads-up: with this option, uploaded contents leave your own Azure tenant boundary. Retention follows your tenant's <strong>Data Retention Days</strong> setting and is enforced by the cascade-delete pipeline — old packages are removed automatically. Review the diagnostics paths section below to see what is collected.
@@ -302,7 +302,7 @@ export default function DiagnosticsSection({
updated[idx] = { ...entry, includeSubfolders: !entry.includeSubfolders };
setTenantDiagPaths(updated);
}}
className="w-3.5 h-3.5 rounded border-amber-400 text-amber-600 focus:ring-amber-500"
className="w-3.5 h-3.5 rounded border-amber-400 text-green-600 focus:ring-green-500"
/>
<span className="text-xs text-amber-600">Include subfolders</span>
</label>
@@ -348,7 +348,7 @@ export default function DiagnosticsSection({
setNewDiagSubfolders(false);
}}
disabled={!newDiagPath.trim()}
className="px-4 py-1.5 bg-amber-600 text-white rounded-lg text-sm font-medium hover:bg-amber-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
className="px-4 py-1.5 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
>
Add
</button>
@@ -358,7 +358,7 @@ export default function DiagnosticsSection({
type="checkbox"
checked={newDiagSubfolders}
onChange={() => setNewDiagSubfolders(!newDiagSubfolders)}
className="w-3.5 h-3.5 rounded border-gray-400 text-amber-600 focus:ring-amber-500"
className="w-3.5 h-3.5 rounded border-gray-400 text-green-600 focus:ring-green-500"
/>
<span className="text-xs text-gray-500">Include subfolders</span>
</label>
@@ -80,7 +80,7 @@ function WebhookToggle({
disabled={!interactive}
className="sr-only peer"
/>
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer peer-checked:bg-blue-600 peer-disabled:opacity-50 transition-colors" />
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-green-500 rounded-full peer peer-checked:bg-blue-600 peer-disabled:opacity-50 transition-colors" />
<div className="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform peer-checked:translate-x-4" />
</div>
</label>
@@ -143,7 +143,7 @@ export default function HardwareRejectionInsights({
<p className="text-sm text-red-600">Failed to load hardware rejections: {error}</p>
<button
onClick={fetchData}
className="mt-2 text-sm text-blue-600 hover:text-blue-800 underline"
className="mt-2 text-sm text-green-700 hover:text-green-800 underline"
>
Retry
</button>
@@ -234,7 +234,7 @@ export default function HardwareRejectionInsights({
<td className="py-3 pr-4">
<button
onClick={() => setExpandedRow(isExpanded ? null : rowKey)}
className="text-left font-medium text-gray-900 hover:text-blue-600 transition-colors"
className="text-left font-medium text-gray-900 hover:text-green-600 transition-colors"
title="Toggle details"
>
{row.manufacturer || "\u2013"}
@@ -106,12 +106,12 @@ function WhitelistEditor({
addItem();
}
}}
className="flex-1 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
className="flex-1 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
/>
<button
onClick={addItem}
disabled={!trimmed || isDuplicate}
className="px-4 py-1.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
className="px-4 py-1.5 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
>
Add
</button>
@@ -86,7 +86,7 @@ export default function NotRegisteredDevicesInsights({
<p className="text-sm text-red-600">Failed to load unregistered devices: {error}</p>
<button
onClick={fetchData}
className="mt-2 text-sm text-blue-600 hover:text-blue-800 underline"
className="mt-2 text-sm text-green-700 hover:text-green-800 underline"
>
Retry
</button>
@@ -396,14 +396,14 @@ function OffboardingInProgressBanner({
disabled={submitting}
rows={4}
placeholder="Pricing, missing features, bugs that bit us, alternatives we picked, …"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm disabled:bg-gray-100"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm disabled:bg-gray-100"
/>
<div className="flex items-center justify-between text-xs text-gray-500">
<span>{feedbackText.length}/{FEEDBACK_MAX_CHARS}</span>
<button
onClick={handleSubmitFeedback}
disabled={submitting || feedbackText.trim().length === 0}
className="px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm font-medium"
className="px-3 py-1.5 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm font-medium"
>
{submitting ? "Submitting…" : "Submit feedback"}
</button>
@@ -50,7 +50,7 @@ export default function SaveResetBar({ onSave, onReset, saving, canSave = true }
onClick={handleSave}
disabled={saving || !canSave}
className={`px-4 py-2 rounded-md text-sm text-white font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center space-x-2 ${
saveResult === "saved" ? "bg-emerald-600" : "bg-indigo-600 hover:bg-indigo-700"
saveResult === "saved" ? "bg-emerald-600" : "bg-green-600 hover:bg-green-700"
}`}
>
{saving ? (
@@ -115,7 +115,7 @@ export default function UnrestrictedModeSection({
type="checkbox"
checked={acknowledged}
onChange={(e) => setAcknowledged(e.target.checked)}
className="mt-0.5 h-4 w-4 rounded border-red-300 text-red-600 focus:ring-red-500"
className="mt-0.5 h-4 w-4 rounded border-red-300 text-red-600 accent-red-600 focus:ring-red-500"
/>
<span className="text-sm font-medium text-red-900">
I understand the risks and accept full responsibility for enabling Unrestricted Mode for my tenant.
@@ -1,5 +1,6 @@
"use client";
import { SegmentedControl, TIME_RANGE_OPTIONS } from "@/components/SegmentedControl";
import { useCallback, useEffect, useState } from "react";
import { useAuth } from "../../../../contexts/AuthContext";
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
@@ -121,21 +122,11 @@ export function SectionMcpUsage() {
</span>
)}
{/* Date Range Selector */}
<div className="flex rounded-md shadow-sm">
{(["7d", "30d", "90d"] as DateRange[]).map((range) => (
<button
key={range}
onClick={() => setDateRange(range)}
className={`px-3 py-1.5 text-sm font-medium border ${
dateRange === range
? "bg-indigo-600 text-white border-indigo-600 z-10"
: "bg-white text-gray-700 border-gray-300 hover:bg-gray-50"
} ${range === "7d" ? "rounded-l-md" : ""} ${range === "90d" ? "rounded-r-md" : ""} -ml-px first:ml-0`}
>
{range}
</button>
))}
</div>
<SegmentedControl
options={TIME_RANGE_OPTIONS}
value={dateRange}
onChange={(v) => setDateRange(v as DateRange)}
/>
<button
onClick={() => fetchUsage(dateRange)}
disabled={loading}
@@ -41,7 +41,7 @@ export function SectionContact() {
value={contactEmail}
onChange={(e) => setContactEmail(e.target.value)}
placeholder={canEditConfig ? "it-operations@contoso.com" : "Not configured"}
className="mt-1 block w-full max-w-md rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 disabled:bg-gray-50 disabled:text-gray-600"
className="mt-1 block w-full max-w-md rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-green-500 focus:ring-green-500 disabled:bg-gray-50 disabled:text-gray-600"
/>
{looksInvalid && (
<p className="mt-1 text-sm text-amber-600">That does not look like an email address.</p>
@@ -189,7 +189,7 @@ export function SectionOptionalGraphCapabilities() {
<button
onClick={onRefresh}
disabled={refreshing}
className="px-3 py-1.5 text-sm rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
className="px-3 py-1.5 text-sm rounded-md bg-green-600 text-white font-medium hover:bg-green-700 disabled:opacity-50"
>
{refreshing ? "Refreshing …" : "Refresh permission status"}
</button>
@@ -201,7 +201,7 @@ export function SectionPlan() {
<p className="text-sm text-gray-600">
This tenant has already used its Enterprise trial. To move to Enterprise,{" "}
<a
href="https://github.com/okieselbach/Autopilot-Monitor/issues"
href="https://github.com/okieselbach/AutopilotMonitor/issues"
target="_blank"
rel="noopener noreferrer"
className="text-purple-700 hover:underline"
@@ -205,7 +205,7 @@ export function SectionSubmitLogs() {
value={comment}
onChange={e => setComment(e.target.value)}
placeholder="What's going on? Which device / scenario do these files relate to?"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
rows={3}
disabled={submitting}
/>
@@ -221,7 +221,7 @@ export function SectionSubmitLogs() {
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="your.email@company.com"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
disabled={submitting}
/>
<p className="text-xs text-gray-500 mt-1">
@@ -312,7 +312,7 @@ export function SectionSubmitLogs() {
<button
onClick={handleSubmit}
disabled={submitting || !!logError || !hasAnyContent}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center gap-2"
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 flex items-center gap-2"
title={!hasAnyContent ? "Add a comment, log file, or screenshot first" : undefined}
>
{submitting ? (
@@ -28,6 +28,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${BASE_URL}/get-started`,
lastModified: lastmod("/get-started"),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/privacy`,
lastModified: lastmod("/privacy"),
+12 -12
View File
@@ -27,6 +27,7 @@ import { trackEvent } from "@/lib/appInsights";
import { useGlobalAdminScope } from "@/hooks";
import { GlobalAdminBanner, globalAdminSubtitle } from "@/components/GlobalAdminBanner";
import { TenantScopeSelector } from "@/components/TenantScopeSelector";
import { SegmentedControl, TIME_RANGE_OPTIONS } from "@/components/SegmentedControl";
import Link from "next/link";
import { CardSkeleton } from "@/components/skeletons/PageSkeleton";
@@ -193,7 +194,7 @@ export default function SlaPage() {
{/* Header */}
<header className="bg-white dark:bg-gray-800 shadow">
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-center justify-between gap-y-3">
<div>
<h1 className="text-2xl font-normal text-gray-900 dark:text-white">SLA Compliance</h1>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
@@ -213,21 +214,20 @@ export default function SlaPage() {
)}
</p>
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-2">
<TenantScopeSelector scope={scope} themed />
<select
<SegmentedControl
options={[
{ value: 1, label: "1 Month" },
{ value: 3, label: "3 Months" },
{ value: 6, label: "6 Months" },
]}
value={months}
onChange={(e) => {
const next = Number(e.target.value);
onChange={(next) => {
trackEvent('sla_months_changed', { months: next });
setMonths(next);
}}
className="text-sm border border-gray-300 dark:border-gray-600 rounded-md px-2 py-1.5 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value={1}>Last month</option>
<option value={3}>Last 3 months</option>
<option value={6}>Last 6 months</option>
</select>
/>
<button
onClick={() => {
trackEvent('sla_refresh_clicked', { months });
@@ -270,7 +270,7 @@ export default function SlaPage() {
</p>
<Link
href="/settings/tenant/sla-targets"
className="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded-md text-sm hover:bg-indigo-500 transition-colors"
className="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-md text-sm hover:bg-green-700 transition-colors"
>
Configure SLA Targets
</Link>
@@ -6,7 +6,7 @@ export const metadata: Metadata = {
description:
"Terms of Use for Autopilot Monitor. Read the usage terms, conditions, and acceptable use policies for the Windows Autopilot monitoring and troubleshooting platform.",
openGraph: {
title: "Terms of Use | AutopilotMonitor",
title: "Terms of Use | Autopilot Monitor",
description: "Terms of Use for Autopilot Monitor. Read the usage terms, conditions, and acceptable use policies for the monitoring platform.",
url: `${SITE_URL}/terms`,
},
@@ -1,21 +1,30 @@
import { PublicPageHeader } from "../../components/PublicPageHeader";
import { LandingNavbar } from "../../components/landing/LandingNavbar";
import { SiteFooter } from "../../components/SiteFooter";
import { DOCS_URL } from "@/utils/config";
const LAST_UPDATED = "21 July 2026";
const DOCS_SECURITY_FAQ = `${DOCS_URL}/trust/security-faq`;
const DOCS_PLANS = `${DOCS_URL}/plans`;
const GITHUB_ADVISORY = "https://github.com/okieselbach/Autopilot-Monitor/security/advisories/new";
const GITHUB_ADVISORY = "https://github.com/okieselbach/AutopilotMonitor/security/advisories/new";
export default function TermsPage() {
return (
<div className="min-h-screen bg-gray-50">
<PublicPageHeader title="Terms of Use" />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<div className="landing-v2 min-h-screen bg-[var(--lp-bg)]">
<LandingNavbar />
<header className="px-4 sm:px-6 lg:px-8 pt-14 sm:pt-16">
<div className="max-w-7xl mx-auto">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[var(--lp-ink-faint)]">Legal</p>
<h1 className="mt-3 text-3xl sm:text-4xl font-bold tracking-tight text-[var(--lp-ink)]">
Terms of Use
</h1>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6 text-[15px]">
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<p className="text-sm text-gray-500">Last updated: {LAST_UPDATED}</p>
<p className="text-gray-700">
Autopilot Monitor is provided by <strong>glueckkanja AG</strong>, a German company certified to ISO/IEC
27001 — see the <a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">Imprint</a>.
27001 — see the <a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Imprint</a>.
Both plans run on the same service and the same infrastructure; the plan determines operating limits,
support, and contractual commitments, not how the service works.
</p>
@@ -26,7 +35,7 @@ export default function TermsPage() {
</p>
<div className="grid gap-4 md:grid-cols-2 mt-4">
<div className="p-4 border border-gray-200 rounded-lg">
<div className="p-4 border border-[var(--lp-line-soft)] rounded-lg">
<h3 className="font-semibold text-gray-900 mb-1">Community</h3>
<p className="text-sm text-gray-700">
Free, invite-only, currently in Private Preview, and maintained by Oliver Kieselbach as an open
@@ -34,7 +43,7 @@ export default function TermsPage() {
availability, support, or fitness for a particular purpose. Support is community-based via GitHub.
</p>
</div>
<div className="p-4 border border-gray-200 rounded-lg">
<div className="p-4 border border-[var(--lp-line-soft)] rounded-lg">
<h3 className="font-semibold text-gray-900 mb-1">Enterprise</h3>
<p className="text-sm text-gray-700">
Commercial plan under a written agreement with glueckkanja AG. Includes support and reliability
@@ -45,11 +54,11 @@ export default function TermsPage() {
</div>
<p className="text-gray-700">
The sections below apply to both plans unless stated otherwise. Plan details are documented under{" "}
<a href={DOCS_PLANS} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">Plans</a>.
<a href={DOCS_PLANS} target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Plans</a>.
</p>
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 rounded">
<p className="text-blue-900 text-sm">
<div className="bg-[var(--lp-accent-soft)] border-l-4 border-[var(--lp-accent)] p-4 rounded">
<p className="text-[var(--lp-ink-soft)] text-sm [&_strong]:text-[var(--lp-ink)]">
<strong>Who can use it.</strong> Autopilot Monitor requires a Microsoft Entra ID tenant with Intune and
registered Autopilot devices. The <strong>Community</strong> plan is open to anyone with such a tenant —
organizations, non-profits, and individuals running their own lab or personal tenant alike. The{" "}
@@ -59,8 +68,8 @@ export default function TermsPage() {
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<h2 className="text-xl font-semibold text-gray-900">Acceptable Use</h2>
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Acceptable Use</h2>
<p className="text-gray-700">By using this service, you agree that:</p>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>The service is used to monitor and troubleshoot Windows Autopilot deployments for devices your organization is entitled to manage.</li>
@@ -80,26 +89,26 @@ export default function TermsPage() {
<p className="text-gray-700">
<strong>Security research is welcome</strong> and is not a violation of these terms when reported privately.
Report findings through{" "}
<a href={GITHUB_ADVISORY} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">GitHub Security Advisories</a>{" "}
<a href={GITHUB_ADVISORY} target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">GitHub Security Advisories</a>{" "}
rather than a public issue — see{" "}
<a href={DOCS_SECURITY_FAQ} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">Security &amp; Privacy FAQ</a>{" "}
<a href={DOCS_SECURITY_FAQ} target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Security &amp; Privacy FAQ</a>{" "}
for what happens next. Do not access other tenants&apos; data, degrade the service, or run automated scans
against production while testing.
</p>
</div>
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<h2 className="text-xl font-semibold text-gray-900">Intellectual Property &amp; Licensing</h2>
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Intellectual Property &amp; Licensing</h2>
<div className="space-y-2 text-gray-700">
<p><strong>Open source.</strong> The Autopilot Monitor source code is published on <a href="https://github.com/okieselbach/Autopilot-Monitor" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">GitHub</a> under the licence stated in that repository. That licence governs the code — it does not grant rights to the hosted service, its infrastructure, its data, or the Autopilot Monitor name and branding.</p>
<p><strong>Your data stays yours.</strong> Enrollment telemetry, configuration, and diagnostics belong to your organization. Using the service grants only the rights needed to operate it for you, as described in the <a href="/privacy" className="text-indigo-600 hover:text-indigo-800 underline">Privacy Policy</a>.</p>
<p><strong>Open source.</strong> The Autopilot Monitor source code is published on <a href="https://github.com/okieselbach/AutopilotMonitor" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">GitHub</a> under the licence stated in that repository. That licence governs the code — it does not grant rights to the hosted service, its infrastructure, its data, or the Autopilot Monitor name and branding.</p>
<p><strong>Your data stays yours.</strong> Enrollment telemetry, configuration, and diagnostics belong to your organization. Using the service grants only the rights needed to operate it for you, as described in the <a href="/privacy" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Privacy Policy</a>.</p>
<p><strong>Rules you contribute</strong> are covered by the licence grant described under &quot;Your Data, Suspension and Termination&quot; below.</p>
<p><strong>Third-party components</strong> remain the property of their respective owners and are used under their own licences; see the attributions below.</p>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<h2 className="text-xl font-semibold text-gray-900">Delegated (MSP) Administration</h2>
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Delegated (MSP) Administration</h2>
<p className="text-gray-700">
Delegated administration lets a managing organization see a defined set of customer tenants from one place. It
is an Enterprise capability and applies only where access has been granted.
@@ -113,12 +122,12 @@ export default function TermsPage() {
</ul>
</div>
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<h2 className="text-xl font-semibold text-gray-900">Availability, Support and Data</h2>
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Availability, Support and Data</h2>
<div className="bg-amber-50 border-l-4 border-amber-500 p-4 rounded">
<h3 className="font-semibold text-amber-900 mb-2">Community plan: provided &quot;AS-IS&quot;, no warranty</h3>
<p className="text-amber-800 text-sm">
<div className="bg-[var(--lp-warn-soft)] border-l-4 border-[var(--lp-warn)] p-4 rounded">
<h3 className="font-semibold text-[var(--lp-ink)] mb-2">Community plan: provided &quot;AS-IS&quot;, no warranty</h3>
<p className="text-[var(--lp-ink-soft)] text-sm">
The Community plan is provided free of charge, &quot;AS-IS&quot;, and without warranty of any kind, express
or implied, including the implied warranties of merchantability and fitness for a particular purpose. It is
currently in Private Preview: updates are frequent, availability is not guaranteed, and data structures may
@@ -128,17 +137,17 @@ export default function TermsPage() {
<div className="space-y-2 text-gray-700">
<p><strong>Availability.</strong> The Community plan carries no uptime or availability commitment; interruptions, maintenance, and changes can occur without prior notice. The Enterprise plan carries the availability commitments set out in its agreement.</p>
<p><strong>Support.</strong> Community support is best-effort via <a href="https://github.com/okieselbach/Autopilot-Monitor/issues" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">GitHub issues</a>, provided by the project maintainer and the community, with no guaranteed response or resolution time; built-in rules and IME log patterns are community-maintained. Enterprise support follows the response commitments in its agreement.</p>
<p><strong>Support.</strong> Community support is best-effort via <a href="https://github.com/okieselbach/AutopilotMonitor/issues" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">GitHub issues</a>, provided by the project maintainer and the community, with no guaranteed response or resolution time; built-in rules and IME log patterns are community-maintained. Enterprise support follows the response commitments in its agreement.</p>
<p><strong>Data durability.</strong> Autopilot Monitor is a monitoring system, not a system of record. Configuration, authorization, and rule data is backed up daily; session and event telemetry is time-bounded operational data and is <strong>not</strong> backed up. Retain anything you need for compliance or reporting purposes in your own systems.</p>
<p><strong>Liability.</strong> To the extent permitted by law, glueckkanja AG is not liable for indirect, incidental, special, consequential, or punitive damages, or for loss of data, profit, or business, arising from use of or inability to use the service. For the Community plan, which is provided free of charge, liability is limited to intent and gross negligence. Liability for injury to life, body or health and under mandatory statutory provisions remains unaffected. For the Enterprise plan, the liability provisions of the written agreement apply.</p>
<p><strong>Use at your own risk.</strong> The service reports on enrollments; it does not perform them. Operational decisions you take based on its output remain your responsibility.</p>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<h2 className="text-xl font-semibold text-gray-900">Your Data, Suspension and Termination</h2>
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Your Data, Suspension and Termination</h2>
<div className="space-y-2 text-gray-700">
<p><strong>Ownership.</strong> Your enrollment telemetry remains yours. Autopilot Monitor processes it to provide the service, as described in the <a href="/privacy" className="text-indigo-600 hover:text-indigo-800 underline">Privacy Policy</a>.</p>
<p><strong>Ownership.</strong> Your enrollment telemetry remains yours. Autopilot Monitor processes it to provide the service, as described in the <a href="/privacy" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Privacy Policy</a>.</p>
<p><strong>Your controls.</strong> You set the retention period, delete individual sessions, and offboard your tenant entirely at any time — no support ticket required.</p>
<p><strong>Rules you contribute.</strong> Analyze rules, gather rules, and IME log patterns you author are detection definitions, not device data. By creating them you grant a non-exclusive, royalty-free right to retain them and to include them in the shared community rule pool, so that other organizations can benefit from a detection you built. This is the reciprocal side of a product whose built-in rules are community-maintained. You keep the right to use your own rules however you like, and you can request removal from the pool at any time.</p>
<p><strong>Suspension.</strong> Access may be suspended where use threatens the integrity, security, or availability of the service or of other tenants — for example abusive request volumes or attempts to reach other tenants&apos; data. Where circumstances permit, notice is given first.</p>
@@ -147,61 +156,62 @@ export default function TermsPage() {
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<h2 className="text-xl font-semibold text-gray-900">Security and Transparency</h2>
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Security and Transparency</h2>
<p className="text-gray-700">
The security architecture, data residency, external services, retention and deletion behaviour, and an explicit
statement of what the service does <em>not</em> do are published in the{" "}
<a href={DOCS_SECURITY_FAQ} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">Security &amp; Privacy FAQ</a>.
<a href={DOCS_SECURITY_FAQ} target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Security &amp; Privacy FAQ</a>.
A signed data processing agreement is part of the Enterprise plan.
</p>
</div>
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<h2 className="text-xl font-semibold text-gray-900">Third-Party Data Sources &amp; Attributions</h2>
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Third-Party Data Sources &amp; Attributions</h2>
<p className="text-gray-700">
The vulnerability correlation feature uses the following external data sources to identify known
vulnerabilities in installed software. These are inbound reference-data pulls — no customer data is sent to
them.
</p>
<div className="space-y-3">
<div className="p-4 border border-gray-200 rounded-lg">
<div className="p-4 border border-[var(--lp-line-soft)] rounded-lg">
<h3 className="font-semibold text-gray-900 mb-1">National Vulnerability Database (NVD)</h3>
<p className="text-sm text-gray-700 mb-2">
This product uses the NVD API but is not endorsed or certified by the NVD.
</p>
<p className="text-sm text-gray-500">
The NVD is maintained by the National Institute of Standards and Technology (NIST). CVE and CPE data is sourced from the NVD API 2.0. For more information, visit{" "}
<a href="https://nvd.nist.gov/" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">nvd.nist.gov</a>.
<a href="https://nvd.nist.gov/" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">nvd.nist.gov</a>.
</p>
</div>
<div className="p-4 border border-gray-200 rounded-lg">
<div className="p-4 border border-[var(--lp-line-soft)] rounded-lg">
<h3 className="font-semibold text-gray-900 mb-1">CISA Known Exploited Vulnerabilities (KEV) Catalog</h3>
<p className="text-sm text-gray-500">
Actively exploited vulnerability data is sourced from the CISA KEV Catalog maintained by the Cybersecurity and Infrastructure Security Agency. For more information, visit{" "}
<a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">cisa.gov</a>.
<a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">cisa.gov</a>.
</p>
</div>
<div className="p-4 border border-gray-200 rounded-lg">
<div className="p-4 border border-[var(--lp-line-soft)] rounded-lg">
<h3 className="font-semibold text-gray-900 mb-1">Microsoft Security Response Center (MSRC)</h3>
<p className="text-sm text-gray-500">
Microsoft-specific vulnerability data is sourced from the MSRC Security Update Guide API. For more information, visit{" "}
<a href="https://msrc.microsoft.com/" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">msrc.microsoft.com</a>.
<a href="https://msrc.microsoft.com/" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">msrc.microsoft.com</a>.
</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<h2 className="text-xl font-semibold text-gray-900">Changes and Governing Law</h2>
<div className="bg-[var(--lp-surface)] border border-[var(--lp-line-soft)] rounded-xl p-6 space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Changes and Governing Law</h2>
<div className="space-y-2 text-gray-700">
<p><strong>Changes.</strong> These terms may be updated; the &quot;last updated&quot; date above reflects the current version, and material changes are announced through the service announcements in the portal. Continued use after a change constitutes acceptance.</p>
<p><strong>Governing law.</strong> These terms are governed by German law. For the Enterprise plan, the governing-law and venue provisions of the written agreement apply.</p>
<p><strong>Severability.</strong> If a provision is found unenforceable, the remaining provisions stay in effect.</p>
<p><strong>Contact.</strong> Company details are in the <a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">Imprint</a>. For the project and the Community edition, reach the maintainer via <a href="https://www.linkedin.com/in/oliver-kieselbach" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">LinkedIn</a> or open a <a href="https://github.com/okieselbach/Autopilot-Monitor/issues" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-800 underline">GitHub issue</a>.</p>
<p><strong>Contact.</strong> Company details are in the <a href="https://www.glueckkanja.com/en/imprint" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">Imprint</a>. For the project and the Community edition, reach the maintainer via <a href="https://www.linkedin.com/in/oliver-kieselbach" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">LinkedIn</a> or open a <a href="https://github.com/okieselbach/AutopilotMonitor/issues" target="_blank" rel="noopener noreferrer" className="text-[var(--lp-accent-ink)] hover:opacity-80 underline">GitHub issue</a>.</p>
</div>
</div>
</main>
<SiteFooter />
</div>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 150 KiB

@@ -176,7 +176,7 @@ export default function UsageMetricsPage() {
<GlobalAdminBanner show={scope.isGlobalAdmin} delegated={scope.isDelegatedScope} subtitle={globalAdminSubtitle(scope)} />
<header className="bg-white shadow">
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-center justify-between gap-y-3">
<div>
<div>
<h1 className="text-2xl font-normal text-gray-900">Usage Metrics</h1>
@@ -197,7 +197,7 @@ export default function UsageMetricsPage() {
</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-2">
<TenantScopeSelector scope={scope} />
<button
onClick={() => fetchMetrics(true)}
@@ -227,7 +227,7 @@ export default function UsageMetricsPage() {
<p className="text-gray-500">Unable to load usage metrics.</p>
<button
onClick={() => fetchMetrics()}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
className="mt-4 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Retry
</button>
@@ -0,0 +1,12 @@
/**
* The Autopilot Monitor brand mark — the green sail from app/icon.svg,
* kept here as the single inline-SVG source for navbars, footer, and hero.
* The favicon (app/icon.svg) must stay in sync with this path.
*/
export function BrandMark({ className = "w-6 h-6" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 209 191" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M0 180.201L208.401 190.502L188.157 76.2438L5.48363e-06 0L0 180.201Z" fill="#33B161" />
</svg>
);
}
@@ -70,10 +70,10 @@ export function CalculatingCard({
estimateMs: number;
}) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="min-h-screen bg-[var(--lp-bg)] flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md w-full">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto"></div>
<h2 className="mt-4 text-xl font-semibold text-gray-900">{title}</h2>
<p className="mt-2 text-sm text-gray-600">{subtitle}</p>
<ProgressBarWithCaption elapsedMs={elapsedMs} estimateMs={estimateMs} />
@@ -366,7 +366,7 @@ function DownloadItem({ download: dl, progressPercent }: { download: DownloadIte
{dl.eventData && Object.keys(dl.eventData).length > 0 && (
<button
onClick={() => setShowDetails(!showDetails)}
className="text-xs text-blue-600 hover:text-blue-800"
className="text-xs text-green-700 hover:text-green-800"
>
{showDetails ? 'Hide' : 'Details'}
</button>
@@ -411,7 +411,7 @@ function DownloadItem({ download: dl, progressPercent }: { download: DownloadIte
<div className="mt-2">
<button
onClick={() => setShowDoStats(!showDoStats)}
className="flex items-center space-x-1 text-xs text-blue-600 hover:text-blue-800"
className="flex items-center space-x-1 text-xs text-green-700 hover:text-green-800"
>
<svg className="w-3.5 h-3.5 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
@@ -170,7 +170,7 @@ export default function FeedbackBubble() {
onChange={(e) => setComment(e.target.value.slice(0, FEEDBACK_MAX_CHARS))}
placeholder="Any feedback? (optional)"
rows={2}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-600 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none placeholder-gray-400 dark:placeholder-gray-500"
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-600 rounded-lg resize-none focus:ring-2 focus:ring-green-500 focus:border-green-500 outline-none placeholder-gray-400 dark:placeholder-gray-500"
/>
{comment.length > 0 && (
<p className="text-xs text-gray-400 text-right mt-0.5">{comment.length}/{FEEDBACK_MAX_CHARS}</p>
@@ -230,7 +230,7 @@ export default function GlobalSearch() {
onKeyDown={handleKeyDown}
onFocus={() => { if (query.length >= 2) setShowDropdown(true); }}
placeholder="Search serial, device, session... (Ctrl+K)"
className="w-full pl-9 pr-8 py-1.5 text-sm bg-gray-100 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent focus:bg-white transition-colors"
className="w-full pl-9 pr-8 py-1.5 text-sm bg-gray-100 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent focus:bg-white transition-colors"
/>
{query && (
<button
@@ -276,7 +276,7 @@ export default function GlobalSearch() {
onChange={(e) => handleInputChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search serial, device, session..."
className="w-full pl-9 pr-8 py-1.5 text-sm bg-gray-100 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent focus:bg-white transition-colors"
className="w-full pl-9 pr-8 py-1.5 text-sm bg-gray-100 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent focus:bg-white transition-colors"
/>
{query && (
<button
@@ -8,7 +8,6 @@ import { useSidebar, PageSectionItem } from "../contexts/SidebarContext";
import { CollapseState } from "../hooks/useSidebarState";
import { DefaultSectionIcon, BookOpenIcon, InformationCircleIcon, DocumentTextIcon, ShieldCheckIcon } from "../lib/sidebarIcons";
import { DASHBOARD_ITEM, NAV_GROUPS, EXPANDABLE_NAV_GROUPS, REGULAR_USER_ITEMS, NavItem, NavGroup, ExpandableNavGroup, ExpandableNavItem } from "../lib/globalNavConfig";
import { PublicSiteNavbar } from "./PublicSiteNavbar";
import { useAdminMode } from "../hooks/useAdminMode";
import { DOCS_URL } from "@/utils/config";
@@ -268,24 +267,19 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
}
}, [pathname]); // eslint-disable-line react-hooks/exhaustive-deps
// All public pages that should show sidebar + navbar
// (docs + changelog moved to docs.autopilotmonitor.com)
const PUBLIC_PATHS = ["/terms", "/privacy", "/about"];
const isPublicPage = PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith(p + "/"));
// Landing page: never show sidebar
if (pathname === "/") {
return <>{children}</>;
}
// Not authenticated and not a public page: no sidebar
if (!isAuthenticated && !isPublicPage) {
// Not authenticated: no sidebar. Public pages (/about, /terms, /privacy,
// /get-started) bring their own landing shell (LandingNavbar + SiteFooter).
if (!isAuthenticated) {
return <>{children}</>;
}
// Whether a top navbar is present (determines sidebar top offset)
// Authenticated users always have the app navbar; public pages have the PublicSiteNavbar
const hasNavbar = isAuthenticated || isPublicPage;
const hasNavbar = isAuthenticated;
// --- Render helpers ---
@@ -305,7 +299,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
if (active) {
return isGlobal
? "bg-purple-50 text-purple-700 font-semibold dark:bg-purple-900/30 dark:text-purple-300"
: "bg-blue-50 text-blue-700 font-semibold dark:bg-blue-900/30 dark:text-blue-300";
: "bg-green-50 text-green-700 font-semibold dark:bg-green-900/30 dark:text-green-300";
}
return isGlobal
? "text-purple-600 hover:bg-purple-50 hover:text-purple-800 dark:text-purple-400 dark:hover:bg-purple-900/20 dark:hover:text-purple-300"
@@ -517,7 +511,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
itemHasActive
? isGlobalStyle
? "bg-purple-50 text-purple-700 font-semibold dark:bg-purple-900/30 dark:text-purple-300"
: "bg-blue-50 text-blue-700 font-semibold dark:bg-blue-900/30 dark:text-blue-300"
: "bg-green-50 text-green-700 font-semibold dark:bg-green-900/30 dark:text-green-300"
: isGlobalStyle
? "text-purple-600 hover:bg-purple-50 hover:text-purple-800 dark:text-purple-400 dark:hover:bg-purple-900/20"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
@@ -542,7 +536,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
itemHasActive
? isGlobalStyle
? "text-purple-700 font-semibold dark:text-purple-300"
: "text-blue-700 font-semibold dark:text-blue-300"
: "text-green-700 font-semibold dark:text-green-300"
: isGlobalStyle
? "text-purple-600 hover:bg-purple-50 hover:text-purple-800 dark:text-purple-400 dark:hover:bg-purple-900/20"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
@@ -583,7 +577,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
subActive
? isGlobalStyle
? "bg-purple-50 text-purple-700 font-medium dark:bg-purple-900/30 dark:text-purple-300"
: "bg-blue-50 text-blue-700 font-medium dark:bg-blue-900/30 dark:text-blue-300"
: "bg-green-50 text-green-700 font-medium dark:bg-green-900/30 dark:text-green-300"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
>
@@ -628,7 +622,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
<>
{/* Divider / spacing between nav and page sections */}
{collapseState === "icons" && !isMobile ? (
<hr className="mx-2 my-2 border-blue-200 dark:border-blue-800" />
<hr className="mx-2 my-2 border-green-200 dark:border-green-800" />
) : (
<div className="mt-4 mb-2 mx-3">
<hr className="border-gray-200 dark:border-gray-700" />
@@ -636,7 +630,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
)}
{/* Title only for ungrouped sections (grouped sections have their own headers) */}
{!groupedSections && (collapseState === "full" || isMobile) && (
<p className="text-[11px] font-semibold uppercase tracking-wider mb-1 px-3 text-blue-500 dark:text-blue-400">
<p className="text-[11px] font-semibold uppercase tracking-wider mb-1 px-3 text-green-600 dark:text-green-400">
{pageSectionsTitle}
</p>
)}
@@ -659,7 +653,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
onClick={() => setMobileDrawerOpen(false)}
className={`flex items-center justify-center px-2 py-2 rounded-md text-sm transition-colors relative group ${
groupHasActive
? "bg-blue-50 text-blue-700 font-semibold dark:bg-blue-900/30 dark:text-blue-300"
? "bg-green-50 text-green-700 font-semibold dark:bg-green-900/30 dark:text-green-300"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
title={group.name}
@@ -682,7 +676,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
onClick={() => toggleGroup(group.name)}
className={`w-full flex items-center gap-2.5 px-3 py-1.5 rounded-md text-sm transition-colors ${
groupHasActive
? "text-blue-700 font-semibold dark:text-blue-300"
? "text-green-700 font-semibold dark:text-green-300"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
>
@@ -717,7 +711,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
onClick={() => setMobileDrawerOpen(false)}
className={`block pl-10 pr-3 py-1 rounded-md text-[13px] transition-colors ${
active
? "bg-blue-50 text-blue-700 font-medium dark:bg-blue-900/30 dark:text-blue-300"
? "bg-green-50 text-green-700 font-medium dark:bg-green-900/30 dark:text-green-300"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
>
@@ -732,7 +726,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
onClick={() => scrollTo(item.id)}
className={`w-full text-left block pl-10 pr-3 py-1 rounded-md text-[13px] transition-colors ${
active
? "bg-blue-50 text-blue-700 font-medium dark:bg-blue-900/30 dark:text-blue-300"
? "bg-green-50 text-green-700 font-medium dark:bg-green-900/30 dark:text-green-300"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
>
@@ -760,9 +754,6 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
return (
<>
{/* ===== Public pages: show branded navbar without section links ===== */}
{!isAuthenticated && isPublicPage && <PublicSiteNavbar showSectionLinks={false} fullWidth />}
{/* ===== Mobile: overlay ===== */}
{mobileDrawerOpen && (
<div
@@ -313,7 +313,7 @@ function InstallItemRow({ item }: { item: InstallItem }) {
{item.eventData && Object.keys(item.eventData).length > 0 && (
<button
onClick={() => setShowDetails(!showDetails)}
className="text-xs text-blue-600 hover:text-blue-800"
className="text-xs text-green-700 hover:text-green-800"
>
{showDetails ? 'Hide' : 'Details'}
</button>
@@ -7,7 +7,8 @@ import { useTenantNotifications } from '@/contexts/TenantNotificationContext';
import { useTheme } from '@/contexts/ThemeContext';
import { useState, useRef, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { usePathname, useRouter } from 'next/navigation';
import { BrandMark } from './BrandMark';
import { trackEvent } from '@/lib/appInsights';
import { useAdminMode } from '@/hooks/useAdminMode';
import GlobalSearch from './GlobalSearch';
@@ -20,6 +21,7 @@ export default function Navbar() {
const { tenantNotifications, dismissTenantNotification, dismissAllTenant } = useTenantNotifications();
const { theme, toggleTheme } = useTheme();
const pathname = usePathname();
const router = useRouter();
const [showNotifications, setShowNotifications] = useState(false);
const [showUserMenu, setShowUserMenu] = useState(false);
@@ -132,6 +134,13 @@ export default function Navbar() {
return `${Math.floor(diffHours / 24)}d ago`;
};
// Notification rows with an href navigate on click — close the dropdown first
// so it doesn't linger over the target page.
const openNotificationHref = (href: string) => {
setShowNotifications(false);
router.push(href);
};
const getUserInitials = () => {
if (user?.displayName) {
const names = user.displayName.split(' ');
@@ -160,11 +169,9 @@ export default function Navbar() {
<div className="flex justify-between h-14">
<div className="flex items-center">
<Link href="/progress" prefetch={false} className="flex items-center space-x-2.5">
<svg className="w-7 h-7" viewBox="0 0 209 191" fill="none">
<path d="M0 180.201L208.401 190.502L188.157 76.2438L5.48363e-06 0L0 180.201Z" fill="#33B161" />
</svg>
<span className="text-lg font-bold text-gray-600 dark:text-gray-400">
<span className="hidden md:inline">AutopilotMonitor</span>
<BrandMark className="w-6 h-6" />
<span className="text-[15px] font-bold tracking-tight text-gray-900 dark:text-gray-100">
<span className="hidden md:inline">Autopilot Monitor</span>
<span className="md:hidden">AP Monitor</span>
</span>
</Link>
@@ -173,7 +180,7 @@ export default function Navbar() {
{/* User Menu */}
<div className="relative" ref={userMenuRef}>
<button onClick={() => setShowUserMenu(!showUserMenu)} className="flex items-center space-x-1.5 p-1.5 rounded-lg hover:bg-gray-100 transition-colors">
<div className="w-7 h-7 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold text-xs">
<div className="w-7 h-7 rounded-full bg-green-600 flex items-center justify-center text-white font-semibold text-xs">
{getUserInitials()}
</div>
<svg className="w-3.5 h-3.5 text-gray-500" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor">
@@ -183,7 +190,7 @@ export default function Navbar() {
{showUserMenu && (
<div className="absolute right-0 mt-2 w-72 bg-white rounded-lg shadow-lg border border-gray-200 z-50">
<div className="px-3 py-2.5 border-b border-gray-200 flex items-start space-x-2.5">
<div className="w-8 h-8 rounded-full bg-blue-600 flex-shrink-0 flex items-center justify-center text-white font-semibold text-xs">
<div className="w-8 h-8 rounded-full bg-green-600 flex-shrink-0 flex items-center justify-center text-white font-semibold text-xs">
{getUserInitials()}
</div>
<div className="min-w-0">
@@ -216,11 +223,9 @@ export default function Navbar() {
{/* Logo and Title */}
<div className="flex items-center">
<Link href="/" prefetch={false} className="flex items-center space-x-2.5">
<svg className="w-7 h-7" viewBox="0 0 209 191" fill="none">
<path d="M0 180.201L208.401 190.502L188.157 76.2438L5.48363e-06 0L0 180.201Z" fill="#33B161" />
</svg>
<span className="text-lg font-bold text-gray-600 dark:text-gray-400">
<span className="hidden lg:inline">AutopilotMonitor</span>
<BrandMark className="w-6 h-6" />
<span className="text-[15px] font-bold tracking-tight text-gray-900 dark:text-gray-100">
<span className="hidden lg:inline">Autopilot Monitor</span>
<span className="hidden md:inline lg:hidden">AP Monitor</span>
<span className="md:hidden">AP Mon</span>
</span>
@@ -290,7 +295,7 @@ export default function Navbar() {
{(unreadCount > 0 || hasClearable) && (
<div className="flex space-x-2">
{unreadCount > 0 && (
<button onClick={markAllAsRead} className="text-xs text-blue-600 hover:text-blue-800">
<button onClick={markAllAsRead} className="text-xs text-green-700 hover:text-green-800">
Mark all read
</button>
)}
@@ -314,7 +319,7 @@ export default function Navbar() {
<div className="divide-y divide-gray-100">
{/* Tenant-scoped persistent notifications (e.g. hardware rejections) — top */}
{tenantNotifications.map((tn) => (
<div key={`tn-${tn.id}`} className="px-4 py-3 hover:bg-blue-50/50 transition-colors border-l-4 border-blue-500 bg-blue-50/30">
<div key={`tn-${tn.id}`} className={`px-4 py-3 hover:bg-blue-50/50 dark:hover:bg-blue-900/45 transition-colors border-l-4 border-blue-500 bg-blue-50/30 dark:bg-blue-900/20 ${tn.href ? 'cursor-pointer' : ''}`} onClick={() => { if (tn.href) openNotificationHref(tn.href); }}>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-2.5 flex-1">
<span className="text-lg">{tn.type === 'hardware_rejection' ? '🖥️' : '🔔'}</span>
@@ -327,8 +332,8 @@ export default function Navbar() {
<Link
href={tn.href}
prefetch={false}
onClick={(e) => { e.stopPropagation(); }}
className="text-[10px] text-blue-600 hover:text-blue-800 font-medium underline"
onClick={(e) => { e.stopPropagation(); setShowNotifications(false); }}
className="text-[10px] text-green-700 hover:text-green-800 font-medium underline"
>
View
</Link>
@@ -348,7 +353,7 @@ export default function Navbar() {
))}
{/* Persistent Global Admin Notifications */}
{visibleGlobal.map((gn) => (
<div key={`ga-${gn.id}`} className="px-4 py-3 hover:bg-purple-50/50 transition-colors border-l-4 border-purple-500 bg-purple-50/30">
<div key={`ga-${gn.id}`} className={`px-4 py-3 hover:bg-purple-50/50 dark:hover:bg-purple-900/45 transition-colors border-l-4 border-purple-500 bg-purple-50/30 dark:bg-purple-900/20 ${gn.href ? 'cursor-pointer' : ''}`} onClick={() => { if (gn.href) openNotificationHref(gn.href); }}>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-2.5 flex-1">
<span className="text-lg">{gn.type === 'session_report' ? '\uD83D\uDCCB' : '\uD83C\uDF1F'}</span>
@@ -358,7 +363,19 @@ export default function Navbar() {
<span className="text-[9px] font-semibold text-purple-700 bg-purple-100 px-1 py-0.5 rounded">GA</span>
</div>
<p className="text-xs text-gray-600 mt-0.5">{gn.message}</p>
<p className="text-[10px] text-gray-400 mt-1">{formatTime(new Date(gn.createdAt))}</p>
<div className="flex items-center gap-3 mt-1">
<p className="text-[10px] text-gray-400">{formatTime(new Date(gn.createdAt))}</p>
{gn.href && (
<Link
href={gn.href}
prefetch={false}
onClick={(e) => { e.stopPropagation(); setShowNotifications(false); }}
className="text-[10px] text-green-700 hover:text-green-800 font-medium underline"
>
View
</Link>
)}
</div>
</div>
</div>
{user?.isGlobalAdmin && (
@@ -373,7 +390,7 @@ export default function Navbar() {
))}
{/* Ephemeral Notifications */}
{notifications.map((notification) => (
<div key={notification.id} className={`px-4 py-3 hover:bg-gray-50 transition-colors cursor-pointer ${!notification.read ? 'bg-blue-50' : ''}`} onClick={() => { if (!notification.read) { markAsRead(notification.id); } }}>
<div key={notification.id} className={`px-4 py-3 hover:bg-gray-50 transition-colors cursor-pointer ${!notification.read ? 'bg-blue-50' : ''}`} onClick={() => { if (!notification.read) { markAsRead(notification.id); } if (notification.href) { openNotificationHref(notification.href); } }}>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-2.5 flex-1">
<span className="text-lg">{getNotificationIcon(notification.type)}</span>
@@ -386,8 +403,8 @@ export default function Navbar() {
<Link
href={notification.href}
prefetch={false}
onClick={(e) => { e.stopPropagation(); markAsRead(notification.id); }}
className="text-[10px] text-blue-600 hover:text-blue-800 font-medium underline"
onClick={(e) => { e.stopPropagation(); markAsRead(notification.id); setShowNotifications(false); }}
className="text-[10px] text-green-700 hover:text-green-800 font-medium underline"
>
View
</Link>
@@ -732,7 +749,7 @@ export default function Navbar() {
{/* User Menu */}
<div className="relative" ref={userMenuRef}>
<button onClick={() => setShowUserMenu(!showUserMenu)} className="flex items-center space-x-1.5 p-1.5 rounded-lg hover:bg-gray-100 transition-colors">
<div className="w-7 h-7 rounded-full bg-blue-600 flex items-center justify-center text-white font-semibold text-xs">
<div className="w-7 h-7 rounded-full bg-green-600 flex items-center justify-center text-white font-semibold text-xs">
{getUserInitials()}
</div>
<svg className="w-3.5 h-3.5 text-gray-500" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor">
@@ -744,7 +761,7 @@ export default function Navbar() {
{showUserMenu && (
<div className="absolute right-0 mt-2 w-72 bg-white rounded-lg shadow-lg border border-gray-200 z-50">
<div className="px-3 py-2.5 border-b border-gray-200 flex items-start space-x-2.5">
<div className="w-8 h-8 rounded-full bg-blue-600 flex-shrink-0 flex items-center justify-center text-white font-semibold text-xs">
<div className="w-8 h-8 rounded-full bg-green-600 flex-shrink-0 flex items-center justify-center text-white font-semibold text-xs">
{getUserInitials()}
</div>
<div className="min-w-0">
@@ -80,7 +80,7 @@ export function ProtectedRoute({ children, requireGlobalAdmin = false, requireGl
// interaction_in_progress) — stay on this origin and offer a manual retry.
if (!isAuthenticated && !isLoading && loginFailed) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="min-h-screen bg-[var(--lp-bg)] flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md w-full text-center">
<h2 className="text-xl font-semibold text-gray-900 mb-2">Sign-in required</h2>
<p className="text-gray-600 mb-6">We couldn&apos;t sign you in automatically.</p>
@@ -89,7 +89,7 @@ export function ProtectedRoute({ children, requireGlobalAdmin = false, requireGl
setLoginFailed(false);
login().catch(() => setLoginFailed(true));
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Sign in
</button>
@@ -101,9 +101,9 @@ export function ProtectedRoute({ children, requireGlobalAdmin = false, requireGl
// Show loading spinner while MSAL settles or while re-login redirect is pending.
if (isLoading || (!isAuthenticated && wasAuthenticated.current)) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div className="min-h-screen bg-[var(--lp-bg)] flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
@@ -123,7 +123,7 @@ export function ProtectedRoute({ children, requireGlobalAdmin = false, requireGl
(requireFleetScope && user && !hasFleetScope);
if (platformDenied) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="min-h-screen bg-[var(--lp-bg)] flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md w-full text-center">
<svg className="h-12 w-12 text-red-500 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
@@ -132,7 +132,7 @@ export function ProtectedRoute({ children, requireGlobalAdmin = false, requireGl
<p className="text-gray-600 mb-6">You need Global Admin permissions to access this page.</p>
<button
onClick={() => router.push("/dashboard")}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Back to Home
</button>
@@ -1,11 +0,0 @@
"use client";
export function PublicPageHeader({ title }: { title: string }) {
return (
<header className="bg-white shadow">
<div className="py-6 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-normal text-gray-900">{title}</h1>
</div>
</header>
);
}

Some files were not shown because too many files have changed in this diff Show More