Date
+ Type
Session
Tenant
Submitted By
@@ -321,8 +337,13 @@ export function SessionReportsSection({
{new Date(r.submittedAt).toLocaleString()}
+
+
+
- {r.sessionId.length > 8 ? `${r.sessionId.slice(0, 8)}...` : r.sessionId}
+ {r.reportType === "diagFiles"
+ ? —
+ : r.sessionId.length > 8 ? `${r.sessionId.slice(0, 8)}...` : r.sessionId}
{r.tenantId.length > 8 ? `${r.tenantId.slice(0, 8)}...` : r.tenantId}
@@ -396,12 +417,20 @@ export function SessionReportsSection({
-
Session ID
-
- {selectedReport.sessionId}
-
+ Type
+
+
+ {selectedReport.reportType !== "diagFiles" && selectedReport.sessionId && (
+
+
Session ID
+
+ {selectedReport.sessionId}
+
+
+
+ )}
Tenant ID
diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/[section]/page.tsx b/src/Web/autopilot-monitor-web/app/settings/tenant/[section]/page.tsx
index 7e924f15..94fd52a8 100644
--- a/src/Web/autopilot-monitor-web/app/settings/tenant/[section]/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/settings/tenant/[section]/page.tsx
@@ -9,6 +9,7 @@ import { SectionNotifications } from "../sections/SectionNotifications";
import { SectionAccessManagement } from "../sections/SectionAccessManagement";
import { SectionBootstrapSessions } from "../sections/SectionBootstrapSessions";
import { SectionSlaTargets } from "../sections/SectionSlaTargets";
+import { SectionSubmitLogs } from "../sections/SectionSubmitLogs";
const SECTION_COMPONENTS: Record = {
"autopilot": SectionAutopilotValidation,
@@ -17,6 +18,7 @@ const SECTION_COMPONENTS: Record = {
"sla-targets": SectionSlaTargets,
"access-management": SectionAccessManagement,
"bootstrap-sessions": SectionBootstrapSessions,
+ "support": SectionSubmitLogs,
};
export default function TenantSectionPage() {
diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionSubmitLogs.tsx b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionSubmitLogs.tsx
new file mode 100644
index 00000000..c5b889f6
--- /dev/null
+++ b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionSubmitLogs.tsx
@@ -0,0 +1,329 @@
+"use client";
+
+import { useRef, useState } from "react";
+import { zipSync } from "fflate";
+import { useAuth } from "../../../../contexts/AuthContext";
+import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
+import { api } from "@/lib/api";
+import { trackEvent } from "@/lib/appInsights";
+
+const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5 MB
+const LOG_ACCEPT = ".log,.txt,.zip,.json,.jsonl,.ndjson";
+
+function formatFileSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+async function fileToBase64(file: File): Promise {
+ const buffer = await file.arrayBuffer();
+ return btoa(
+ new Uint8Array(buffer).reduce((data, byte) => data + String.fromCharCode(byte), ""),
+ );
+}
+
+async function bundleFiles(files: File[], zipName: string): Promise<{ base64: string; fileName: string } | null> {
+ if (files.length === 0) return null;
+ if (files.length === 1) {
+ return { base64: await fileToBase64(files[0]), fileName: files[0].name };
+ }
+ const entries: Record = {};
+ for (const f of files) {
+ entries[f.name] = new Uint8Array(await f.arrayBuffer());
+ }
+ const zipped = zipSync(entries);
+ const base64 = btoa(zipped.reduce((data, byte) => data + String.fromCharCode(byte), ""));
+ return { base64, fileName: zipName };
+}
+
+export function SectionSubmitLogs() {
+ const { user, getAccessToken } = useAuth();
+
+ const [comment, setComment] = useState("");
+ const [email, setEmail] = useState("");
+ const [logFiles, setLogFiles] = useState([]);
+ const [screenshotFiles, setScreenshotFiles] = useState([]);
+ const [logError, setLogError] = useState(null);
+ const [submitting, setSubmitting] = useState(false);
+ const [submitResult, setSubmitResult] = useState<"success" | "error" | null>(null);
+ const [submitErrorMessage, setSubmitErrorMessage] = useState(null);
+
+ const logInputRef = useRef(null);
+ const screenshotInputRef = useRef(null);
+
+ const addLogs = (newFiles: File[]) => {
+ setLogError(null);
+ const merged = [...logFiles];
+ for (const f of newFiles) {
+ if (!merged.some(existing => existing.name === f.name && existing.size === f.size)) {
+ merged.push(f);
+ }
+ }
+ const totalSize = merged.reduce((sum, f) => sum + f.size, 0);
+ if (totalSize > MAX_LOG_SIZE) {
+ setLogError(`Total size (${formatFileSize(totalSize)}) exceeds 5 MB limit. Remove some files or add smaller ones.`);
+ return;
+ }
+ setLogFiles(merged);
+ if (logInputRef.current) logInputRef.current.value = "";
+ };
+
+ const removeLog = (index: number) => {
+ setLogError(null);
+ setLogFiles(prev => prev.filter((_, i) => i !== index));
+ };
+
+ const addScreenshots = (newFiles: File[]) => {
+ const merged = [...screenshotFiles];
+ for (const f of newFiles) {
+ if (!merged.some(existing => existing.name === f.name && existing.size === f.size)) {
+ merged.push(f);
+ }
+ }
+ setScreenshotFiles(merged);
+ if (screenshotInputRef.current) screenshotInputRef.current.value = "";
+ };
+
+ const removeScreenshot = (index: number) => {
+ setScreenshotFiles(prev => prev.filter((_, i) => i !== index));
+ };
+
+ const handleSubmit = async () => {
+ if (!user?.tenantId) {
+ setSubmitResult("error");
+ setSubmitErrorMessage("Tenant context unavailable. Please reload the page.");
+ return;
+ }
+ setSubmitting(true);
+ setSubmitResult(null);
+ setSubmitErrorMessage(null);
+ try {
+ const logBundle = await bundleFiles(logFiles, "diag-files.zip");
+ const screenshotBundle = await bundleFiles(screenshotFiles, "screenshots.zip");
+
+ const response = await authenticatedFetch(
+ api.diagFilesReports.submit(),
+ getAccessToken,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ tenantId: user.tenantId,
+ comment,
+ email,
+ screenshotBase64: screenshotBundle?.base64 ?? null,
+ screenshotFileName: screenshotBundle?.fileName ?? null,
+ agentLogBase64: logBundle?.base64 ?? null,
+ agentLogFileName: logBundle?.fileName ?? null,
+ }),
+ },
+ );
+
+ if (!response.ok) {
+ const data = await response.json().catch(() => null);
+ throw new Error(data?.message || `Failed to submit (${response.status}).`);
+ }
+
+ trackEvent("diag_files_report_submitted");
+ setSubmitResult("success");
+ setComment("");
+ setEmail("");
+ setLogFiles([]);
+ setScreenshotFiles([]);
+ setLogError(null);
+ } catch (err: unknown) {
+ if (err instanceof TokenExpiredError) {
+ setSubmitErrorMessage("Session expired. Please reload the page and try again.");
+ } else {
+ setSubmitErrorMessage(err instanceof Error ? err.message : "Failed to submit report.");
+ }
+ setSubmitResult("error");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const logTotalSize = logFiles.reduce((sum, f) => sum + f.size, 0);
+ const hasAnyContent = comment.trim().length > 0 || logFiles.length > 0 || screenshotFiles.length > 0;
+
+ return (
+
+
+
+
+
Submit Logs to Support
+
+ Private Preview
+
+
+
+
+
+ Send log files, JSON state snapshots, or screenshots to the Autopilot Monitor team for
+ analysis — no need to share via OneDrive. Submissions are stored in the same backend
+ pipeline as Session Reports and visible only to Global Admins.
+
+
+ {submitResult === "success" && (
+
+
+
+
+
+
Report submitted successfully
+
+ The diagnostic files have been sent for analysis by the Autopilot Monitor team.
+
+
+
+ )}
+
+ {submitResult === "error" && submitErrorMessage && (
+
+
+
+
+
+
Failed to submit report
+
{submitErrorMessage}
+
+
+ )}
+
+ {/* Comment */}
+
+
+ Comment (optional)
+
+
+
+ {/* Email */}
+
+
+ Email (optional)
+
+
setEmail(e.target.value)}
+ placeholder="your.email@company.com"
+ className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+ disabled={submitting}
+ />
+
+ No guarantee of response. Issues may be silently fixed — check the changelog.
+
+
+
+ {/* Log / state files */}
+
+
+ Log & state files (optional, max 5 MB total)
+
+
addLogs(e.target.files ? Array.from(e.target.files) : [])}
+ className="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
+ disabled={submitting}
+ />
+ {logError &&
{logError}
}
+ {logFiles.length > 0 && (
+
+ {logFiles.map((file, i) => (
+
+
{file.name} ({formatFileSize(file.size)})
+
removeLog(i)}
+ disabled={submitting}
+ className="flex-shrink-0 text-gray-400 hover:text-red-500 disabled:opacity-50"
+ title="Remove"
+ >
+
+
+
+
+
+ ))}
+
+ {logFiles.length} file{logFiles.length !== 1 ? "s" : ""} — {formatFileSize(logTotalSize)} total
+
+
+ )}
+
+ Accepted: .log, .txt, .zip, .json, .jsonl, .ndjson. Multiple files are zipped automatically.
+
+
+
+ {/* Screenshots */}
+
+
+ Screenshots (optional)
+
+
addScreenshots(e.target.files ? Array.from(e.target.files) : [])}
+ className="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
+ disabled={submitting}
+ />
+ {screenshotFiles.length > 0 && (
+
+ {screenshotFiles.map((file, i) => (
+
+
{file.name} ({formatFileSize(file.size)})
+
removeScreenshot(i)}
+ disabled={submitting}
+ className="flex-shrink-0 text-gray-400 hover:text-red-500 disabled:opacity-50"
+ title="Remove"
+ >
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ {submitting ? (
+ <>
+
+ Submitting...
+ >
+ ) : (
+ "Submit Report"
+ )}
+
+
+
+ );
+}
diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/tenantNavSections.ts b/src/Web/autopilot-monitor-web/app/settings/tenant/tenantNavSections.ts
index 169003be..60f2d0cc 100644
--- a/src/Web/autopilot-monitor-web/app/settings/tenant/tenantNavSections.ts
+++ b/src/Web/autopilot-monitor-web/app/settings/tenant/tenantNavSections.ts
@@ -5,6 +5,7 @@ export const TENANT_NAV_SECTIONS = [
{ id: "notifications", label: "Notifications", description: "Webhook notification configuration" },
{ id: "sla-targets", label: "SLA Targets", description: "SLA targets and breach notification settings" },
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", description: "Create and manage bootstrap tokens" },
+ { id: "support", label: "Submit Logs", description: "Send diagnostic files to the Autopilot Monitor team" },
] as const;
export type TenantSectionId = (typeof TENANT_NAV_SECTIONS)[number]["id"];
diff --git a/src/Web/autopilot-monitor-web/lib/api.ts b/src/Web/autopilot-monitor-web/lib/api.ts
index aeb2fb7b..dcbca23d 100644
--- a/src/Web/autopilot-monitor-web/lib/api.ts
+++ b/src/Web/autopilot-monitor-web/lib/api.ts
@@ -285,6 +285,11 @@ export const api = {
`${API_BASE_URL}/api/global/session-reports/${reportId}/note`,
},
+ // ── Diag Files Reports (no session context) ───────────────────────────────
+ diagFilesReports: {
+ submit: () => `${API_BASE_URL}/api/diag-files-reports`,
+ },
+
// ── Distress Reports ──────────────────────────────────────────────────────
distressReports: {
list: () => `${API_BASE_URL}/api/global/distress-reports`,