Commit Graph

2196 Commits

Author SHA1 Message Date
Oliver Kieselbach eb94c612e7 fix(agent): gather phase/event triggers follow the engine-reduced timeline
phase_change/phase_exit/on_event gather triggers (and the phase-scope
gates) were driven by the pre-reduce SignalIngress.SignalPosted stream,
so any raw signal carrying an EspPhase payload fired them before the
decision engine had applied its gates. In session 32312a32 (RealmJoin
tenant) a Phase Start rule on FinalizingSetup fired at the raw ESP-exit
EspPhaseChanged(FinalizingSetup) signal, 7 minutes before the engine's
RealmJoin-gated phase_transition(FinalizingSetup) - and read
HKLM\SOFTWARE\RealmJoin\Custom\BIOS before the RealmJoin package that
creates it had run (exists=false).

GatherRuleExecutorHost now subscribes to the new TimelineEventStream:
EventTimelineEmitter publishes every emitted event's (eventType, phase)
post-reduce, so phase triggers fire exactly when the UI timeline shows
the phase, and on_event sees engine-emitted event types
(enrollment_complete, phase_transition, realmjoin_resolved, ...) that
never existed on the raw signal stream - restoring V1 semantics and the
documented "On Event with enrollment_complete" pattern. Raw-signal
consumers with OS-state semantics (DeviceInfoHost, UserEspKeepAwakeHost,
ProvisioningPackageHost) stay on the raw feed deliberately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v2.0.1389
2026-08-04 14:44:35 +02:00
Oliver Kieselbach ed5fcf3dc3 chore(deps): bump transitive npm deps to resolve Dependabot alerts
- mcp: hono 4.12.31->4.13.0 (ReDoS in CORS middleware), fast-uri
  3.1.4->3.1.5 (host confusion), ip-address 10.1.0->10.4.0 (SSRF
  bypasses), postcss 8.5.21->8.5.25 (sourceMappingURL file read)
- web: postcss 8.5.22->8.5.25, brace-expansion (DoS, npm audit)

