Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
032dcf5
docs(qa): add canonical user-story tracker and dialog landmarks
adamgell Aug 18, 2026
9619578
fix(log): apply cached tab snapshot before folder restore
adamgell Aug 18, 2026
5d74650
fix(qa): align tracker and File Open IME/dsregcmd paths
adamgell Aug 18, 2026
58332b7
fix(qa): close CodeRabbit typecheck and tab-switch gaps
adamgell Aug 18, 2026
e06fc81
fix(timeline): keep empty folder opens as a no-op
adamgell Aug 18, 2026
67e3d3f
fix(qa): close CodeRabbit review gaps for tracker
adamgell Aug 18, 2026
edf71c0
test: isolate SecureBoot platform fixture
adamgell Aug 18, 2026
14da6cf
fix: close PR 577 review regressions
adamgell Aug 19, 2026
e8eaa1d
fix: close async source loading races
adamgell Aug 19, 2026
87074a4
fix: isolate dropped path failures and stale restores
adamgell Aug 19, 2026
b1a06a5
fix: harden IPC responses and stale source loads
adamgell Aug 19, 2026
8d4b4b9
test: stabilize remaining PR 577 fixtures
adamgell Aug 19, 2026
6180f42
fix: validate every frontend command response
adamgell Aug 19, 2026
28352ae
fix: align E2E association status fixture
adamgell Aug 19, 2026
dff078a
fix: keep workspace decoder IDs exhaustive
adamgell Aug 19, 2026
f2ef5b1
fix: derive IPC responses from decoder registry
adamgell Aug 19, 2026
0aaa618
fix: harden workspace source loading
adamgell Aug 19, 2026
306f8cc
fix: preserve stale source load semantics
adamgell Aug 19, 2026
932857e
fix: close review gaps in source loading and timeline actions
adamgell Aug 19, 2026
16fc8ad
fix: reset replacement and progress ownership synchronously
adamgell Aug 19, 2026
4262a77
fix: close remaining review findings
adamgell Aug 19, 2026
51f2aef
fix: propagate event log parse failures
adamgell Aug 19, 2026
c9e7331
fix: close remaining review findings
adamgell Aug 19, 2026
6e584ef
test: close PR 577 review gaps
adamgell Aug 19, 2026
6737f27
test: close remaining PR 577 review gaps
adamgell Aug 19, 2026
75b0ea3
fix: close remaining PR 577 review gaps
adamgell Aug 19, 2026
329fdcc
fix: close remaining PR 577 review gaps
adamgell Aug 19, 2026
b427fa9
test: close remaining PR 577 review gaps
adamgell Aug 19, 2026
de71d85
fix: close remaining PR 577 review gaps
adamgell Aug 19, 2026
65a3504
fix: cover keyboard and selection review gaps
adamgell Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.

### Added

- **Canonical user-story tracker**: Inventory every chrome and workspace feature in `docs/qa/user-stories.csv` with expected behavior derived from the current code, plus fixture/RTL coverage for those surfaces.

