Compare commits

...

1 Commits

2 changed files with 97 additions and 21 deletions
@@ -47,6 +47,39 @@ function formatDuration(ms: number): string {
return `${hours}h ${remainingMinutes}m`;
}
interface DetectionBadge {
label: string;
/** Label below `sm`; null hides the pill there — it then appears in the row's Details only. */
phoneLabel: string | null;
tone: string;
title: string;
}
// IME's check right after the install, or after the restart the installer requested (D-392).
// Any result other than Detected/NotDetected is IME's DetectedDifferentVersion.
function postInstallDetectionBadge(result: string, afterReboot: boolean | undefined): DetectionBadge {
const check = `Intune's detection check right after the ${afterReboot ? "restart the installer requested" : "installer finished"}`;
if (result === "Detected") {
// Only confirms what the check icon and the green row already say, so on a phone it yields
// its room to the app name instead of squeezing it to a few letters.
return { label: "Detected after install", phoneLabel: null, tone: "bg-green-100 text-green-700", title: `${check}: the app was found.` };
}
if (result === "NotDetected") {
return {
label: "Not detected after install",
phoneLabel: "Not detected",
tone: "bg-red-200 text-red-800",
title: `${check}: the app was not found, so Intune counts the install as failed (0x87D1041C).`,
};
}
return {
label: "Different version detected",
phoneLabel: "Other version",
tone: "bg-amber-200 text-amber-700",
title: `${check}: a different version was found; Intune counts the app as installed.`,
};
}
export default function InstallProgress({ events, summaryStats, observedUntilMs = null, userPhaseBoundary = null }: InstallProgressProps) {
// Legacy-agent guard: split off app events replayed from a previous enrollment's IME log
// (newer agents suppress them at the source) so week-old installs never render as current.
@@ -263,6 +296,10 @@ function InstallItemRow({ item }: { item: InstallItem }) {
? liveElapsed.elapsedMs
: null;
// A failed row already says "Detection failed", so its NotDetected verdict is not repeated.
const detection = item.postInstallDetection && !(item.state === "Failed" && item.postInstallDetection === "NotDetected")
? postInstallDetectionBadge(item.postInstallDetection, item.postInstallAfterReboot)
: null;
// Session 080edee9 follow-up — only the genuine "no HRESULT available" timeout
// case wears the hedged orange treatment. Detection-failures and install-failures
@@ -343,29 +380,19 @@ function InstallItemRow({ item }: { item: InstallItem }) {
Uninstall
</span>
)}
{item.postInstallDetection && !(item.state === "Failed" && item.postInstallDetection === "NotDetected") && (
// IME's check right after the install (D-392); a failed row already says "Detection failed".
{detection && (
// The pill never shrinks (nowrap) while the name does, so on a phone a long label
// squeezed the app name to a few letters: short label there, or none at all.
<span
className={`text-xs px-2 py-0.5 rounded-full font-medium whitespace-nowrap ${
item.postInstallDetection === "Detected"
? "bg-green-100 text-green-700"
: item.postInstallDetection === "NotDetected"
? "bg-red-200 text-red-800"
: "bg-amber-200 text-amber-700"
}`}
title={`Intune's detection check right after the ${item.postInstallAfterReboot ? "restart the installer requested" : "installer finished"}: ${
item.postInstallDetection === "Detected"
? "the app was found."
: item.postInstallDetection === "NotDetected"
? "the app was not found, so Intune counts the install as failed (0x87D1041C)."
: "a different version was found; Intune counts the app as installed."
}`}
className={`text-xs px-2 py-0.5 rounded-full font-medium whitespace-nowrap ${detection.tone}${detection.phoneLabel == null ? " hidden sm:inline" : ""}`}
title={detection.title}
>
{item.postInstallDetection === "Detected"
? "Detected after install"
: item.postInstallDetection === "NotDetected"
? "Not detected after install"
: "Different version detected"}
{detection.phoneLabel == null ? detection.label : (
<>
<span className="sm:hidden">{detection.phoneLabel}</span>
<span className="hidden sm:inline">{detection.label}</span>
</>
)}
</span>
)}
{item.state === "Failed" && item.isLikelyStuck && (
@@ -516,6 +543,15 @@ function InstallItemRow({ item }: { item: InstallItem }) {
</div>
) : null}
{/* Phones: the row header shows the detection result short or not at all, and touch has
no hover tooltip — Details carries the full verdict. */}
{showDetails && detection && (
<div className="sm:hidden mt-3 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
<span className={`px-2 py-0.5 rounded-full font-medium whitespace-nowrap ${detection.tone}`}>{detection.label}</span>
<span className="text-gray-600">{detection.title}</span>
</div>
)}
{/* Event details (expandable) */}
{showDetails && item.eventData && (
<div className="mt-3 p-2 bg-gray-900 rounded text-xs text-gray-100 font-mono overflow-x-auto">
@@ -0,0 +1,40 @@
import { describe, it, expect } from "vitest";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import InstallProgress from "../InstallProgress";
import type { InstallEvent } from "@/lib/installProgress";
function evt(eventType: string, timestamp: string, data: Record<string, unknown>): InstallEvent {
return { eventType, timestamp, data };
}
function renderWithCheckAfterInstall(postInstallDetection: string): string {
return renderToStaticMarkup(
createElement(InstallProgress, {
events: [
evt("app_install_started", "2026-10-21T16:40:00Z", { appName: "Contoso VPN" }),
evt("app_install_completed", "2026-10-21T16:42:39Z", { appName: "Contoso VPN", state: "Installed" }),
evt("app_post_install_detection", "2026-10-21T16:42:41Z", { appName: "Contoso VPN", postInstallDetection, afterReboot: "false" }),
],
}),
);
}
describe("InstallProgress — detection pill on narrow screens", () => {
it("hides the green confirmation below sm so the app name keeps its room", () => {
const html = renderWithCheckAfterInstall("Detected");
expect(html).toContain("bg-green-100 text-green-700 hidden sm:inline");
expect(html).toContain(">Detected after install</span>");
expect(html).not.toContain('class="sm:hidden"');
});
it("keeps the red and amber verdicts on phones, with a short label", () => {
const notDetected = renderWithCheckAfterInstall("NotDetected");
expect(notDetected).toContain('<span class="sm:hidden">Not detected</span>');
expect(notDetected).toContain('<span class="hidden sm:inline">Not detected after install</span>');
const otherVersion = renderWithCheckAfterInstall("DetectedDifferentVersion");
expect(otherVersion).toContain('<span class="sm:hidden">Other version</span>');
expect(otherVersion).toContain('<span class="hidden sm:inline">Different version detected</span>');
});
});