Lockfile-only changes within existing semver ranges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:57:04 +02:00
Oliver Kieselbach 9ba67162aa fix: session duration inflated by inherited tz-skewed IME log timestamps (#123)
Session df1fcf47 (WG Part 1, real ~1h16m) reported 8h15m: one do_telemetry
event inherited an unrelated, 8h-shifted IME log line timestamp via
LastMatchedLogTimestamp, and the backend re-anchored StartedAt on it.

Three layers:
- Agent: OnDoTelemetryReceived now carries the source-line timestamp; the
  DO-collector poll path passes null and the adapter stamps the clock
  instead of inheriting whatever line matched last.
- Backend: StartedAt alignment and duration-start selection reject
  earliest-event timestamps more than 2h before the current anchor
  (MaxStartedAtBackwardShift), on all four mutation sites incl. the
  SessionsIndex dual-write decision.
- Parser: CMTrace time fields with a UTC-bias suffix ("+480") now parse
  and honor the writer-declared offset instead of failing to match.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v2.0.1388
2026-08-04 11:54:41 +02:00
dependabot[bot] efc96258d6 chore(deps-dev): bump the web group (#122)
Bumps the web group in /src/Web/autopilot-monitor-web with 2 updates: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom).


Updates `@types/react` from 19.2.17 to 19.2.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 19.2.3 to 19.2.4
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.2.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 11:19:47 +02:00
Oliver Kieselbach 9d8b0fc254 feat: tenant-config field schema - MCP writes get types right on the first attempt (#121)
Follow-up to the E2E finding (95 vs 95.0): a model driving update_tenant_config
had to guess field names and JSON types from a config read. New reflection-
generated schema surface makes guessing unnecessary:

- Backend: GET config/fields-schema (GlobalAdminOnly) - built from the
  TenantConfiguration model + the patch service deny-lists at runtime, so it can
  NEVER drift from what the patch endpoint enforces. Per field: camelCase name,
  JSON type (string/boolean/integer/number), nullability (null clears), format
  hints (json / date-time), writability + deny reason, GA-only marker (phase 2),
  revert-protected marker. Literal route; FindPolicy literal-over-template
  precedence pinned by test (same mechanism as config/all).
- MCP: new strictGa READ_ONLY tool get_tenant_config_schema; update_tenant_config
  description + GA instructions headline steer the model to call it before
  composing a patch.
- Tests: drift canary (every model property appears exactly once), deny-list
  consistency, type exemplars incl. the decimal SLA column class, policy +
  route-precedence pins, MCP catalog/registration/request-shape. Backend 3854
  green, vitest 465 green, tsc clean.

Deploy order: backend first, then MCP redeploy.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 17:33:04 +02:00
Oliver Kieselbach f3028041af feat(web): tenant list shows plan badge (Community/Pro) instead of Preview
The green "Preview" badge was the activation-gate whitelist marker from the
preview era. With public availability + auto-approve live it carried no
information. Rows now show the plan tier (legacy "enterprise" resolves to
Pro, everything else Community); the amber "Waitlist" badge still appears
for unapproved tenants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 15:50:56 +02:00
Oliver Kieselbach 3628c7bc79 fix(backend): revert rehydration - whole-valued double columns must not become Int32
Prod finding during the first live E2E (gktatooine): revert_tenant_config failed
fail-closed with "could not be rehydrated". Root cause: JSON cannot distinguish
95.0 from 95, so the snapshot rehydrator turned the whole-valued SLA rate columns
(decimal? in the model, double in storage) into Int32 - and TableEntity.GetDouble
throws on the type mismatch. The rehydrator now forces GetDouble for columns the
model declares as decimal/double/float, mirroring the existing DateTime handling.

The roundtrip canary test is hardened into a reflection filler that sets EVERY
writable property non-default (with a deliberately whole-numbered decimal), so no
column type can escape the fidelity check again; plus a focused pin on the exact
prod failure. Suite 3849 green. No data was harmed: the fail-closed guard refused
the write, the snapshot itself is intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 14:45:11 +02:00
Oliver Kieselbach 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>
2026-08-03 14:30:05 +02:00
Oliver Kieselbach 5edb64f3c9 fix(web): confirm-dialog errors surface in-dialog + dark-mode sheet gaps
The GA offboard and app-homing confirm dialogs reported failures via the
admin page-level error banner, which sits behind the tenant editor modal
(z-50) and the dialogs themselves (z-[60]) - the user saw nothing. Both
dialogs now take an error prop and render the failure inline; the section
routes homing/offboard failures into dedicated states cleared on open.

Dark mode: class-by-class audit of the offboard UI against the global
override sheet. The dialogs were already covered; the real gaps were
missing family members in globals.css - border-{red,amber,green,purple}-100,
border-{blue,sky,yellow,rose,emerald}-300, and text-red-600/700 (mapped to
red-400, mirroring the cyan/rose 600-to-400 convention).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 10:56:17 +02:00
Oliver Kieselbach 46965af290 feat(backend): finalize + arm the offboarding farewell email
Replace the [DRAFT] placeholder with the final copy (thank-you, gentle
feedback pointer to GitHub/LinkedIn, welcome-back note, last-email notice)
and remove the OffboardFarewellEmailArmed scaffolding gate. Tests move from
the disarmed-by-default contract to the armed contract (gate ordering,
never-throw, template pins). GitHub links in both email templates now point
to the renamed repo okieselbach/AutopilotMonitor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 10:20:27 +02:00
Oliver Kieselbach 1bfdb68ff6 Merge branch 'main' of https://github.com/okieselbach/Autopilot-Monitor 2026-08-02 18:02:08 +02:00
Oliver Kieselbach 5ee5114d5c updated modification datetimes page 2026-08-02 18:02:04 +02:00
Oliver Kieselbach a7bb7ebf15 feat: Pro requires a tenant contact address - gated at plan entry, never as a runtime lockout
Backend: POST config/{id}/trial now denies with 409 ContactEmailRequired
when no contact address is stored (extracted EvaluateTrialStart verdict,
order test-pinned: consumed/already-Pro win over the contact prompt). The
gate is live at the API even while the trial CTA stays behind its
"coming soon" flag; GA plan assignment stays unblocked by design. The
feature-flags payload gains contactEmailSet (bool only, never the
address - contract test updated).

Web: trial CTA shows a "set a contact address first" note instead of the
start button when the flag flips on; dashboard gets an amber non-blocking
banner for Pro tenants without a contact (admins only, fail-safe: only an
explicit contactEmailSet:false nags, so loading/error/older-backend states
stay quiet); GA tenant editor warns when assigning Pro to a tenant
without a contact address.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:58:49 +02:00
Oliver Kieselbach a7a1cc43c1 fix(backend): welcome mail survives the auto-approve race - send-on-save + once-only marker
First production auto-approve (mueller-steinag.ch) activated 64s after signup,
5s before the user saved their notification address on the activation page -
the approval path found no address and silently skipped the welcome mail.

Both racing paths now write-then-read: the notification-email save path checks
approval fresh (bypassing the 30s negative cache) and triggers the send when
activation already happened. A conditional-insert welcome-email-sent marker in
PreviewWhitelist makes the send once-only across both paths; it is consumed
strictly after the address check so it can never be burned without an address.
Revoke clears the marker, the GA resend endpoint sends unconditionally and
consumes it best-effort.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:33:59 +02:00
Oliver Kieselbach 2a5ad65dd7 fix(web): explicit JSX spaces where Turbopack eats them - "onlyto" class of typos
Turbopack drops the ambient space between an inline element and adjacent
prose in certain multi-line JSX text layouts (Babel/SWC preserve it), so
the live portal rendered "onlyto reach you", "IsEnabledflag",
"...ps1script", "5 minutesfor GitHub's", "Unavailableand the cascade".
User-reported on the Contact section; the other five sites were found by
scanning the compiled bundle for space-less text nodes after elements.

Fix: explicit {" "} at each affected boundary - it compiles to its own
text-node and cannot be trimmed. Verified in a fresh build: all six sites
now emit ..."})," ","to reach... Sites whose gap comes from ml-*/mx-*
margins or flex gap were left alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:31:04 +02:00
dependabot[bot] 960b3d8ed9 chore(deps): bump github/codeql-action from 4 to 4.37.3 (#119)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4...v4.37.3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-02 16:52:33 +02:00
Oliver Kieselbach 86bb255b11 feat(web): GA offboard button in tenant management - with type-to-confirm and ban-semantics note
The admin tenant editor gains a danger zone that queues the existing
offboarding cascade (DELETE tenants/{id}/offboard, policy already admits
GA). Guarded by a type-OFFBOARD confirmation dialog so a stray click can
never trigger it, Global Admin included.

Both the suspension block and the danger zone now spell out the
semantics: suspension is the durable lock-out lever (blocks sign-in and
auto-activation, data stays); offboarding is NOT a ban - the cascade
deletes the config row including any suspension, so a completed offboard
lets the tenant re-onboard and auto-activate on the next sign-in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 16:48:53 +02:00
Oliver Kieselbach d3a10dfb96 fix(backend): tenant activation is race-proof - conditional insert dedupes welcome mail and ops event
Double signup on two instances enqueued two auto-approve envelopes; both
workers could pass the approved-check (stale 30s negative cache) and both
ran the activation side effects - duplicate welcome mail plus duplicate
TenantAutoApproved ops event/Telegram ping. Same duplicate on queue
redelivery after a failed message delete.

The whitelist add is now a conditional INSERT (AddEntity, 409 = already
approved) so storage arbitrates concurrent activations: exactly one caller
sees "newly added" and runs mail/promote/ops event; losers drop quietly.
The GA approve endpoint stays idempotent (200 + alreadyApproved instead of
a duplicate mail on re-approve).

Also fixes a silent-failure path: the repo swallowed storage errors and
returned false, which ApproveAsync ignored - a failed whitelist write
still sent the welcome mail and reported success. Storage errors now
throw (queue path retries, HTTP path 500s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 16:32:13 +02:00
Oliver Kieselbach 80d0b30cba fix(backend): activation-gate review findings - OnboardedBy immutable via PUT, suspension gate reads fresh
Two findings from the re-run public-availability review:

- UpdateTenantConfigurationFunction preserved HomedAppClientId, LastAuth*
  and plan fields against round-trip clobber but not OnboardedBy, despite
  AuthFunction documenting it as write-once: any own-tenant admin PUT could
  overwrite or null it, corrupting audit provenance and letting a
  revoke -> re-approve cycle auto-promote an attacker-chosen UPN. Now
  preserved like the other system-owned fields.

- TenantAutoApproveHandler checked the suspension gate through the cached
  TryGetConfigurationAsync (5-min per-instance TTL): a tenant suspended
  right after signup could still be auto-approved from a stale view. Now
  reads via GetConfigurationFreshAsync; a storage error propagates so the
  message retries (flag re-checked per attempt) instead of silently
  downgrading the tenant to manual approval.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 16:13:31 +02:00
Oliver Kieselbach aee723e497 fix(backend): app-homing flip reads config fresh - stale cache could rewind concurrent writes
FlipAsync read the tenant config through the cached GetConfigurationAsync
(5-min per-instance TTL, fail-open to CreateDefault) and saved the whole
entity back: a flip could silently rewind fields another writer changed
within the TTL, and a transient storage read error would have persisted a
default config over the real row. Now reads via GetConfigurationFreshAsync
(cache-bypassing, errors propagate) and aborts loudly when no config row
exists instead of materializing a default one.

Daily-review finding 2026-07-31.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 16:01:25 +02:00
Oliver Kieselbach 427c923530 feat(web): mobile menu for landing navbar - sign in was unreachable on phones
Below md the landing navbar only showed the Get started CTA: Sign in was
hidden sm:block and the nav links (Product/Capabilities/Compare/Docs) had
no mobile counterpart at all. Returning users on a phone had no visible
way to log in.

- burger toggle (<md) opens a solid panel under the bar: nav links,
  GitHub, and a prominent Sign in button; closes on link click, backdrop
  tap, and Escape
- Get started stays always visible in the top bar; GitHub icon and Sign
  in text button move into the menu below md
- panel uses opaque --lp-surface: nested backdrop-blur inside the nav
  does not blur page content, a translucent panel let the hero bleed
  through
- 320px fit: px-4 below sm and the wordmark hides below 360px (mark only)

Verified via Playwright at 320/390/1280px: menu open/close paths, anchor
scroll to #features, desktop layout unchanged; tsc and ESLint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 13:12:34 +02:00
Oliver Kieselbach b46a8f1571 fix(rules): merge collision guard - stale tenant rule copies no longer crash rule notifications
Tenants seeded in the pre-global-partition era can still carry per-tenant
copies of built-in rules (IsBuiltIn=false debris, e.g. ANALYZE-ID-002 on one
prod tenant). They pass the custom-rule filter and put the same RuleId twice
into the merged list, blowing up ToDictionary(r => r.RuleId) in
AnalyzeOnEnrollmentEndHandler.SafeNotifyRuleChannelsAsync and silently
dropping rule channel notifications for that tenant.

Guard the merge in AnalyzeRuleService + GatherRuleService: a tenant row whose
RuleId collides with a merged global rule is skipped (global wins) with a
warning log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 00:40:29 +02:00
Oliver Kieselbach 6e294c010e fix(web): validation-gate toggles persist - disable was local state only
Prod report 2026-08-01 (tenant b54dc1af): corporate-identifier validation
"came back on" after every disable. Audit log shows the truth: not a single
True->False entry ever - the disable NEVER reached the server. The validation
section has no save bar, and only the consent-driven ENABLE paths persisted;
the toggle setters (disable via confirm dialog, and enabling the second gate
while the first already carries the consent) wrote plain React state that
silently reverted on the next config load.

New TenantConfigContext.saveValidationGate routes those setter calls through
the shared config PUT with explicit override values; local gate state re-syncs
from the server response (or snaps back on failure), and the section save
indicator covers the write.

tsc clean, vitest 631/631, ESLint ratchet holds (115).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 00:32:51 +02:00
Oliver Kieselbach dc54c9555c fix(web): correct Entra roles for admin consent - Application Administrator is not sufficient
The validation info box and the detect-access error still claimed Application
Administrator could grant the consent. Per MS Learn (grant-admin-consent),
Application/Cloud Application Administrator are excluded for Microsoft Graph
app roles (application permissions) - our consent includes the app-only
DeviceManagementServiceConfig.Read.All. Correct roles: Global Administrator
or Privileged Role Administrator, matching the funnel banner and docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:21:28 +02:00
Oliver Kieselbach 784fd99240 fix(web): consent verify rides AAD propagation; funnel banner actions + dark mode
- Post-consent verification now POLLS the access-check (up to 5 attempts,
  15s apart) instead of a single shot: right after a successful admin consent
  the app-only token often mints with an EMPTY roles claim for 30-90s
  (observed live: tenant 5ca2b350, GrantedRoleCount=0 at T+43s), which the
  single-shot probe misreported as "permission not granted" while the homing
  flip and consent were actually fine. The access-check invalidates and
  re-mints fresh per call, so the poll converges as soon as Microsoft has
  propagated the role; the consent spinner stays up meanwhile. Final "absent"
  message now names propagation as the likely cause instead of sounding fatal.
- Funnel banner gains always-visible actions ("Grant consent for the new app"
  + "Detect existing access" on the autopilot gate, idempotent when already
  enabled): tenants whose validations are already ON had no consent button to
  click, and the per-toggle detect link under Corporate Identifier would
  enable that validation as a side effect.
- Dark mode: funnel/flipped/legacy banners and the admins-only notice were
  light-palette-only and unreadable on dark.

tsc clean, vitest 631/631, ESLint ratchet holds (115).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:15:50 +02:00
Oliver Kieselbach 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>
2026-07-31 20:20:19 +02:00
Oliver Kieselbach 8d4f7ea0f9 fix(web): app-homing funnel banner - prominent card with permission hint + docs link
The plain info box was easy to overlook. Upgrade to a bordered gradient card
with icon and heading, state that admin consent needs a tenant-wide-consent
role (Global Administrator / Privileged Role Administrator), and link the
portal-setup docs article describing the required permissions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:17:54 +02:00
Oliver Kieselbach 0e2d620ad4 feat(auth): self-service app-homing flip via consent flow + GA lever
Re-consent, verify and flip collapse into the existing consent flow: the
consent URL funnels legacy-homed tenants to the primary app registration,
and consent-status/access-check verify admin consent via an app-only token
probe under explicit primary credentials, then auto-flip HomedAppClientId.

- AdminConfiguration.SelfServiceAppHomingEnabled: kill switch (default off,
  cache-bypassing fail-closed read); gates funnel + auto-flip + tenant-admin
  manual flip. GA path deliberately survives the kill switch.
- POST config/{tenantId}/app-homing (TenantAdminOrGA): tenant admins flip to
  primary only, probe-gated; GA gets force + revert. Pure decision helper
  EvaluateManualFlip shared by function and tests.
- GraphTokenService.GetAccessTokenForAppAsync: probe seam with explicit app
  credentials, cache-safe via app-scoped token keys.
- EntraAppRolesEnabled does not block a flip; it emits a Warning ops event
  (AppHomingFlippedWithEntraRoles) reminding to re-assign enterprise-app roles.
- Web: homing badge + "App Registration Homing" panel with confirm dialog in
  GA tenant management (typed homedAppClientId hardens the PUT round-trip),
  funnel/success banners in tenant settings with switchAuthApp re-login,
  admin-config kill-switch round-trip, ops event types dual-registered.
- Browser telemetry (App Insights): pre-init event buffer so module-init auth
  events survive; strategic events across the whole flow (app_homing_flipped,
  consent_flow_*, consent_verify_failed, auth_app_fallback_*, auth_app_switched,
  auth_app_homing_learned, app_homing_manual_flip*).

Backend must deploy before web (additive JSON fields); everything is inert
while EntraId:LegacyClientId is unset. Enable the kill switch in the admin
portal after the dual app-reg config swap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 19:52:23 +02:00
Oliver Kieselbach 69d61c6c26 feat(backend): dynamic poison-queue enumeration for health + ops monitoring
The static MonitoredPoisonQueues watch-list had already drifted twice
(tenant-auto-approve-poison, session-deletion-maintenance-poison missing).
Health check and maintenance watcher now enumerate every existing *-poison
queue in the storage account at runtime; an absent queue has never had a
poison-move and is healthy by definition, so enumeration covers everything
a list would. Enumeration failure surfaces as warning (never a silent
all-clear) on the health card and skips the maintenance tick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:59:21 +02:00
Oliver Kieselbach cfb28d83b6 added png logo 2026-07-31 18:48:12 +02:00
Oliver Kieselbach 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>
2026-07-31 18:12:25 +02:00
Oliver Kieselbach d9d22bc443 fix(web): drop Beta pills on Submit Logs and Report Session
Feature-maturity badges were a leftover of the preview era — with the product
publicly available, a lone "Beta" pill only signals uncertainty. Both features
are fully functional; the dashboard banner already carries the actively-developed
framing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 14:38:43 +02:00
Oliver Kieselbach 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>
2026-07-31 14:30:57 +02:00
Oliver Kieselbach d6affae8fc fix(web): plan card — bootstrap bullet wording (run already before MDM enrollment)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 13:40:56 +02:00
Oliver Kieselbach 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>
2026-07-31 13:04:47 +02:00
Oliver Kieselbach e62bac9fea fix(web): hooks-refs batch 2.5 — ProtectedRoute wasAuthenticated ref to state; refs category at zero
wasAuthenticated was written AND read during render (the only such
case). Historical per-mount state belongs in useState, adjusted during
render via the guarded official pattern — behavior-identical: the value
is only consulted in !isAuthenticated renders, which can only follow a
render that already set it. Auth-gate control flow (auto re-login,
loading early-return, post-login return URL) untouched.

react-hooks/refs: 26 -> 0. Lint ratchet 115.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 10:29:27 +02:00
Oliver Kieselbach 1e94a7aa91 fix(web): hooks-refs batch 2.4 — useDashboardSessions mirror refs via useLatest
Nine render-phase mirror writes replaced: scope/filter/pagination
mirrors sync post-commit via useLatest; consuming callbacks list the
stable refs in their deps. Drops two write-only refs
(joinGlobalAdminsRef, loadingMoreRef) that nothing read. Imperative
refs (fetchGen/fetchLock/loadAllToken/submittedFilter) untouched.
Lint ratchet 119.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 10:24:32 +02:00
Oliver Kieselbach c330e5c91a fix(web): hooks-refs batch 2.3 — useDashboardStats mirror refs via useLatest
Seven scope mirror refs sync post-commit via useLatest instead of
render-phase writes; fetchStats lists the stable refs it reads in its
deps. Lint ratchet 128.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 10:19:22 +02:00
Oliver Kieselbach 6015548172 fix(web): hooks-refs batch 2.2 — useFleetHealth mirror refs via useLatest
Four scope mirror refs (routeGlobal, selectedTenantId, tenantId, days)
now sync post-commit via useLatest instead of render-phase writes.
fetchData lists the stable refs it reads in its deps. Lint ratchet
135.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 10:16:10 +02:00
Oliver Kieselbach 3f661d097d fix(web): hooks-refs batch 2.1 — useLatest helper; useAuthenticatedFetch + presence off render-phase ref writes
Render-phase ref mirror writes tear under concurrent rendering
(react-hooks/refs). New hooks/useLatest.ts syncs the mirror in a passive
effect instead; refs from custom hooks are not linter-known-stable, so
consumers list them in dep arrays (identity is stable — no re-creation).
Lint ratchet 141 -> 139.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 10:11:59 +02:00
Oliver Kieselbach 3392a23da6 feat(bootstrap): promote dev script v2.2 to stable — download.autopilotmonitor.com endpoint + OOBE-relax (#118)
The stable Install-AutopilotMonitor.ps1 is now the previously field-tested
-dev variant (v2.2): agent download via the Front Door alias
https://download.autopilotmonitor.com/agent/ instead of the legacy blob
host, mandatory SHA-256 manifest verification (Content-MD5 fallback
removed), and the OOBE-relax exemption for the Windows Backup for
Organizations restore case. Test-ShouldBootstrapAgent.ps1 mirrors the
same guard logic; the separate -dev.ps1 files are gone.

CI dev-render anchor literals in build-agent.yml updated to the alias
URLs (stable + dev channel) so the literal-substitution check keeps
passing; local build.ps1 anchors updated too (gitignored).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v2.0.1387
2026-07-31 01:37:17 +02:00
Oliver Kieselbach 5312216f6b fix(web): hooks-warnings batch 1 — static-components, purity, immutability to zero (#117)
First burn-down batch of the React-Compiler hooks warnings (163 -> 141,
CI ratchet lowered to match).

static-components (14, real remount bugs): SortIcon (geographic-
performance), SortIndicator (VulnerabilityReportSection) and SearchIcon/
ResultsDropdown (GlobalSearch) were defined inside render — a new
component type every render, so React unmounted and rebuilt their
subtrees each time (GlobalSearch tore down its dropdown on every
keystroke). All hoisted to module level with explicit props.

purity (6): Date.now() during render replaced with mount-time clocks
(useState lazy init) for the date-input caps (Maintenance sections) and
trial-expiry checks (TenantManagement, SectionTenantConfigReport);
FeedbackSection refreshes its clock per fetch so "Xm ago" labels track
the data; DownloadProgress falls back to NaN instead of wall-clock for
unparseable event timestamps (rate calc already guards via
Number.isFinite).

immutability (5): fetch functions were declared below the effects that
call them — moved above (diagnosis, fleet-health, DistressReports).
This unblocked deeper compiler analysis, surfacing 3 latent
set-state-in-effect findings in those files (93 -> 96); they belong to
the set-state category and stay for its own batch.

Verified: eslint 0 errors / 141 warnings (gate exits 0), tsc clean,
vitest 611/611, static export builds.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 01:21:29 +02:00
Oliver Kieselbach 72392f6f4c chore(web): lint findings to zero errors + CI lint gate (#116)
* feat(web): typed routes — broken internal links are now compile errors

typedRoutes: true (stable in Next 16; `next typegen` already runs in CI
before tsc since #114). A typo'd <Link>/router.push target now fails the
build instead of 404ing at runtime — the failure class behind the recent
RSC/routing incidents.

- lib/routes.ts: registry helpers return `Route`; withQuery validates the
  base path literal. New: deviceBlockUrl (OpsEventsSection previously
  hand-built /admin/security/device-block against the registry rule),
  route() to compile-check dynamic-section literals (nav configs,
  section index redirects), trustedRoute() as the single named seam for
  runtime-data hrefs (backend-emitted notification deep links, persisted
  post-login return URL).
- Nav types (NavItem, ExpandableSubItem, PageSectionItem) carry Route
  hrefs — every sidebar/navbar entry is compile-checked.
- ClientRedirect is generic so dynamic-section literals infer.

Verified: tsc clean; negative test (typo'd static + dynamic routes)
fails compilation as intended; 611/611 vitest; static export builds
with an unchanged route list.

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

* chore(web): lint findings to zero errors + CI lint gate

Package deal — fixes only land together with the gate, otherwise they
drift right back. CI web job now runs `eslint . --max-warnings 163`:
errors fail outright, the warning cap is a ratchet (lower it when fixing
warnings, never raise it).

Fixed to zero (mechanical categories, behavior unchanged):
- no-unused-vars (49): dead imports/locals removed; where a prop is part
  of a live call-site contract, only the destructured binding dropped
- no-explicit-any (29): real structural types for event payloads
  (EnrollmentEvent.data et al -> Record<string, unknown> + per-event
  interfaces), SignalR passthroughs typed via the library's own
  signatures; five consumer files got typing-only follow-through
- no-unescaped-entities (15), prefer-const, jsx-no-comment-textnodes
- no-require-imports (5): scripts/**/*.js override — Node CJS scripts,
  require() is correct there
- one stale eslint-disable directive removed

Deliberately NOT fixed inline: the React-Compiler hooks rules new in
eslint-plugin-react-hooks v6 (set-state-in-effect 93, refs 26,
static-components 14, purity 6, immutability 5,
preserve-manual-memoization 3) are demoted to warn in eslint.config.mjs —
each is a per-site behavioral refactor in auth/SignalR-adjacent code, to
be burned down incrementally under the ratchet.

Verified: eslint 0 errors / 163 warnings (gate command exits 0), tsc
clean, vitest 611/611, static export builds.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 00:55:38 +02:00
Oliver Kieselbach d65157a092 feat(web): typed routes — broken internal links are now compile errors (#115)
typedRoutes: true (stable in Next 16; `next typegen` already runs in CI
before tsc since #114). A typo'd <Link>/router.push target now fails the
build instead of 404ing at runtime — the failure class behind the recent
RSC/routing incidents.

- lib/routes.ts: registry helpers return `Route`; withQuery validates the
  base path literal. New: deviceBlockUrl (OpsEventsSection previously
  hand-built /admin/security/device-block against the registry rule),
  route() to compile-check dynamic-section literals (nav configs,
  section index redirects), trustedRoute() as the single named seam for
  runtime-data hrefs (backend-emitted notification deep links, persisted
  post-login return URL).
- Nav types (NavItem, ExpandableSubItem, PageSectionItem) carry Route
  hrefs — every sidebar/navbar entry is compile-checked.
- ClientRedirect is generic so dynamic-section literals infer.

Verified: tsc clean; negative test (typo'd static + dynamic routes)
fails compilation as intended; 611/611 vitest; static export builds
with an unchanged route list.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 00:55:26 +02:00
Oliver Kieselbach cf45911b69 Web upgrade: Next 16 + React 19 + MSAL v4 (#114)
* chore(web): bump to next 16, react 19, msal-browser 4, msal-react 3

react 19.2 aligns the declared dependency with the React the app router
has been running all along (Next-vendored 19 canary). msal-react 3.0.29
is the React-19-compatible bridge that only needs msal-browser 4 —
deliberately NOT msal v5, whose COOP redirect bridge requires new
bridge pages per origin plus app-registration changes (separate
project). The react-leaflet peer overrides are gone: react 19
satisfies react-leaflet 5 natively.

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

* fix(web): react 19 / msal v4 / next 16 code adaptations

- msalConfig: allowNativeBroker -> allowPlatformBroker (v4 rename);
  storeAuthStateInCookie stays (deprecated in v4, removed only in v5)
- zero-arg useRef() removed in @types/react 19: GlobalSearch (2x),
  dashboard autoLoadTimerRef
- React.MutableRefObject -> React.RefObject in 5 session/progress hooks
  (all refs are mutable in 19 types)
- layout: data-scroll-behavior="smooth" opt-in so route changes keep
  jumping instead of animating (Next 16 stopped suppressing CSS
  smooth-scroll during navigation)
- next.config: analyzer comment - Turbopack default build skips the
  webpack-only analyzer, use --webpack for ANALYZE runs

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

* chore(web): untrack Next-managed next-env.d.ts, generate types in CI

Next 16 rewrites next-env.d.ts on every dev/build run and the routes
import inside flips between .next/types and .next/dev/types (vercel/
next.js#86001) - official guidance is to gitignore it. CI now runs
`next typegen` before tsc so the type stubs exist on a fresh checkout.

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

* chore(web): migrate lint script to eslint CLI (next lint removed in 16)

Minimal flat config from eslint-config-next@16 (core-web-vitals +
typescript) with build-output ignores. Lint stays out of CI as before;
the 264 pre-existing findings are untouched.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:54:12 +02:00
Oliver Kieselbach 1593232898 fix(web): every nav click was a full-page reload — skipTrailingSlashRedirect broke RSC fetches
skipTrailingSlashRedirect (added for the next-dev /api proxy 308) also defines
__NEXT_MANUAL_TRAILING_SLASH, which disables the client router's trailing-slash
normalization. Under output:'export' the router then requested RSC payloads as
/<path>.txt while the build only emits /<path>/index.txt — every fetch 404ed and
Next silently degraded EVERY <Link> navigation to an MPA (full-page) load. Most
visible symptom: the sidebar remounted on each click and its expanded groups
collapsed.

- next.config.ts: dev-gate skipTrailingSlashRedirect (prod build normalizes
  links to /foo/ again; RSC fetches hit the emitted index.txt files)
- GlobalSidebar: normalize usePathname() (client nav now yields /foo/, hard
  loads /foo — exact matches against the slash-less nav hrefs need both), and
  re-run the auto-expand effect when visibleExpandableGroups arrives so a hard
  load (deep link, F5) still expands the active group once MSAL resolves

Verified: static export + local serve, marker survives Link nav (no document
request, /terms/index.txt 200); tsc clean; vitest 611/611.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 23:03:52 +02:00
Oliver Kieselbach 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>
2026-07-30 22:21:18 +02:00
Oliver Kieselbach 9c50d98792 fix(web): sign-in landed back on www — HostRoutingGuard bounced before MSAL settled
Two defects of the same shape in the portal→www direction:
- Deterministic: a signed-in user opening portal root was always bounced to
  www (guard effect fires at hydration, before MSAL restores the session, so
  AuthGate never got to route "/" to /dashboard).
- Race: MSAL's module-level init strips the #code hash before hydration, so
  the auth-response exception saw nothing and the bounce destroyed the
  in-flight token redemption — the user ended up unauthenticated on www.

Portal→www now waits for settled, unauthenticated auth state; www→portal
stays auth-free. Decision extracted as pure decideHostBounce with the safety
rules pinned in hostRouting.guard.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 12:18:12 +02:00
Oliver Kieselbach 20b43e5349 feat(agent): gather debug log — phase transitions + per-match regex traces
- New "phase" stage: OnPhaseChanged writes a transition marker line
  (phase change: Old -> New) so rule-level lines read against the current
  phase; only on real changes, written outside _scopeLock.
- LogParser traces each regex match: line number, matched text (<=200 chars)
  and capture groups (<=100 chars, same selection as event data), capped at
  10 lines per file per run; the per-file summary keeps the full count.
- Invalid-regex error line now includes the offending pattern string.

Tests: 4 new (transition dedup, match trace, cap, pattern in error);
agent suite 2137/2137 green. OKF doc + docs/log.md updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v2.0.1386
2026-07-29 20:07:52 +02:00
Oliver Kieselbach 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>
2026-07-29 17:22:07 +02:00