mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
7e0c578a59
Every status writer now declares the code path that produced the status (VerdictPaths vocabulary, stamped for all statuses — unlike FailureSource which is Failed-only), the timeout classifier returns its rule id, and a write that overrides a prior verdict preserves it as PriorStatus/ PriorVerdictPath (admin mark, late completion, retro). Pre-instrumentation rows derive a path read-side (VerdictPathDerivation, legacy:* when ambiguous). Foundation for the verdict calibration aggregate + radar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
654 lines
32 KiB
C#
654 lines
32 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace AutopilotMonitor.Shared.Models
|
|
{
|
|
/// <summary>
|
|
/// Request to register a new session
|
|
/// </summary>
|
|
public class RegisterSessionRequest
|
|
{
|
|
public SessionRegistration Registration { get; set; } = default!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Identifies which validator authorized the device during session registration.
|
|
/// Surfaced in the RegisterSession response so the agent can reconcile against its
|
|
/// own registry-based detection and, when needed, switch its enrollment flow.
|
|
/// </summary>
|
|
public enum ValidatorType
|
|
{
|
|
Unknown = 0,
|
|
AutopilotV1 = 1, // AutopilotDeviceValidator (windowsAutopilotDeviceIdentities)
|
|
CorporateIdentifier = 2, // CorporateIdentifierValidator (importedDeviceIdentities)
|
|
DeviceAssociation = 3, // DevPrep DeviceAssociationValidator (tenantAssociatedDevices) — future
|
|
Bootstrap = 4, // Bootstrap token auth (pre-MDM OOBE)
|
|
CloudPc = 5 // CloudPcDeviceValidator (virtualEndpoint/cloudPCs, cert-CN bound) — W365
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response from session registration
|
|
/// </summary>
|
|
public class RegisterSessionResponse
|
|
{
|
|
public string SessionId { get; set; } = default!;
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; } = default!;
|
|
public DateTime RegisteredAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Non-null when the session was already marked as terminal by an admin before agent restart.
|
|
/// Values: "Succeeded", "Failed". Agent should run cleanup instead of starting monitoring.
|
|
/// </summary>
|
|
public string? AdminAction { get; set; }
|
|
|
|
/// <summary>
|
|
/// Authoritative signal: which validator accepted this device.
|
|
/// Lets the agent reconcile its registry-based enrollment-type detection
|
|
/// against the backend's verdict (e.g. AutopilotV1 → Classic flow, DeviceAssociation → DevPrep flow).
|
|
/// Older backends that do not set this return Unknown — agent falls back to its own detection.
|
|
/// </summary>
|
|
public ValidatorType ValidatedBy { get; set; } = ValidatorType.Unknown;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request to ingest events (batched)
|
|
/// </summary>
|
|
public class IngestEventsRequest
|
|
{
|
|
public string SessionId { get; set; } = default!;
|
|
public string TenantId { get; set; } = default!;
|
|
public List<EnrollmentEvent> Events { get; set; }
|
|
public bool IsCompressed { get; set; }
|
|
|
|
/// <summary>
|
|
/// Device-clock send time of the carrying upload request (X-Send-Time-Utc header),
|
|
/// parsed by the ingest function. One value per request; stamped onto every event
|
|
/// in <c>EventIngestProcessor.StampServerFields</c>. Null when the agent pre-dates
|
|
/// the header or sent an unparseable value.
|
|
/// </summary>
|
|
public DateTime? SentAt { get; set; }
|
|
|
|
public IngestEventsRequest()
|
|
{
|
|
Events = new List<EnrollmentEvent>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response from event ingestion
|
|
/// </summary>
|
|
public class IngestEventsResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public int EventsReceived { get; set; }
|
|
public int EventsProcessed { get; set; }
|
|
public string Message { get; set; } = default!;
|
|
public DateTime ProcessedAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the request was rejected due to rate limiting
|
|
/// </summary>
|
|
public bool RateLimitExceeded { get; set; }
|
|
|
|
/// <summary>
|
|
/// Rate limit details (only populated if RateLimitExceeded is true)
|
|
/// </summary>
|
|
public RateLimitInfo? RateLimitInfo { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the device has been temporarily blocked by an admin
|
|
/// </summary>
|
|
public bool DeviceBlocked { get; set; }
|
|
|
|
/// <summary>
|
|
/// When the block expires (only populated if DeviceBlocked is true)
|
|
/// </summary>
|
|
public DateTime? UnblockAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the device has been issued a remote kill signal (graceful self-destruct).
|
|
/// The agent should execute its self-destruct routine and exit.
|
|
/// </summary>
|
|
public bool DeviceKillSignal { get; set; }
|
|
|
|
/// <summary>
|
|
/// Non-null when an admin has externally changed the session status.
|
|
/// Values: "Succeeded", "Failed". Agent should treat as terminal signal and run cleanup.
|
|
/// </summary>
|
|
public string? AdminAction { get; set; }
|
|
|
|
/// <summary>
|
|
/// Generic server→agent action channel. Null/empty when no actions are pending.
|
|
/// Delivered at-least-once; agents must handle actions idempotently.
|
|
/// Unknown action types should be logged and skipped (forward-compatibility).
|
|
/// </summary>
|
|
public List<ServerAction>? Actions { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rate limit information for UI display
|
|
/// </summary>
|
|
public class RateLimitInfo
|
|
{
|
|
/// <summary>
|
|
/// Number of requests in current window
|
|
/// </summary>
|
|
public int RequestsInWindow { get; set; }
|
|
|
|
/// <summary>
|
|
/// Maximum allowed requests
|
|
/// </summary>
|
|
public int MaxRequests { get; set; }
|
|
|
|
/// <summary>
|
|
/// Window duration in seconds
|
|
/// </summary>
|
|
public int WindowDurationSeconds { get; set; }
|
|
|
|
/// <summary>
|
|
/// Seconds to wait before retrying
|
|
/// </summary>
|
|
public int RetryAfterSeconds { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request to get a short-lived SAS URL for diagnostics package upload.
|
|
/// Called by the agent just before upload — the URL is never cached in config.
|
|
/// </summary>
|
|
public class GetDiagnosticsUploadUrlRequest
|
|
{
|
|
public string TenantId { get; set; } = default!;
|
|
public string SessionId { get; set; } = default!;
|
|
public string FileName { get; set; } = default!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response containing a short-lived SAS URL for diagnostics package upload.
|
|
/// </summary>
|
|
public class GetDiagnosticsUploadUrlResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? UploadUrl { get; set; }
|
|
public DateTime ExpiresAt { get; set; }
|
|
public string? Message { get; set; }
|
|
|
|
/// <summary>
|
|
/// Canonical blob path the agent MUST persist as
|
|
/// <c>SessionSummary.DiagnosticsBlobName</c> after a successful upload. Encodes
|
|
/// the destination-specific path layout so the download path can find the blob
|
|
/// later without having to reconstruct it:
|
|
/// <list type="bullet">
|
|
/// <item><c>CustomerSas</c>: equal to the request filename (e.g.
|
|
/// <c>AgentDiagnostics-...zip</c>).</item>
|
|
/// <item><c>Hosted</c>: tenant-prefixed (e.g.
|
|
/// <c>{tenantId}/AgentDiagnostics-...zip</c>).</item>
|
|
/// </list>
|
|
/// Null for older backends that predate this field — the agent then falls back
|
|
/// to the request filename for back-compat.
|
|
/// </summary>
|
|
public string? BlobName { get; set; }
|
|
|
|
/// <summary>
|
|
/// Which destination minted the SAS in <see cref="UploadUrl"/>:
|
|
/// <c>"CustomerSas"</c> or <c>"Hosted"</c>. Surfaced so the agent's ingest path
|
|
/// can attach it to the next telemetry batch (the download path needs the
|
|
/// per-session destination, NOT the current tenant setting — those can diverge
|
|
/// if the admin switches modes after some sessions have already uploaded).
|
|
/// </summary>
|
|
public string? Destination { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Session summary for UI display
|
|
/// </summary>
|
|
public class SessionSummary
|
|
{
|
|
public string SessionId { get; set; } = default!;
|
|
public string TenantId { get; set; } = default!;
|
|
public string SerialNumber { get; set; } = default!;
|
|
public string DeviceName { get; set; } = default!;
|
|
public string Manufacturer { get; set; } = default!;
|
|
public string Model { get; set; } = default!;
|
|
public DateTime StartedAt { get; set; }
|
|
public DateTime? CompletedAt { get; set; }
|
|
|
|
// Serialize as integer (0-7) not string for frontend compatibility
|
|
public int CurrentPhase { get; set; }
|
|
public string CurrentPhaseDetail { get; set; } = default!;
|
|
public SessionStatus Status { get; set; }
|
|
public string FailureReason { get; set; } = default!;
|
|
|
|
/// <summary>
|
|
/// Origin of a Failed status — persisted ONLY on a Failed write (a non-Failed verdict
|
|
/// discards it; see <see cref="VerdictPath"/> for the status-independent attribution). Values:
|
|
/// - "" / null: agent-reported (default; terminal enrollment_failed event)
|
|
/// - "rule:<RuleId>": session failed because an analyze rule with MarkSessionAsFailed fired
|
|
/// - "manual": operator flipped the session via the portal
|
|
/// - "max_lifetime_watchdog": the agent's max-lifetime shutdown verdict was Failed
|
|
/// Consumers use this to render rule-based failures distinctly (badge + link to rule).
|
|
/// </summary>
|
|
public string FailureSource { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Machine-readable origin of the CURRENT <see cref="Status"/> — which code path wrote it
|
|
/// (<see cref="VerdictPaths"/> vocabulary, e.g. <c>agent:complete</c>, <c>sweep:r5_incomplete</c>,
|
|
/// <c>manual:failed</c>). Stamped on every status write regardless of status, so the verdict
|
|
/// calibration aggregate can count each path. Null on rows written before instrumentation
|
|
/// (read-side derivation covers those).
|
|
/// </summary>
|
|
public string? VerdictPath { get; set; }
|
|
|
|
/// <summary>
|
|
/// The verdict this session carried BEFORE the current status overrode it — set only when a
|
|
/// write replaces a prior Succeeded/Failed/Incomplete/AwaitingUser (admin mark, late agent
|
|
/// completion upgrading a sweep verdict, retro-reclassification). Together with
|
|
/// <see cref="PriorVerdictPath"/> this is the correction stream: "path X was overridden".
|
|
/// Null when the status never overrode a verdict.
|
|
/// </summary>
|
|
public string? PriorStatus { get; set; }
|
|
|
|
/// <summary>The <see cref="VerdictPath"/> of the overridden prior verdict; see <see cref="PriorStatus"/>.</summary>
|
|
public string? PriorVerdictPath { get; set; }
|
|
|
|
/// <summary>
|
|
/// Non-empty only when the BACKEND (not the agent) declared this session Succeeded:
|
|
/// either the maintenance timeout sweep reconciled it (e.g. "user completed setup —
|
|
/// desktop + Windows Hello observed") or a late completion report upgraded a prior
|
|
/// Failed/Incomplete/AwaitingUser verdict. Carries the human-readable justification so
|
|
/// operators can always tell a backend-declared success from an agent-reported one.
|
|
/// Admin-marked successes are attributed via <see cref="AdminMarkedAction"/> instead
|
|
/// and leave this empty.
|
|
/// </summary>
|
|
public string ReconcileReason { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// True only on a Succeeded session that completed while an ESP-failure advisory was
|
|
/// still unresolved: the ESP demonstrably gave up on at least one blocking item
|
|
/// (typically the 30-min timeout wall) and the user most likely pressed
|
|
/// "Continue anyway" — the device reached a working desktop, but not everything the
|
|
/// ESP tracked finished inside its window. Rendered as the amber "completed with
|
|
/// issues" badge. False/absent = clean success.
|
|
/// </summary>
|
|
public bool EspSoftFailure { get; set; }
|
|
|
|
/// <summary>
|
|
/// How a soft-failure completion was detected. Values:
|
|
/// - "continue_anyway_observation": Device-phase ESP failure, tenant opted into the
|
|
/// Continue-Anyway observation window, real-user desktop arrived inside it
|
|
/// - "continue_anyway_post_accountsetup": classic advisory defang (AccountSetup had
|
|
/// already been entered when the ESP failure fired)
|
|
/// Empty on clean successes.
|
|
/// </summary>
|
|
public string CompletionSource { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Non-null only when an administrator explicitly flipped the session via the portal
|
|
/// (MarkSessionSucceeded / MarkSessionFailed). Values: <c>null</c> (default, agent-driven),
|
|
/// <c>"Succeeded"</c>, <c>"Failed"</c>.
|
|
/// <para>
|
|
/// This is the authoritative source for the backend's <c>AdminAction</c> response field
|
|
/// sent to agents. Previously the backend inferred admin-override from "status is terminal
|
|
/// + current event is not a completion marker", which fired falsely on every post-completion
|
|
/// event the agent sent (agent_shutting_down, diagnostics_uploaded, enrollment_summary_shown).
|
|
/// The dedicated field eliminates that false-positive.
|
|
/// </para>
|
|
/// </summary>
|
|
public string? AdminMarkedAction { get; set; }
|
|
|
|
/// <summary>
|
|
/// Which backend device-validation path accepted this device at session registration —
|
|
/// <see cref="ValidatorType"/> name as string: "AutopilotV1" (Autopilot S/N lookup),
|
|
/// "CorporateIdentifier", "DeviceAssociation" (DevPrep, future hard gate), or
|
|
/// "Bootstrap" (pre-MDM token). Latest non-Unknown validation wins (a Bootstrap
|
|
/// session re-registering under cert auth upgrades to the cert-path validator).
|
|
/// Empty for sessions that predate this field or tenants with device validation off.
|
|
/// </summary>
|
|
public string ValidatedBy { get; set; } = string.Empty;
|
|
|
|
public int EventCount { get; set; }
|
|
public int? DurationSeconds { get; set; }
|
|
|
|
/// <summary>
|
|
/// Session-wide average HTTP round-trip latency (ms) of the agent's calls to the
|
|
/// backend API, as measured on the device (includes network RTT + TLS + server
|
|
/// processing). Projected at ingest from the cumulative counters of the latest
|
|
/// <c>agent_metrics_snapshot</c> (<c>net_total_latency_ms / net_total_requests</c>).
|
|
/// Null for V1 sessions and sessions from agents that predate the field.
|
|
/// Aggregate across sessions weighted by <see cref="ApiRequestCount"/> — e.g. per
|
|
/// GeoCountry to compare regional backend reachability.
|
|
/// </summary>
|
|
public double? AvgApiLatencyMs { get; set; }
|
|
|
|
/// <summary>
|
|
/// Number of HTTP requests behind <see cref="AvgApiLatencyMs"/> (the cumulative
|
|
/// <c>net_total_requests</c> at the latest snapshot). Needed as the weight when
|
|
/// averaging latency across sessions. Null whenever <see cref="AvgApiLatencyMs"/> is.
|
|
/// </summary>
|
|
public int? ApiRequestCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// Active network connection type during enrollment: "WiFi" or "Ethernet".
|
|
/// Projected at ingest from the latest <c>network_interface_info</c> event
|
|
/// (last emission wins — a device that switches media mid-enrollment reports
|
|
/// the most recent state). Null for sessions that predate the projection.
|
|
/// </summary>
|
|
public string? ConnectionType { get; set; }
|
|
|
|
/// <summary>
|
|
/// Enrollment type: "v1" (Autopilot Classic/ESP) or "v2" (Windows Device Preparation).
|
|
/// Defaults to "v1" for sessions that predate this field.
|
|
/// </summary>
|
|
public string EnrollmentType { get; set; } = "v1";
|
|
|
|
/// <summary>
|
|
/// Blob name of the uploaded diagnostics archive (null if not uploaded).
|
|
/// Used to construct a download URL.
|
|
/// <para>
|
|
/// Path semantics depend on <see cref="DiagnosticsBlobDestination"/>:
|
|
/// <list type="bullet">
|
|
/// <item><c>CustomerSas</c>: blob name only (e.g. <c>AgentDiagnostics-...zip</c>);
|
|
/// download builds the URL from the tenant's container SAS.</item>
|
|
/// <item><c>Hosted</c>: full path including the <c>{tenantId}/</c> prefix
|
|
/// (e.g. <c>{tenantId}/AgentDiagnostics-...zip</c>); download streams
|
|
/// directly via the backend connection string.</item>
|
|
/// </list>
|
|
/// </para>
|
|
/// </summary>
|
|
public string DiagnosticsBlobName { get; set; } = default!;
|
|
|
|
/// <summary>
|
|
/// Where the diagnostics archive for THIS session was uploaded.
|
|
/// Frozen at upload time so the download path can route correctly even if the
|
|
/// tenant later switches <see cref="TenantConfiguration.DiagnosticsUploadDestination"/>.
|
|
/// <list type="bullet">
|
|
/// <item><c>"CustomerSas"</c> — blob lives in the customer's storage; download uses
|
|
/// the tenant's SAS URL (current behaviour).</item>
|
|
/// <item><c>"Hosted"</c> — blob lives in the backend's storage under
|
|
/// <c>{tenantId}/</c>; download streams via the Functions connection string.</item>
|
|
/// <item><c>null</c> (legacy rows that predate this field) — treated as
|
|
/// <c>"CustomerSas"</c> by the download path for back-compat.</item>
|
|
/// </list>
|
|
/// </summary>
|
|
public string? DiagnosticsBlobDestination { get; set; }
|
|
|
|
/// <summary>
|
|
/// Timestamp of the most recently received event for this session.
|
|
/// Updated on every event batch ingestion. Used by maintenance to detect
|
|
/// sessions that are still actively sending data beyond the configured window.
|
|
/// Null for sessions that predate this field.
|
|
/// </summary>
|
|
public DateTime? LastEventAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// SERVER-side time of the most recent event ingest for this session.
|
|
/// <para>
|
|
/// <see cref="LastEventAt"/> lives in the DEVICE clock frame (it is the maximum
|
|
/// agent-supplied event timestamp). Comparing it against a server-derived cutoff mixes
|
|
/// two clock frames: a device whose clock runs slow — or one whose IME-derived events
|
|
/// carry a timezone skew — is judged silent far too early or not at all (measured in the
|
|
/// field: skews from -17 h to +1 h, see tasks/todo.md). This field is stamped from the
|
|
/// server clock on every ingest, so silence detection can compare like with like.
|
|
/// </para>
|
|
/// Null for sessions that predate this field — callers MUST fall back to
|
|
/// <see cref="LastEventAt"/> so rollout has no blind window.
|
|
/// </summary>
|
|
public DateTime? LastIngestAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether this session used WhiteGlove (Pre-Provisioning).
|
|
/// Set when a whiteglove_complete event is processed.
|
|
/// </summary>
|
|
public bool IsPreProvisioned { get; set; }
|
|
|
|
/// <summary>
|
|
/// Timestamp when the WhiteGlove session resumed for user enrollment (Part 2).
|
|
/// Set when the agent sends a whiteglove_resumed event or re-registers from Pending state.
|
|
/// Used to compute the user enrollment duration (Duration 2) for Teams notifications.
|
|
/// </summary>
|
|
public DateTime? ResumedAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Timestamp when the session was marked as Stalled.
|
|
/// Set when the agent sends a session_stalled event (after 60 min without progress)
|
|
/// or when the backend 2h maintenance sweep detects agent silence.
|
|
/// Cleared (null) when the session heals back to InProgress via a new real event.
|
|
/// </summary>
|
|
public DateTime? StalledAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the Autopilot profile indicates Hybrid Azure AD Join.
|
|
/// Derived from CloudAssignedDomainJoinMethod == 1 in the Autopilot profile.
|
|
/// </summary>
|
|
public bool IsHybridJoin { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the Autopilot profile carries the self-deploying/kiosk OOBE marker
|
|
/// (CloudAssignedOobeConfig bits 0x20|0x40). Sent by the agent at registration;
|
|
/// sticky-true across re-registrations.
|
|
/// </summary>
|
|
public bool IsSelfDeployingProfile { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the agent identified the device as a Windows 365 Cloud PC (Windows365
|
|
/// registry key + CloudManagedDesktopExtension service, marker AND). Sent by the
|
|
/// agent at registration; sticky-true across re-registrations. Agent-reported
|
|
/// context — independent of ValidatedBy == CloudPc, which is the server-derived
|
|
/// cert-CN-bound Graph verification.
|
|
/// </summary>
|
|
public bool IsCloudPc { get; set; }
|
|
|
|
// Device detail fields — stored in the Sessions table but omitted from earlier versions
|
|
public string OsName { get; set; } = default!;
|
|
public string OsBuild { get; set; } = default!;
|
|
public string OsDisplayVersion { get; set; } = default!;
|
|
public string OsEdition { get; set; } = default!;
|
|
public string OsLanguage { get; set; } = default!;
|
|
public bool IsUserDriven { get; set; }
|
|
public string AgentVersion { get; set; } = default!;
|
|
public string ImeAgentVersion { get; set; } = default!;
|
|
|
|
// Geographic location fields — populated from device_location event geo data
|
|
public string GeoCountry { get; set; } = string.Empty;
|
|
public string GeoRegion { get; set; } = string.Empty;
|
|
public string GeoCity { get; set; } = string.Empty;
|
|
public string GeoLoc { get; set; } = string.Empty;
|
|
|
|
// Script execution counts — incremented during ingest
|
|
public int PlatformScriptCount { get; set; }
|
|
public int RemediationScriptCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// Number of system reboots observed during the enrollment. Counts the agent's
|
|
/// <c>system_reboot_detected</c> events (V2 only — one per reboot, detected via the
|
|
/// System event-log boot time on the next agent start). Maintained incrementally per
|
|
/// ingest batch for a live value, then overwritten with an authoritative distinct count
|
|
/// from the Events table when the session reaches a terminal status (self-corrects any
|
|
/// at-least-once batch double-count). 0 for V1 sessions and sessions that predate this field.
|
|
/// </summary>
|
|
public int RebootCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// True once maintenance has emitted an ExcessiveSessionEvents ops alert for this session.
|
|
/// Prevents duplicate alerts on subsequent maintenance runs for the same runaway session.
|
|
/// </summary>
|
|
public bool ExcessiveEventsAlerted { get; set; }
|
|
|
|
/// <summary>
|
|
/// True once maintenance has auto-blocked or auto-killed the device for this runaway
|
|
/// session (see <see cref="AutopilotMonitor.Shared.Models.AdminConfiguration.ExcessiveEventAutoActionMode"/>).
|
|
/// Independent of <see cref="ExcessiveEventsAlerted"/> so warn and auto-action are
|
|
/// each idempotent on their own — admins can change the auto-action mode mid-flight
|
|
/// without re-firing the warn.
|
|
/// </summary>
|
|
public bool ExcessiveEventsAutoActioned { get; set; }
|
|
|
|
/// <summary>
|
|
/// JSON-serialized <see cref="System.Collections.Generic.List{T}"/> of <see cref="ServerAction"/>
|
|
/// pending delivery to the agent. Empty string when no actions are queued.
|
|
/// The Ingest function reads this alongside the session's status fields (no extra I/O),
|
|
/// attaches the actions to the response, and clears the column via a merge.
|
|
/// </summary>
|
|
public string PendingActionsJson { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// When the first pending action was queued. Used for TTL and staleness detection —
|
|
/// maintenance can purge actions older than a threshold to prevent zombie signals
|
|
/// on long-dead sessions.
|
|
/// </summary>
|
|
public DateTime? PendingActionsQueuedAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Compact JSON snapshot of "last known session state" written by the maintenance
|
|
/// 5h-timeout sweep when a session graduates to terminal Failed (Hybrid User-Driven
|
|
/// completion-gap fix, 2026-05-01). Captures the canonical lifecycle anchors —
|
|
/// last ESP phase, desktop arrival, Hello policy, AAD-join state, missing signals —
|
|
/// so operators don't have to scroll through hundreds of events to reconstruct
|
|
/// where a stuck session was when the watchdog fired. Empty / null on
|
|
/// healthy-completion paths and on sessions that predate the field. Built by
|
|
/// <see cref="AutopilotMonitor.Functions.Services.FailureSnapshotBuilder"/>.
|
|
/// </summary>
|
|
public string FailureSnapshotJson { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Cascade-delete state-machine value (Plan §1 P7 / PR3). One of
|
|
/// <see cref="AutopilotMonitor.Shared.Models.Deletion.SessionDeletionState"/> constants.
|
|
/// Empty / null means no cascade in flight (legacy rows; treated as <c>None</c>).
|
|
/// Written to the primary Sessions table only — the deletion CAS path does not sync
|
|
/// SessionsIndex, so this is NOT part of the index mirror today and is read on the
|
|
/// truth-served detail/guard paths. Mirroring it into SessionsIndex (so list/search can flag
|
|
/// locked sessions) is a deferred follow-up.
|
|
/// </summary>
|
|
public string DeletionState { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// ULID of the in-flight cascade manifest when <see cref="DeletionState"/> is non-None.
|
|
/// Used by the producer to detect concurrent re-enqueues (same ManifestId → resume,
|
|
/// different ManifestId → 409 Conflict). Null otherwise.
|
|
/// </summary>
|
|
public string? PendingDeletionManifestId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lightweight result for the global quick-search typeahead.
|
|
/// </summary>
|
|
public class QuickSearchResult
|
|
{
|
|
public string SessionId { get; set; } = string.Empty;
|
|
public string SerialNumber { get; set; } = string.Empty;
|
|
public string DeviceName { get; set; } = string.Empty;
|
|
public SessionStatus Status { get; set; }
|
|
public DateTime StartedAt { get; set; }
|
|
/// <summary>Which field matched the query: "sessionId", "serialNumber", or "deviceName".</summary>
|
|
public string MatchedField { get; set; } = string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Status of an enrollment session
|
|
/// </summary>
|
|
public enum SessionStatus
|
|
{
|
|
InProgress,
|
|
Pending, // WhiteGlove pre-provisioning complete, awaiting user enrollment
|
|
Stalled, // Agent reported no progress for >60 min, or backend sweep detected >2h silence. Non-terminal, can heal back to InProgress.
|
|
Succeeded,
|
|
Failed,
|
|
Unknown,
|
|
// Appended 2026-07-08 (tasks/enrollment-status-reclassification.md). New members
|
|
// MUST stay appended so existing persisted ordinals never shift.
|
|
AwaitingUser, // Device Setup (ESP DeviceSetup) fully succeeded but the user/Account-Setup
|
|
// phase has not completed and the agent went silent within the grace window.
|
|
// NON-TERMINAL: reconciles to Succeeded on a late completion, or graduates to
|
|
// Incomplete once SessionGraceHours elapses. NOT a failure.
|
|
Incomplete // Terminal, NON-FAILURE. The session never produced a terminal completion or
|
|
// failure signal and the grace window expired (or it went silent before Device
|
|
// Setup completed with no explicit failure). Excluded from the failure-rate
|
|
// denominator; surfaced to operators as "Incomplete".
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggregated session counters for the dashboard stats cards.
|
|
/// Computed server-side over a windowed scan of the SessionsIndex so the
|
|
/// numbers don't drift with whatever the client happens to have paginated
|
|
/// into memory.
|
|
/// </summary>
|
|
public class SessionStats
|
|
{
|
|
/// <summary>Window the windowed counters were computed over (matches request).</summary>
|
|
public int Days { get; set; }
|
|
|
|
/// <summary>InProgress sessions inside the window. Used for the "Active Sessions" card.</summary>
|
|
public int ActiveCount { get; set; }
|
|
|
|
/// <summary>Total sessions started inside the window.</summary>
|
|
public int TotalLastNDays { get; set; }
|
|
|
|
public int SucceededLastNDays { get; set; }
|
|
public int FailedLastNDays { get; set; }
|
|
|
|
/// <summary>
|
|
/// Terminal, non-failure sessions in the window (tasks/enrollment-status-reclassification.md):
|
|
/// the sweep saw no completion or explicit failure. Reported as the third headline bucket and
|
|
/// deliberately excluded from <see cref="SuccessRatePct"/> (which is over Succeeded + Failed only).
|
|
/// </summary>
|
|
public int IncompleteLastNDays { get; set; }
|
|
|
|
/// <summary>
|
|
/// Succeeded / (Succeeded + Failed) * 100, rounded. Zero when no terminal
|
|
/// sessions are in the window (the card renders "0%" rather than NaN).
|
|
/// </summary>
|
|
public int SuccessRatePct { get; set; }
|
|
|
|
/// <summary>Average duration of Succeeded sessions in the window, in minutes (rounded).
|
|
/// Kept for API compatibility; the dashboard card leads with the median, which
|
|
/// multi-hour outliers cannot drag around.</summary>
|
|
public int AvgDurationMinutes { get; set; }
|
|
|
|
/// <summary>Median duration in minutes over the same Succeeded population.</summary>
|
|
public int MedianDurationMinutes { get; set; }
|
|
|
|
/// <summary>90th-percentile duration in minutes over the same population — the tail signal.</summary>
|
|
public int P90DurationMinutes { get; set; }
|
|
|
|
/// <summary>Sessions whose StartedAt is on or after UTC midnight of the current day.</summary>
|
|
public int TotalToday { get; set; }
|
|
public int FailedToday { get; set; }
|
|
|
|
/// <summary>UTC timestamp of when the snapshot was produced (server clock).</summary>
|
|
public DateTime ComputedAt { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// A tracked IME version sighting. Permanent archive that survives data retention.
|
|
/// </summary>
|
|
public class ImeVersionHistoryEntry
|
|
{
|
|
public string Version { get; set; } = default!;
|
|
public DateTime FirstSeenAt { get; set; }
|
|
public string FirstSeenSessionId { get; set; } = string.Empty;
|
|
public string FirstSeenTenantId { get; set; } = string.Empty;
|
|
public DateTime LastSeenAt { get; set; }
|
|
public int SessionCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// Outcome of the automatic installer archiving (<c>ImeMsiArchiver</c>):
|
|
/// <c>Archived</c> or a <c>Failed:*</c> status; null for versions sighted before the
|
|
/// feature existed or while the archive job is still queued. Like the FirstSeen*
|
|
/// identifiers, the archive fields are only serialized for Global Admin callers.
|
|
/// </summary>
|
|
public string? MsiArchiveStatus { get; set; }
|
|
|
|
/// <summary>Blob path inside the <c>ime-archive</c> container, e.g. <c>1.104.102.0/IntuneWindowsAgent.msi</c>.</summary>
|
|
public string? MsiArchiveBlobPath { get; set; }
|
|
|
|
/// <summary>SHA-256 of the archived installer (hex, lowercase).</summary>
|
|
public string? MsiSha256 { get; set; }
|
|
|
|
/// <summary>Size of the archived installer in bytes.</summary>
|
|
public long? MsiBytes { get; set; }
|
|
|
|
/// <summary>The URL the installer was downloaded from (CSP-reported or canonical fallback).</summary>
|
|
public string? MsiSourceUrl { get; set; }
|
|
}
|
|
}
|