WhiteGlove sessions showed IsPreProvisioned=false and Status=InProgress
despite whiteglove_complete being stored in Events table. Root cause:
phase_changed events repeatedly wrote Status=InProgress via
UpdateSessionStatusAsync, creating ETag contention and silent failures.
Changes:
- Remove InProgress writes from IngestEventsFunction — Status is only
set by key events (enrollment_complete, enrollment_failed,
whiteglove_complete, gather_rules_collection_completed)
- Phase-change events now use IncrementSessionEventCountAsync with
new currentPhase parameter (lightweight, no Status write)
- Add verification after whiteglove_complete: return HTTP 500 if
IsPreProvisioned not persisted, so agent retries the batch
- Add IsPreProvisioned to SignalR sessionDelta for real-time UI updates
- Remove now-dead promotion rule from UpdateSessionStatusAsync
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The performance commit (477b573) removed GetEarliestSessionEventTimestampAsync
from UpdateSessionStatusAsync, relying solely on the current batch's
earliestEventTimestamp. When concurrent Azure Function invocations process
different batches for the same session, the completion batch could read a stale
StartedAt before the earlier batch's Merge committed — producing wrong
DurationSeconds (e.g. 3s instead of 679s).
Fix: re-introduce the single-row Events table lookup (maxPerPage: 1) only at
completion (Succeeded/Failed) — once per session lifecycle, minimal cost.
Revert MapToSessionSummary to use stored DurationSeconds with null-fallback
for in-progress sessions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add AgentSelfMetricsCollector that measures the agent's own CPU, memory,
threads, handles, and HTTP network traffic (bytes up/down, latency,
request count) with zero overhead — no WMI, no PerformanceCounters,
just Process properties and lock-free Interlocked counters.
NetworkMetrics tracks all BackendApiClient HTTP calls via Stopwatch and
byte counting in finally blocks. The collector emits agent_metrics_snapshot
events (Severity.Debug) through the existing spool/upload pipeline.
Add Galactic Admin-only /platform-metrics page that aggregates agent
footprint data across sessions with CPU/memory/network assessment badges,
per-session breakdowns, and agent version distribution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove full event-count recount (partition scan) from UpdateSessionStatusAsync;
IncrementSessionEventCountAsync already maintains the count atomically
- Remove redundant GetEarliestSessionEventTimestampAsync calls from
UpdateSessionStatusAsync and IncrementSessionEventCountAsync; the caller
(IngestEventsFunction) already computes and passes the timestamp
- Switch UpdateSessionStatusAsync from Replace to Merge mode, writing only
changed fields to drastically reduce ETag conflicts under concurrency
- Increase ETag retry limit from 3 to 5 with jitter-based exponential backoff
- Add server-side time-range filter (90 days) to UsageMetrics queries,
avoiding loading the entire sessions table into memory
- Remove full session object from SignalR eventStream message; session delta
updates now flow via the existing newevents tenant-group message
- Add newevents handler to session detail page for live session delta updates
- Auto-rejoin SignalR groups on reconnect to prevent silent update loss
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move SAS expiry calculation from page-level state into DiagnosticsSection
so the expiry indicator updates immediately when the user types or pastes
a new URL, instead of requiring a save and page reload.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- H1: Require TenantAdmin or GalacticAdmin for mutative tenant endpoints
(DeleteSession, MarkSessionFailed, UpdateTenantConfiguration,
AnalyzeRules CRUD, GatherRules CRUD)
- M1: Add EnsureValidGuid validation to TenantOffboardFunction to
prevent OData injection via tenantId route parameter
- M2: Remove ex.Message from error responses in UsageMetrics,
PlatformUsageMetrics, TriggerMaintenance, TenantOffboard,
ReseedFromGitHub to prevent internal error leakage
- M4: Use requireGalacticAdmin on ProtectedRoute and server-derived
user.isGalacticAdmin instead of localStorage for admin-configuration
- N1: Add 1-second Regex timeout to agent GatherRuleExecutor and
ImeLogTracker to prevent ReDoS from malicious patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New RuleStates table stores per-tenant enabled/disabled state separately
from rule definitions, so built-in/community rule updates propagate
centrally without losing tenant preferences
- New IsCommunity flag on GatherRule and AnalyzeRule models; community
rules behave like built-in but show a Community badge in the UI
- Merge logic updated: global partition holds definitions (built-in +
community), RuleStates holds per-tenant toggles, tenant partition
holds custom rules only
- Fixed AnalyzeRule Trigger field not being persisted to/from Table Storage
- Fixed EnsureBuiltInRulesSeededAsync comparison to include Version,
Trigger, and TriggerPhase so definition changes auto-propagate
- Removed Edit (Global) and Delete (Global) from Gather/Analyze rules UI;
rule management is now exclusively via Git + Reseed
- Added Community filter option to type filter dropdowns
- ReseedFromGitHubFunction now deletes both IsBuiltIn and IsCommunity
rules from global partition before reseeding
- GalacticAdminService dependency removed from Gather/AnalyzeRulesFunction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rules/dist/ was excluded by the global dist/ .gitignore pattern.
Added !rules/dist/ exception so the combine-rules workflow can
commit the generated files. Also adds the initial dist files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the missing operator to the enum so ANALYZE-APP-006.json passes
schema validation in the combine-rules workflow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Rules as GitHub single source of truth
- Add 75 individual JSON rule files (2 gather, 18 analyze, 55 IME patterns)
under rules/gather/, rules/analyze/, rules/ime-log-patterns/
- Add JSON Schemas for all three rule types with VS Code IntelliSense support
- Add CI combine script (rules/scripts/combine.js) + GitHub Action that
validates schemas with ajv-cli and generates rules/dist/ combined files
- Convert BuiltIn*.cs from hardcoded C# objects to embedded JSON resource loaders
- Add GitHubRuleRepository service to fetch dist/ files from raw.githubusercontent.com
- Add ReseedFromGitHubFunction: POST /api/rules/reseed-from-github?type=all|gather|analyze|ime
## IME Log Patterns
- Extend ImeLogPattern model with Description and IsBuiltIn properties
- Add ImeLogPatterns table + API constants
- Add TableStorageService CRUD methods for IME log patterns
- Add ImeLogPatternService (CRUD, merge, auto-seed from embedded JSON)
- Add ImeLogPatternsFunction: GET/PUT /api/rules/ime-log-patterns (Phase 1: read-only
for tenants, Galactic Admin can enable/disable/edit globally)
- Add IME Log Patterns management page (/ime-log-patterns)
- Wire GetAgentConfigFunction to use ImeLogPatternService instead of static class
## API route consolidation
- All rule endpoints now under /api/rules/* for single certificate exclusion path:
/api/rules/gather, /api/rules/analyze, /api/rules/ime-log-patterns,
/api/rules/reseed-from-github
## Export buttons
- Add per-rule and export-all JSON download buttons to gather-rules,
analyze-rules, and ime-log-patterns pages
## Admin configuration
- Replace old individual reseed buttons with unified "Fetch & Reseed from GitHub"
(calls new GitHub endpoint) plus compact individual-type reseed buttons
- Add CDN cache hint: GitHub raw.githubusercontent.com caches up to 5 minutes
## Preview mode
- ?preview=1 URL param seeds localStorage.previewMode (persists across navigation)
- ?preview=0 clears it; toggle in Navbar settings dropdown (indigo, eye icon)
- IME Log Patterns nav link gated behind galacticAdminMode OR previewMode
- Preview badge shown on nav link when active for normal users
## Tests
- Add xUnit test project (net8.0) with 5 tests for embedded resource loading,
known IME actions validation, and valid categories check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- fix: handleToggleRule sent only {enabled} — now sends full rule object
to prevent overwriting all fields with empty values on toggle
- feat: GatherRule global delete (Galactic Admin) — new DeleteGlobalRuleAsync,
?global=true query param on DELETE endpoint, Delete (Global) button in UI
- fix: admin-configuration page mobile layout — Maintenance, Reseed Analyze,
Reseed Gather panels: action buttons moved to bottom (flex vertical)
- fix: tenant management list mobile layout — badges and buttons below info text
- fix: session detail event timeline — filter badges + expand/collapse in one
flex-wrap row; expand/collapse show icon-only on mobile, text on sm+
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Model name now renders fully above the bar (no truncation, break-words).
Bar height reduced from h-4 to h-2. Percentage and device count shown
inline right-aligned next to the name row.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tenant ID column in galactic admin mode now shows only the first two
GUID segments (e.g. b54dc1af-5320). Full ID shown as tooltip. Click
copies the full tenant ID to clipboard; copy icon appears on hover.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Path and description stack vertically in their own row (break-all for long paths)
- Replaced rounded-full chips with rounded-lg full-width entries in both settings and admin-config
- Removed 'Core collectors Hello Detector...' info box from settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The fetchUserInfo call to GET /api/auth/me had no timeout. When Azure
Functions are cold (scale-to-zero), this could block for 20-60 seconds,
holding isLoading=true and showing only the spinner on the landing page.
Add an AbortController with an 8-second deadline. On timeout the abort
signal causes the fetch to throw, which is caught by the existing catch
block that falls back to MSAL token claims — the user can still log in
normally. isGalacticAdmin/isTenantAdmin will be false until the backend
is warm and the next auth refresh succeeds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Form/JSON toggle to Gather Rules editor (create + edit), matching Analyze Rules pattern
- Add missing parameter UI for eventlog (source/provider), file (readContent), and logparser (pattern, trackPosition, maxLines) collector types
- Add two disabled built-in gather rules: dsregcmd /status on DevicePreparation phase change, and last startup event (Event ID 12) from System eventlog
- Fix file collector to read last 4000 chars instead of first (more useful for log files)
- Docs: expand registry allowlist to all 21 prefixes, update file read description
- Docs: add comprehensive Analyze Rules section with confidence scoring, condition types, trigger types, and examples
- Add Documentation link in Analyze Rules page header
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agent:
- Rewrite EventLog collector from System.Diagnostics.EventLog to
EventLogReader — supports operational/analytic logs (e.g.,
Microsoft-Windows-Shell-Core/Operational), not just classic logs
- New parameters: eventId (XPath filter), messageFilter (*contains*),
source (provider name), maxEntries (1-50)
- Event entries now include Event ID in output: [ID:62407]
UI (gather-rules):
- Dynamic target placeholder and hint text per collector type
- EventLog parameter fields: Event ID, Max Entries, Message Filter
- buildParameters() serializes params per collector type (create+edit)
- on_event placeholder updated to show enrollment_complete/failed
- Documentation link in header → /docs#gather-rules
Docs:
- New "Gather Rules" section with all 6 collector types documented
- Each collector: target format, parameters, allowlists, examples
- Trigger types documented (startup, interval, phase_change, on_event)
- Hash-based deep linking from gather-rules page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CSV now sorted by Timestamp+Sequence (matches Azure Table order exactly)
- Added RowKey as first CSV column (format: yyyyMMddHHmmssfff_Sequence)
- Backend: map entity.RowKey in MapToEnrollmentEvent, expose via EnrollmentEvent
- Tenant ID field replaced with dropdown (domainName + tenantId) + free-text input
- cleanId() helper strips leading/trailing quotes and whitespace from ID inputs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The event sequence counter reset to 0 on every agent restart, causing three
bugs in WhiteGlove (pre-provisioning) sessions: spool sort order mixed boot
events, backend status regressed from Pending to InProgress, and UI timeline
split broke. Additionally, EventSpool.RemoveEvents used a glob pattern that
could delete files from both boots sharing the same sequence number.
Changes:
- SessionPersistence: persist sequence in session.seq alongside session.id
- EventSpool: RemoveEvents uses exact filename instead of glob, new
GetMaxSequence for crash recovery
- MonitoringService: init sequence from persistence + spool ceiling, periodic
save every 50 events + on critical events, save before WhiteGlove exit
- TableStorageService: document WhiteGlove guard as promotion rule
- EnrollmentEvent: clarify Timestamp is agent-side, not server-side
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause found via WhiteGlove session log (session-46ec9146):
On the user's second boot the esp_exiting event triggered the 30s Hello-Wait
timer. The Hello wizard had not yet appeared within those 30s, so
OnHelloWaitTimeout() emitted hello_wait_timeout and immediately fired
HelloCompleted → enrollment_complete. This was wrong: Hello policy was
explicitly detected as enabled (hello_policy_detected: helloEnabled=true).
The Hello wizard launched shortly after (hello_provisioning_willlaunch at seq
27), but the agent had already sent enrollment_complete.
Fix: track _isHelloPolicyEnabled in CheckHelloPolicy(). In OnHelloWaitTimeout,
if Hello policy is known to be enabled, do not fire HelloCompleted — instead
emit the hello_wait_timeout event with action=extended_wait and start the long
HelloCompletion timer (1500s). This gives the wizard up to 25 minutes to appear.
The enrollment_complete path via timeout is now only taken when Hello policy is
disabled or not yet detected.
The original session (46ec9146) was also affected by the spool-drain bug
(whiteglove_complete was seq 126+, never uploaded before exit), so the session
never reached Pending. Both bugs contributed to the incorrect Succeeded result.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace ad-hoc Task.Run block with dedicated HandleWhiteGloveComplete()
method mirroring HandleEnrollmentComplete() quality:
- StopEventCollectors() + _spool.StopWatching() before draining
- Full spool drain loop (same fix as previous commit, now in proper place)
- Task.Delay(2000) for final network transmission
- Diagnostics package upload with "preprov" suffix before exit
- Structured logging matching the enrollment complete sequence
- DiagnosticsPackageService.CreateAndUploadAsync() gains optional
fileNameSuffix parameter: "preprov" yields
AgentDiagnostics-{sessionId}-{ts}-preprov.zip, distinguishable
from the later full-enrollment package uploaded after Part 2
- UploadDiagnosticsWithTimelineEvents() passes suffix through to service
WhiteGlove devices now have a preprov diagnostics package in Pending
state, and a full package after user enrollment completes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UploadEventsAsync fetches at most MaxBatchSize (100) events per call.
If whiteglove_complete was beyond event #100 in the spool (e.g. Seq 126),
it was never sent before Environment.Exit(0) — leaving the session stuck
on InProgress instead of transitioning to Pending.
Fix: loop until spool is empty (with a safety cap of 20 iterations) so
all events including whiteglove_complete are guaranteed to reach the backend.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the fragile setTimeout(500ms) with a deterministic guard:
wasAuthenticated.current already prevents spurious redirects during
MSAL's double handleRedirectPromise cycle. The real fix was setting
navigateToLoginRequestUrl: false in msalConfig. No timer needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: MSAL fires handleRedirectPromise twice on page load, causing
isAuthenticated to briefly flip false between the two settlements. This
triggered ProtectedRoute to redirect to '/', which then re-authenticated
and redirected to '/dashboard', causing the dashboard page to mount and
unmount periodically while the user was viewing a session.
Two fixes:
1. navigateToLoginRequestUrl: false — prevents MSAL from navigating to
the stored login request URL after redirect settlement, eliminating
the spurious navigation that caused the dashboard to mount.
2. ProtectedRoute unauthenticated redirect now waits 500ms before
executing, giving MSAL time to complete its double-settlement cycle
before we conclude the user is truly unauthenticated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When galacticAdminMode is true, the initial data fetch effect fires
immediately (before MSAL settles). If getAccessToken() returns null,
hasInitialFetch was left as true, permanently blocking any retry.
The session page would load empty and never recover.
Fix: reset hasInitialFetch on token/network failures so the effect
can retry. Added `user` to dependencies so the effect re-fires once
MSAL settles and user info is available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>