When WhiteGlove pre-provisioning completes (status=Pending), all reached
phases should show as completed (green) since pre-provisioning is done.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Frontend: Add per-block durations to WhiteGlove event timeline headers,
pause-excluded combined duration in Session Info, visual dashed separator
between Pre-Provisioning and User Enrollment blocks, and group labels with
vertical separator in the Enrollment Progress tracker.
Backend: Store ResumedAt timestamp when WhiteGlove Part 2 starts, use it
to compute Duration 2 (user enrollment only) for Teams enrollment notifications.
Rename "WhiteGlove Pre-Provisioning" to "Pre-Provisioning" in notification text.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Galactic Admin Mode for Audit Logs: banner + cross-tenant endpoint (galactic/audit/logs)
- Audit page pagination (12 entries per page) with Previous/Next controls
- Platform Metrics: moved computation to backend PlatformMetricsService with 5-min cache
- New endpoint GET /api/galactic/metrics/platform replaces N+1 client-side fetches
- IME log tracker enhancements, DO telemetry pattern, and download progress improvements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes Azure Table Storage 64KB truncation by removing the full apps array
from app_tracking_summary (could be 150+ apps) and replacing it with
lightweight state-breakdown counters (downloading, installing, installed,
skipped, failed, pending). Summary emission is now event-driven with
30s timer as backstop. New InstallProgress UI component shows per-app
install state with duration tracking. ConfigVersion 10 -> 11.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend: DeserializeEventData now returns raw string in _rawDataJson key
when JSON parsing fails (truncated data), instead of empty dict.
UI: EventTimeline detects _rawDataJson and shows an amber warning with
the raw text, so truncated events remain inspectable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DataJson and Message are now capped at 30K chars before writing to Table Storage,
preventing PropertyValueTooLarge failures that silently dropped events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agent persists a whiteglove.complete marker when Part 1 finishes. On Part 2
boot, the agent detects the marker, emits a whiteglove_resumed event, and
clears it. The backend handles whiteglove_resumed by transitioning the session
from Pending to InProgress — only if actually Pending (guards against
overwriting Succeeded/Failed). Registration also retries with exponential
backoff (5 attempts) to handle delayed network during OOBE boot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Windows EventLog record.TimeCreated returns DateTime with Kind=Local. The Azure
Data Tables SDK rejects Local DateTimes with NotSupportedException, causing
UpdateSessionStatusAsync to fail deterministically for every WhiteGlove session
(Shell-Core EventID 62407 = whiteglove_complete). This was the root cause of the
systematic 6/6 WhiteGlove failures, not ETag conflicts or transient Azure errors.
Agent fix: convert record.TimeCreated to UTC at all 3 EventLog timestamp sources.
Backend fix: EnsureUtc() safety net on all 11 DateTime writes to the sessions table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The initial fix (43c2674) resolved the 500 loop but the fallback only set
IsPreProvisioned, leaving Status=InProgress instead of Pending. This extends
the fallback to set both fields and adds retry logic for transient Azure
errors (429/503/408) in UpdateSessionStatusAsync.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The IngestEventsFunction verification check returned HTTP 500 when the
WhiteGlove IsPreProvisioned session update failed, causing the agent to
retry the same batch in a tight loop (same whiteglove_complete event,
same failing update, same 500). After 20 retries the agent gave up with
events stuck in the local spool and the session metadata broken.
- Add unconditional fallback (ETag.All merge) via SetSessionPreProvisionedAsync
when UpdateSessionStatusAsync fails despite internal ETag retries
- Replace the 500 verification response with a warning log + continue
with 200 so the agent can drain its spool
- Simplify SetSessionPreProvisionedAsync to use ETag.All unconditional
merge, removing the unnecessary read-then-write pattern
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix 500 error on download URL: replace GenerateSasUri() with
blobClient.Uri (SAS already embedded in service-level connection string)
- Add AdminNote field to SessionReportMetadata shared model
- Read AdminNote from Table Storage in GetAllReportsAsync
- Add UpdateAdminNoteAsync to SessionReportService
- New PATCH /api/galactic/session-reports/{reportId}/note endpoint
- Frontend: editable Admin Note textarea in report detail modal with
inline save feedback; Admin Note column in reports table with icon
- Add copy-to-clipboard buttons for Session ID, Tenant ID,
Submitted By, and Email fields in report detail modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After submitting a report, the modal now displays a green success block
(with checkmark) or a red error block (with message) directly inside the
dialog instead of only relying on the auto-dismissing notification banner.
On success the form is hidden and the button becomes a green Close button;
on error the form stays visible for retry. The modal resets cleanly on
re-open. Report Session button is now always visible (not gated on
adminMode).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New Azure Function GET /api/galactic/session-reports/download-url generates a 15-minute SAS URL for a session report blob (Galactic Admin only)
- SessionReportsSection modal now shows a Download ZIP button that fetches the SAS URL and triggers a browser download
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consistent with /api/galactic/sessions and /api/galactic/app-metrics —
all cross-tenant Galactic Admin endpoints live under /api/galactic/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tenant Admins can report sessions for analysis via a new button on the
session details page. Creates a ZIP with session JSON, events JSON,
analysis results, timeline TXT, events CSV, optional agent log (5 MB
limit), and optional screenshot — uploaded to central blob storage.
Admin-config page gets a new Session Reports table (Galactic Admin only)
showing all submitted reports with comment, email, and blob reference.
Export utilities (generateUiExport, generateCsvExport) extracted to
shared lib/sessionExportUtils.ts for reuse across admin export and
report modal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>