- Screenshots (optional)
+ Screenshots (optional, max 8 MB total)
+ {screenshotError && (
+
{screenshotError}
+ )}
{screenshotFiles.length > 0 && (
{screenshotFiles.map((file, i) => (
@@ -374,6 +393,12 @@ export default function ReportSessionModal({
{screenshotFiles.length > 1 &&
{screenshotFiles.length} screenshots (will be zipped) }
{hasDiagnostics && includeDiagnostics &&
Uploaded diagnostics archive (copied server-side) }
+ {eventsStreaming && (
+
+ ⚠ The event timeline is still loading — a report submitted now will contain a
+ partial export. Wait a moment for the complete timeline if possible.
+
+ )}
>
)}
@@ -398,7 +423,7 @@ export default function ReportSessionModal({
{submitting ? (
diff --git a/src/Web/autopilot-monitor-web/app/sessions/page.tsx b/src/Web/autopilot-monitor-web/app/sessions/page.tsx
index 7e2889fc..be8d86d1 100644
--- a/src/Web/autopilot-monitor-web/app/sessions/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/sessions/page.tsx
@@ -305,7 +305,12 @@ function SessionDetailContent() {
screenshotFileName,
agentLogBase64,
agentLogFileName,
- includeDiagnostics
+ includeDiagnostics,
+ // Completeness signal: lets the operator detect a partial export
+ // (client had not finished streaming all event pages at submit time).
+ exportedEventCount: events.length,
+ sessionEventCount: session?.eventCount,
+ eventStreamActive: eventsApi.isStreamingMore
})
}
);
@@ -782,6 +787,7 @@ function SessionDetailContent() {
onSubmit={handleSubmitReport}
onCancel={() => setShowReportModal(false)}
submitting={reportSubmitting}
+ eventsStreaming={eventsApi.isStreamingMore}
/>
diff --git a/src/Web/autopilot-monitor-web/utils/sessionExportUtils.ts b/src/Web/autopilot-monitor-web/utils/sessionExportUtils.ts
index f16c5849..e9cdd557 100644
--- a/src/Web/autopilot-monitor-web/utils/sessionExportUtils.ts
+++ b/src/Web/autopilot-monitor-web/utils/sessionExportUtils.ts
@@ -103,13 +103,30 @@ export interface SessionCsvData {
lastEventAt?: string;
durationSeconds: number;
diagnosticsBlobName?: string;
+ rebootCount?: number;
+ failureSource?: string;
+ reconcileReason?: string;
+ adminMarkedAction?: string;
+ validatedBy?: string;
+ isHybridJoin?: boolean;
+ isSelfDeployingProfile?: boolean;
+ connectionType?: string;
+ geoCountry?: string;
+ geoRegion?: string;
+ geoCity?: string;
+ avgApiLatencyMs?: number;
+ apiRequestCount?: number;
+ stalledAt?: string;
+ failureSnapshotJson?: string;
}
export function generateSessionCsvExport(session: SessionCsvData): string {
const esc = (v: string | undefined | null) => `"${String(v ?? "").replace(/"/g, '""')}"`;
- // Columns match the Sessions Azure Table Storage schema exactly
+ // Columns mirror the Sessions Azure Table Storage schema
// PartitionKey = TenantId, RowKey = SessionId
- const header = "PartitionKey,RowKey,SerialNumber,DeviceName,Manufacturer,Model,OsName,OsBuild,OsDisplayVersion,OsEdition,OsLanguage,IsUserDriven,IsPreProvisioned,StartedAt,CompletedAt,AgentVersion,EnrollmentType,CurrentPhase,Status,EventCount,FailureReason,LastEventAt,DurationSeconds,DiagnosticsBlobName";
+ // Newer fields are APPENDED (never inserted) so the column prefix stays stable
+ // for anything parsing older exports.
+ const header = "PartitionKey,RowKey,SerialNumber,DeviceName,Manufacturer,Model,OsName,OsBuild,OsDisplayVersion,OsEdition,OsLanguage,IsUserDriven,IsPreProvisioned,StartedAt,CompletedAt,AgentVersion,EnrollmentType,CurrentPhase,Status,EventCount,FailureReason,LastEventAt,DurationSeconds,DiagnosticsBlobName,RebootCount,FailureSource,ReconcileReason,AdminMarkedAction,ValidatedBy,IsHybridJoin,IsSelfDeployingProfile,ConnectionType,GeoCountry,GeoRegion,GeoCity,AvgApiLatencyMs,ApiRequestCount,StalledAt,FailureSnapshotJson";
const row = [
esc(session.tenantId),
esc(session.sessionId),
@@ -135,6 +152,21 @@ export function generateSessionCsvExport(session: SessionCsvData): string {
esc(session.lastEventAt),
String(session.durationSeconds ?? ""),
esc(session.diagnosticsBlobName),
+ String(session.rebootCount ?? ""),
+ esc(session.failureSource),
+ esc(session.reconcileReason),
+ esc(session.adminMarkedAction),
+ esc(session.validatedBy),
+ String(session.isHybridJoin ?? ""),
+ String(session.isSelfDeployingProfile ?? ""),
+ esc(session.connectionType),
+ esc(session.geoCountry),
+ esc(session.geoRegion),
+ esc(session.geoCity),
+ String(session.avgApiLatencyMs ?? ""),
+ String(session.apiRequestCount ?? ""),
+ esc(session.stalledAt),
+ esc(session.failureSnapshotJson),
].join(",");
return "\uFEFF" + header + "\n" + row;
}