- **Administrator restart with source restoration (#384)**: Application-wide restart as administrator on supported Windows builds, restoring open sources after elevation.
- **Company Portal macOS unified-log evidence (#390)**: Normalize Apple unified-log evidence for Company Portal on macOS so enrollment and portal diagnostics correlate without inventing outcomes.
- **Intune Device Inventory Agent log family (#397 / #354)**: Discover and parse the full Microsoft Device Inventory Agent log set under Program Files (harvester, Inventory Adaptor, and rotation-failure dialects) with known-sources entry, folder aggregation for `.log` / rotations / `.log_`, and logical-record-aware real-time tailing.
Expand All @@ -29,6 +31,11 @@ All notable changes to this project will be documented in this file.

### Fixed

- **Dialog landmarks**: Filter, Collect Diagnostics, Collection Complete, Update, and first-run file-association overlays expose `role="dialog"` / `aria-modal` so they are reachable as dialog landmarks.
- **Tab switch restores the selected log**: Cached tab switches apply the file snapshot before the sidebar folder listing, so swapping tabs no longer leaves the previous file on screen while the listing is in flight.
- **Timeline File > Open Folder IME sources**: Opening an IME log folder from File > Open now also adds the folder path so `extract_ime_events` can run, matching File > New Timeline from Folder. Empty folders are a no-op instead of a zero-event IntuneEvents source.
- **dsregcmd drag-and-drop**: Dropping a dsregcmd evidence path again analyzes the file with folder fallback and records Recent, instead of treating an uninspectable path as a generic file source.

- **Unicode decimal digit panics (#413 / #502)**: Reject non-ASCII Unicode decimal fields in CCM and related time grammars so multi-byte digits cannot panic the parser mid-slice.
- **Signless CCM timestamp display (#410 / #504)**: Treat signless fractional tails as milliseconds (not fabricated timezone offsets); short fractions pad correctly for public `LogEntry` projection.
- **IPv6 redaction residual (#416 / #503)**: Redact bare unspecified IPv6 forms in macOS export paths without destroying C++ `std::` symbols.
Expand Down
119 changes: 119 additions & 0 deletions docs/qa/user-stories.csv

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion e2e/fixtures/tauri-shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ const DEFAULT_RESPONSES: Record<string, unknown> = {
"esp-diagnostics",
],
get_known_log_sources: [],
get_file_association_prompt_status: "dismissed",
get_file_association_prompt_status: {
supported: false,
shouldPrompt: false,
isAssociated: false,
},
get_esp_elevation_state: {
isElevated: false,
restartSupported: true,
Expand Down
8 changes: 8 additions & 0 deletions src-tauri/src/commands/file_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,11 @@ pub fn open_log_file(
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct ParseProgressPayload {
request_id: u64,
file_path: String,
file_name: String,
completed: u32,
global_completed: u32,
total: u32,
entries: u32,
file_size: u64,
Expand All @@ -201,6 +203,8 @@ struct ParseProgressPayload {
#[tauri::command]
pub fn parse_files_batch(
paths: Vec<String>,
request_id: u64,
completed_offset: u32,
state: State<'_, AppState>,
app: AppHandle,
) -> Result<Vec<ParseResult>, crate::error::AppError> {
Expand Down Expand Up @@ -244,9 +248,11 @@ pub fn parse_files_batch(
let _ = app.emit(
"parse-progress",
ParseProgressPayload {
request_id,
file_path: path.clone(),
file_name,
completed: done,
global_completed: completed_offset.saturating_add(done),
total,
entries: result.entries.len() as u32,
file_size: result.file_size,
Expand All @@ -266,9 +272,11 @@ pub fn parse_files_batch(
let _ = app.emit(
"parse-progress",
ParseProgressPayload {
request_id,
file_path: path.clone(),
file_name,
completed: done,
global_completed: completed_offset.saturating_add(done),
total,
entries: 0,
file_size: 0,
Expand Down
8 changes: 8 additions & 0 deletions src/components/dialogs/AboutDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ describe("AboutDialog", () => {
getVersionMock.mockResolvedValue("1.3.2");
});

it("exposes a dialog landmark", async () => {
render(<AboutDialog isOpen onClose={() => {}} />);

const dialog = await screen.findByRole("dialog", { name: "About CMTrace Open" });
expect(dialog).toHaveAttribute("aria-modal", "true");
expect(await screen.findByText("CMTrace Open")).toBeVisible();
});

it("shows main channel app metadata", async () => {
render(<AboutDialog isOpen onClose={() => {}} />);

Expand Down
3 changes: 3 additions & 0 deletions src/components/dialogs/AboutDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export function AboutDialog({ isOpen, onClose }: AboutDialogProps) {
}}
>
<div
role="dialog"
aria-modal="true"
aria-label="About CMTrace Open"
Comment thread
adamgell marked this conversation as resolved.
style={{
backgroundColor: tokens.colorNeutralBackground1,
color: tokens.colorNeutralForeground1,
Expand Down
140 changes: 140 additions & 0 deletions src/components/dialogs/CollectDiagnosticsDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { CollectDiagnosticsDialog } from "./CollectDiagnosticsDialog";
import { CollectionCompleteDialog } from "./CollectionCompleteDialog";
import { useUiStore } from "../../stores/ui-store";
import { COLLECTION_PRESETS } from "../../lib/collection-categories";

const collectDiagnostics = vi.hoisted(() => vi.fn());

vi.mock("../../lib/commands", () => ({
collectDiagnostics,
}));

vi.mock("../../lib/log-source", () => ({
loadPathAsLogSource: vi.fn(),
}));

describe("CollectDiagnosticsDialog", () => {
afterEach(() => {
cleanup();
useUiStore.setState({ collectionProgress: null, collectionResult: null });
});

it("exposes a dialog landmark when open", () => {
render(<CollectDiagnosticsDialog isOpen onClose={() => {}} />);
const dialog = screen.getByRole("dialog", { name: "Collect Diagnostics" });
expect(dialog).toHaveAttribute("aria-modal", "true");
});

it("shows presets, category checkboxes, and starts collection", async () => {
collectDiagnostics.mockResolvedValue({
bundlePath: "C:/Users/Public/cmtrace-bundle",
bundleId: "bundle-1",
artifactCounts: { collected: 4, missing: 1, failed: 0, total: 5 },
durationMs: 1200,
gaps: [{ artifactId: "cbs", category: "general", reason: "not present" }],
});
const onClose = vi.fn();
render(<CollectDiagnosticsDialog isOpen onClose={onClose} />);

expect(screen.getByText("Collect Diagnostics")).toBeInTheDocument();
expect(screen.getByText("Quick Presets")).toBeInTheDocument();
for (const preset of COLLECTION_PRESETS) {
expect(screen.getByRole("button", { name: preset.label })).toBeInTheDocument();
}
expect(screen.getByText("Intune & MDM")).toBeInTheDocument();
expect(screen.getByText("Autopilot & Provisioning")).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: "Intune + Autopilot" }));
fireEvent.click(screen.getByRole("button", { name: "Collect" }));
expect(onClose).toHaveBeenCalled();
expect(collectDiagnostics).toHaveBeenCalled();
});
});

describe("CollectionCompleteDialog", () => {
afterEach(() => {
cleanup();
});

it("exposes a dialog landmark when complete", () => {
render(
<CollectionCompleteDialog
onClose={() => {}}
result={{
bundlePath: "C:/Users/Public/cmtrace-bundle",
bundleId: "bundle-1",
artifactCounts: { collected: 4, missing: 1, failed: 0, total: 5 },
durationMs: 1500,
gaps: [],
}}
/>,
);
const dialog = screen.getByRole("dialog", { name: "Collection Complete" });
expect(dialog).toHaveAttribute("aria-modal", "true");
});

it("traps focus and restores the opener when closed", () => {
const opener = document.createElement("button");
document.body.appendChild(opener);
opener.focus();

const rendered = render(
<CollectionCompleteDialog
onClose={() => {}}
result={{
bundlePath: "C:/Users/Public/cmtrace-bundle",
bundleId: "bundle-1",
artifactCounts: { collected: 1, missing: 0, failed: 0, total: 1 },
durationMs: 100,
gaps: [],
}}
/>,
);
const dialog = screen.getByRole("dialog", { name: "Collection Complete" });
const close = screen.getByRole("button", { name: "Close" });
const openBundle = screen.getByRole("button", { name: "Open Bundle" });

expect(dialog.contains(document.activeElement)).toBe(true);
expect(document.activeElement).toBe(close);

openBundle.focus();
fireEvent.keyDown(window, { key: "Tab" });
expect(document.activeElement).toBe(close);
fireEvent.keyDown(window, { key: "Tab", shiftKey: true });
expect(document.activeElement).toBe(openBundle);

opener.focus();
fireEvent.keyDown(window, { key: "Tab" });
expect(document.activeElement).toBe(close);

rendered.rerender(<CollectionCompleteDialog onClose={() => {}} result={null} />);
expect(document.activeElement).toBe(opener);
opener.remove();
});

it("shows counts, gaps, Close, and Open Bundle", () => {
const onClose = vi.fn();
render(
<CollectionCompleteDialog
onClose={onClose}
result={{
bundlePath: "C:/Users/Public/cmtrace-bundle",
bundleId: "bundle-1",
artifactCounts: { collected: 4, missing: 1, failed: 0, total: 5 },
durationMs: 1500,
gaps: [{ artifactId: "cbs", category: "general", reason: "CBS.log not present" }],
}}
/>,
);
expect(screen.getByText("Collection Complete")).toBeInTheDocument();
expect(screen.getByText("Collected")).toBeInTheDocument();
expect(screen.getByText("Missing")).toBeInTheDocument();
expect(screen.getByText("Failed")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /Show 1 missing/i }));
expect(screen.getByText(/CBS.log not present/)).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Open Bundle" })).toBeInTheDocument();
});
});
8 changes: 8 additions & 0 deletions src/components/dialogs/CollectDiagnosticsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import {
} from "../../lib/collection-categories";
import { collectDiagnostics } from "../../lib/commands";
import { useUiStore } from "../../stores/ui-store";
import { useModalFocus } from "../../hooks/use-modal-focus";

interface CollectDiagnosticsDialogProps {
isOpen: boolean;
onClose: () => void;
}

export function CollectDiagnosticsDialog({ isOpen, onClose }: CollectDiagnosticsDialogProps) {
const dialogRef = useRef<HTMLDivElement>(null);
useModalFocus(isOpen, dialogRef);
const setCollectionProgress = useUiStore((s) => s.setCollectionProgress);
const setCollectionResult = useUiStore((s) => s.setCollectionResult);
const collectingRef = useRef(false);
Expand Down Expand Up @@ -200,6 +203,11 @@ export function CollectDiagnosticsDialog({ isOpen, onClose }: CollectDiagnostics
}}
>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label="Collect Diagnostics"
Comment thread
adamgell marked this conversation as resolved.
tabIndex={-1}
style={{
width: "520px",
maxHeight: "80vh",
Expand Down
10 changes: 9 additions & 1 deletion src/components/dialogs/CollectionCompleteDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useState, useCallback, useMemo } from "react";
import { useState, useCallback, useMemo, useRef } from "react";
import { tokens } from "@fluentui/react-components";
import { CheckmarkRegular, DismissRegular } from "@fluentui/react-icons";
import type { CollectionResult } from "../../lib/commands";
import { loadPathAsLogSource } from "../../lib/log-source";
import { useUiStore } from "../../stores/ui-store";
import { useModalFocus } from "../../hooks/use-modal-focus";
import { getThemeById } from "../../lib/themes";

interface CollectionCompleteDialogProps {
Expand All @@ -13,6 +14,8 @@ interface CollectionCompleteDialogProps {

export function CollectionCompleteDialog({ result, onClose }: CollectionCompleteDialogProps) {
const [showGaps, setShowGaps] = useState(false);
const dialogRef = useRef<HTMLDivElement>(null);
useModalFocus(result !== null, dialogRef);
const themeId = useUiStore((s) => s.themeId);
const statusPalette = useMemo(
() => getThemeById(themeId).severityPalette.status,
Expand Down Expand Up @@ -53,6 +56,11 @@ export function CollectionCompleteDialog({ result, onClose }: CollectionComplete
}}
>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label={isError ? "Collection Failed" : "Collection Complete"}
tabIndex={-1}
style={{
width: "420px",
backgroundColor: tokens.colorNeutralBackground1,
Expand Down
Loading
Loading