mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
82b26b4ab90caee55043d60117dfc8d52d18c63c
16 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
82b26b4ab9 |
fix(security): HTML-encode domain in transactional mails; freeze DomainName on config PUT, validate first-login seed
Welcome/farewell mails rendered DomainName unencoded while the full-model config PUT persisted it verbatim, letting a tenant admin ship attacker HTML in a signed product mail to an arbitrary recipient. Encode at Render() and the built-in builders, preserve DomainName from the stored row on PUT for every caller, and only seed a strict DNS host name at first login. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a306cd9e40 |
feat(security): resolve admin identity bindings automatically at grant time; per-tenant bindings panel
Granting a cross-tenant role is "type the UPN" again. AdminIdentityResolver derives the home tenant + object id from sign-in history (UserActivity: both values came from validated tokens, most recent sign-in wins), falls back to the UPN domain of an onboarded tenant (tid only, oid pinned on first sign-in), and never guesses when a UPN was seen under two tenants. Only a never-seen domain yields 422 HomeTenantUnresolved, on which the portal shows a home-tenant picker. - IMetricsRepository.GetSignInIdentitiesByUpnAsync (filtered UserActivity scan, grant-time only) - Grant endpoints: homeTenantId/objectId optional overrides; bindings dropped from the list bodies - Web: identity fields and binding pills removed from the delegated / tenant-group forms; new collapsed "Identity bindings" panel in Tenant Management -> Edit Tenant Configuration (bindings homed in that tenant; inline edit / remove via global/identity-bindings) - Tests: AdminIdentityResolverTests; grant validation now accepts an absent home tenant Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8ec8524252 |
fix(security): bind platform and delegated roles to the immutable Entra tid+oid, not the UPN string
GlobalAdmins, DelegatedAdmins and TenantGroupAssignments resolved their role from the upn/preferred_username claim alone while the API accepts tokens from every Entra tenant, so a genuine token minted in a foreign tenant (re-registered domain) or by a recycled UPN in the home tenant inherited platform admin. - New AdminIdentityBindings table: RK = UPN -> home TenantId + ObjectId; in TableNames.All, CriticalBackupTables and the full-restore-forbidden auth set; offboarding wipes bindings homed in the offboarded tenant - AdminIdentity (upn + tid + oid) is what GlobalAdminService.GetGlobalRoleAsync and DelegatedAdminService.GetScopeAsync resolve on; order row -> binding -> Pro gate, fail-closed (no oid / no binding / tid or oid mismatch => no role), mismatches logged at Warning - Object id pinned on the first sign-in from the bound tenant (ETag-conditional, verified against the stored winner); a foreign-tenant token can never claim the pin - Grants (auth/global-admins, global/delegated-admins, tenant-group assignees) require homeTenantId (+ optional objectId) and answer 409 instead of re-homing a bound UPN; GET/PUT/DELETE global/identity-bindings is the audited rebind surface - RequestContext.ObjectId; throttle identity prefers oid so shared UPN strings across tenants no longer collide in one rate-limit bucket - Web: home tenant / object id fields on the grant and assign forms, per-UPN binding pill - Tests: binding verdict matrix + race semantics, middleware-level denials for foreign-tenant and recycled-UPN tokens; OKF doc docs/backend/admin-identity-binding.md Legacy role rows are inert until their binding is seeded - seed before deploying the backend. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
395fdd49df |
Transactional tenant-config writes via MCP + ConfigurationBackups safety net (#120)
* feat(backend): pre-write config backup net - every tenant/admin config save snapshots first New ConfigurationBackups table (PK=tenantId or GlobalConfig, RK=reverse-ticks+guid, newest-first) holds the raw stored row as EntityJson before any overwriting save of TenantConfiguration or AdminConfiguration. Hook lives in TableConfigRepository, the single choke point all writers funnel through - fail-soft (a backup hiccup never breaks a save), skips row creations and noise-only changes (LastAuthClientId flips), prunes to the newest 2 per partition. Source/reason tagging via a new SaveTenantConfigurationAsync overload (not optional params - Moq expression trees, CS0854); portal-put/plan/auth/app-homing/offboard call sites tagged. Offboarding: backup partition wiped in the bulk phase (TenantPartitionTables) and by the marker-cleanup tombstone sweep; the GlobalConfig partition never matches a tenant GUID and survives. Table registered in TableNames.All + CriticalBackupTables.All. Groundwork for the transactional MCP config-write flow (patch/revert endpoints follow). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(backend): transactional tenant-config field patch + revert with verified writes New GA-only surface (policy catalog: GlobalAdminOnly + RouteParam): - PATCH config/{tenantId}/fields - field-level patch through TenantConfigPatchService: fresh ETag read -> field gate (deny-list incl. plan/trial, HomedAppClientId, auth provenance; unknown fields 400 with the name) -> patch onto a clone -> shared validation -> fail-CLOSED backup -> CAS replace -> fresh re-read -> verify that EXACTLY the intended fields changed -> automatic ETag-conditional rollback on drift (catches Store/Map serialization asymmetry). - POST config/{tenantId}/revert - restore from a ConfigurationBackups snapshot (latest by default); snapshots current state first (revert is revertible), preserves plan/homing/provenance fields unless includeProtectedFields. - GET config/{tenantId}/backups - metadata + masked diff only, never EntityJson. Supporting changes: IConfigRepository gains GetTenantConfigurationWithEtagAsync + TryReplaceTenantConfigurationAsync (412 -> false, no implicit backup hook); validators moved from the PUT into shared TenantConfigValidation (PUT re-pointed, forwarding shims keep test call sites); GET config/{tenantId} honors ?view=redacted so a GA's MCP session never pulls clear-text secrets into model context. Caller-tier deny-lists (TenantAdmin/DelegatedAdmin) ship now for the phase-2 flip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(mcp): tenant-config write tools - update/revert with snapshot-backed verified writes First mutating surface on the MCP server, strictGa (real Global Admin) only: - get_tenant_config: full config read, ALWAYS ?view=redacted (secrets never reach model context; pinned by config-write-tools tests) - update_tenant_config: field-level patch via PATCH config/{tenantId}/fields; required reason; response = applied fields + masked diff + backupId - list_tenant_config_backups: snapshot metadata + masked diff only - revert_tenant_config: restore latest/named snapshot; protected fields stay current unless includeProtectedFields; revert is itself revertible New MUTATING annotation + TenantGuidSchema (path-traversal guard) in shared.ts; role-aware instructions headline (non-GA keeps the exact READ-ONLY wording); catalog snapshot test extends GA_FULL + new CONFIG_WRITE_GA_STRICT difference list (Global Reader/tenant/delegated see none of the four). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(backend): de-flake session-deletion heartbeat test - condition-gated handler instead of wall-clock window The heartbeat test held the handler for a FIXED 350ms and expected the 50ms heartbeat timer to tick >=3 times inside that window. On loaded CI runners the heartbeat Task.Delay stretches disproportionately to the handler delay, the handler completes with <3 heartbeats fired, and the awaited condition can never become true afterwards (CI failure on PR #120). The handler now holds the message in-flight UNTIL the test observed 3 heartbeats (with ct + own 15s deadline as termination guards), so the assertion is reached deterministically regardless of scheduler jitter. Verified: 12/12 local runs green, full suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
607d6615b0 |
fix(auth): app-homing flip survives stale full-model config writers
Prod incident 2026-07-31 18:03Z (tenant b54dc1af): the consent-driven homing
flip landed and 600ms later the frontend persisted the validation toggle via
the generic config PUT - round-tripping a config loaded BEFORE the flip. The
GA branch treated the stale null HomedAppClientId as an intentional revert to
legacy, silently undoing the flip (App Insights: AppHomingFlipped 18:03:00.44,
PUT revert warning 18:03:01.03).
- HomedAppClientId is now IMMUTABLE via the generic PUT for ALL callers, GA
included: a full-model round-trip can never carry flip intent. The only
writers are the consent auto-flip and POST config/{tenantId}/app-homing
(which the GA tenant-management panel already uses).
- HandleAuthClientIdTrackingAsync (same failure class, cross-instance): the
last-seen tracking write now mutates a cache-BYPASSING fresh read via new
TenantConfigurationService.GetConfigurationFreshAsync instead of blind-saving
the up-to-5-min-stale cached view - the login right after a flip is exactly
the one that carries a changed audience and triggers this write.
3 regression tests (fresh-entity mutation preserves a concurrent flip,
fresh-already-current suppresses the write, steady state stays zero-I/O).
Backend xUnit 3767/3767.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
d8e8cd15fd |
feat(entitlements): Community/Enterprise editions with self-service trial
Fail-closed entitlement layer (FeatureEntitlementCatalog + TenantEntitlementService): edition resolves at read time (PlanTier == 'enterprise' OR active trial; legacy free/pro => Community, no migration).
- Plan/trial endpoints: PATCH config/{tid}/plan (GA: tier + grant/extend/end trial, audited, cache-safe) and POST config/{tid}/trial (tenant-admin self-service 30d, once per tenant, 409 after). Generic PUT preserves plan/trial fields for all callers. Fixes SetPlanTier writing past the config cache without audit/UpdatedBy.
- Rate limits: Enterprise floor 150/min for BOTH user (portal/JWT) and device (agent/cert) paths - floor raises the global default, an explicit per-tenant override still wins; MSP users follow their home tenant.
- Retention cap: Community 90d / Enterprise 365d, clamped in the retention fanout (warn log) + non-GA write validation 7..cap (only on changed values; 0=infinite stays GA-only).
- Delegated admin (MSP): Enterprise gate on the delegated admin's HOME tenant (JWT tid, fail-closed on unknown) - managed target tenants may be any edition. Grant-time target check removed; inert grants are suppressed at resolve time (single choke point in GetScopeAsync, applied after the UPN-keyed cache).
- MCP quota: McpQuotaService + McpQuotaEnforcementMiddleware (X-Client-Source: mcp only, GA exempt) enforce daily/monthly budgets - per-user plan override > tenant edition plan; limits from SectionUsagePlans with catalog fallbacks (community 100/3000, enterprise 1000/20000); 429 with X-MCP-Quota-* headers + Retry-After; fail-open on counter reads, fail-closed on plan resolution. /api/metrics/mcp-usage/me reports effective plan + counters.
- Trial visibility: TrialExpirySweepFunction (daily 03:30 UTC, informational) emits TenantTrialExpired (Warning, 24h look-back) + TenantTrialExpiring (Info, <=3d heads-up); dual-registered in web OPS_EVENT_TYPES.
- Web: EditionBadge + one-time trial CTA in settings header, Plan & Trial card in the GA tenant edit modal (own PATCH save), retention cap hints from entitlements, pure lib/edition.ts (vitest), feature-flags surface edition/trial/entitlements; MCP TENANT_SAFE_FIELDS + trial fields.
Verified: solution build clean; backend 2653/2653 tests; web tsc + 516/516 vitest; MCP server tsc + tests green.
|
||
|
|
a5075b2236 |
feat(mcp): delegated-admin (MSP) support over MCP — tenant-bounded cross-tenant reads
Delegated (scoped-global / MSP) callers get cross-tenant ROUTING bounded to their managed tenant set: every tool call must name a managed tenantId (no aggregate), platform-only tools stay hidden, and enforceDelegatedTenant(ForPage) validates the effective tenant client-side as defense-in-depth. Backend: McpUserService resolves delegated scope and auto-grants MCP access under WhitelistOnly; McpUserFunction emits delegatedTenantIds/delegatedRole. Recatalog global/metrics/summary to QueryParam so delegated readers reach it only on their managed ?tenantId= path (aggregate stays blocked). Authoritative enforcement is the PolicyEnforcementMiddleware delegated rescue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ab9b3d6211 |
feat(web+backend): surface delegated (MSP) scope on /auth/me + AuthContext [Phase 3a]
Foundation for the web MSP/fleet experience: the app now knows whether the signed-in user is a delegated admin and which tenants they manage. Backend (/api/auth/me): - Resolve the caller's delegated scope (cached) alongside the existing parallel fetches and return delegatedTenantIds (the managed tenant set) + isDelegated. - Delegated admins bypass the private-preview gate, exactly like Global Admin / Reader — an MSP whose own home tenant is not on the waitlist must still be able to log in and manage its authorized tenants. Gate 1 (suspended home tenant) still takes precedence. BuildAuthResult gains an optional delegatedTenantIds param (existing call sites unaffected); 3 new tests + the side-effect harness updated. Web (AuthContext): - UserInfo gains isDelegated + delegatedTenantIds; AuthContextType gains hasFleetScope = hasGlobalScope || isDelegated for fleet/switcher gating. All /auth/me parse paths (success, prefetch, preview/error fallbacks) populate them. Full backend suite green (2472); web tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a36bf8bf0c | fix onboarding bug, onboardedBy and auto promote admin | ||
|
|
0b80f09a74 |
refactor(notifications): bell on pure SignalR push, kill 60s polling
The two notification-bell React contexts polled GET /api/notifications and
GET /api/global/notifications every 60s regardless of activity, generating
3017+618 = 3635 calls/24h with near-zero hit rate (notifications are emitted
sparsely). SignalR was already used app-wide for live session updates; the
bell was the lone hold-out on a pull model.
Switch to the same push model. Steady-state traffic drops ~86% and the bell
updates sub-second instead of within 60s.
Architecture (Option C+):
- Mount -> 1x HTTP fetch (cold-start hydration via existing GET endpoint)
- SignalR onreconnected -> re-fetch (covers the disconnect window)
- SignalR push -> live append/dismiss
- No periodic polling, no setInterval
Audience-tier security: TenantNotificationAudienceCatalog still drives
visibility (Member vs Admin), but enforcement moves into SignalR group
membership: tenant-{id}-notify-member is joined by every tenant member,
tenant-{id}-notify-admin only by Tenant Admins / Global Admins. The
AddToGroup function fail-closes with 403 on Member-tier joins to the
admin group, so Admin-tier notifications cannot leak via push.
Backend
- ISignalRNotificationService extracted; SignalRNotificationService
implements it. Resolves the long-standing test-mocking blocker
(the real service's constructor validates AzureSignalRConnectionString
and BuildServiceManager(); previously you could not unit-test
consumers). Tenant + Global NotificationService consume the
interface; the three legacy callers (Analyze, Ingest, VulnCorrelate)
still bind to the concrete class -- migrate per test need.
- Six new push methods: SendTenantNotification{,Dismissed,DismissedAll}
and SendGlobalNotification{,Dismissed,DismissedAll}. Tenant variant
routes to the audience-correct group.
- Tenant + Global service push after a successful repo write
(fire-and-forget; never throws into the caller).
- SignalRGroupHelper: parsing handles the two new -notify-{member,admin}
suffixes; new IsTenantNotifyAdminGroup predicate; group-name builders.
- SignalRAddToGroupFunction: explicit 403 when a non-admin attempts
to join the admin-tier group.
Frontend
- TenantNotificationContext: setInterval gone. useSignalR() to
join member-group always, admin-group iff isTenantAdmin || isGlobalAdmin.
Listens for tenantNotification (append + dedupe), tenantNotificationDismissed
(filter), tenantNotificationsDismissedAll (clear). onreconnected
triggers re-fetch.
- GlobalNotificationContext: same pattern against global-admins group.
- app/layout.tsx: SignalRProvider hoisted above the notification
providers so useSignalR() resolves inside them.
Tests
- SignalRGroupHelperTests (8): parsing for all four group formats
incl. new -notify suffixes, IsTenantNotifyAdminGroup predicate.
- TenantNotificationServiceSignalRPushTests (8): audience-correct push
on Create (Admin / Member / Unknown -> Admin fail-closed), no-push
on repo failure, no-push on DismissAll(count=0), no-push on empty
tenantId.
- FakeSignalRNotificationService: in-memory spy implementing the
new interface; replaces the unmockable real service in tests.
- AuthFunctionSideEffectTests + TenantNotificationServiceTests:
constructor signatures updated for the new SignalR dep.
Suites
Backend xUnit: 1161/1161 (was 1146, +15 net).
Web vitest: 259/259 (unchanged; existing notification-API +
gate tests still cover).
Solution build: 0 warnings, 0 errors.
Expected post-deploy reduction (24h):
GET GetTenantNotifications: 3017 -> ~50
GET GetGlobalNotifications: 618 -> ~10
POST AddToGroup: ~246 -> ~350-450 (two joins per tab)
Net bell load: -86%, sub-second live UX.
Disconnect tradeoff (accepted): if SignalR drops without reconnect
(rare -- withAutomaticReconnect retries 1/2/4/8/16/30s ... forever),
the bell shows the snapshot from the last fetch but no live updates
until next reload. The initial-3-retry give-up path of the SignalR
client still gets the user a hydrated bell because the HTTP fetch
runs independently on mount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
45db8f093f | more tests for critical AuthFunction |