Two low-risk fixes ahead of the first live Device Preparation enrollment:
- StoreSessionAsync: EnrollmentType is now sticky-v2 across re-registration
(same contract as the sticky-true booleans) so a degraded AutopilotSettings
read after a reboot can no longer flip an observed v2 session back to v1.
- EnrollmentScenarioProfileUpdater: ImeUserSessionCompleted,
SelfDeployingDeadlineConfirmed and WhiteGloveSealingConfirmed no longer
overwrite an established DevicePreparation profile (seeded at Medium from
the registry-deterministic v2 fact).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Telemetry follow-ups from the 7d tool-quality review:
- tool_call log lines now carry a capped errorMessage (thrown message or
leading toolError text) so error drilldowns name the failure class
instead of guessing from args; absent on success.
- get_rule_stats omits per-rule daily trend rows by default (they drove
50% of calls over the response cap at platform scope); new
includeTrends arg opts back in, trendsNote marks the omission.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The presence page header was a non-wrapping `flex justify-between`, so on a
phone viewport the title column was compressed to a few characters wide and
"Active Users" plus its description broke one or two words per line next to
the Window/Refresh controls.
Stack the title and controls vertically below the `sm` breakpoint and keep
the existing side-by-side row from `sm` up. The controls also wrap among
themselves on very narrow screens.
The user table had the same problem: its card used `overflow-hidden`, which
clipped the Tenant and Last seen columns off-screen with no way to reach
them. Wrap the table in the repo's usual `overflow-x-auto` container so it
scrolls inside its card, keep cells on one line, and trim cell padding to
`px-4` on mobile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwjVj2PvpVRk7QUjQf3i8C
A single corrupt session average (request spanning sleep/hibernate counts
wall-clock hours) inflated the request-weighted location mean by orders of
magnitude (Amersfoort: 73286 ms from one 1.38M-ms session). Add
MedianApiLatencyMs per location + global (upper median over per-session
averages, duration convention), base ApiLatencyVsGlobalPct on medians, and
switch web display/sort to the median with the weighted avg in the tooltip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test-only fetch stub matched the Entra authority via substring
(includes), which CodeQL flags as incomplete URL sanitization. Parse the
URL and compare the hostname exactly instead — also strictly more precise
for what the stub intends (intercept only the Entra host).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- useProgressSearch calls the server-side lookup; the client-side filter
over the tenant list is gone with the list itself.
- useProgressEvents passes the serial on every events read and refreshes
the session summary via the lookup (guarded by sessionId so a newer
re-enrollment of the same serial cannot swap the selected session).
- SignalRContext.joinGroup takes {serialNumber, onDenied}; the serial is
remembered per group and re-presented on the automatic post-reconnect
rejoin — without that, a roleless viewer's live updates would die on
the first network blip. A refused join is now visible (console.warn +
progress-page notification) instead of a silently frozen page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Security finding: GET progress/sessions shipped the tenant's latest 100
sessions to any authenticated (roleless) user with the serial filter
applied only in the browser; events and SignalR session-group joins were
open to any known sessionId.
Model now: knowing the device's serial number is the authorization proof.
- progress/sessions/lookup (AuthenticatedUserWithRole + QueryParam)
resolves at most ONE session server-side: roleless callers need the
exact serial/device name, members/GA keep substring search. The
tenant-wide list route is gone (catalog fail-closes it).
- progress events require ?serial= re-proof on every read; missing
session and serial mismatch answer the same 404 (no existence oracle).
- Session-group joins by roleless same-tenant callers require the serial
(SerialKnowledgeProof, shared with the events route); member/GA/
delegated joins are unchanged. MemberRead on progress routes stays
deliberately rejected (regression history c4dabeee).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- /oauth/token: AbortSignal.timeout(15s) on the outbound Entra fetch;
TimeoutError maps to 504 token_exchange_timeout (502 stays for hard
failures). Undici defaults previously allowed ~300s per stalled request.
- Boot guards (fail-fast, matching config.ts posture): AUTOPILOT_ENTRA_AUTHORITY
must be https (sovereign clouds allowed, host not pinned); MCP_PUBLIC_URL pin
must be https (http tolerated for loopback dev only), trailing slashes
stripped; production log hint when AUTOPILOT_ENTRA_CLIENT_SECRET is unset
(public-client vs AADSTS7000218 misconfig).
- State one-time-use: documented as accepted residual risk (a nonce store would
reintroduce the shared state the stateless multi-replica design removed;
impact bounded by single-use PKCE-bound Entra codes + callback re-validation).
- Tests: oauth-boot-guards.test.ts (validators) + token-timeout route tests
with Entra-host-scoped fetch stub. Suite: 559 passed, tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full verify pass over all 54 built-in rules (validate_rule sweep, 30d/7d
get_rule_stats, test_analyze_rule spot checks). Fixes:
- ANALYZE-APP-007 v1.1.0: declare enrollment_failed as a non-required
condition — the exists-factor referenced an undeclared signal and could
never apply.
- ANALYZE-APP-014 v1.1.0: add non-required event_data conditions for
espCategory/espErrorCode/topCandidatePackage/candidateCount/confidence
so all explanation {{tokens}} resolve instead of rendering literally.
- ANALYZE-ENRL-002 v1.2.0: capture session_timeout.timeoutHours so
{{timeoutHours}} interpolates (verified via dry-run: renders "5").
- RuleEngine TryFlatLookup/ResolveDotPath: flatten IEnumerable data
values to a comma-joined string — List fields (e.g. app_tracking_summary
.pendingNames) stringified to "System.Collections.Generic.List`1[…]",
breaking contains-matching and {{pendingNames}} interpolation.
- GetRuleStatsAsync: raise the 500-row cap to 10000 and log when hit.
The scan walks dates ascending, so the old cap silently dropped the
NEWEST dates of a 30d window — newly added rules (e.g. ANALYZE-SEC-008)
were entirely missing from global stats.
- MCP validate_rule lint: accept event_data_array itemField as a {{token}}
resolver — {{identity}} in ANALYZE-SEC-005/006 was a false positive.
Backend 4157 tests green, MCP 548 green + tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The evaluateOn feature was API/MCP-only — portal users could not see or set
evaluation triggers. The rule editor gains an Evaluation Triggers section
(enrollment end / WhiteGlove seal checkboxes + on_event type list) with
client-side grammar + hard-block validation sourced from the baked guardrails
mirror (combine.js now emits BLOCKED_INTERIM_TRIGGER_EVENT_TYPES for the web);
rule cards show an 'Interim: …' badge for interim-enabled rules (incl. the
built-in ANALYZE-ID-004). Create/edit payloads round-trip evaluateOn; the
template-copy path already deep-clones it server-side.
Suites: web 762 + tsc + lint clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A warning does not stop an end user — telemetry-cadence types (performance_snapshot,
agent_metrics_snapshot, download_progress, network_state_change/connectivity_check,
log_entry, agent_trace, stall_probe_check) would turn every ingest batch into an
analyze run. Now enforced in three layers:
- AnalyzeRuleTriggers.BlockedOnEventTypes (code-only enforcement): CRUD write is
rejected with an explicit message, and OnEventTypes() drops blocked types at
runtime so even a persisted row (pre-validation data, direct table write) is
inert for the trigger registry and the engine alike.
- rules/guardrails.json gains blockedInterimTriggerEventTypes as the display/
pre-flight mirror (baked into the MCP RULE_GUARDRAILS via combine.js); a
parity test pins mirror and code set against drift — same pattern as the
gather hard blocks.
- MCP validate_rule: the high-frequency finding is now an ERROR sourced from the
baked mirror, not a hardcoded warning.
Suites: Backend 4155, MCP 548.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OKF concept doc (status SPEC): opt-in interim analyze runs via evaluateOn
(enrollment_end default, whiteglove_sealed, on_event:<type>), RuleResults
update-semantics replacing the permanent dedupe freeze, KO/stat suppression
on interim runs, notification marker decoupled from row existence, and the
validate_rule lints that keep interim rules monotonic. Closes the design for
both the sits-d WhiteGlove notification gap and the esa never-terminal
stuck-session gap with one mechanism.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The /settings layout now admits any own-tenant role or platform scope via
hasOwnTenantOrPlatformRole — the same set the sidebar's Configuration
visibility uses, closing the "sidebar shows it, guard bounces it" mismatch
the nav matrix test had ratcheted. Read-only stays enforced by the data
layer: MemberRead serves non-admins redacted config, canEditConfig is
false, and every write path (PUT + field PATCH) is TenantAdminOrGA.
This also realigns the code with the customer docs, which already
documented Operators/Viewers as read-only settings viewers.
The guard-consistency ratchet is now empty and may only stay empty.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P5.x-A: Constants.SignalRMessages catalog (13 names) + SignalRGroupHelper.HubName;
all backend send sites and web subscriptions typed against the shared-manifest
union (SignalRContext.on/off) — unknown names are tsc errors. Dead eventStream
session-payload handling removed from diagnosis page.
P5.x-B: RowKeyCodec as the single inverted-tick encoder (persisted widths pinned,
UserActivity D20 legacy); DeletionRowDump.Table on tombstone rows with
DeletionTombstoneTables.Resolve replacing the duplicated Contains('_') heuristic
(legacy fallback retained for pre-field manifests).
P7.1: PATCH config/{tenantId}/fields flipped to TenantAdminOrGA (phase 2);
Settings sections save via per-section diff PATCH from sectionFieldMap.ts —
the ~39-field stale round-trip vector is gone; legacy webhook clearing now only
on the notifications save; agent-settings reset no longer discards contact edits.
P7.2/7.3: useGlobalAdminScope is a projection over useAggregatedAdminScope
(resolveConcreteScopeView, test-pinned); scopedApi builders own the tenant/global
endpoint-pair routing for all 11 former hand-rolled call sites; scopedFetch adds
the shared JSON fetch layer.
P7.4: navVisibility extraction + roles x nav-config matrix and nav<->route-guard
consistency test with a ratchet for the two known pre-existing mismatches
(Operator and GlobalReader vs /settings — user decision pending).
P7.5: web build stamp public/version.json (prebuild, GITHUB_SHA), SWA no-store
route, deploy-web verify poll + versions/web.json manifest, Portal Build card.
P7.6: OkAsyncBaselineGuardTests freezes the 44 anonymous-response sites
(shrink-only ratchet).
Verified: dotnet build 0W/0E, Functions.Tests 4123, DecisionCore 519, V2.Core
2206, SummaryDialog 44; tsc 0, ESLint 0, vitest 762/762; mutation proofs for all
new drift nets. Docs: docs/drift-guards.md + versioning.md web row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live verification after the P5/P6 deploy caught a request storm: the dashboard
auto-sweep effect refires whenever the loading flags toggle, and a q= sweep
whose matches were all already loaded changes nothing the effect watches — so
the same /api/search/sessions request looped indefinitely (~6 req/s observed).
The old loadAll() sweep terminated the effect via hasMore=false; searchAll()
deliberately leaves pagination state alone and therefore needs its own latch.
searchAll now runs at most once per trimmed query (latched before the first
await, also on failure); refetch/refetchWith/global-mode-toggle clear the latch
so a new backend scope may search again, and the explicit "Search all sessions"
button bypasses it via force. Ref declared with the sibling refs BEFORE the
mutating callbacks — the React compiler only accepts ref mutations when the
useRef declaration precedes the closure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four-tier model: Member (Progress Portal only) < Viewer (sees everything,
changes nothing) < Operator < Admin. Backend policies already treated
Viewer correctly (MemberRead admits it, every write tier excludes it) —
this change makes the tier reachable and safe:
- role assignment now validates against Constants.TenantRoles
(case-insensitive, canonicalized, 400 on unknown) — previously ANY string
was persisted; also fixes the case-sensitive last-admin guard
- Viewer is assignable in all four role dropdowns; About Roles describes
the four tiers
- shared hasTenantReadScope() replaces four hand-rolled predicates
(AuthGate, dashboard x2, useDashboardSessions); Viewers land on the
dashboard and the full navbar/sidebar ("adminOrOperator" visibility
renamed to "tenantMember"); Settings admits Viewer read-only
- "Analyze now" stays Admin/GA/Operator (recompute trigger, not a view)
- deleted dead IsTenantMemberAsync (meant Admin-or-Operator — the inverse
of RequestContext.IsTenantMemberRole)
- 16 endpoint tests for role validation, 15 tests for the scope helper
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A snapshot written by a different state-schema generation (e.g. the
pre-self-update binary) is no longer trusted: it is quarantined like a
corrupt cache and the state is rebuilt from the authoritative SignalLog,
whose signals carry their own KindSchemaVersion — replaying them through
the current reducer is the defined upgrade path. Unreadable log escalates
to the existing total-loss quarantine. Closes the silent-Newtonsoft-default
window where a reshaped field could cause mid-session amnesia across a
self-update. Two adversarial recovery tests pin both paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P3 — shared manifest infrastructure: SharedManifestParityTests reflects the
wire-relevant catalogs out of AutopilotMonitor.Shared into a committed JSON
manifest (regenerate: AM_WRITE_SHARED_MANIFESTS=1 dotnet test); a web codegen
lifts it into literal types; sharedManifestChecks.ts enforces
AdminConfiguration field parity at COMPILE time and 12 vitest tests pin the
value catalogs (sources/operators, lanes/verdicts, severities incl. the
missing Trace=-1, phases both directions, terminal statuses, event-type
typo guard). KnownSources/KnownOperators made internal for the export.
P4 — agent safety nets: DecisionState builder/serializer completeness
(4-site sync tripwire), signal-kind fixture coverage with a shrink-only debt
set (found and fixed realmjoin-timeout-v1.json, which never deserialized),
RemoteConfigMerger completeness tripwire, DefaultComponentFactory ordering
invariants as real construction tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GET now computes writableLanes with the SAME function the PUT re-gates
with (IsLaneWritableByCaller, own-tenant binding included) and the card
renders exactly that list — the hand-mirrored TS matrix is deleted. Fixes a
latent UX authz bug (a GA without an own-tenant admin role was offered the
tenantadmin lane and would have hit a 403). Older backends without the field
fail closed to read-only, so web can ship independently; save affordances
appear once the backend is deployed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- adminConfig.ts: drop retired V1 latestAgent*/customSettings ghost fields,
add the 8 backend fields the interface was missing
- ops alerts: register the Sla category (BreachNotification/ConsecutiveFailures/
EvaluationCompleted) + OrphanEventsCleaned so operators can route them
- analyze rules: expose app_install_duration in the builder SOURCES;
fix stale XML docs (event_data_array, count_per_group_gte/in/not_in)
- UploadIntervalSeconds: collapse 5 diverged defaults (10 vs 30) onto
Constants.DefaultUploadIntervalSeconds = 10 (the served value)
- combine-rules workflow: commit ALL three combine.js outputs and include
rules/guardrails.json in the path filters
- api.ts apps sessions builders: move trailing optional strings into an
options object (adjacent optional strings swallowed a forgotten tenantId)
- SummaryDialog: canonical phase display names ("Apps (Device)")
- mcp-server.bicep: fix pre-EU-cutover hostname in deploy docs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The confidence bar and the two fleet-context notes on an analysis finding
shared a non-wrapping flex row, so on phones they squeezed into
word-per-line columns — the row now wraps, each note taking its own line.
Also visible in the same screenshot: "critical path NaNh NaNm" in the
ESP-blocking apps block. The backend omits null fields (WhenWritingNull),
so espAppsOccupancySeconds arrives as undefined and the strict !== null
guard let it through to formatDuration. Loose != null check now hides the
label when the value is absent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
versionBreakdown on the app analytics responses now carries measuredInstalls,
medianDurationSeconds and p95DurationSeconds per version (measured population:
succeeded, non-skip, plausible duration, no AppId collision). A new pure
AppVersionRegressionRadar detects "median install duration rose from X to Y min
after version Z": versions ordered by first-seen install time (never string
sort), fires at median lift >=2x AND >=300s absolute with >=10 measured
installs on both sides.
Episodes follow the rule-radar pattern in a fourth tracker keyspace
appversionregression|{app}|{version} (409-dedup, refresh keeps FirstNotifiedAt,
re-arm on drain / median back under 1.5x, 30d retention sweep re-arms): one
tenant bell app_version_duration_regression (Admin audience, appDetailUrl href)
plus one AppVersionDurationRegression ops event per episode, dual-registered in
the web OPS_EVENT_TYPES catalog. Runs in the maintenance timer after the rule
radar over one cross-tenant 35d install-summary read; kill switch
AppVersionRegressionRadarDisabled (fail-open). Apps detail page shows an amber
regression banner and a median-duration-by-version chart; OKF concept doc added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each fired analyze rule on the session detail page now reads "Fired in N
enrollment(s) in the last 14 days" (summed client-side from the existing
rule-stats trend — enrollments, never devices; RuleStats carries no serial)
with a "View sessions" deep link. The dashboard accepts ?ruleId=, fetches
the rule's hit-session ids once from the new GET /api/metrics/rule-hit-sessions
endpoint (MemberRead + QueryParam scoping, capped 2000 with a truncated flag)
and filters the loaded list client-side behind a clearable chip.
Side fix: the section's stats fetch now follows the session's tenant for
GA/delegated viewers (global route + tenantId) instead of showing the
viewer's own-tenant numbers next to a foreign session. Dashboard links in
fleet/fleet-health now go through the new canonical dashboardUrl() builder.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A CVE published against an already-referenced package must not block an
unrelated hotfix deploy; advisories are handled by bumping the package.
WarningsNotAsErrors NU1901-NU1904 - everything else stays fatal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repo-wide Directory.Build.props: TreatWarningsAsErrors +
MSBuildTreatWarningsAsErrors. Applies to compiler, MSBuild and NuGet
restore warnings including audit advisories (NU1901-NU1904) - a newly
published CVE fails the build until addressed (fail-closed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CmTraceLogParser: annotate TryParseLine (string? line, [NotNullWhen(true)]
out entry), init string properties (CS8618/CS8625/CS8604)
- Shared: internal NotNullWhen polyfill for netstandard2.0; multi-target
netstandard2.0;net10.0 so net10 consumers use the BCL attribute and the
polyfill cannot collide via InternalsVisibleTo (CS0433)
- AgentEndpointMigrationRules.TryNormalizeTarget: now carries [NotNullWhen(true)]
- Functions.csproj: drop DataProtection/Cryptography.Xml security pins made
redundant by .NET 10 framework-package pruning (NU1510)
- Functions.Tests.csproj: add Microsoft.AspNetCore.App FrameworkReference so
pruning applies there too and the pins stay unnecessary (NU1903/NU1904)
- Agent tests: nullable test-helper param, Assert.NotNull before dereference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/annotations now carries the standard cross-tenant scope (GA/Reader: "All
tenants" aggregate + per-tenant drill-down via GET global/session-annotations,
delegated: managed subset), with a tenant column and tenant-qualified session
deep links. The global list endpoint now derives excludeGlobalAdminLane from
HasGlobalScope so delegated-rescue callers never receive the platform-internal
globaladmin lane (endpoint-test-pinned).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 99-warning legacy backlog from the React 19 / eslint-plugin-react-hooks
v6 upgrade is fully burned down — CI now fails on any new warning. The
compiler rules stay demoted to "warn" so local dev output remains readable;
the eslint config comments now point to the established fix patterns
instead of the retired ratchet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- AuthContext: delete the SSR isLoading effect — effects never run on the
server, the guard was unreachable dead code.
- TenantContext: mirror user.tenantId via adjust-during-render; the value
is still retained while auth resolves and cleared on settled sign-out.
- Global/Tenant notification contexts: scope-masked empty list derived in
the provider value instead of effect-clearing; loading flag moved into
the canonical async wrapper.
- New shared hooks/useMediaQuery (useSyncExternalStore, configurable
server snapshot) — consumed by GlobalSidebar (isDesktop) and
McpTerminalDemo (prefers-reduced-motion, finished transcript derived).
- GlobalSidebar: route-mode active section derived from pathname;
active-group and pathname auto-expansion as adjust-during-render, state
now holds user toggles only.
- ThemeContext: theme read via useSyncExternalStore backed by
lib/themeStore (manual preference beats OS preference in the snapshot;
same-tab writes notify through the store's listener list).
- TipOfTheDay: random tip via useSyncExternalStore with module-memoized
pick (build-time Math.random would freeze one tip and mismatch on
hydration); re-rolls per mount via cleanup.
- Navbar: previewMode block removed entirely — nothing in the codebase
consumes the flag anymore, it only persisted itself.
- activation page: ?demo flag via useSyncExternalStore, tri-state null
sentinel dropped.
Lint is at zero react-hooks warnings repo-wide.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Structural fixes for the set-state-in-effect sites that were genuine
state synchronization, using the React-documented patterns:
- adjust-during-render (compare-prev) for reset-on-dep-change: dashboard
page reset + tenant-filter clear, InstallsTab/MappedSoftwareTab page
resets, TenantManagementSection URL seeding + page reset,
OpsAlertRulesSection draft re-seeding, SessionReportsSection deep-link
auto-open, ReportSessionModal result reset.
- clamp/derive at read: dashboard effectivePage (also removes the
documented handleNextPage race), backups detail effectiveTableName,
useBlockDevice scope-masked set, InstallProgress and
useProgressDerivedData live timers (startedAt-tagged tick state),
sessions timeBreakdown read guard.
- key-based reset: SessionReportsSection note editor, DeleteConfirmModal
content remount.
- inverted state: sessions page stores collapsedPhases so new phases are
expanded by default with no sync effect; hash deep-link expandAll moved
into its timer callback.
- tagged result state: useScriptDisplayNames stores {key, map} and serves
the empty map on key mismatch — same staleness guarantee, no
effect-phase clearing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- useProgressDerivedData: hoist the currentDownload computation into a
module-level pure helper — the in-loop Map mutation and early return
kept the compiler from memoizing the body inline.
- DeviceDetailsCard: derive bootTime from events inside the uptime memo
(deps [events]) — the outer const is a live alias into event data that
the compiler's alias analysis treats as maybe-mutated.
- useSessionEvents: route the queued follow-up re-invocation through a
ref — a memoized callback that calls itself directly cannot be
memoized. The now-traceable mount effect gets the standard wrapper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Convert every useEffect that directly invokes a state-setting fetch
callback to the canonical inner-async shape (const run = async () => {...};
void run();) — the react-hooks/set-state-in-effect rule traces direct
calls but accepts effect-scoped async functions. Semantic no-op: an async
body runs synchronously until its first await, so execution order is
byte-identical. Leading loading/error/data resets that are part of the
fetch flow move to the top of run in unchanged order.
52 files, ~60 sites. Lint 99 -> 36 warnings; remaining sites are
reset/derive effects and sensitive context/sidebar refactors (next
batches).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The annotations overview pushed the ESLint warning count to 100, one over
the frozen --max-warnings 99 cap, failing CI since 66d25f57's run. The
react-hooks/set-state-in-effect rule flags any direct effect call into a
state-setting useCallback, even with every setState behind an await.
Restructure instead of suppressing: loading/error begin-state moves from
fetchPage into the interaction sites (filter selects, load-more; mount is
covered by the initial state), and the effect invokes fetchPage through an
inner async wrapper so no setState is synchronously reachable. Lint is
back to exactly 99.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A 404 (backend without the route yet, deploy skew) now renders the
regular empty state instead of an error; genuine load failures show a
calm non-technical retry hint in amber instead of raw statusText in red.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Node 20 actions are deprecated on GitHub runners; actions/cache@v4 was
the last remaining action targeting it (MCP server tests job warning).
v6 runs on node24 like the rest of the workflow actions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Autopilot Monitor Team" read like a vendor-to-customer message channel.
The lane only ever renders for platform-scope users, so it now says
"Internal (Autopilot Monitor)" and carries a "never visible to the
tenant" badge on the session card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/api/session-annotations sat outside every clientCertExclusionPaths
prefix, so browsers got the client-cert picker. Rename-over-exclusion
per the diag-files precedent (the Azure list is portal-only config and
dies on app recreation). Three segments (sessions/annotations/list) on
purpose: a two-segment literal would be eaten by sessions/{sessionId}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On Windows 365 Cloud PCs a missing Autopilot profile (ProfileAvailable=0)
is the expected state - Cloud PCs are provisioned by the Windows 365
service, not by Autopilot - so the warning shown for physical devices is
misleading there.
- Agent: emit autopilot_profile_missing as Info with a Cloud-PC-specific
message when CloudPcDetector matches; payload now carries isCloudPc.
Evidence collection (ZTD verdict, registry, endpoint probe) unchanged.
- Web: DeviceDetailsCard renders a neutral info note instead of the amber
warning for Cloud PC sessions (session.isCloudPc, sticky, covers
existing sessions; or the new event marker).
- MCP: resource-catalog documents the new isCloudPc data field.
- Docs: Cloud PC exception noted in agent/autopilot-ztd-diagnostics.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>