mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
f795fbcb4eb24f2f77779d1735d2360cb1d8af8d
756 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f795fbcb4e |
feat(auth): dual app-reg parallel operation - legacy gktatooine app alongside new C4A8 app
True parallel operation, no forced re-consent: existing tenants keep signing in via the legacy app (1a400946), new tenants land on the C4A8 app (886ab5e2). Model "default legacy, funnel primary, learn in the background": - Web: am_auth_app flag -> NEXT_PUBLIC_ENTRA_DEFAULT_APP -> primary; get-started CTA hands over with ?authapp=primary; auth/me homedApp updates the flag so an operator tenant flip takes effect on each user's NEXT login; AADSTS90094/65001 one-shot cross-app retry, AADSTS65004 -> failed screen with previous-app link; am_login_attempt pins redirect returns to the app that started them. - Backend: TenantConfiguration.HomedAppClientId (null = legacy, GA-only flip via PUT config) + LastAuthClientId/Since observability; EntraAppRegistry resolves the acting app per tenant (no-op until the legacy config pair is set); Graph client-credentials, admin-consent URLs and the surfaced client id follow the homed app (app-scoped cache keys); JwtBearer [Authorize] pipeline now shares the middleware audience builders (was single-audience - would 401 legacy tokens after the swap); middleware trusts EntraId:LegacyClientId implicitly. - well-known association file now lists both app ids (publisher domain); deploy-web passes NEXT_PUBLIC_ENTRA_LEGACY_CLIENT_ID / _DEFAULT_APP; mcp-server.bicep default client id -> C4A8 app. Backend xUnit 3730/3730, vitest 625/625 (13 new), tsc/ESLint clean, next build ok. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6bd5728652 |
feat(platform): public availability — tenant activation replaces Private Preview, optional auto-approve
Backend: shared TenantApprovalService (whitelist add + auto-promote shape-guard + welcome email), AdminConfiguration.AutoApproveNewTenants (default off, uncached fail-closed accessor), tenant-auto-approve queue (1-min visibility delay, worker drops on flag-off/approved/suspended/missing config, approves as "System (auto-approve)" + TenantAutoApproved ops event), 30s negative whitelist cache, gate 2 error PendingActivation with mode-neutral message, signup strings and welcome email without preview wording. Web: /preview → /activation (SWA 301, robots, AuthGate), AuthContext accepts both error codes and clears pending state on successful auth/me, activation page polls every 30s and auto-redirects into the portal, dashboard banner reframed to "Actively developed" + feedback call, terms/privacy/get-started updated, feature badges → Beta, admin-config web chain + auto-approve checkbox, OPS_EVENT_TYPES. Deliberately unchanged: PreviewWhitelist/PreviewConfig tables, internal preview/* API routes, preview_signup notification type, Microsoft DevPrep preview mentions. Verified: solution build 0 errors, xUnit 3701/3701, tsc 0 errors, vitest 612/612, ESLint ratchet holds (0/115), next build exports /activation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a70c5de667 |
feat(plan): rename Enterprise plan to Pro; Pro includes Bootstrap by default and Unrestricted Mode on request
- FeatureEntitlementCatalog: TenantEdition.Pro, canonical tier "pro", legacy stored "enterprise" stays readable as Pro (no data migration); new entitlements BootstrapIncluded + UnrestrictedModeAvailable - TenantEntitlementService: IsBootstrapEnabled (Pro-included OR additive GA flag) and IsUnrestrictedModeActive (Pro AND GA gate AND tenant toggle, read-time - trial expiry/downgrade re-arms guardrails fail-closed) - All bootstrap endpoint gates + SecurityValidator use the effective check; /auth/me and feature-flags expose effective availability values - PlanManagement accepts community|pro; AlreadyPro; Pro trial wording - Web: edition.ts accepts legacy "enterprise" (deploy-order safety), Pro labels/cards + new Pro feature bullets, effective section gating, terms/privacy renamed with Last-updated bump - Tests: catalog matrix, effective-gate payload tests, helper tests incl. trial-expiry re-gate (backend 3692 green, web 612 green) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e4c06a9564 |
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> |
||
|
|
d0d1d28371 |
perf(backend): column projection for unmatched-software scan
GetUnmatchedSoftwareFunction runs a full cross-tenant scan of SoftwareInventory on every request (including every pagination click) but only reads 9 properties. GetAllSoftwareInventoryAsync now accepts an optional $select projection and the function passes the consumed columns, cutting transfer and entity materialization. Server-side filtering on CpeUri is documented as impossible: rows re-created by the cascade-delete 404 path carry no CpeUri property and Table Storage cannot express "property missing" — those rows must still count as unmatched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
89c5c7da03 |
feat: richer, self-describing session report content
The report ZIP now captures everything an operator needs even after the session is gone, and states its own completeness: - session.csv: append the 15 newer Sessions fields that had drifted out of the export (failureSource, reconcileReason, adminMarkedAction, validatedBy, rebootCount, isHybridJoin, isSelfDeployingProfile, connectionType, geo*, avgApiLatencyMs, apiRequestCount, stalledAt, failureSnapshotJson) - completeness signal: exportedEventCount / sessionEventCount / eventStreamActive in the request and report-metadata.json; the modal warns while event pages are still streaming - includedDiagnostics records sourceBlobName/sourceDestination so a failed copy still points at the original archive - tenantContext snapshot (fail-soft): upload mode/destination, gather- rule debug flag, custom gather/analyze rule IDs + UpdatedAt, IME pattern IDs — no secrets, no definitions - robustness: 8 MB client-side screenshot cap (20 MB body cap headroom), attachments status in metadata instead of silently dropping invalid base64, SanitizeZipEntryName strips path components from customer- supplied file names (both report types) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
48308df4ff |
feat: opt-in preservation of session diagnostics archive with session reports
Session reports persist while sessions and their diagnostics blobs are
removed by user deletion or retention. A new checkbox in the Report
Session modal (shown only when a diagnostics ZIP was uploaded, default
checked) lets the submitter include the archive: the backend streams a
server-side copy into the durable session-reports container.
- SessionReportDiagnosticsArchiveCopier: fail-soft copy (never blocks the
report), reuses the Hosted/CustomerSas read paths, re-verifies the
hosted tenant prefix via ClassifyBlobName, honours the admin size cap
and download timeout; outcome persisted as DiagnosticsCopyStatus
- Report row/API gain DiagnosticsBlobName + DiagnosticsCopyStatus
(written only when requested, omitted for legacy rows)
- report-metadata.json records includedDiagnostics {requested, blobName, status}
- Admin reports UI: second Download Diagnostics button + failure status line
- Privacy page: explicit exception for the opt-in report copy (Last updated bump)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f744ad8406 | fix code review findings | ||
|
|
ffe88a47c6 |
fix: /sla dead-ended on www — it is a portal page, not a public one
/sla sat in PUBLIC_PATH_PREFIXES only because SLA notifications historically linked www…/sla. It is a fully authenticated page (ProtectedRoute + token fetches), and since ProtectedRoute stands down on the public host, www/sla waited forever on "Loading SLA metrics...". Removed from the public prefixes: the nav stays on portal (no bounce), old www…/sla notification links bounce to portal/sla where auth works, and SlaBreachEvaluationService.DashboardUrl now links the portal host directly for future notifications. /sla added to robots disallow. Web tsc + 603/603, backend build clean + SLA suite 38/38. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
18b28260a7 |
feat: portal static export — eliminate the SWA SSR runtime (cold-start root fix)
output:'export' + trailingSlash: the SWA now serves plain static files; the
managed SSR runtime — the portal's last cold-start source ("Backend call
failure" when a cold start exceeded SWA's 45s limit) — no longer exists.
- Detail routes are query-string based (/sessions?id=, /sessions/inspector?id=,
/diagnosis?id=, /apps/detail?name=, /admin/backups/detail?id=,
/admin/customs-archive/detail?tenantId=&rowKey=). Single-definition builders
in lib/routes.ts; all 21 link sites migrated, duplicate sessionHref helpers
consolidated. Suspense boundaries added everywhere useSearchParams drives
page identity (hard export-build errors otherwise).
- 9 [section] families prerender via server shells (generateStaticParams from
the *NavSections registries, dynamicParams=false, client bodies extracted to
SectionClient) — 37 pages; server redirect() index pages became
ClientRedirect shells. robots/sitemap/og-image routes force-static.
- Legacy PATH-shaped deep links are preserved FOREVER (sent Teams/Slack/
webhook notifications cannot be edited): SWA wildcard rewrites serve the
family page and the globally mounted LegacyPathRedirect rewrites to the
canonical query URL, preserving query + #event hashes (mapping pinned by
lib/__tests__/legacyPathRedirect.test.ts).
- middleware.ts deleted → client-side HostRoutingGuard (prefix list moved to
lib/hostRouting.ts). Legacy /go/[code] route + bootstrapValidation deleted —
BootstrapSessions table verified free of active pre-cutover sessions.
- staticwebapp.config.json now carries production routing: 19 docs/marketing
redirects (order-sensitive), legacy rewrites (exact before wildcard), 404
override, all security headers incl. literalized prod CSP (no unsafe-eval),
Content-Type routes for the extension-less og images. Guarded by the new
utils/__tests__/swaConfig.guard.test.ts. Maintenance mode is now a
first-routes-entry insert, never a whole-file swap (README rewritten).
- Backend notification deep links go through Constants.PortalSessionUrl
(query shape, escaped, pinned); 3 producers migrated.
- deploy-web.yml uploads out/ with is_static_export.
Export build: 79 static pages, no go/ output. Web: tsc clean, vitest 53
files green (618 incl. new guards; 2 deleted legacy suites). Backend:
solution build clean, 3622/3622 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6cbd507b4f |
fix: client-IP extraction was dead on Flex — RemoteIpAddress fallback + AFD-aware rate-limit key
Post-deploy verification of the go endpoint showed every anonymous caller keyed as "unknown": Flex Consumption does not surface X-Forwarded-For to the isolated worker, so ALL per-IP rate-limit buckets (bootstrap-validate, distress, diag-ticket-download, stats/platform) had silently collapsed into one shared bucket — confirmed by DistressReports.SourceIp being "unknown" across the board. - GetTrustedClientIp falls back to HttpContext.Connection.RemoteIpAddress (transport-derived, unspoofable; IPv4-mapped addresses normalized). Heals every existing caller without signature changes. - New GetRateLimitClientIp for AFD-fronted endpoints: honors X-Azure-ClientIP only when X-Azure-FDID carries exactly our profile ID (FrontDoorProfileId app setting, set on autopilotmonitor-api-eu); fail-closed to GetTrustedClientIp on unset/mismatch/multi-value. Used by GetBootstrapScriptFunction (traffic arrives via go.autopilotmonitor.com, where the trusted hop is AFD egress). - FdidMatches table tests incl. append/spoof cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
26bcab9054 |
feat: move OOBE bootstrap script endpoint into the backend (go.autopilotmonitor.com)
The portal's Next.js /go/[code] route was the web app's only request-time
server code and blocked the planned static export. The script generation
now lives in the Function App as GET /api/bootstrap/go/{code}
(GetBootstrapScriptFunction), reached by customers as
irm https://go.autopilotmonitor.com/{code} | iex via a Front Door custom
domain whose /* route rewrites onto the API path.
- New Constants.BootstrapGoBaseUrl; CreateBootstrapSessionFunction now
hands out the go URL (previously unguarded — CreateBootstrapSessionUrlShapeTests
pins it now). go host added to both URL-guard enforced lists.
- BootstrapScriptValueValidator ports the bootstrapValidation.ts injection
invariants (exact GUIDs, https + host allowlist + /agent/*.zip path,
expiry <= 14d) as defense-in-depth for in-process values.
- OobeBootstrapScriptGenerator ports the PS template as a Replace-token
raw string; golden test pins byte parity with the TS template
(TestData/oobe-bootstrap-golden.ps1, captured from route.ts).
- Always-HTTP-200 text/plain contract incl. rate limit (own bucket
bootstrap-script:{ip} 20/min); disabled tenant indistinguishable from
unknown code. Catalog: PublicAnonymous; NoStoreCacheMiddleware covers
/api/bootstrap/go/ (script inlines the bearer token).
- Web: BOOTSTRAP_GO_URL registry entry, sessions list shows the go host,
legacy route marked LEGACY (alive for pre-migration URLs <= 168h,
removal in the static-export PR).
- Docs: new OKF backend/oobe-bootstrap-go-flow.md, url-registry go
section, index/log entries.
Backend tests 3611/3611, web vitest 651/651, tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4ebd60fb1e |
feat: EnableGatherRuleDebugLog — per-tenant gather-rule debug trace (ConfigVersion 36)
Customers cannot tell why a gather rule produces nothing in the timeline (e.g. a logparser rule with emit_mode on_change that never emits). New per-tenant boolean EnableGatherRuleDebugLog (EnableImeMatchLog-style plumbing end to end): when enabled, the agent writes %ProgramData%\AutopilotMonitor\Logs\gather_rules_debug.log with one pipe-delimited line per evaluation decision across all previously silent "produced nothing" exits — disabled rules, interval without intervalSeconds, first-tick-after-one-full-interval, phase-scope skips incl. currentPhase=Unknown, per-(rule, phase) dedup, empty collector results (emitOnlyIfExists), on_change hash suppression (hash prefix + streak), guard blocks, logparser per-file outcomes (position skips, format=text hint when every line fails CMTrace parsing, maxLines and wildcard caps), and full exception stacks the agent log drops. - Backend: ConfigVersion 35 -> 36; TenantConfiguration/AgentConfigResponse field + TableConfigRepository Store+Map - Agent: GatherRuleDebugLog writer (10 MB cap, one .old generation, failures swallowed, optional console echo), GatherRuleContext.DebugLog (null-safe no-op), trace points in executor + collectors; never writes under _scopeLock (reasons captured inside, written after release); merger bool->path projection with --gather-debug-log CLI override tolerance; deliberately NOT in the offline-cache strip lists; --run-gather-rules always traces; zero-rules delivery leaves a one-line file instead of nothing - Side fix: GatherRuleExecutorHost now passes the logger into InformationalEventPost so gather-event emissions hit the DEBUG log - Web: tenant settings toggle (Settings -> Agent) + admin tenant-config report rows - Tests: merger on/off/override, writer format+rotation, executor debug lines, logparser debug lines (agent 2131, backend 3561, tsc green) - Docs: docs/agent/gather-rule-debug-log.md + index/log entries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dff1532be5 |
feat: project ConnectionType (WiFi/Ethernet) onto SessionsIndex
The agent already reports connectionType in every network_interface_info event, but it was only stored as a JSON blob in DeviceSnapshot — answering "how many machines enroll over Ethernet vs WiFi?" required a slow full-table scan via deviceProperties. Project it as a typed column instead (template: the AvgApiLatencyMs chain), making it a cheap indexed one-call query. - Ingest: TryExtractConnectionType — last network_interface_info emission of the batch wins (a device switching media mid-enrollment is relabeled); only exact "WiFi"/"Ethernet" accepted, fire-and-forget fail-soft writer - Storage: UpdateSessionConnectionTypeAsync (Merge on Sessions + index mirror, tombstone-safe), BuildSessionIndexEntity superset entry, MapToSessionSummary, re-registration preserve in StoreSessionAsync - Search: connectionType= filter with OData eq push-down on the index, client-side backstop on the non-OData paths, fields=connectionType projection, pagination fingerprint entry, raw endpoint parity - MCP: connectionType z.enum param on search_sessions/query_raw_sessions, fields doc updated (server redeploy required) - Web: optional sortable/filterable "Connection" column in the session list - No backfill: legacy sessions lack the column and are excluded by the filter (intended, same semantics as RebootCount); the deviceProperties scan path still covers historical data Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3ec716a3ec |
feat: expose API latency to MCP field projection and tool docs
- SearchSessionsFunction fields whitelist += avgApiLatencyMs, apiRequestCount so search_sessions lean sweeps (e.g. fields=geoCountry,avgApiLatencyMs,apiRequestCount) can aggregate latency without full SessionSummary payloads - search_sessions tool doc lists the new fields incl. the weighting hint; get_geographic_metrics doc names avgApiLatencyMs / apiLatencyVsGlobalPct as the backend-region distance metric Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ffd70e0383 |
feat: geographic API-latency view (per-location weighted avg by geo)
Surfaces the persisted per-session AvgApiLatencyMs in the geographic views so operators can compare regions when deciding on new backend locations (Asia/US): - ComputeGeographicMetrics: per-location and global request-weighted latency (sum(avg*requests)/sum(requests)) + ApiLatencyVsGlobalPct; serves both the tenant and the GA cross-tenant endpoint - GeoMetricsSessionProjection extended (column-projected scan would otherwise drop the new fields); equivalence test pins it - Drilldown: LocationSessionRow carries the per-session values; sessions table + summary tile show them - geographic-performance page: sortable "API Latency" column with absolute color buckets (<250 green .. >=800 red), global benchmark - PlatformMetricsService: session AvgLatency now exact from cumulative counters when present (fallback: mean of interval averages) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c18154d76d |
feat: persist per-session agent-to-backend API latency (AvgApiLatencyMs)
The V2 agent already measures HTTP round-trip latency per request (NetworkMetrics, cumulative _totalLatencyMs) but only emitted the per-interval delta average. Operators deciding on new backend regions (Asia/US) need the session-wide average, aggregatable by GeoCountry. Measured baseline: Jakarta sessions average ~600-1050 ms client-side vs 94 ms server-side processing — the gap is pure network distance to GWC. - Agent: agent_metrics_snapshot now carries cumulative net_total_latency_ms (total/requests = whole-session average; idempotent by construction) - Ingest: last matching snapshot of a batch projects AvgApiLatencyMs + ApiRequestCount onto the Session row (fire-and-forget, fail-soft, last-write-wins; agents predating the field simply never match) - SessionsIndex: both fields mirrored (merge + BuildSessionIndexEntity superset) so list/GA views can aggregate latency per geo - Web: session detail shows "API Latency" when present Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d55b0ff864 |
test(backend): pin the cross-tenant redaction on metrics/ime-versions
GET metrics/ime-versions is MemberRead and the archive is one global
partition, so every tenant member reads the same platform-wide rows. The only
thing between an ordinary tenant user and another tenant's identifiers was an
inline projection with no test behind it.
There is no HTTP-level harness in this suite - nothing constructs an
HttpRequestData with a RequestContext - so the projection was unreachable from
a test while it lived inside Run. Extracted it to a static
BuildResponsePayload(versions, hasGlobalScope), matching the pattern
GetTpmPssUnsupportedFunction already uses for the same reason. Behaviour is
unchanged: the payload is returned as `object`, and System.Text.Json resolves
an `object` declared type via the runtime type, so both branches keep the wire
format they had inline. A test asserts that rather than trusting it.
10 tests, asserting on serialized JSON in camelCase (Program.cs's naming
policy) because a property dropped from an anonymous type is only a real
redaction once it is absent from the wire:
- non-global: firstSeenTenantId / firstSeenSessionId absent, and their GUID
VALUES absent anywhere in the payload (a model rename must not let the
identifier survive under another key)
- non-global: exactly {version, firstSeenAt, lastSeenAt, sessionCount}
- non-global: sessionCount survives - the counts are platform-wide by
decision, pinned so it cannot drift either way unnoticed
- global (Admin or Reader): full entry including both identifiers
- both scopes: array shape, empty archive, row order preserved
Mutation-checked both directions: forcing the global branch fails 3 tests,
dropping sessionCount from the projection fails 2. Full suite 3521 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0efeb0f734 |
fix(backend): alert-webhook 500 on non-seekable body - bounded read replaces Length check
With ASP.NET Core integration the HttpRequestData body is a forward-only stream whose Length property throws NotSupportedException (verified in prod: 500 + System.NotSupportedException on the first POST). The size cap is now enforced by reading at most cap+1 chars via ReadBlockAsync instead of asking the stream for its length. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7b975d46b6 |
feat(backend): Azure Monitor alert webhook - platform alerts join the ops-event bus
New anonymous POST ops/alert-webhook receives Common Alert Schema notifications from an Action Group webhook receiver and re-emits them as ops events (category Platform, type AzureMonitorAlert), so Azure Monitor alerts flow through the existing OpsEventService -> OpsAlertDispatchService fan-out (Telegram/Teams/Slack + portal ops-events list) like every internally raised operational event. The delivery path is independent of the portal: a SWA outage cannot suppress the notification; the action group's e-mail receiver stays as the backend-independent fallback. Security: action-group webhooks cannot send custom headers, so the shared secret travels as ?secret= and is compared in constant time (FixedTimeEquals) against the OpsAlertWebhookSecret app setting - fail-closed while unset. 64 KB payload cap, defensive essentials-only parsing (only alertRule is mandatory), severity mapped Sev0->Critical, Sev1->Error, Sev2->Warning, Sev3/4->Info with unknown values falling back to Info so schema drift can never escalate. Dual-registered per convention: EndpointAccessPolicyCatalog entry (PublicAnonymous), OPS_EVENT_TYPES in OpsAlertRulesSection (rule wiring) and Platform category in OpsEventsSection (badge + filter). Backend suite 3511 green (incl. new helper tests + catalog completeness), web tsc + vitest 651 green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ba7d14b070 |
fix(insights): post-deploy MCP findings - projection drop, aggregate starvation, duration artifacts
Three defects found by measuring production after the 1.5.1012 deploy: - AppMetricsProjection dropped TerminalState, so the PR0 skip classification never reached metrics/app (totalSkipped stuck at 0). Column added; equivalence fixture now populates TerminalState/AppId/ EspBlocking/AppIdCollision and asserts them (proven: test fails without the projection fix). - BuildTimeAttributionAggregates excluded breakdowns on ANY quality flag; 903/1000 carry BlockingSetUnknown, starving fleet stats (Clean=12 vs 331 excluded on the largest tenant). New TimeAttributionFlagQuality.DurationCriticalFlags mask - only ClockSkew/PartialObservation/WGAnchors exclude; blocking-set flags keep their measured spans (positive-evidence semantics unchanged). - HasMeasuredDuration gets an upper plausibility bound (6h = default AgentMaxLifetimeMinutes): 490 rows carried back-stamped completions (up to 117 days; 60175s app rows in a 2697s session) that dominated slowestApps with six-figure averages. Read-side, heals old + new rows. Backend 3484/3484 green; solution build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7842d121a0 |
fix(insights): codex review findings - WG pause clamping, late-event healing, stale buckets, date windows, banner count
All six review findings verified against code and real; five fixed here, the agent-side race lands in the companion agent commit. [P1] WhiteGlove pause counted as time: ClampToWindows clipped intervals against the HULL of both WG windows, but the excluded pause lies inside the hull - a reboot gap or app interval straddling the pause counted the entire pause (a ~2-day "reboot" in a 30-minute enrollment was possible). Seconds are now the per-window intersection sum (chronological endpoints kept); an interval wholly inside the pause makes no claim and no longer raises ClockSkewDropped. What-if savings are additionally capped at the session's WallClockSeconds - endpoint deltas can still straddle the pause, and no removal can save more than the enrollment took. [P1] Late-arriving events never updated breakdowns: the terminal-seam compute is one-shot and the sweep only healed missing/version-stale rows. SessionTimeBreakdown now stores EventCountAtCompute (0 = pre-fix row, recomputed once); the sweep recomputes on EventCount drift and then also re-runs ResolveEspBlockingForSessionAsync (newly exposed on IMetricsRepository) so late esp_config_detected re-emits and app batches reach both the breakdown and the blocking evidence. [P2] Stale aggregate buckets: the sweeps now reconcile every partition they wrote - buckets that existed but were not regenerated (sessions deleted, class left the window) are deleted via new targeted deletes; rolling30 rows additionally get a 48h ComputedAt freshness filter at read time, so a fully quiet tenant stops serving year-old numbers as "last 30 days". [P2] Date windows included an extra calendar day: both inclusive range ends meant days=30 returned 31 keys and days=1 summed yesterday AND today. InclusiveWindowStart(today, days) = today-(days-1) in both response builders; the sweep's rolling30 source is filtered to the same 30 calendar days (its query window stays one day wider for backfill). [P3] Device-history banner counted later sessions as "previous enrollments" when viewing an older session: selectPriorSessions (deviceHistoryPrior.ts) anchors on the session's own chain entry (live sessions: new sessionStartedAt prop), sessionId tie-break mirroring the server chain sort. Verified: solution build clean, Backend 3480/3480 (+6: three WG-pause golden fixtures, window-start facts, RK builder/delete consistency, EventCountAtCompute roundtrip), Web tsc + vitest 633/633 (+6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1bf37f6761 |
feat(insights): F3 regression radar - Wilson-gated rule hit-rate drift detection (PR6)
Analyze rules only (gather stats stay per-batch-deduped until Q6 is
fixed - never silently included). Per (tenant, rule) the radar compares
the trailing 7-day hit rate (FireCount/SessionsEvaluated from the daily
RuleStats rows) against the prior 28-day baseline and fires only when
the window has >=5 hit sessions and >=20 evaluated sessions, the rate
is >=2x baseline AND the Wilson 95% intervals are disjoint
(MetricsMath.WilsonInterval/RateIncreaseSeparated, hand-computed test
vectors). Suppressed for deleted rules, rules inside the CreatedAt
grace period (<35d), rules edited inside the window, empty baselines,
and the RuleRegressionRadarDisabled app-setting kill switch.
Alert episodes live as ruleregression|{ruleId} rows in the notification
tracker (third keyspace): AddEntity-409 dedup = one bell + one
RuleFrequencyRegression ops event (category Tenant, Warning, full
numbers, dual-registered in the web OPS_EVENT_TYPES) per episode;
numbers refresh while active without moving FirstNotifiedAt; episodes
re-arm under 1.5x baseline, on fire-stop, or via the tracker's 30d
retention prune (spec-intended reminder). On-fire dimension correlation
(osBuild/model/agentVersion/imeVersion; hit set from a RuleResults
range scan intersected with the window sessions) reports both shares
with the "correlated, not necessarily causal" wording contract or
honestly "no clear dimension concentration". Radar runs in the daily
maintenance pass after the rule-stats aggregation, anchored on
yesterday (whole days only).
Surfaces: regressions[] in the rule-stats response (MCP get_rule_stats
inherits it), tenant bell deep-linking to the rule card, and the
analyze-rules page gains a densified 30-day fire sparkline plus a
regression badge while an episode is active. OKF doc
backend/rule-regression-radar.md + index/log.
Backend 3474/3474 (27 new: Wilson vectors, every suppression branch,
window boundary dates, re-arm matrix, dimension gates, tracker
round-trips incl. tri-states, keyspace dedup/prune, wording pins),
web tsc + vitest 627/627, MCP tsc + 455 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
88da317bf4 |
feat(insights): F2 surfaces - device-history banner, fleet FTR panel, API routes, MCP tool (PR5)
Three read routes over the PR4 data: metrics/device-history
(MemberRead + tenant query param - one route serves the web banner AND
MCP, GA cross-tenant via ?tenantId= with a sessionId-based owner
fallback; history:null is a normal outcome), metrics/device-journeys
and global/metrics/device-journeys (days selector honored because FTR
counts are additive - window rate = ratio of summed daily rows; the
tenant-less global variant serves the "global" partition and discloses
repeatDevices:null). Attempt numbers are server-computed
(DeviceJourneyCalculator.ComputeAttemptNumber: chain position within
the journey; live sessions via a virtual non-successful attempt so the
redeploy and 30d-gap rules apply) - no consumer re-derives journey
semantics. Repeat-devices violator list: last journey >=2 attempts with
newest terminal ref in-window, attempts desc then recency, cap 10,
failure reason from the newest failed attempt (bounded point-reads).
Web: DeviceHistoryBanner on session detail ("Attempt N for this device
- View history", expandable terminal chain, durations strictly the
authoritative durationSeconds, WG/DevPrep/admin-marked badges) and the
Fleet Health "First-time-right" section (rate gated at >=20 completed
journeys with disclosed n, junk-serial exclusion note, Monday-UTC
weekly trend, attempt histogram, repeat devices) following the 7/30/90
selector. MCP: get_device_history (serial mode / fleet FTR mode,
role-aware routing, all roles - GA_FULL privilege snapshot extended
deliberately). Docs: OKF backend/device-journeys.md + index/log.
Backend 3447/3447 (14 new: attempt-number vectors incl. gap boundaries
and live sessions, additive sums, days clamping, repeat-device
gate/sort/cap), web tsc + vitest 627/627, MCP tsc + 455 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1e6de47433 |
feat(insights): F2 journey grouping + DeviceHistories chains + daily FTR aggregates (PR4)
Device-key normalization (trim+casefold, junk-serial exclusion incl. the agent's "Unknown" WMI sentinel), pure journey grouping (terminal set Succeeded/Failed/Incomplete; journey ends at first success; >30d gap from the previous attempt's CompletedAt starts a new journey; WG Pending stays an open session, never an attempt), and the DeviceHistories table (PK tenant, RK encoded normalized serial): terminal-only session-ref chains capped at 20 with derived journey counts, written inline at the session-terminal seam (all three terminal statuses, DeletionState-guarded) and healed by a rolling 30d maintenance sweep. The sweep prunes chain refs of deleted sessions tombstone-driven (SessionTombstones cover cascade AND retention deletions), deletes emptied rows, and recomputes daily FTR aggregates (DeviceJourneyAggregates, PK tenant/"global", RK date): completed journeys, first-time-right count, attempt histogram and junk-serial disclosure — additive counts, bucketed on the completing success session's StartedAt date. Chain refs carry the authoritative DurationSeconds verbatim (never CompletedAt-StartedAt) plus an adminMarked flag. Both tables registered in Constants.All + offboarding TenantPartitionTables; aggregates under the 180d retention block; deliberately NO per-session deletion-manifest step for DeviceHistories. Backend 3433/3433 (51 new tests: junk/normalization vectors, terminal set, exact 30d gap boundaries, WG one-row-one-attempt, redeploy split, deletion pruning, entity round-trips incl. row-key escaping, FTR aggregation core), Agent 2044+493 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6de21e821a |
feat(insights): F1 surfaces — session lane, fleet panel, API routes, MCP tool (PR3)
Insights expansion PR3 (tasks/insights-expansion-spec.md §F1 "Surfaces").
Completes F1: schema (PR1) -> persistence/aggregation (PR2) -> surfaces.
- Rolling range rows: the sweep now also writes "rolling30|{class}"
aggregate rows — range statistics over the window's actual sessions. A
median of per-day medians is not the range median, so the fleet panel
reads these and is deliberately fixed to "last 30 days"; the daily rows
stay in the payload for trends/MCP.
- Routes (all registered in EndpointAccessPolicyCatalog):
GET sessions/{sessionId}/time-attribution (MemberRead + tenant query
param, GA fallback via FindSessionTenantId; breakdown=null is SUCCESS —
a missing lane is a normal state, not an error), GET
metrics/time-attribution (MemberRead) and GET
global/metrics/time-attribution (GlobalReadOrAdmin + tenantId filter),
both returning { windowDays, classes (rolling), daily }.
- Web: TimeAttributionLane under the session-detail phase timeline —
proportional stacked bar of segment totals incl. the explicit
unattributed remainder (bar always sums to the session's authoritative
duration), ESP-blocking apps with critical-path occupancy, reboot badge
(cross-cutting, never a bar slice), quality-flag chips; fetched
fail-soft for terminal sessions only. Fleet Health gains the "Time
attribution" section: per-class stacked medians gated at n>=20
("insufficient data (n=...)" below), flagged/missing exclusions
disclosed, and a top-blocking-apps table with class-scoped rows and
"up to X (p75 Y)" what-if wording. Deliberate deviation: the spec's
per-app trend column is omitted — per-day app rows are too sparse under
the daily n>=5 gate; revisit once rolling history exists.
- MCP: get_time_attribution (one tool, session + fleet mode, role-aware
routing like get_app_install_metrics, registered for all roles) — the
privilege-leak-guard catalog snapshot extended deliberately.
- Docs: OKF concept doc backend/time-attribution.md (invariant, windows,
segment mapping, positive-evidence blocking, Q7 reboot spans,
persistence + surfaces) registered in index.md + log.md.
Backend 3382/3382 (+1 rolling-aggregate test), web tsc clean + vitest
627/627, MCP tsc clean + 455 passed. Post-deploy: web deploy +
MCP redeploy (new tool + docs corpus).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
55c898bb9c |
feat(insights): F1 breakdown persistence + daily attribution aggregates (PR2)
Insights expansion PR2 (tasks/insights-expansion-spec.md §F1 "Data & compute
changes"):
- SessionTimeBreakdowns table (PK=TenantId, RK=SessionId): span/reboot/
blocking-app JSON columns, QualityFlags, AttributionVersion,
EspAppsOccupancySeconds kept tri-state (absent column = unknown, never 0).
Written once at the terminal single-writer seam (TableSessionRepository,
next to the counter reconcile + EspBlocking resolution; Succeeded/Failed
only — Incomplete deliberately stores no duration). Deleted with its
session: new deletion-manifest step 16 (PkRkExact; tombstone moves to 19)
plus tenant-offboarding partition wipe — no separate age sweep.
- Self-healing 30d maintenance sweep instead of a one-off backfill (rides
the existing 2h tick + manual parity): backfills pre-deploy sessions,
heals inline-compute misses, folds late-terminating sessions into their
StartedAt-date aggregates, and enforces AttributionVersion purity — a
stale-version breakdown whose events aged out counts as missing rather
than mixing algorithm semantics (truthfulness rule 8). Skips sessions
with a deletion cascade in flight.
- TimeAttributionAggregates table (PK=TenantId or "global",
RK="{date}|{class}"): per enrollment class (whiteglove /
device_preparation / self_deploying / user_driven — never mixed),
median/p75/p90 per segment over the fixed six-segment stack (incl.
unattributed; absent segment = honest 0), clean/flagged/missing counts
disclosed (rule 7), rows written below the n>=20 UI gate on purpose
(the UI needs the n — rule 4). 180d retention alongside UsageMetrics.
- What-if bound: WhatIfSavingSeconds implements the spec formula
max(0, cpEnd - cpEndWithoutX) including the idle gap before X (the ESP
waits for X to start, too); sole-interval fallback = own start. Per-app
aggregate rows gate at >=5 sessions, top 20 by median interval, carry
median/p75 of interval and saving ("up to" wording is the consumer
contract).
Backend 3381/3381 (13 new tests: entity round-trips incl. tri-state,
aggregation bucketing/disclosure/gates, what-if vectors, class precedence,
manifest step-count pins 17/19), solution build clean, web untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e637317bdb |
feat(insights): F1 schema + time attribution calculator (PR1)
Insights expansion PR1 (tasks/insights-expansion-spec.md §F1, audit §0.5): - AppInstallSummary + AppId / EspBlocking (bool?, tri-state) / AppIdCollision: sentinel-gated columns + mapper + both projections. appId adopted from app events (first-seen wins); a second appId under the same display name flags the name-keyed row as a collision (audit Q3) — in-batch and cross-batch (sticky in the reconcile). Collision rows leave every per-app fleet aggregate (app metrics payload, apps list, app analytics) with a disclosed exclusion count; the fleet DO rollup and per-app session drill-down keep them. - EspBlocking resolution at the session-terminal single-writer seam (TableSessionRepository.UpdateSessionStatusAsync, next to the counter reconcile — covers ingest, admin marks, maintenance sweep, rule engine): joins the session's app rows against the LATEST list-carrying esp_config_detected emission as a positive-evidence set (audit Q2) — listed => true, absent => stays unknown, never false. EspBlockingSets parser handles truncation via the uncapped espTracked*Count fields. - TimeAttributionCalculator (pure, AttributionVersion 1) + SessionTimeBreakdown models: exact partition of the session's authoritative DurationSeconds into device_prep / esp_apps / identity_hello / user_esp / desktop_handoff spans plus an explicit unattributed remainder — invariant sum(spans)+unattributed == wall clock enforced in every test. WhiteGlove = two observation windows in ONE session (part1 anchored on whiteglove_part1_complete, pause never attributed). Per-app blocking intervals from EVENT timestamps (first start -> LAST terminal, audit Q1 retry freeze), occupancy = merged union clipped to esp_apps. Reboot spans from event gaps around lastBootUtc (audit Q7), deduped per boot. QualityFlags: ClockSkewDropped, PartialObservation, BlockingSetUnknown, BlockingSetTruncated, WhiteGloveAnchorsIncomplete. Persistence (SessionTimeBreakdowns) and surfaces follow in PR2/PR3. Backend 3368/3368 (35 new tests: 19 calculator golden fixtures + 16 identity/blocking/aggregate-exclusion), solution build clean, web untouched (new response fields are additive). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4eac7d042c |
fix(metrics): app duration/rate stats over real installs only — TerminalState column + out-of-order reconcile
Source-data audit (tasks/insights-expansion-spec.md §0.5) verified against production: 17.9% of Succeeded AppInstallSummaries rows carry no duration — 78% are genuine skips (app_install_completed with payload state=Skipped, "Update for X" WinGet policies not applicable) whose state the ingest discarded, the rest are real installs whose start the agent never observed (attach window). Averaging them as 0s understated avg install duration by 18% and distorted the slowest-apps ranking. - AppInstallSummary.TerminalState (Installed/Skipped/Postponed/Error) from the closing event's payload state; sentinel-gated column + mapper + AppsDashboardProjection. Status stays "Succeeded" for compatibility. - MetricsMath.IsSkipTerminalState/HasMeasuredDuration: durations computed over measured rows only; skips leave the failure/success rate (decision 2026-07-26 — a skip is not an install attempt); per-app skipped/unmeasured counts + totals exposed; slowest ranking gates on measured installs. Applied consistently: app metrics payload, apps dashboard (list/analytics/ time series/trend/versions/device models), SLA weekly/snapshot/breach. - StoreAppInstallSummaryAsync: reconcile extracted to internal static ReconcileAppInstallSummaryWithExisting (test seam). Fixes Q4 (terminal batch before started batch left duration permanently unset) and hardens terminal status against late/replayed InProgress batches. - Web: measured-only hints on app detail cards, skip suffix in apps list, fleet-health "measured installs" caption. Backend 3333/3333 (20 new tests), web tsc clean, vitest 627/627. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f311e8746b |
docs(trust): on-demand log collection in customer-facing claims + tracker dedup tests
The "Collect Logs" feature (
|
||
|
|
8a794a1b52 |
fix(rules): cross-tenant GA rule edits no longer misroute to home tenant
A Global Admin editing a gather/analyze rule while viewing a FOREIGN
tenant (override) got a green "saved" banner, but the write landed in
the GA's OWN home tenant: the JWT-scoped rules/{type}/{ruleId} routes
resolve the tenant from the caller's token, and no cross-tenant write
route existed (reads had /api/global/rules/*, writes did not). The
reload re-read the foreign tenant and showed the unchanged rule.
Fix: symmetric cross-tenant WRITE routes mirroring the global reads.
Backend:
- GlobalRulesFunction: PUT/DELETE global/rules/gather/{ruleId} and
global/rules/analyze/{ruleId}; tenant from ?tenantId= (400 if
missing), same 1 MB guard + ValidateScopeAndEmitMode as the JWT
variants, same service calls (toggle-partial/built-in-state/full-edit
semantics are tenant-parameterized and identical).
- Catalog: 4 entries GlobalAdminOnly + TenantScoping.QueryParam — a
write tier, so the delegated-read rescue never fires; only a platform
admin may mutate another tenant's rules. No global CREATE route:
create stays JWT-only and the portal hides Create in override.
Web:
- api.rules.globalGatherRule/globalAnalyzeRule helpers; both rule pages
route save/toggle/mark-failed/notify/delete through a
*RuleWriteUrl(ruleId) switch on isGlobalOverride.
- Analyze template-enable (a create) is blocked in override with a
clear message instead of silently writing to the wrong tenant.
Tests: CrossTenantAccessTests guards the 4 routes are GA-only +
QueryParam and that a GA reaches the named target tenant. Backend
3295/3295, web vitest 627/627, tsc clean.
Note: the old bug planted orphan copies of foreign rules into the GA's
home tenant on each attempted cross-tenant edit; those are not
auto-cleaned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
6c53dc33e7 |
feat(distress): tenant-visible TpmPssUnsupported devices + one-shot bell
TpmPssUnsupported distress reports (TPM cannot RSA-PSS-sign, Schannel drops
the client cert on Win11 25H2+, agent can never authenticate) were operator-
only, yet the remediation is tenant-side: TPM firmware update or device
replacement.
- New GET audit/tpm-pss-unsupported (MemberRead), aggregated per serial,
mirroring audit/device-not-registered
- Bell notification "tpm_pss_unsupported" (Admin audience) once per
(tenant, serial); dedup via the existing rejection tracker table with a
disjoint "tpmpss|{serial}" row-key space; no webhook by design
- Web: TpmIncompatibleDevicesInsights panel in the Hardware Whitelist
section; renders only when a device is affected (no permanent card)
No agent change, no ConfigVersion bump. Backend tests 3287/3287, tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1f6efca95b |
fix(backend): stamp DiagnosticsBlobName from on-demand server_action_executed events
Session 8e4cc4ae: two "Collect Logs" round trips completed on the agent (both
ZIPs landed in hosted storage) but the portal never flipped to Download and
the button hung at "Collecting..." — the ServerActionDispatcher confirms an
on-demand upload only via server_action_executed(actionType=request_diagnostics,
blobName=...) and emits NO diagnostics_uploaded (only the terminal upload path
does), so UpdateSessionDiagnosticsBlobAsync never ran.
The ingest classification now treats such an executed event as the upload
confirmation (a real diagnostics_uploaded in the same batch still wins — it
carries destination). Because the executed event has no destination field,
InferDiagnosticsDestination derives "Hosted" from the backend-built
"{tenantId}/{filename}" blob-name shape (CustomerSas blobs are bare
filenames); without it the download route would look in the customer's
container for a hosted blob.
Backend-only — works with the already-deployed agent fleet, no agent release.
Backend suite 3274/3274.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9ec8644be2 |
feat(settings): operator read-only settings view with real values
Operators previously saw editable settings forms bound to hardcoded defaults
whose Save always 403'd. Now they see the tenant's real configuration
read-only, with no save affordances at all.
Backend:
- GET config/{tenantId}: TenantAdminOrGlobalReader -> MemberRead. Safe because
the handler redacts by default (CanViewSecrets: only GA or own-tenant Admin
see secrets); Operators/Viewers receive RedactedCopyForReader().
- POST diagnostics/files: TenantAdminOrGA -> TenantAdminOrOperator (submitting
logs is a troubleshooting action; Viewer and read-only GlobalReader excluded).
- Tests: policy pins updated; new middleware tests (own-tenant Operator allowed
+ redacted, cross-tenant Operator 403); CanViewSecrets theory for
Operator/Viewer.
Web:
- TenantConfigContext loads the full config for all settings users (403
fallback to feature-flags minimal config for deploy-order safety) and
exposes canEditConfig; saveConfiguration guards on it.
- New ReadOnlyFieldset wraps section content (native fieldset disable, values
stay copyable); SaveResetBar is not rendered at all for read-only viewers.
Applied to Contact, SLA Targets, Hardware Whitelist (+ rejection-insights
toggle), Notifications, Agent Settings, Agent Analyzers, Diagnostics, Data
Management.
- Hidden entirely from Operators (in-page admin-only card + sidebar sub-item
gate): Autopilot Validation, Access Management, Offboarding, Unrestricted
Mode. GlobalReader sidebar parity kept via hasGlobalScope.
Tests: backend 3257/3257, web vitest 627/627, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
2fee50276e |
feat(backend): operator-tier session actions + diagnostics groundwork for on-demand log collection
- New EndpointPolicy.TenantAdminOrOperator (Admin/Operator/GA, no Viewer) with
middleware evaluator; POST sessions/{sessionId}/actions moves to this tier so
troubleshooting operators can queue request_diagnostics ("Collect Logs").
- QueueSessionActionFunction re-gates per type: terminate_session and
rotate_config stay Admin/GA-only (403 for operators).
- GET config/{tenantId}/feature-flags exposes diagnosticsUploadConfigured so
members below Admin can see whether an on-demand upload would work.
- Offboarding cascade: hosted diagnostics deletion now sweeps the session
prefix {tenantId}/AgentDiagnostics-{sessionId}- (plus the manifest-named
blob) so on-demand packages overwritten on the Sessions row by the terminal
package no longer survive deletion. CustomerSas branch unchanged.
- Notification dismiss routes deliberately NOT migrated: dismissal is still
tenant-shared; operators would clear admin-tier notifications they cannot see.
Tests: middleware role matrix on the actions route, per-type gate matrix,
feature-flag matrix, prefix-sweep + real hosted-branch cascade tests.
Backend suite 3253/3253.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a44e37a7c2 |
fix(metrics): keep full failure-reason text for fleet-health display
BuildFleetFailureReasons truncated each reason to a 50-char prefix + "..." and used that as both the grouping key AND the display value. The full text never reached the UI, so its tooltip/tap-to-expand had nothing to reveal and near-identical rows were indistinguishable past the cut. Group by the 50-char prefix as before (variants still collapse into one row), but carry the longest full reason as the display value. The UI truncates and expands it on demand, so the complete text is now available end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
71eda93e8b |
fix(metrics): app-install failure rate over finished installs only
Second half of the terminal-only rate convention (follows
|
||
|
|
1cfc32c9aa |
fix(metrics): success rate over finished enrollments only (geo + fleet health)
Customer feedback: the geographic performance pane showed a poor success rate for a site mid-rollout because in-progress/pending sessions counted against the rate. Success rate is an outcome quota — align Geo and Fleet Health with the SLA convention already used by dashboard stats, usage snapshots and the fleet rollup: Succeeded / (Succeeded + Failed). Incomplete (terminal, non-failure) stays excluded. - Geo: per-location SuccessRate over finished sessions; table badge and map tooltip render an em dash instead of a red 0% when nothing has finished yet - Fleet Health: Stats.SuccessRate terminal-based; card shows "X of Y finished enrollments" and a neutral em dash state; per-model health gets a Failed count (additive wire field) so the client rate excludes in-flight devices; TopFailingModels FailureRate now failed/finished - Tests: FleetHealthPayloadTests flipped to the new convention plus zero-finished, model-failed and in-flight-dilution cases; new GeographicMetricsSuccessRateTests pin the geo semantics Backend 3216/3216, web tsc clean, vitest 602/602. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
540f543834 |
feat(agent): extend historic IME-replay suppression to app events + app aggregation
Part 2 of the eaf3d8c4 fix (follows |
||
|
|
088c3b0479 |
fix(agent): suppress historic IME-log script replay + timeline-consistent durations
Session eaf3d8c4: a previous enrollment's IME logs survived on disk (dated 2026-07-16, 7 days old) and the agent replayed them at startup — 156 phantom script_completed events in a 7-minute session, each showing ~170h durations (clock-clamped completion minus RAW stale start), plus an immediateUpload flood (3x ingress_backpressure) and RemediationScriptCount inflated to 156. Agent: - Script events whose source line is >24h stale are suppressed entirely; a one-shot historic_script_replay_detected Info event (new EventType) dates the replayed window via earliestRejectedSourceTimestamp. Future-skew rejections and clock-only fallbacks still emit. - durationSeconds now uses a timeline-consistent pair: on rejected-timestamp emissions the raw source completion pairs with the raw start (true runtime instead of cross-timeline garbage). 24h plausibility backstop omits implausible values; same consistency for script_timeout_suspected. - Stale pending platform-script slots (>24h older than the current start line) are discarded so an old orphaned start can't pair with a fresh run (extracted HandlePlatformScriptStarted as internal test seam). Web: partitionHistoricScriptEvents hides legacy-agent replay events (muted "N historic executions hidden" note) and the reducer blanks >24h durations — covers sessions recorded before the agent fix rolls out. Backend: IsHistoricScriptReplay guard keeps replayed events from inflating Platform-/RemediationScriptCount during the rollout window. Registered historic_script_replay_detected in Constants.EventTypes, MCP resource-catalog (script_events) + KEY_EVENT_TYPES, gather-rules eventTypes. Tests: 8 agent (suppression/one-shot/raw-pair/backstop/slot-hardening), 8 web (partition/blanking), 10 backend (predicate). Agent 2026, backend 3210, web 595, MCP 449 all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ad3632e9fb |
fix(emergency-break): survivable self-destruct, reliable break report, ops visibility
Incident 2026-07-22: the 48h emergency break left exe + loaded DLLs orphaned forever — a second agent instance held the images while the cleanup script (waiting on the launcher PID only) force-deleted marker + state around the locks AFTER removing the Scheduled Task, destroying the retry path. Agent — cleanup script (LIFE-F7): - wait for ALL agent processes by name (PID-reuse-proof), then force-kill stragglers; single-flight lock file serializes concurrent cleanup scripts - probe-rename (GUID target) BEFORE task removal; any failure renames back and aborts with task + marker + files intact for a next-boot retry; the destructive Remove-Item-on-original fallback is gone - script generation extracted to BuildCleanupScript, pinned by 13 tests; all 4 variants parser-validated + E2E-smoked (happy path, locked tree) Agent — break report delivery: - EmergencyReporter counts DELIVERED reports: reservation rolls back on failure (mark-before-send permanently suppressed a key after its first transport failure); optional in-reservation retries - break path: NIC wait <=15s, 3 attempts x 15s timeout (survives Functions cold starts observed as >5s 503s), 75s overall bound Backend: - backend-materialized events (agent_emergency_break, session_timeout) now upsert the cross-session EventType index — they were invisible to every search-by-eventType surface (portal + MCP) - new AgentEmergencyBreak ops event (Agent/Warning, once per session) so operators see silently-lost agents; dual-registered in OPS_EVENT_TYPES - materialization extracted to a static seam, covered by 4 new tests Agent 1963/1963, Backend 3198/3198, tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b2d2d37c6f |
feat(realmjoin): detect aborted RJ ESP (missing 110) and release the completion gate
An interactive logon during the RealmJoin first deployment (phase 100/101)
makes RJ reclassify the run as a secondary-user deployment, whose branch
writes 200/210 and never CompletedFirstDeployment (110) — no crash, no
reboot request. The V2 completion gate waited for exactly 110, so these
sessions hung until the 60-min hard timeout (session 224b2087; siblings
558bf067/3cc18724 delayed ~50 min the same way).
- promote realmjoin_phase_changed to a typed RealmJoinPhaseChanged signal
(dual-emitted by the adapter) so RealmJoinFacts.LastDeploymentPhase is
persisted across agent restarts
- abort rule: prev in {100,101} and current in {200,210} with no outcome
yet -> emit realmjoin_first_deployment_incomplete (Warning), cancel the
hard-timeout deadline, release the gate with outcome
FirstDeploymentIncomplete; also evaluated on the RealmJoinDetected
replay path (restart swallows the transition)
- deliberately NOT triggered by 101->0 (RJ service restart may still
complete the first deployment) nor by a first observation of 210
(session 6f1959c0: RJ already completed before agent boot)
- fix realmjoin_timeout always claiming "last phase: 0" - the persisted
phase now feeds the message
- backend EnrollmentTimeoutClassifier counts the new event as a gate
terminal; MCP resource catalog dual-synced
- CompleteIfDeferredOrBookkeep widened to a leading-effect list
Verified by replaying session 224b2087's signal log: gate releases at
15:05:17 (101->200) instead of hanging until the 15:54 deadline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e0ad51b965 |
refactor(urls): one guard-enforced URL registry per component, agent downloads move to the alias
Every well-known host -- own subdomains, both blob accounts, the API origin, Graph, Entra login -- now lives in exactly one registry per component: Constants.cs (*BaseUrl) for C#, utils/config.ts for the web app, src/config.ts for the MCP server. lib/hostRouting.ts derives its hostnames from the registry and middleware.ts imports from it, retiring what turned out to be a fourth independent copy of the www/portal host pair. Three new guard tests (backend xUnit, web vitest, MCP vitest) scan non-test source and fail on any enforced host outside its registry. This is the lesson of the EU cutover: the registry existed then too, but LatestVersionsService (feeds the portal's version badge) and ValidateBootstrapCodeFunction carried their own literal copies of the legacy blob host and were missed -- kept working only by the fail-soft legacy mirror in build-agent.yml, whose silent failure would have pinned badge and self-update to stale bits with a green release. Both guards proved themselves on their first run, flagging the middleware host copy and a literal in an MCP tool description. Test files stay exempt on purpose: literals there are independent oracles, and ValidateBootstrapCodeResponseShapeTests now pins the full alias URL against the Constants-built one so registry changes must be acknowledged consciously. The two legacy readers plus the agent SelfUpdater now use Constants.AgentDownloadBaseUrl (download.autopilotmonitor.com/agent); the alias was verified live for version.json, version-v2.json and both ZIPs before the switch. The portal's bootstrap allow-list accepts the alias AND the legacy host (AGENT_DOWNLOAD_HOSTNAMES) for already-deployed customer scripts, which imposes a deploy order: web before or with backend. The legacy keepalive probes and the release mirror stay untouched -- they exist for exactly this transition. Also: the bootstrap short-link now targets www directly instead of riding the registrar's apex 301, and the welcome-mail docs button points at docs. instead of bouncing through www./docs. Suites: backend 3194, agent core 1925, DecisionCore 474, web 582, MCP 449 -- all green; next build verified (it, not tsc, proves the next.config.ts registry import). Docs: docs/url-registry.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bbb55034e3 |
feat(versioning): one version contract for agent, backend and MCP
All three components now report <major>.<minor>.<build> plus a 7-char commit and a build timestamp. The backend had been pinned at a hand-written 1.0.0 since inception and starts at 1.5.1000; the MCP server's package.json version was bumped by hand and starts at 1.5.300, and it now reports its own commit on /health instead of only the documentation bundle's. The build number keeps coming from a counter blob reserved before the build with ETag-CAS, but there is now one implementation instead of two copies: .github/scripts/Request-BuildNumber.ps1, called by all three workflows and by the local build.ps1. It lives under .github/ because scripts/Deployment/ is gitignored (those scripts carry SAS tokens) and would therefore be absent from a CI checkout; scripts/Build/ and scripts/Release/ are shadowed by the [Bb]uild/ and [Rr]elease/ artifact ignores. The counter cannot be replaced by the published manifest, which is the obvious simplification to reach for: version.json is written only on stable publish, so two consecutive dev builds would derive the same number and the second would overwrite the first's artifact. git rev-list --count fails the same way, two builds of one commit collide. Both deploy workflows now poll their health endpoint until it reports exactly the version and commit just deployed, and publish versions/<component>.json only then. A green deploy step alone means the package was accepted, not that it is being served -- so a manifest that disagrees with the live endpoint now means the running instance drifted (rollback, stale revision) rather than a pipeline that stopped halfway. The agent manifest gains the same commit and buildUtc fields additively; the bootstrap reads it by field name, so older installers ignore them, and sha256/bootstrapVersion stay untouched. Requires the versions container plus AZURE_BLOB_VERSIONS_SAS_TOKEN and the two seeded counters (backend 999, mcp 299) before the first deploy run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
71d2c0e47f |
fix(maintenance): remove the time-window auto-block, it never measured data volume
GetExcessiveDataSendersAsync gated on LastEventAt - StartedAt alone. No event count, no bytes, no rate — the name asserted something the code never checked. What it actually selected was every enrollment session that spanned a night, which for Autopilot is the ordinary case: device unboxed in the evening, ESP runs, machine powered off, user signs in the next morning. The per-device alerts from the previous commit made this visible. All three devices blocked at the time of the analysis were false positives: CND3320949 / 64ed21e8 23 events over 15.5h (1.5 events/hour) 5CG4303CPT / db5751bc 192 events over 16.8h (blocked mid Account Setup 1/5) PF5YANFB / 2069de87 781 events over 31.5h 14 such blocks in 30 days across 9 customer tenants. Over the same window the event-count detector fired 5 times and was right every time (2742, 2714, 2712, 2506 events). Each false block costs a live enrollment 8 hours of telemetry. Repairing it rather than removing it does not pay: once a volume condition is added, "many events AND long span" is a subset of "many events", which is what the count detector already does. Volume is the quantity that costs anything; a 23-event session spanning 15 hours costs nothing. So the detector, its two admin settings, the ExcessiveDataBlocked ops event and the portal section go, and the event-count path is the only automatic block left. MaxSessionWindowHours and MaintenanceBlockDurationHours are deleted rather than deprecated: the config row is written with Replace, so the columns drop out on the next portal save, and nothing reads them meanwhile. A comment on the surviving detector records why there is no second one, so it does not get rebuilt a year from now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3b2f5ed46d |
feat(runaway-detection): name the device in the time-window alert, link it in the block list
The two runaway detectors emitted alerts of unequal usefulness. The event-count
path fires per session and carries sessionId + serialNumber, so the Ops Events
detail modal can offer View session / Block / Kill. The time-window path
aggregated a whole tenant into one event with {devicesBlocked, windowHours} —
no device, no session, no shortcuts, on precisely the alert where an operator
wants to act. It now writes one event per blocked device with the same payload
shape; the aggregate count stays in the audit entry, where a sum is the useful
view.
The Active Blocks table showed the serial as plain text although the API already
returns BlockedSessionIds. Maintenance blocks now link the serial to the session
that triggered them; manual whole-device blocks have no session and stay text.
Two defects in the event-count path, found while comparing the detectors:
- No isBlocked guard, so the auto-action re-blocked an already-blocked device and
overwrote BlockedAt/UnblockAt/Reason of the existing block — usually the
time-window block on the same session, whose reason then vanished from the list.
- No status filter, so a session that finished days ago with a high EventCount
still cost its device a 24h block. The fix needed a layer below the obvious one:
the runaway query never selected Status, and SessionStatus.InProgress is
ordinal 0, so a guard on the unpopulated field would have been silently
always-true. ParseStatusForAutoAction is stricter than MapToSessionSummary --
Enum.TryParse maps "0" onto InProgress and ORs comma-separated names into an
unrelated member, both of which would open the gate.
Not addressed, and deliberately: a block is session-scoped and stops the upload,
so whichever detector fires first freezes the other for that session. With
MaxSessionWindowHours=12 the time detector wins in practice. That is correct
behaviour, not a defect; merging the detectors would be its own change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
757427ffea |
chore(build): clear the compiler and xUnit analyzer warnings CI annotates
The CI test job surfaced 10 annotations, but GitHub caps them at 10 per job — a full rebuild showed 31 nullable warnings plus 26 xUnit analyzer warnings. Production code: - CertificateValidator: X509Certificate2(byte[]) is obsolete (SYSLIB0057) -> X509CertificateLoader.LoadCertificate. - AuthenticationMiddleware: IsValidTenantId now carries [NotNullWhen(true)], so the guard above BuildTenantAuthority is visible to flow analysis. - AgentEndpointMigrationRules: the out params are genuinely nullable on the false path; netstandard2.0 has no usable [NotNullWhen], so the contract is documented instead. - Constants.ClassifyEspAppsFailure: errorCode is optional by design (the "no HRESULT" branch is the timeout fallback) - signature says so now. Tests: nullable annotations on helper signatures, Assert.Single/DoesNotContain overloads instead of pre-filtering with Where, and [InlineData(null)] theories declare nullable parameters. GatherRule helpers now seed Parameters with the model default (empty) rather than null, matching a deserialized rule. No behaviour change. 5616 tests pass; solution builds warning-free apart from the two pre-existing NU1510 pruning notices on the pinned security packages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b56c774103 |
fix(logging): strip Unicode line separators in LogSanitizer
The sanitizer matched \p{Cc}, which covers the C0/C1 control ranges but not
U+2028 LINE SEPARATOR or U+2029 PARAGRAPH SEPARATOR - those are categories
Zl/Zp. A log viewer, a JSON-lines consumer or any JavaScript-side reader still
breaks a line on them, so they were a residual log-forging vector.
Matching \p{Zl} and \p{Zp} rather than listing escapes: the two categories
contain exactly those two characters, and it keeps the source free of
invisible literals.
Verified the test is not vacuous - the old pattern left "a<U+2028>b" as "a b",
the new one yields "ab".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
68767f436f |
fix(config): validate ContactEmail and seed it conditionally
The tenant contact address had no server-side validation, so a direct API caller could store any string in the field service notices are sent to. UpdateTenantConfiguration now rejects non-addresses, recipient lists (a comma would silently widen who receives those notices), display-name forms and control characters (mail-header injection), and normalizes what it stores. Empty stays valid - having no contact address is a legitimate state. The seed path had a worse problem than missing validation. It was a read-modify-write of the whole model through SaveTenantConfigurationAsync, which upserts with TableUpdateMode.Replace and no ETag: a concurrent portal save was clobbered by the seeder's stale snapshot of EVERY field, not just ContactEmail. It also wrote straight through IConfigRepository, bypassing the TenantConfigurationService cache, so a seeded address stayed invisible for the full 5-minute TTL. TrySeedTenantContactEmailAsync now writes ETag-conditionally in Merge mode - one property, and a 412 means someone else wrote the row so the seed abandons rather than overwriting the winner. A lost seed is recoverable on the next maintenance run; a lost admin edit is not. The same seed logic existed twice (preview notification-email save and the maintenance backfill). Both now call a single owner on TenantConfigurationService, which holds the never-overwrite invariant, the cache eviction and the fail-soft guarantee. Fail-soft lives in the service rather than the repository so it holds for any implementation. Tests cover the validator, the service contract, and the repository against a mocked TableClient: Merge mode, the ETag actually passed, only ContactEmail in the patch, the 412 race, 404, storage faults. Plus a regression pin that SaveTenantConfigurationAsync is never called from the seed path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c7b06be8b7 |
fix(ratelimit): key the user throttle on CallerId, not the UPN
Follow-up to
|