mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 21:19:38 +00:00
66d25f57dbb0556db7a4a0bb34480eb0dffeb773
785 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
84a0424789 |
annotations: tenant-scoped overview page listing judged sessions
New GET /api/session-annotations (MemberRead + QueryParam, tenant always the middleware-validated target) backing the portal /annotations page in the Monitoring sidebar group (deliberately no new Insights group). For callers without global scope the repository excludes the globaladmin lane server-side (Lane ne clause), so hidden rows never consume page budget and an explicit lane filter self-contradicts to empty. Rows deep-link to the session annotation section. Verdict pill styling moved to the shared logic module. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b2723168fd |
reports: snapshot session annotations into the report ZIP
SubmitReportAsync adds annotations.json (all lanes) to the package and annotationCount to report-metadata.json. All lanes are safe: report blobs are readable only through the GA-only session-reports routes. Fail-soft like the tenant-context snapshot; the copy outlives the session=s retention/delete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2c48968ec6 |
session annotations: role-separated human verdicts on sessions (flywheel labels)
New SessionAnnotations table (PK=tenantId, RK={sessionId}_{lane}, lanes
operator/tenantadmin/globaladmin) storing structured verdicts + notes with
server-stamped authorship and a fired-rule-id snapshot for join-free
rule-quality evaluation. Per-lane write matrix re-gated in-function with
own-tenant binding; the globaladmin lane is platform-internal and filtered
from tenant callers. Portal: SessionAnnotationsCard on session detail.
MCP: list_session_annotations (ga) + annotate_session (strictGa, GA lane
only) + annotations key in get_session_summary. Lifecycle: cascade steps
17-19, offboarding wipe, critical-backup member. Privacy page gains the
customer-entered-content line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
2f8ad82b0a |
gather-rules: mirror hard blocks in portal/MCP, stamp author on PUT-upsert; agent: bootstrap retry tests
Codex follow-ups to
|
||
|
|
1ebc2675b1 |
gather-rules: WQL property projections, portal enable-gating, author from JWT
- WMI guard accepts SELECT <property-list> FROM <class> for any class already
allowed via SELECT * (strict data subset); same algorithm in agent, portal
validator, and MCP validate_rule (incl. trim fix); guardrails.json unchanged
- Portal refuses to enable custom rules with not-allowed targets: create lands
disabled, invalid edit force-disables, "Blocked on devices" badge, GitHub-issue
hint for allowlist requests; agent stays the enforcement boundary
- Rule Author is stamped from the creator's token (display name > UPN) at create
and preserved through every update, incl. cross-tenant GA edits
- bumpVersion keeps the segment count ("1.0.0" -> "1.1.0")
Customer feedback: SELECT BatteryStatus FROM Win32_Battery + on_change was
impossible because SELECT * re-emits on every charge-percent fluctuation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
54044c5d9c |
esp: continue-anyway observation mode — bounded 60-min defang for device-phase failures
Classic UD fleets held at the Device-ESP timeout wall by one slow blocking app showed ~100% Failed (tenant c9787ba2: 948/1000 esp_terminal_failure) although the profile allows "Continue anyway" and users demonstrably reach the desktop — the classic advisory defang requires AccountSetupEnteredUtc and never applied there. New operator-set tenant opt-in EnableEspContinueAnywayObservation (ConfigVersion 36 -> 37, default off): - DecisionCore: a Device-phase EspTerminalFailure with ContinueAnyway allowed + opt-in observed (EspConfigDetected payload fact — kernel stays config-free) + Classic/Unknown non-pre-prov scenario defangs into the advisory rail with a 60-min AdvisoryCompletion window. Real end via the DAD-validated real-user desktop + Hello gate (IME user-session gate is structurally unavailable without the AccountSetup anchor): eager completion on DesktopArrived, category recovery via the 4910a5a5 hook, expiry un-defangs to the original esp_terminal_failure — every observed session still gets a terminal verdict. - Honest outcome: enrollment_complete under an unresolved advisory carries espSoftFailure/completionSource (also on the classic post-AccountSetup advisory path); backend stamps EspSoftFailure/CompletionSource on the Sessions row (+ index mirror, terminal-tuple preservation); web renders an amber "with issues" badge plus a session-detail banner. No new terminal status — Succeeded + marker. - Agent: remote-config field merged into AgentConfiguration; stamped into both esp_config_detected emitters (orchestrator bootstrap via live Func<bool> seam, DeviceInfoCollector via host/factory pass-through). - Admin UI: GA-only toggle in the tenant management modal (next to Bootstrap Token) + stored/runtime rows in the tenant config report; GA-gated on both write paths (PUT silently reverts for tenant admins, field patch rejects with 400). Tests: 13 new ContinueAnywayObservationTests; DecisionCore 513, Agent V2 2162, Functions 3948, SummaryDialog 44, vitest 632, tsc clean. Docs: docs/agent/continue-anyway-observation.md (OKF) + index/log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3b3e8a7614 |
analyze: delay auto-analyze visibility 30s so rules stop racing the terminal flush
The enqueue fires in the very ingest batch that carries enrollment_failed /
enrollment_complete — by design the FIRST event of the agent's terminal
sequence. The worker's hot loop dequeued sub-second and evaluated rules
against a stream still missing the terminal-flush rows (stuck-promotion's
synthetic app_install_failed, app_tracking_summary, agent_shutting_down),
and the RuleResult dedup made the gap permanent. Session 53d1e9f6:
ANALYZE-ESP-004's optional app_install_failed condition never matched and
{{appName}} rendered as a literal placeholder (confidence 80, factor never
fired). A 30 s visibilityTimeout covers the flush including the agent's
late-event grace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8ec5a5debd |
offboarding: close the un-offboard race — config freeze + fail-closed wipe guard
While a tenant offboarding is queued or draining, the TenantConfiguration
tombstone (Disabled=true, 'Offboarding in progress') is the cascade's only
auth gate. A GA lifting Disabled mid-cascade re-opened the tenant against a
wipe that was still coming: data written afterwards was destroyed, and
sessions registered after enumeration survived as orphans while their event
partitions were wiped.
Three layers close it:
- PUT config/{tenantId} returns 409 while the row is the offboarding
tombstone — for all callers, GA included
- TenantConfigPatchService refuses patch AND revert against the tombstone
(a revert restores a pre-offboarding snapshot with Disabled=false — the
exact race)
- TenantOffboardingHandler re-reads the config before the destructive
post-drain phases and fail-closes with 'unoffboard_race' when the
tombstone is gone but the wipe has not run (last line of defense)
Shared predicate: TenantOffboardFunction.IsOffboardingTombstone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
312e849573 |
email: welcome + farewell templates to green branding
Header gradient, CTA button, and links move from the old blue (#2563eb/#4f46e5) to the landing-page green palette (#1e8a4c/#33b161). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d86ae18a5e |
distress: carry the W365 Cloud PC marker into Devices Not Registered
First W365 field run: the rejected Cloud PC appeared in the report as an anonymous "Microsoft Corporation / Virtual Machine" row — nothing told the admin that the fix is the Cloud PC validator, not an Autopilot-registration hunt. The agent's CloudPcDetector verdict (Windows365 key AND CloudManagedDesktopExtension service) now rides the pre-auth distress channel: - Agent: DistressReporter captures isCloudPc at construction (optional ctor arg, test fakes stay source-compatible); BackendClientFactory feeds it from CloudPcDetector; DistressReport.IsCloudPc on the wire (~18 bytes, fits the 1536-byte payload cap without a bump — comment updated) - Backend: DistressReportEntry.IsCloudPc, table column + read mapping (legacy rows -> false), structured log + AgentDistressReport AI event key; GetDeviceNotRegisteredFunction aggregates sticky-true per serial (same OR semantics as the Sessions IsCloudPc merge) and the data-quality notice names the new self-reported field. GA global/distress-reports serializes the full entry and picks it up automatically. - Web: Cloud PC badge in the Devices Not Registered table (same sky badge as the session table) + conditional hint pointing at the Windows 365 Cloud PC Validation toggle and the W365CloudPcValidation Graph add-on. - Tests: sticky-true + no-marker aggregation cases; MakeEntry helper gains the optional flag. Docs: third transport rail in backend/cloudpc-device-validation.md + log entry. Unverified like every distress field — triage context only, never an auth input. Verified: web tsc clean, eslint 0 errors (file's single warning pre-exists). .NET build/tests run in CI (no SDK in this sandbox). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqEFrcAHhWgCvgJgHGzMuv |
||
|
|
1c5f69a9fe |
w365: CloudPc device validator — cert-CN-bound fallback for Cloud PCs (#127)
* w365: CloudPc device validator — cert-CN-bound fallback for Cloud PCs Windows 365 Cloud PCs are never Autopilot-registered, so the serial lookup (and the Corporate Identifier fallback) structurally reject them and Cloud PC first-connect enrollment could not be monitored. New stage-3 fallback in the device-validation chain: extract the Intune device id from the chain-validated MDM client certificate's Subject CN (field-verified: CN == managedDevice id == cloudPC.managedDeviceId) and require a matching cloudPC object in the claimed tenant (Graph v1.0 virtualEndpoint/cloudPCs, $filter=managedDeviceId eq). Only service-provisioned Cloud PCs have such an object — a regular enrolled non-Autopilot device still ends in the 403, and local W365 markers are never consulted for auth. - Shared: ValidatorType.CloudPc = 5, TenantConfiguration.ValidateCloudPcDevice (default false, tenant-admin writable), GraphAppPermissions.CloudPCReadAll, GraphFeatureCatalog feature W365CloudPcValidation -> CloudPC.Read.All - Backend: CloudPcDeviceValidator (AutopilotDeviceValidator sibling contract: 30/5 min cache, 2 attempts, transient->503; GUID gate before the OData filter; exact-match guard; Graph 403 = definitive "grant missing", not transient), SecurityValidator CN extraction + fallback stage + hard gate, wiring through the 5 agent endpoints, TableConfigRepository persistence - Consent script: W365CloudPcValidation in catalog/ValidateSet/help; NEW GraphFeatureCatalogSyncTests pins C# catalog <-> grant script (previously comment-only lock-step) - Bootstrap dev script v2.3-dev.3: guard 4 (uptime window) exempted while the Cloud-PC relax is active — Cloud PCs run headless for days before first connect; the fresh-profile window is the time anchor there (+ Pester tests) - Web: validation toggle + context plumbing, ValidatedBy "CloudPc" label, privacy-page validation claim generalized (Last updated bumped) - Docs: OKF concept doc backend/cloudpc-device-validation.md + index + log Verified: web tsc clean, ESLint 0 errors (99 pre-existing warnings = ratchet cap, none added). .NET build/tests and Pester run in CI — SDK download and pwsh are unavailable in this sandbox's network policy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqEFrcAHhWgCvgJgHGzMuv * w365: agent-side IsCloudPc flag + engine expectation "no Device-ESP phase" Companion to the CloudPc validator: the agent now classifies Windows 365 Cloud PCs locally with the bootstrap's field-verified marker AND (HKLM\SOFTWARE\Microsoft\Windows365 key + installed CloudManagedDesktopExtension service, probed via its Services registry key so net48 needs no System.ServiceProcess). SKIP-safe: any error resolves false. Never an auth input - backend admission stays exclusively the cert-CN-bound Graph lookup (ValidatedBy=CloudPc); the two signals are independent and may disagree. Two transport rails: - Session metadata: SessionRegistration.IsCloudPc -> Sessions table (sticky-true OR across re-registrations, IsSelfDeployingProfile pattern) -> SessionsIndex full mirror -> SessionSummary -> new isCloudPc search filter end to end (SearchSessions + QueryRawSessions + pagination fingerprint + all three in-memory predicates; MCP search_sessions / query_raw_sessions params + summary projection; web type, Cloud PC pill, SessionInfoCard enrollment label, CSV export appended column). - Decision engine: isCloudPc rides the EnrollmentFactsObserved payload, recorded set-once for BOTH values in EnrollmentScenarioObservations.CloudPc and annotates the scenario profile reason cloud_pc_first_connect:no_device_esp_expected - the engine-recorded expectation that Device-ESP already ran headless at provisioning and the session starts at Account Setup. Deliberately reason+observation only: no completion arm depends on DeviceSetup, Mode stays AccountSetup/IME- classified (Classic), EspConfig keeps configured-FirstSync semantics. Positive marker surfaces as cloud_pc_marker in the audit-trail census. Updater idempotency: the cloudPcAnnotates gate keeps the generic v1 reason branch from re-claiming the slot on repeat posts (would bump EvidenceOrdinal every post). Additive-nullable observation - no state schema bump (v4 convention), serializer roundtrip pinned. Tests: CloudPcDetector resolver (marker AND, throwing probe), registration body, facts-signal payload shape, updater annotation/idempotency/ self-deploying interplay, reducer observation both values + census fold, serializer roundtrip, index projection superset, sticky-true both directions. Suites: DecisionCore 500, Functions 3941, Agent V2 2154, vitest 632 - all green; tsc web+MCP clean; ESLint 0 errors (cap 99). Docs: cloudpc-device-validation.md follow-up section replaced with the shipped mechanism + log entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ca22be5cd0 |
notifications: dedupe duplicate Pre-Provisioning Completed webhook + traffic-light icons
The V2 agent emits whiteglove_complete twice (raw Shell-Core event 62407 + engine timeline event, ~1s apart). The Pending status write was not idempotent (terminal guard allowed Pending->Pending as a "transition") and the ingest fallback additionally forced the transitioned flag, so tenants received the Teams card twice (HSD report). Pending->Pending is now refused in IsTerminalTransitionAllowed, and the ingest fallback distinguishes an already-sealed session (duplicate, no notification) from a genuine write failure. Notification titles now use neutral traffic-light circles (green/yellow/red) instead of rocket/checkmark/cross/siren emojis (customer feedback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4def73f193 |
rules: reserve the built-in rule-ID namespace against tenant custom rules
Tenant rule IDs were free-form with only a point-in-time existence check, so a customer could occupy a gap or retired ID like ANALYZE-SEC-002; a later built-in with that ID would then silently shadow the tenant's rule at merge time (global wins, tenant copy skipped) and their RuleStates would start toggling our rule. Enforcement: RuleIdPolicy (Shared) reserves (ANALYZE|GATHER)-<CAT>-<NUM> case-insensitively, with the CUSTOM category as the sanctioned tenant namespace (we never ship built-ins in a category named CUSTOM). Both rule services reject reserved IDs on create AND on the custom-branch of update (updates upsert, so PUT was a bypass); update endpoints now map InvalidOperationException to 409 like create. Web forms mirror the check inline; MCP validate_rule emits a warning finding and the authoring guide now documents the reservation (guide templates moved to GATHER-CUSTOM-101/ANALYZE-CUSTOM-101 — the old numeric examples would be rejected). No existing prod custom rule violates the pattern (verified against all 13 tenant-partition rows), so nothing needs grandfathering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
75197a4184 |
Logparser pattern testing with the agent's exact .NET engine (test_log_pattern) (#125)
* updated commands * feat(backend): logparser pattern test endpoint with agent-exact semantics POST /api/rules/gather/test-pattern evaluates a logparser gather-rule regex against pasted sample lines exactly the way the agent would on the device. Motivated by a field incident: a customer verified a pattern with a PHP regex tester and the .NET engine matched differently. - CmTraceLogParser moves Agent -> AutopilotMonitor.Shared.Logging so agent (IME tracker, logparser collector, stall probe, IME matching mode) and backend run the SAME parsing implementation. Pure mechanical move, agent behavior unchanged (V2.Core suite 2147/2147 green). - TestLogPatternFunction mirrors LogParserCollector verbatim: Regex(Compiled, 1s timeout, NO IgnoreCase -> case-sensitive), first match per line, group "0" excluded / unsuccessful groups omitted, format cmtrace (default, match against parsed message) vs text (raw line), per-line outcomes incl. parse_failed/regex_timeout, and the agent's "set parameter format=text" hint verbatim. Pure compute, no tenant data. Caps: 200 lines, 8KB/line. - Policy: TenantAdminOrGlobalReader (same tier as the analyze dry-run). - Tests pin the parity showcases: case-sensitivity, envelope invisibility in cmtrace mode, optional-group omission, format hint, request validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(mcp): test_log_pattern tool + logparser lint and guide hardening - test_log_pattern: wraps POST /api/rules/gather/test-pattern — tests a logparser regex against pasted sample lines with the agent-exact .NET semantics (case-sensitive, cmtrace/text mode). Description steers clients away from JS/PHP/Python regex testing. - validate_rule: logparser parameter lint (missing/non-compiling pattern, unknown format value = silently cmtrace on the agent) + info pointing to test_log_pattern; nextStep for valid logparser drafts routes there too. - rule_authoring_guide: logparser section now states the matching semantics (case-SENSITIVE, .NET, first match per line, cmtrace default) and two new pitfalls (engine mismatch, plain-text log under cmtrace default). - Catalog snapshot + handler/validator tests extended (MCP suite 532 green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(okf): test-pattern endpoint + shared CmTraceLogParser in rule authoring surface Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3c1229a301 |
MCP rule authoring: deterministic context, validate_rule, and session dry-run (#124)
* feat(backend): dry-run endpoint for draft analyze rules (rule authoring)
POST /api/rules/analyze/dryrun evaluates a DRAFT analyze rule against one
session's events and returns a full diagnostic trace: per-condition
matched/evidence (incl. per-eventType occurrence counts), per-factor
confidence breakdown, verdict, and the would-be MatchedConditions map for
{{token}} interpolation preview. Persists nothing: no RuleResult row, no
session-status change, no notifications.
- RuleEngine.DryRun.cs: DryRunRuleAsync / DryRunEvaluate share the exact
evaluators (EvaluatePrecondition/EvaluateCondition/EvaluateConfidenceFactor)
with the production path but evaluate ALL conditions instead of breaking on
the first failed required one; parity with EvaluateRule is pinned by tests.
- DryRunAnalyzeRuleFunction: TenantAdminOrGlobalReader (read-in-effect POST),
GA/GlobalReader cross-tenant via session tenant resolution, 1 MB body cap,
structural draft validation (unknown source/operator, correlation without
joinField, invalid regex, non-evaluable confidenceFactor DSL) with
actionable error messages.
- EndpointAccessPolicyCatalog: register the new route.
- RuleEngineDryRunTests: trace semantics, production parity matrix,
side-effect freedom (VerifyNoOtherCalls), draft validation matrix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(mcp): rule-authoring resources — guide, schemas, guardrails
Deterministic authoring context for customer AIs, replacing chunked
search_docs retrieval for rule creation: get_resource (and the MCP-protocol
resources) now serve
- rule_authoring_guide: hand-written, complete authoring knowledge (collector
types, triggers, condition sources, operators, confidence model incl. the
exact factor-condition DSL shapes, {{token}} interpolation order, guardrail
matching semantics, pitfalls, validate->dry-run workflow)
- rule_schemas: the JSON Schemas (2020-12) for gather + analyze rules
- rule_guardrails: the on-device collection allowlists
Schemas + guardrails are generated into rule-authoring.generated.ts by a new
combine.js emitter (same pattern as the web guardrails mirror; rules/ stays
the single source of truth and CI guardrails-in-sync guards the file).
rule-authoring-drift.test.ts additionally pins generated == repo source,
guide enums == schema enums, and the get_resource size cap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(mcp): validate_rule + test_analyze_rule tools and author-rule prompt
Closes the authoring loop for customer AIs: draft -> validate -> dry-run
against a real session -> iterate -> create in the portal.
- validate_rule (MCP-local, no backend call): ajv 2020-12 validation against
the baked schemas, guardrail pre-flight mirroring the agent matching
semantics (hive-stripped segment-bounded registry/file prefixes,
whitespace-bounded WMI, exact commands, /-bounded event log channels, hard
blocks), and semantic lint for schema-valid-but-dead rules (unreachable
threshold, non-evaluable factor DSL, unknown eventTypes, unanchored
not_regex allowlists, unresolvable {{tokens}}, trigger cross-fields,
outputEventType conventions/collisions).
- test_analyze_rule: calls the new POST /api/rules/analyze/dryrun and adds
the {{token}}-interpolated explanation/remediation preview via the
existing interpolate-rule-template.
- author-rule MCP prompt guiding scenario -> resources -> draft -> validate
-> dry-run.
- Both tools are visible to every role (backend enforces
TenantAdminOrGlobalReader on the dryrun route); catalog snapshot updated.
- deps: ajv + ajv-formats promoted to direct dependencies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(okf): rule authoring surface concept doc
docs/rules/rule-authoring-surface.md: the AI-assisted authoring stack
(deterministic MCP resources via combine.js baking, MCP-local validate_rule
layers, the dry-run trace twin with its parity + side-effect invariants, and
what is deliberately absent); registered in index.md, logged 2026-08-05.
Customer docs (rules/ai-assisted-rule-authoring.md + SUMMARY + FAQ entry) are
committed in the autopilotmonitor-docs repository separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
f795fbcb4e |
feat(auth): dual app-reg parallel operation - legacy gktatooine app alongside new C4A8 app
True parallel operation, no forced re-consent: existing tenants keep signing in via the legacy app (1a400946), new tenants land on the C4A8 app (886ab5e2). Model "default legacy, funnel primary, learn in the background": - Web: am_auth_app flag -> NEXT_PUBLIC_ENTRA_DEFAULT_APP -> primary; get-started CTA hands over with ?authapp=primary; auth/me homedApp updates the flag so an operator tenant flip takes effect on each user's NEXT login; AADSTS90094/65001 one-shot cross-app retry, AADSTS65004 -> failed screen with previous-app link; am_login_attempt pins redirect returns to the app that started them. - Backend: TenantConfiguration.HomedAppClientId (null = legacy, GA-only flip via PUT config) + LastAuthClientId/Since observability; EntraAppRegistry resolves the acting app per tenant (no-op until the legacy config pair is set); Graph client-credentials, admin-consent URLs and the surfaced client id follow the homed app (app-scoped cache keys); JwtBearer [Authorize] pipeline now shares the middleware audience builders (was single-audience - would 401 legacy tokens after the swap); middleware trusts EntraId:LegacyClientId implicitly. - well-known association file now lists both app ids (publisher domain); deploy-web passes NEXT_PUBLIC_ENTRA_LEGACY_CLIENT_ID / _DEFAULT_APP; mcp-server.bicep default client id -> C4A8 app. Backend xUnit 3730/3730, vitest 625/625 (13 new), tsc/ESLint clean, next build ok. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6bd5728652 |
feat(platform): public availability — tenant activation replaces Private Preview, optional auto-approve
Backend: shared TenantApprovalService (whitelist add + auto-promote shape-guard + welcome email), AdminConfiguration.AutoApproveNewTenants (default off, uncached fail-closed accessor), tenant-auto-approve queue (1-min visibility delay, worker drops on flag-off/approved/suspended/missing config, approves as "System (auto-approve)" + TenantAutoApproved ops event), 30s negative whitelist cache, gate 2 error PendingActivation with mode-neutral message, signup strings and welcome email without preview wording. Web: /preview → /activation (SWA 301, robots, AuthGate), AuthContext accepts both error codes and clears pending state on successful auth/me, activation page polls every 30s and auto-redirects into the portal, dashboard banner reframed to "Actively developed" + feedback call, terms/privacy/get-started updated, feature badges → Beta, admin-config web chain + auto-approve checkbox, OPS_EVENT_TYPES. Deliberately unchanged: PreviewWhitelist/PreviewConfig tables, internal preview/* API routes, preview_signup notification type, Microsoft DevPrep preview mentions. Verified: solution build 0 errors, xUnit 3701/3701, tsc 0 errors, vitest 612/612, ESLint ratchet holds (0/115), next build exports /activation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a70c5de667 |
feat(plan): rename Enterprise plan to Pro; Pro includes Bootstrap by default and Unrestricted Mode on request
- FeatureEntitlementCatalog: TenantEdition.Pro, canonical tier "pro", legacy stored "enterprise" stays readable as Pro (no data migration); new entitlements BootstrapIncluded + UnrestrictedModeAvailable - TenantEntitlementService: IsBootstrapEnabled (Pro-included OR additive GA flag) and IsUnrestrictedModeActive (Pro AND GA gate AND tenant toggle, read-time - trial expiry/downgrade re-arms guardrails fail-closed) - All bootstrap endpoint gates + SecurityValidator use the effective check; /auth/me and feature-flags expose effective availability values - PlanManagement accepts community|pro; AlreadyPro; Pro trial wording - Web: edition.ts accepts legacy "enterprise" (deploy-order safety), Pro labels/cards + new Pro feature bullets, effective section gating, terms/privacy renamed with Last-updated bump - Tests: catalog matrix, effective-gate payload tests, helper tests incl. trial-expiry re-gate (backend 3692 green, web 612 green) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e4c06a9564 |
Landing page v2 — scroll story, portal-first hero, Autopilot Monitor rebrand (#113)
* feat(web): landing page v2 — scroll story, green brand, Autopilot Monitor wordmark Complete rebuild of the landing page as a scroll story of one enrollment: - Hero shows the product live (animated event-stream timeline, real event vocabulary) - Story acts: 09:02 power-on -> 09:41 ANALYZE-ESP-001 hit (most-firing rule, ~22% hit rate) -> 09:43 MCP terminal demo (full session report via get_session_summary + get_time_attribution) -> fleet zoom-out - Floating pill navbar, capabilities strip, restyled comparison, shared SiteFooter (now also mounted on /about, /terms, /privacy which had no footer at all) - Green mark (#33B161) unified as the only brand mark via BrandMark.tsx; blue-indigo gradient marketing mark removed - Wordmark is now "Autopilot Monitor" (two words) across all user-visible text, metadata and JSON-LD; technical identifiers (script names, ProgramData paths) untouched - Theming via lp-* CSS variables on :root/.dark — new components never collide with the global .dark !important overrides; old .landing-page block kept for /preview - StatsBand replaces PlatformStats: adds success rate (successfulEnrollments payload field, previously unused) and a loading skeleton instead of fake defaults - New opengraph/twitter/apple images in the new design (green mark on dark slate) - All animations hand-rolled (rAF/CSS/IntersectionObserver), CSP-safe, zero new dependencies, prefers-reduced-motion respected; auth path (AuthGate/LoginButton/ hostRouting) and staticwebapp.config.json unchanged Checks: tsc clean, vitest 611/611, static export build clean, no horizontal overflow at 375px. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): landing feedback round — linear story timeline, honest examples, dev stats - Story is now one vertical timeline with a strict top-down reading order (time rail left, text left, visual right) instead of the zigzag grid; act 1 gets a "session appeared" card so no column is left empty - Hero live session decoupled from the story device (CONTOSO-0912/Surface Laptop 7 completes; the story's CONTOSO-4711 is the one that gets stuck) - Defender for Endpoint removed from the demo stream (it is onboarded, not installed during ESP) — replaced with Microsoft 365 Apps - StatsBand: sample numbers in local dev only; in production the band shows real figures or hides entirely on fetch failure (never fake numbers, never an endless skeleton) - Capabilities copy: "Pull a diagnostics package" instead of the incorrect "Pull ETL and event logs" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): hero v3 — mission-control cockpit instead of single-session card The first screen now shows the product as a live fleet cockpit: KPI strip (active-now count ticks), five enrollments running concurrently with advancing progress bars, a new session popping in, two completing during the ~23s loop, and the analysis engine visibly flagging the stuck device inline (HIGH ANALYZE-ESP-001 with the culprit app). Event stream column on desktop. Headline block is left-aligned and more compact to break the centered-template look. Deterministic single-tick choreography, IntersectionObserver start, prefers-reduced-motion gets a rich static frame. Replaces LiveTimeline.tsx. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): hero v4 — real portal screenshots instead of fabricated cockpit Hero visual is now the actual product: the live portal dashboard (captured via Playwright from portal.autopilotmonitor.com, demo fleet staged in the DOM — no customer data, preview banner removed, wordmark as it ships) with a real analysis finding card (ANALYZE-APP-002 incl. remediation + evidence JSON, from the docs assets) overlapping bottom-right. Bonus asset time-attribution.png captured for later use. Removes HeroCockpit.tsx. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): hero v5 — clean dashboard, real-analysis terminal, refined header - Analysis-finding overlay removed: the dashboard shot stands alone (was colliding with the stats band and not catching) - Terminal demo moved into the hero as its own "Then just ask" section and rescripted from a real production analysis (anonymized): failed attempt with 0x8007EA61 decoded to unmapped installer exit code 60001 (PSADT), transient proof via clean retry, actionable recommendation - Headline block refined: uppercase eyebrow instead of pill badge, tighter type and buttons - Wordmark "Monitor" back to ink color (green variant parked for compare) - Story act 3 now shows the real time-attribution screenshot instead of duplicating the terminal Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): story act 3 — notification + one-click diagnostics instead of cropped screenshot "See where the minutes went" (half-cut time-attribution raster) replaced with the admin's real next move: the Teams/webhook alert that fired with the rule, and the on-demand diagnostics flow (requested -> collected mid-enrollment -> ZIP ready). Crafted JSX cards consistent with the other acts; unused time-attribution.png removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): hero v6 — portal-first layout from approved design draft - Full-width sticky navbar (brand, Product/Capabilities/Compare/Docs, Sign in + Get started) replaces the floating pill - Hero per draft: gray eyebrow, "See every Autopilot enrollment. Live.", sub left / CTA group right with trust line, portal shot nearly full width (max-w-7xl), green background glow removed - Stats as a full-bleed band with dividers under the shot; "issues detected" replaces success rate (underlines automated analysis), events compact-formatted (8.3M) - "Then just ask" terminal extracted into its own JustAsk section after the band Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): get-started flow page, honest Teams card, calm buttons, light-only landing - New public /get-started page: five-step schematic (sign in -> request access -> consent -> deploy bootstrapper -> watch live); "Get started" in navbar and hero now lead here, "Sign in" keeps the login handoff — resolving the two-identical-CTAs problem. Wired into hostRouting PUBLIC_PATH_PREFIXES, sitemap, robots allow-list. - StoryAction Teams card corrected against the backend renderer: the real adaptive card has exactly one action ("Open session") — the invented "Collect diagnostics" button is gone (NotificationAlertBuilder.cs:60). - Buttons no longer jump (translate-y removed); hover is brightness + shadow only. Final CTA rebuilt as a calm two-column band per draft (no dark box, no glow). - Landing/marketing surface is light-only for now: lp-* tokens re-declared under .dark .landing-v2, theme toggle removed from LandingNavbar (screenshots are light; portal keeps full dark support). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): about/terms/privacy on the landing shell — no more sidebar flash The three public pages now render exactly like /get-started: LandingNavbar + landing-v2 tokens + eyebrow/headline header + SiteFooter. GlobalSidebar no longer treats them as sidebar pages for anonymous visitors — that was the "weird loading": app sidebar + PublicSiteNavbar popping in after hydration on marketing pages. PublicSiteNavbar and PublicPageHeader are dead and deleted; terms/privacy links switched from indigo to the accent green. Authenticated users keep the portal chrome on these pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): landing navbar anchors root-anchored so they work from subpages #story/#features/#comparison -> /#story/... — clicking Product/Capabilities/ Compare on /get-started (or any public subpage) now navigates back to the landing section instead of dead-ending on a missing local anchor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): calmer CTA green, about de-blued, public-page typography aligned - All real CTAs (navbar, hero, final CTA, get-started, story mock button) switch from bright #33b161 to the deep accent-ink green — matches the dark-green family in the portal screenshot, less loud - About page fully on the new design language: rainbow colorMap collapsed to the single green accent, audience cards/step bubbles/links/quick-link hovers de-blued - About/terms/privacy typography scaled to landing sizes: 15px body, smaller page h1s and section headings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): terms callouts on lp tokens — no more stray blue/amber banners Info callout ("Who can use it") now uses the green accent-soft style; the AS-IS warning uses the palette warn tones. Verified terms/about carry no remaining raw blue/amber/indigo utility colors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): about tech stack — drop version numbers, fix stale SSR claim Versions age silently on marketing pages (.NET 10 / Next.js 15 / React 18 removed, technologies stay). Also corrects "server-rendered React app" — the portal has been a static export since the RSC-prefetch incident fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): terminal demo app renamed to Contoso CRM App Neutral fictional app instead of a vendor-adjacent print driver name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): analysis card confidence score, 40+ rules claim, final CTA to /get-started - StoryAnalysis card gets the portal-style confidence bar (80%) - Act 2 copy: "40+ built-in rules" (exactly 40 in rules/analyze today) - "Start monitoring now" leads to /get-started like the other Get-started CTAs; sign-in stays in the navbar and on /get-started itself Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): portal brand aligned to landing, subtle Enterprise anchoring - Portal navbar (both variants) now uses BrandMark + the landing wordmark style (w-6 mark, 15px bold tracking-tight, strong ink in light/dark) — completes the brand consolidation follow-up - Enterprise plan anchored softly, free-first tone preserved: final-CTA subline + /get-started closing note ("same service, nothing to migrate") + "Plans" link in the footer Product column, all pointing to the docs plans page Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): final CTA right-aligned, get-started enterprise note "and more" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(web): social images refreshed to current hero claim and tagline "See every Autopilot enrollment. Live." + the site tagline (real-time monitoring, automated analysis, AI-ready telemetry) on the green-mark dark card; opengraph + twitter regenerated at 1200x630. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(web): social card wordmark single-color, matching the site Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): portal sidebar selection states green — brand-aligned like the docs Active nav items, expandable group actives, page-section title/divider switch from blue to the green brand accent (light + dark variants). Purple Global Admin mode stays purple (semantic distinction); broader portal blues (status pills, buttons, KPI tiles) deliberately untouched — that is a separate re-theme pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): preview feedback round — dark-mode leak, alignment, renamed repo links - About/terms/privacy no longer render half dark for dark-mode visitors: the light-only marketing scope now neutralizes the global .dark utility overrides (bg-white / text-gray-* / border-gray-* / gray hovers) inside .landing-v2 - All landing sections aligned to one container width (max-w-7xl): story, just-ask, capabilities, comparison, how-it-works now share the same left edge as hero, stats band and final CTA - GitHub links updated to the renamed repository okieselbach/AutopilotMonitor (old hyphenated URLs redirected, now correct at the source) - /get-started wording: "Preview" instead of "Private Preview" (legal pages and portal status texts intentionally unchanged) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): preview page on the new design + ?demo=1 review mode; footer width aligned - /preview rebuilt on lp tokens: BrandMark, single wordmark, calm card, accent notify box, neutral next-steps, ghost actions — "Preview" wording instead of "Private Preview" - ?demo=1 skips the auth redirects and renders placeholder identity so the page can be reviewed unauthenticated (locally and on SWA previews) - SiteFooter container widened to max-w-7xl to match navbar and sections Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): preview page — prominent approval-pending badge, honest status copy - Amber "Approval pending" status pill above the headline - Next-steps wording per review ("sign out and wait a bit or contact me…", capacity clause dropped) - Corrected the false "approval status on your dashboard" claim: a blocked tenant never reaches a dashboard — signing in again lands back on this page while pending, and goes straight into the portal once approved (verified against AuthContext PrivatePreview handling) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): about/terms/privacy cards flattened to the landing pinstripe style All raised cards (shadow / shadow-sm) become flat lp-surface panels with the thin lp-line-soft border; inner plan boxes and quick-link tiles switch from gray-200 borders to the same pinstripe line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): portal primary actions green — buttons, avatar, Progress Portal icon - Primary buttons (bg-blue-600/indigo-600 + hover pairs) switch to green-600/700: admin backups, customs archive, vulnerability data, session reports, and the settings SaveResetBar - Navbar user avatar circles green instead of blue (all four variants) - Progress Portal sidebar MonitorIcon gets a deliberate green accent (light + dark) instead of inheriting gray - Semantic blues (status pills, info banners, KPI tiles) untouched Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): portal links and focus rings on brand green - All focus states unified: focus:ring/border blue-500 and indigo-500 (207 occurrences) -> green-500 across inputs, selects, buttons - Link patterns text-blue-600/indigo-600 with hover 800, hover:underline variants (36) -> text-green-700 hover:text-green-800; icon hovers hover:text-blue-600 (12) -> green - New dark override .dark .text-green-700 -> green-400, mirroring the existing blue-700 convention - Semantic blues untouched: info banners, status pills, KPI tiles Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): local dev sign-in repaired; progress portal brand accents green Local sign-in appeared broken (always landed on /progress as a role-less user) — two stacked regressions in the dev API proxy: 1. trailingSlash:true (static-export change) made next dev 308-redirect /api/* to /api/*/ BEFORE the proxy rewrite; the Functions backend rejects trailing-slash routes, so /me silently failed. -> skipTrailingSlashRedirect: true (no effect on the static export; SWA handles trailing slashes itself). 2. .env.local DEV_API_PROXY_TARGET still pointed at the pre-EU-cutover autopilotmonitor-api (dead since 07-18) -> now -api-eu; proxy verified healthy (v1.5.1027). Progress portal: header icon, icon circle, "Change device" link and "Check Status" button on brand green; the blue activity indicators (downloading/installing) stay blue — that is the portal-wide in-progress semantic, not branding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): loading screens and remaining primary buttons on brand green - All four loading spinners (AuthGate, ProtectedRoute, CalculatingCard, UnmappedSoftwareTab) green instead of blue - Blue-indigo gradient loading/error backgrounds (AuthGate overlay, ProtectedRoute states, error page, CalculatingCard, SectionPlatformUsage) replaced with the neutral lp background (theme-aware in the portal) - 19 non-contiguous primary buttons (bg-blue-600 text-white ... hover:bg-blue-700) the first sweep missed -> green, incl. ProtectedRoute retry/back buttons, error page, audit, diagnosis, session table Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): last blue interactive states green; monitoring headers wrap on mobile - Time-range toggles (fleet health, software, usage, dashboard, platform usage), software lens tabs, MCP-usage segmented controls, SLA report button, gather/analyze rule save buttons, and the page-header icons on fleet health / geographic performance -> brand green. Zero remaining bg-blue-600/indigo-600 interactive patterns. - Monitoring page headers (fleet health, software, SLA, usage metrics, geographic performance, audit): title row gets flex-wrap + gap-y-3 and the control group (tenant scope + range buttons) wraps as a compact second line on narrow screens instead of pushing off the right edge; range buttons slightly compacted (px-3 py-1.5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): geo map dev crash — per-mount container key for Leaflet "Map container is already initialized": dev-mode remounts (StrictMode re-runs, Fast Refresh over the dynamic() wrapper) hand Leaflet the same container div twice and it refuses. A per-mount key on MapContainer forces a virgin div for every (re)mount; MapCleanup keeps handling instance teardown. No production behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): form control accents (radios/checkboxes) green portal-wide Checked-state colors for radios and checkboxes (indigo/blue/amber/teal/ sky variants, 19 controls incl. the diagnostics destination radios and subfolder checkboxes) unified on green, plus the diagnostics "Add" button. Deliberately kept: red danger-confirmation checkboxes and the purple global-admin control. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): pin native form-control accent-color to brand green Root cause of the "red" radios/checkboxes: there is no @tailwindcss/forms plugin, so text-* utilities never styled native controls — their check color silently followed the user's OS accent color. A global accent-color rule (green-600, green-500 in dark) pins all radios/checkboxes to the brand; the danger-red and global-admin-purple controls override via accent-* utilities. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): progress portal icon inherits item color again Permanent green stood out against the gray siblings — the sidebar icon now follows the item state like all others: gray at rest, green when active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): geo map root fix (react-leaflet 5) + readable dark muted text - Root cause of "Map container is already initialized": Next 15 app router runs its bundled React 19 regardless of the installed react@18, and react-leaflet 4.x is incompatible with React 19 StrictMode double-invoked effects. Upgraded react-leaflet 4.2.1 -> 5.0.0 (@react-leaflet/core 3) and removed our now-counterproductive workarounds (MapCleanup manual remove, per-mount container key) — v5 owns the map lifecycle including unmount teardown. Verified via a throwaway repro page: map renders, remount toggle survives, console clean (repro page deleted). - Dark mode: .dark .text-gray-400/500 lightened (#64748b -> #7b8ba3 / #8896ad) — notification timestamps and other muted text were nearly invisible on raised dark surfaces (unread bg, hover rows). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): subtler dark hover surfaces — hovered rows stay readable .dark hover:bg-gray-50/100 dropped from #334155 to #283548/#2e3c52 (one step above the card surface): the bright hover wash made notification timestamps and other muted text illegible on highlighted rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): GA/tenant notification rows dark-aware — opacity variants escaped the overrides The global-admin and tenant notification rows use bg-purple-50/30 and bg-blue-50/30 (+ /50 hovers) — opacity-variant class tokens the global .dark .bg-*-50 overrides never match, leaving light rows with light text on dark mode. Explicit dark: variants (purple/blue-900 at low opacity) fix the readability. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): notification row hover clearly distinguishable in dark mode GA (purple) and tenant (blue) rows: dark hover raised from /30 to /45 opacity so the highlighted row visibly differs from its resting tint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): one SegmentedControl for all range/group toggles New shared components/SegmentedControl.tsx (the geo-style connected pills on a gray track) replaces the three mixed patterns: loose button trios on fleet health and software, the months <select> on SLA, and geo's two inline toggles (time range + group-by) now all render the same control with shared 7/30/90-day labels. Dark mode: active pill uses arbitrary-value colors because the global .dark .bg-white override would blend it into the track. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): MCP usage sections use the shared SegmentedControl too The last remaining variant (joined bordered buttons with raw 7d/30d/90d labels in admin metrics + reporting MCP usage) replaced by the shared control with the unified "7/30/90 Days" labels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: clickable notification deep links in the bell dropdown GA notifications now carry hrefs (session/diag-files report -> report modal, preview signup -> tenant management filtered), GA rows render the View link, and every bell row with an href navigates on row click and closes the dropdown. Target sections accept ?reportId= / ?tenantId= via the proven Suspense+useSearchParams pattern from DeviceBlockSection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(web): npm overrides pin react-leaflet peer deps to installed react react-leaflet 5 peer-depends on react 19; the app router runs Next's bundled React 19 at runtime while the installed package stays react 18. Without the overrides a plain npm ci (SWA/Oryx build) fails on the peer conflict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d0d1d28371 |
perf(backend): column projection for unmatched-software scan
GetUnmatchedSoftwareFunction runs a full cross-tenant scan of SoftwareInventory on every request (including every pagination click) but only reads 9 properties. GetAllSoftwareInventoryAsync now accepts an optional $select projection and the function passes the consumed columns, cutting transfer and entity materialization. Server-side filtering on CpeUri is documented as impossible: rows re-created by the cascade-delete 404 path carry no CpeUri property and Table Storage cannot express "property missing" — those rows must still count as unmatched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
89c5c7da03 |
feat: richer, self-describing session report content
The report ZIP now captures everything an operator needs even after the session is gone, and states its own completeness: - session.csv: append the 15 newer Sessions fields that had drifted out of the export (failureSource, reconcileReason, adminMarkedAction, validatedBy, rebootCount, isHybridJoin, isSelfDeployingProfile, connectionType, geo*, avgApiLatencyMs, apiRequestCount, stalledAt, failureSnapshotJson) - completeness signal: exportedEventCount / sessionEventCount / eventStreamActive in the request and report-metadata.json; the modal warns while event pages are still streaming - includedDiagnostics records sourceBlobName/sourceDestination so a failed copy still points at the original archive - tenantContext snapshot (fail-soft): upload mode/destination, gather- rule debug flag, custom gather/analyze rule IDs + UpdatedAt, IME pattern IDs — no secrets, no definitions - robustness: 8 MB client-side screenshot cap (20 MB body cap headroom), attachments status in metadata instead of silently dropping invalid base64, SanitizeZipEntryName strips path components from customer- supplied file names (both report types) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
48308df4ff |
feat: opt-in preservation of session diagnostics archive with session reports
Session reports persist while sessions and their diagnostics blobs are
removed by user deletion or retention. A new checkbox in the Report
Session modal (shown only when a diagnostics ZIP was uploaded, default
checked) lets the submitter include the archive: the backend streams a
server-side copy into the durable session-reports container.
- SessionReportDiagnosticsArchiveCopier: fail-soft copy (never blocks the
report), reuses the Hosted/CustomerSas read paths, re-verifies the
hosted tenant prefix via ClassifyBlobName, honours the admin size cap
and download timeout; outcome persisted as DiagnosticsCopyStatus
- Report row/API gain DiagnosticsBlobName + DiagnosticsCopyStatus
(written only when requested, omitted for legacy rows)
- report-metadata.json records includedDiagnostics {requested, blobName, status}
- Admin reports UI: second Download Diagnostics button + failure status line
- Privacy page: explicit exception for the opt-in report copy (Last updated bump)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f744ad8406 | fix code review findings | ||
|
|
ffe88a47c6 |
fix: /sla dead-ended on www — it is a portal page, not a public one
/sla sat in PUBLIC_PATH_PREFIXES only because SLA notifications historically linked www…/sla. It is a fully authenticated page (ProtectedRoute + token fetches), and since ProtectedRoute stands down on the public host, www/sla waited forever on "Loading SLA metrics...". Removed from the public prefixes: the nav stays on portal (no bounce), old www…/sla notification links bounce to portal/sla where auth works, and SlaBreachEvaluationService.DashboardUrl now links the portal host directly for future notifications. /sla added to robots disallow. Web tsc + 603/603, backend build clean + SLA suite 38/38. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
18b28260a7 |
feat: portal static export — eliminate the SWA SSR runtime (cold-start root fix)
output:'export' + trailingSlash: the SWA now serves plain static files; the
managed SSR runtime — the portal's last cold-start source ("Backend call
failure" when a cold start exceeded SWA's 45s limit) — no longer exists.
- Detail routes are query-string based (/sessions?id=, /sessions/inspector?id=,
/diagnosis?id=, /apps/detail?name=, /admin/backups/detail?id=,
/admin/customs-archive/detail?tenantId=&rowKey=). Single-definition builders
in lib/routes.ts; all 21 link sites migrated, duplicate sessionHref helpers
consolidated. Suspense boundaries added everywhere useSearchParams drives
page identity (hard export-build errors otherwise).
- 9 [section] families prerender via server shells (generateStaticParams from
the *NavSections registries, dynamicParams=false, client bodies extracted to
SectionClient) — 37 pages; server redirect() index pages became
ClientRedirect shells. robots/sitemap/og-image routes force-static.
- Legacy PATH-shaped deep links are preserved FOREVER (sent Teams/Slack/
webhook notifications cannot be edited): SWA wildcard rewrites serve the
family page and the globally mounted LegacyPathRedirect rewrites to the
canonical query URL, preserving query + #event hashes (mapping pinned by
lib/__tests__/legacyPathRedirect.test.ts).
- middleware.ts deleted → client-side HostRoutingGuard (prefix list moved to
lib/hostRouting.ts). Legacy /go/[code] route + bootstrapValidation deleted —
BootstrapSessions table verified free of active pre-cutover sessions.
- staticwebapp.config.json now carries production routing: 19 docs/marketing
redirects (order-sensitive), legacy rewrites (exact before wildcard), 404
override, all security headers incl. literalized prod CSP (no unsafe-eval),
Content-Type routes for the extension-less og images. Guarded by the new
utils/__tests__/swaConfig.guard.test.ts. Maintenance mode is now a
first-routes-entry insert, never a whole-file swap (README rewritten).
- Backend notification deep links go through Constants.PortalSessionUrl
(query shape, escaped, pinned); 3 producers migrated.
- deploy-web.yml uploads out/ with is_static_export.
Export build: 79 static pages, no go/ output. Web: tsc clean, vitest 53
files green (618 incl. new guards; 2 deleted legacy suites). Backend:
solution build clean, 3622/3622 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6cbd507b4f |
fix: client-IP extraction was dead on Flex — RemoteIpAddress fallback + AFD-aware rate-limit key
Post-deploy verification of the go endpoint showed every anonymous caller keyed as "unknown": Flex Consumption does not surface X-Forwarded-For to the isolated worker, so ALL per-IP rate-limit buckets (bootstrap-validate, distress, diag-ticket-download, stats/platform) had silently collapsed into one shared bucket — confirmed by DistressReports.SourceIp being "unknown" across the board. - GetTrustedClientIp falls back to HttpContext.Connection.RemoteIpAddress (transport-derived, unspoofable; IPv4-mapped addresses normalized). Heals every existing caller without signature changes. - New GetRateLimitClientIp for AFD-fronted endpoints: honors X-Azure-ClientIP only when X-Azure-FDID carries exactly our profile ID (FrontDoorProfileId app setting, set on autopilotmonitor-api-eu); fail-closed to GetTrustedClientIp on unset/mismatch/multi-value. Used by GetBootstrapScriptFunction (traffic arrives via go.autopilotmonitor.com, where the trusted hop is AFD egress). - FdidMatches table tests incl. append/spoof cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
26bcab9054 |
feat: move OOBE bootstrap script endpoint into the backend (go.autopilotmonitor.com)
The portal's Next.js /go/[code] route was the web app's only request-time
server code and blocked the planned static export. The script generation
now lives in the Function App as GET /api/bootstrap/go/{code}
(GetBootstrapScriptFunction), reached by customers as
irm https://go.autopilotmonitor.com/{code} | iex via a Front Door custom
domain whose /* route rewrites onto the API path.
- New Constants.BootstrapGoBaseUrl; CreateBootstrapSessionFunction now
hands out the go URL (previously unguarded — CreateBootstrapSessionUrlShapeTests
pins it now). go host added to both URL-guard enforced lists.
- BootstrapScriptValueValidator ports the bootstrapValidation.ts injection
invariants (exact GUIDs, https + host allowlist + /agent/*.zip path,
expiry <= 14d) as defense-in-depth for in-process values.
- OobeBootstrapScriptGenerator ports the PS template as a Replace-token
raw string; golden test pins byte parity with the TS template
(TestData/oobe-bootstrap-golden.ps1, captured from route.ts).
- Always-HTTP-200 text/plain contract incl. rate limit (own bucket
bootstrap-script:{ip} 20/min); disabled tenant indistinguishable from
unknown code. Catalog: PublicAnonymous; NoStoreCacheMiddleware covers
/api/bootstrap/go/ (script inlines the bearer token).
- Web: BOOTSTRAP_GO_URL registry entry, sessions list shows the go host,
legacy route marked LEGACY (alive for pre-migration URLs <= 168h,
removal in the static-export PR).
- Docs: new OKF backend/oobe-bootstrap-go-flow.md, url-registry go
section, index/log entries.
Backend tests 3611/3611, web vitest 651/651, tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4ebd60fb1e |
feat: EnableGatherRuleDebugLog — per-tenant gather-rule debug trace (ConfigVersion 36)
Customers cannot tell why a gather rule produces nothing in the timeline (e.g. a logparser rule with emit_mode on_change that never emits). New per-tenant boolean EnableGatherRuleDebugLog (EnableImeMatchLog-style plumbing end to end): when enabled, the agent writes %ProgramData%\AutopilotMonitor\Logs\gather_rules_debug.log with one pipe-delimited line per evaluation decision across all previously silent "produced nothing" exits — disabled rules, interval without intervalSeconds, first-tick-after-one-full-interval, phase-scope skips incl. currentPhase=Unknown, per-(rule, phase) dedup, empty collector results (emitOnlyIfExists), on_change hash suppression (hash prefix + streak), guard blocks, logparser per-file outcomes (position skips, format=text hint when every line fails CMTrace parsing, maxLines and wildcard caps), and full exception stacks the agent log drops. - Backend: ConfigVersion 35 -> 36; TenantConfiguration/AgentConfigResponse field + TableConfigRepository Store+Map - Agent: GatherRuleDebugLog writer (10 MB cap, one .old generation, failures swallowed, optional console echo), GatherRuleContext.DebugLog (null-safe no-op), trace points in executor + collectors; never writes under _scopeLock (reasons captured inside, written after release); merger bool->path projection with --gather-debug-log CLI override tolerance; deliberately NOT in the offline-cache strip lists; --run-gather-rules always traces; zero-rules delivery leaves a one-line file instead of nothing - Side fix: GatherRuleExecutorHost now passes the logger into InformationalEventPost so gather-event emissions hit the DEBUG log - Web: tenant settings toggle (Settings -> Agent) + admin tenant-config report rows - Tests: merger on/off/override, writer format+rotation, executor debug lines, logparser debug lines (agent 2131, backend 3561, tsc green) - Docs: docs/agent/gather-rule-debug-log.md + index/log entries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dff1532be5 |
feat: project ConnectionType (WiFi/Ethernet) onto SessionsIndex
The agent already reports connectionType in every network_interface_info event, but it was only stored as a JSON blob in DeviceSnapshot — answering "how many machines enroll over Ethernet vs WiFi?" required a slow full-table scan via deviceProperties. Project it as a typed column instead (template: the AvgApiLatencyMs chain), making it a cheap indexed one-call query. - Ingest: TryExtractConnectionType — last network_interface_info emission of the batch wins (a device switching media mid-enrollment is relabeled); only exact "WiFi"/"Ethernet" accepted, fire-and-forget fail-soft writer - Storage: UpdateSessionConnectionTypeAsync (Merge on Sessions + index mirror, tombstone-safe), BuildSessionIndexEntity superset entry, MapToSessionSummary, re-registration preserve in StoreSessionAsync - Search: connectionType= filter with OData eq push-down on the index, client-side backstop on the non-OData paths, fields=connectionType projection, pagination fingerprint entry, raw endpoint parity - MCP: connectionType z.enum param on search_sessions/query_raw_sessions, fields doc updated (server redeploy required) - Web: optional sortable/filterable "Connection" column in the session list - No backfill: legacy sessions lack the column and are excluded by the filter (intended, same semantics as RebootCount); the deviceProperties scan path still covers historical data Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3ec716a3ec |
feat: expose API latency to MCP field projection and tool docs
- SearchSessionsFunction fields whitelist += avgApiLatencyMs, apiRequestCount so search_sessions lean sweeps (e.g. fields=geoCountry,avgApiLatencyMs,apiRequestCount) can aggregate latency without full SessionSummary payloads - search_sessions tool doc lists the new fields incl. the weighting hint; get_geographic_metrics doc names avgApiLatencyMs / apiLatencyVsGlobalPct as the backend-region distance metric Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ffd70e0383 |
feat: geographic API-latency view (per-location weighted avg by geo)
Surfaces the persisted per-session AvgApiLatencyMs in the geographic views so operators can compare regions when deciding on new backend locations (Asia/US): - ComputeGeographicMetrics: per-location and global request-weighted latency (sum(avg*requests)/sum(requests)) + ApiLatencyVsGlobalPct; serves both the tenant and the GA cross-tenant endpoint - GeoMetricsSessionProjection extended (column-projected scan would otherwise drop the new fields); equivalence test pins it - Drilldown: LocationSessionRow carries the per-session values; sessions table + summary tile show them - geographic-performance page: sortable "API Latency" column with absolute color buckets (<250 green .. >=800 red), global benchmark - PlatformMetricsService: session AvgLatency now exact from cumulative counters when present (fallback: mean of interval averages) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c18154d76d |
feat: persist per-session agent-to-backend API latency (AvgApiLatencyMs)
The V2 agent already measures HTTP round-trip latency per request (NetworkMetrics, cumulative _totalLatencyMs) but only emitted the per-interval delta average. Operators deciding on new backend regions (Asia/US) need the session-wide average, aggregatable by GeoCountry. Measured baseline: Jakarta sessions average ~600-1050 ms client-side vs 94 ms server-side processing — the gap is pure network distance to GWC. - Agent: agent_metrics_snapshot now carries cumulative net_total_latency_ms (total/requests = whole-session average; idempotent by construction) - Ingest: last matching snapshot of a batch projects AvgApiLatencyMs + ApiRequestCount onto the Session row (fire-and-forget, fail-soft, last-write-wins; agents predating the field simply never match) - SessionsIndex: both fields mirrored (merge + BuildSessionIndexEntity superset) so list/GA views can aggregate latency per geo - Web: session detail shows "API Latency" when present Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d55b0ff864 |
test(backend): pin the cross-tenant redaction on metrics/ime-versions
GET metrics/ime-versions is MemberRead and the archive is one global
partition, so every tenant member reads the same platform-wide rows. The only
thing between an ordinary tenant user and another tenant's identifiers was an
inline projection with no test behind it.
There is no HTTP-level harness in this suite - nothing constructs an
HttpRequestData with a RequestContext - so the projection was unreachable from
a test while it lived inside Run. Extracted it to a static
BuildResponsePayload(versions, hasGlobalScope), matching the pattern
GetTpmPssUnsupportedFunction already uses for the same reason. Behaviour is
unchanged: the payload is returned as `object`, and System.Text.Json resolves
an `object` declared type via the runtime type, so both branches keep the wire
format they had inline. A test asserts that rather than trusting it.
10 tests, asserting on serialized JSON in camelCase (Program.cs's naming
policy) because a property dropped from an anonymous type is only a real
redaction once it is absent from the wire:
- non-global: firstSeenTenantId / firstSeenSessionId absent, and their GUID
VALUES absent anywhere in the payload (a model rename must not let the
identifier survive under another key)
- non-global: exactly {version, firstSeenAt, lastSeenAt, sessionCount}
- non-global: sessionCount survives - the counts are platform-wide by
decision, pinned so it cannot drift either way unnoticed
- global (Admin or Reader): full entry including both identifiers
- both scopes: array shape, empty archive, row order preserved
Mutation-checked both directions: forcing the global branch fails 3 tests,
dropping sessionCount from the projection fails 2. Full suite 3521 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0efeb0f734 |
fix(backend): alert-webhook 500 on non-seekable body - bounded read replaces Length check
With ASP.NET Core integration the HttpRequestData body is a forward-only stream whose Length property throws NotSupportedException (verified in prod: 500 + System.NotSupportedException on the first POST). The size cap is now enforced by reading at most cap+1 chars via ReadBlockAsync instead of asking the stream for its length. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7b975d46b6 |
feat(backend): Azure Monitor alert webhook - platform alerts join the ops-event bus
New anonymous POST ops/alert-webhook receives Common Alert Schema notifications from an Action Group webhook receiver and re-emits them as ops events (category Platform, type AzureMonitorAlert), so Azure Monitor alerts flow through the existing OpsEventService -> OpsAlertDispatchService fan-out (Telegram/Teams/Slack + portal ops-events list) like every internally raised operational event. The delivery path is independent of the portal: a SWA outage cannot suppress the notification; the action group's e-mail receiver stays as the backend-independent fallback. Security: action-group webhooks cannot send custom headers, so the shared secret travels as ?secret= and is compared in constant time (FixedTimeEquals) against the OpsAlertWebhookSecret app setting - fail-closed while unset. 64 KB payload cap, defensive essentials-only parsing (only alertRule is mandatory), severity mapped Sev0->Critical, Sev1->Error, Sev2->Warning, Sev3/4->Info with unknown values falling back to Info so schema drift can never escalate. Dual-registered per convention: EndpointAccessPolicyCatalog entry (PublicAnonymous), OPS_EVENT_TYPES in OpsAlertRulesSection (rule wiring) and Platform category in OpsEventsSection (badge + filter). Backend suite 3511 green (incl. new helper tests + catalog completeness), web tsc + vitest 651 green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ba7d14b070 |
fix(insights): post-deploy MCP findings - projection drop, aggregate starvation, duration artifacts
Three defects found by measuring production after the 1.5.1012 deploy: - AppMetricsProjection dropped TerminalState, so the PR0 skip classification never reached metrics/app (totalSkipped stuck at 0). Column added; equivalence fixture now populates TerminalState/AppId/ EspBlocking/AppIdCollision and asserts them (proven: test fails without the projection fix). - BuildTimeAttributionAggregates excluded breakdowns on ANY quality flag; 903/1000 carry BlockingSetUnknown, starving fleet stats (Clean=12 vs 331 excluded on the largest tenant). New TimeAttributionFlagQuality.DurationCriticalFlags mask - only ClockSkew/PartialObservation/WGAnchors exclude; blocking-set flags keep their measured spans (positive-evidence semantics unchanged). - HasMeasuredDuration gets an upper plausibility bound (6h = default AgentMaxLifetimeMinutes): 490 rows carried back-stamped completions (up to 117 days; 60175s app rows in a 2697s session) that dominated slowestApps with six-figure averages. Read-side, heals old + new rows. Backend 3484/3484 green; solution build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |