Compare commits

...

1 Commits

Author SHA1 Message Date
Claude 86b8621cff Install Progress on phones: the green detection pill moves into Details, red and amber shorten
The post-install detection pill never shrinks (nowrap), while the app name
(min-w-0) takes all of the squeeze. At phone width a row with "Detected after
install" left the name a letter or two.

Below the sm breakpoint:
- "Detected after install" is hidden. It only confirms what the check icon and
  the green row already say.
- "Not detected after install" and "Different version detected" stay visible
  as real warnings, with the short labels "Not detected" and "Other version".
- Details shows the full verdict and its explanation. Touch has no hover
  tooltip, and the event JSON there does not carry the detection result.

From sm upwards the row is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FumcijqRW3g5RPSGViS4c7
2026-10-31 07:45:16 +00:00
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>');
});
});