Compare commits

...

11 Commits

Author SHA1 Message Date
Oliver Kieselbach 0ed1e6f542 Merge branch 'main' of https://github.com/okieselbach/Autopilot-Monitor 2026-08-18 03:21:54 +02:00
dependabot[bot] 522911c5a7 chore(deps-dev): bump tsx (#130)
Bumps the mcp group in /src/McpServer/autopilot-monitor-mcp with 1 update: [tsx](https://github.com/privatenumber/tsx).


Updates `tsx` from 4.23.11 to 4.23.12
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.23.11...v4.23.12)

---
updated-dependencies:
- dependency-name: tsx
  dependency-version: 4.23.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: mcp
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:20:26 +02:00
dependabot[bot] 0bfe1e327b chore(deps): bump the web group (#131)
Bumps the web group in /src/Web/autopilot-monitor-web with 4 updates: [@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react), [next](https://github.com/vercel/next.js), [@next/bundle-analyzer](https://github.com/vercel/next.js/tree/HEAD/packages/next-bundle-analyzer) and [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).


Updates `@xyflow/react` from 12.11.2 to 12.11.3
- [Release notes](https://github.com/xyflow/xyflow/releases)
- [Changelog](https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md)
- [Commits](https://github.com/xyflow/xyflow/commits/@xyflow/react@12.11.3/packages/react)

Updates `next` from 16.3.0 to 16.3.1
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.0...v16.3.1)

Updates `@next/bundle-analyzer` from 16.3.0 to 16.3.1
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.1/packages/next-bundle-analyzer)

Updates `eslint-config-next` from 16.3.0 to 16.3.1
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.1/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: "@xyflow/react"
  dependency-version: 12.11.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web
- dependency-name: next
  dependency-version: 16.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web
- dependency-name: "@next/bundle-analyzer"
  dependency-version: 16.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web
- dependency-name: eslint-config-next
  dependency-version: 16.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 03:20:10 +02:00
Oliver Kieselbach 2cf7216be5 Token-failure grace window + registry_app_baseline summary
Live findings from the first two 2.0.1402 sessions (Oliver, 2026-08-18):

1. "Failed to get AAD token" is logged routinely during the device phase
   (no user token exists yet) - both test sessions raised the Warning
   although nothing was wrong. The failure is now held pending: any
   IME-TOKEN-SUCCESS match clears it (staleness-gated so a replayed
   success from a previous enrollment cannot mask a current outage), and
   ime_token_failure fires only when no success follows for the 30-min
   grace window, then deduped per errorCode. Genuine auth outages run
   long enough to hit the window; routine device-phase failures never do.

2. New one-shot registry_app_baseline event (Info, only when non-empty):
   summary of the Win32Apps state that predates the agent run - counts
   by enforcement class + capped 50-app typed payload incl.
   LastUpdatedTimeUtc. On WDP the DPP-policy apps install in Batch 1
   before the bootstrap-script agent exists; their outcomes live only in
   this baseline, closing the pre-agent app gap without log backfill.
   The diff rail stays silent on baseline state (anti-replay unchanged).

Event type dual-registered (Constants + MCP catalog + manifests regen).
Tests: grace-window cycle (resolved / unresolved / re-arm dedup),
baseline summary (counts, one-shot, empty). All suites green
(Agent 2274, Functions 4262, MCP 561, web 762), 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 03:16:58 +02:00
Oliver Kieselbach 587adc5ddf ci(msi): build from project dir - wix resolves File/@Source against cwd
WIX0103 on the first run: Invoke-BootstrapDownload.ps1 not found because
wix build resolves File/@Source relative to the working directory, not
the wxs location. Matches the local build documented in Package.wxs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 03:11:12 +02:00
Oliver Kieselbach 84bf2bc2fd commands: bump to 3f98090 (log-name kebab-case in analysis skills) 2026-08-18 03:07:42 +02:00
Oliver Kieselbach dcf1b1c227 ci: Bootstrap-MSI build workflow - blob publish behind the download alias
workflow_dispatch-only, mirroring the agent release contract: validate
the runner script (PS parse + ASCII-only, it runs as SYSTEM on every
enrolling device), wix 5 build, upload versioned + stable blob names to
the agent container (no-cache, no legacy mirror - the MSI channel never
existed there), HEAD-verify the public alias serves the fresh bytes,
Sigstore provenance attestation. MSI version stays source-controlled in
Package.wxs - bumped to 1.0.2 (runner: encoding fix + bootstrap-msi.log
rename). Customer download:
https://download.autopilotmonitor.com/agent/AutopilotMonitor-Bootstrap.msi

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 03:07:08 +02:00
Oliver Kieselbach 6ebdfa41af refactor(agent): unified kebab-case log file naming + date-less agent.log
The agent log directory mixed three conventions (agent_20260817.log,
bootstrap_agent.log, msi-bootstrap.log) while everything else in the
diagnostics ZIP is already kebab-case. All log files now follow
<component>-<content>.log:

  agent.log               (was agent_YYYYMMDD.log; segments agent-NNN.log)
  bootstrap-script.log    (was bootstrap_agent.log; script v2.4)
  bootstrap-msi.log       (was msi-bootstrap.log)
  crash-<utc>.log         (was crash_<utc>.log)
  gather-rules-debug.log  (was gather_rules_debug.log)
  ime-pattern-matches.log (was ime_pattern_matches.log)

The date is dropped from the main log: the agent only lives for one
enrollment, and date rotation split the chronology at local midnight -
which can fall mid-enrollment while the clock is still on the OOBE
default timezone. The existing 50MB size rotation stays as the growth
bound. Legacy dated files are ignored by the rotation probe; crash
detection still picks up legacy crash_*.log once so an in-place
self-update cannot lose a pending crash classification.

Diag collectors glob *.log and need no change. MCP zip-map and docs
note the legacy names for archives from pre-rename agents. Settings UI
hint paths updated. MSI rebuild + bootstrap script publish (v2.4) +
MCP redeploy required to ship.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 02:55:06 +02:00
Oliver Kieselbach a2c4949473 commands: bump to b431556 (ime-decompile blob-first + import fixes)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 02:36:57 +02:00
Oliver Kieselbach 81f4a55d88 Bootstrap MSI 1.0.1: version bump for Intune re-upload + log encoding fix
Intune LOB requires a higher ProductVersion to accept a re-upload. Also
includes the runner logging fix: PS 5.1 *>> redirection writes UTF-16LE
into the otherwise ANSI msi-bootstrap.log - route through Add-Content for
a single encoding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 02:12:11 +02:00
Oliver Kieselbach 4d3d3c56f4 gitignore: restore ime-files/ - local binaries scratch of /ime-decompile
Still needed: the skill extracts each MSI here (msiexec /a payload for grepping
in binaries); only decompilates go to the ime-decompiles repo and MSIs to the
ime-archive blob container.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 01:51:26 +02:00
37 changed files with 481 additions and 146 deletions
+139
View File
@@ -0,0 +1,139 @@
name: Build & Publish Bootstrap MSI
# Thin MDM LOB delivery vehicle for WDP (Autopilot v2) — see
# src/Agent/AutopilotMonitor.BootstrapMsi/Package.wxs. The MSI carries no bootstrap
# logic (only the runner script that downloads the CURRENT server-hosted
# Install-AutopilotMonitor.ps1), so it is rebuilt rarely: only when the runner script
# or the WiX package changes. Version lives in Package.wxs — bump it there before
# dispatching so Intune LOB update detection sees a new ProductVersion.
on:
workflow_dispatch:
inputs:
upload_blob:
description: 'Upload to blob (customer-facing via download.autopilotmonitor.com/agent/)'
required: true
type: boolean
default: true
permissions:
contents: read
id-token: write # keyless Sigstore signing (artifact attestation)
attestations: write # store the provenance attestation
env:
MSI_PROJECT_DIR: src/Agent/AutopilotMonitor.BootstrapMsi
RUNNER_SCRIPT: src/Agent/AutopilotMonitor.BootstrapMsi/Invoke-BootstrapDownload.ps1
STABLE_MSI: AutopilotMonitor-Bootstrap.msi
jobs:
build:
runs-on: windows-latest
name: Build Bootstrap MSI
steps:
- name: Checkout repository
uses: actions/checkout@v7
# The runner executes as SYSTEM on every enrolling device — a parse error or
# non-ASCII mojibake (PS 5.1 codepage roulette) must fail the build, not the fleet.
- name: Validate runner script (parse + ASCII-only)
shell: pwsh
run: |
$errs = $null
[System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $env:RUNNER_SCRIPT), [ref]$null, [ref]$errs) | Out-Null
if ($errs) { $errs | ForEach-Object { Write-Error $_.ToString() }; exit 1 }
if ((Get-Content $env:RUNNER_SCRIPT -Raw) -match '[^\x00-\x7F]') {
Write-Error "Runner script contains non-ASCII characters"
exit 1
}
Write-Host "Runner script parse + ASCII check passed"
- name: Read MSI version from Package.wxs
id: version
shell: pwsh
run: |
$wxs = Get-Content "$env:MSI_PROJECT_DIR/Package.wxs" -Raw
if ($wxs -notmatch 'Version="(\d+\.\d+\.\d+)"') {
Write-Error "Could not parse Version from Package.wxs"
exit 1
}
$version = $Matches[1]
Write-Host "MSI version (from Package.wxs): $version"
"version=$version" >> $env:GITHUB_OUTPUT
- name: Install WiX toolset
run: dotnet tool install --global wix --version "5.*"
- name: Build MSI
shell: pwsh
run: |
# wix resolves File/@Source relative to the working directory, not the wxs —
# build from inside the project dir (same as the local build documented in the wxs).
$out = Join-Path (Get-Location) $env:STABLE_MSI
Push-Location $env:MSI_PROJECT_DIR
wix build Package.wxs -arch x64 -o $out
Pop-Location
if (-not (Test-Path $env:STABLE_MSI)) { Write-Error "wix build produced no MSI"; exit 1 }
$sha256 = (Get-FileHash $env:STABLE_MSI -Algorithm SHA256).Hash.ToLowerInvariant()
Write-Host "Built $env:STABLE_MSI ($([math]::Round((Get-Item $env:STABLE_MSI).Length / 1KB)) KB)"
Write-Host "SHA-256: $sha256"
- name: Upload to Azure Blob Storage
if: inputs.upload_blob
shell: pwsh
run: |
$containerUrl = "https://autopilotmonitoreu.blob.core.windows.net/agent"
$sasToken = "${{ secrets.AZURE_BLOB_AGENT_SAS_TOKEN }}".TrimStart('?')
$version = "${{ steps.version.outputs.version }}"
# Same Send-Blob contract as build-agent.yml: no-cache on every blob because the
# stable name rotates in place behind the download alias. No legacy mirror — the
# MSI channel never existed on the legacy account.
function Send-Blob {
param([string]$Local, [string]$BlobName)
$bytes = [System.IO.File]::ReadAllBytes($Local)
$headers = @{
"x-ms-blob-type" = "BlockBlob"
"Content-Type" = "application/octet-stream"
"x-ms-blob-cache-control" = "no-cache"
}
$md5 = [System.Security.Cryptography.MD5]::Create()
$headers["Content-MD5"] = [Convert]::ToBase64String($md5.ComputeHash($bytes))
$md5.Dispose()
Write-Host "Uploading $BlobName ($([math]::Round($bytes.Length / 1KB)) KB)..."
Invoke-RestMethod -Uri "$containerUrl/$BlobName`?$sasToken" -Method Put -Headers $headers -Body $bytes
Write-Host " uploaded $BlobName"
}
# Versioned copy for provenance/rollback + stable customer-facing name.
Send-Blob -Local $env:STABLE_MSI -BlobName "AutopilotMonitor-Bootstrap-v$version.msi"
Send-Blob -Local $env:STABLE_MSI -BlobName $env:STABLE_MSI
# E2E: the Front Door route /agent/* must serve the fresh bytes on the public alias.
- name: Verify download alias serves the MSI
if: inputs.upload_blob
shell: pwsh
run: |
$localSize = (Get-Item $env:STABLE_MSI).Length
$resp = Invoke-WebRequest -Uri "https://download.autopilotmonitor.com/agent/$env:STABLE_MSI" -Method Head -UseBasicParsing
if ($resp.StatusCode -ne 200) { Write-Error "Alias HEAD returned $($resp.StatusCode)"; exit 1 }
$remoteSize = [long]$resp.Headers['Content-Length'][0]
if ($remoteSize -ne $localSize) {
Write-Error "Size mismatch: alias serves $remoteSize bytes, local MSI is $localSize bytes"
exit 1
}
Write-Host "Alias verified: https://download.autopilotmonitor.com/agent/$env:STABLE_MSI ($remoteSize bytes)"
# Sigstore keyless provenance, same as the agent ZIP. Verify:
# gh attestation verify AutopilotMonitor-Bootstrap.msi --repo okieselbach/AutopilotMonitor
- name: Attest build provenance (MSI)
uses: actions/attest-build-provenance@v4
with:
subject-path: ${{ env.STABLE_MSI }}
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: AutopilotMonitor-Bootstrap-v${{ steps.version.outputs.version }}
path: ${{ env.STABLE_MSI }}
+5
View File
@@ -159,3 +159,8 @@ docs/~$Introduction-AutopilotMonitor.pptx
# changes each run) and served at /version.json from the static export.
src/Web/autopilot-monitor-web/public/version.json
# Local IME binaries scratch used by the /ime-decompile skill (MSI + msiexec-
# extracted payload per version, for grepping in binaries). Decompilates live in
# the private ime-decompiles repo, MSIs in the ime-archive blob - this stays local.
ime-files/
+1 -1
View File
@@ -44,7 +44,7 @@ tenant-config report.
`RemoteConfigMerger` projects the bool onto a runtime path (same pattern as
`EnableImeMatchLog`): `true` → `Constants.GatherRuleDebugLogPath` =
`%ProgramData%\AutopilotMonitor\Logs\gather_rules_debug.log`; `false` → null, unless a
`%ProgramData%\AutopilotMonitor\Logs\gather-rules-debug.log`; `false` → null, unless a
`--gather-debug-log <path>` CLI override supplied a non-default path. The field is not
security-sensitive and deliberately **survives the offline config cache**.
+1 -1
View File
@@ -53,7 +53,7 @@ every line; the spool fsyncs only items flagged `RequiresImmediateFlush`.
| Event sequence | `State\event-sequence.json` | `{ LastAssignedSequence }` | atomic | none; survives WhiteGlove Part-2 resume |
| Spool | `Spool\spool.jsonl` | JSONL (`TelemetryItem`/line) | only `RequiresImmediateFlush` items | none — never truncated, cursor skips uploaded items |
| Upload cursor | `Spool\upload-cursor.json` | `{ LastUploadedItemId }` | atomic | idempotent, never regresses |
| Agent trace log | `Logs\agent_YYYYMMDD[_NNN].log` | text `[utc] [LEVEL] msg` | no | 50 MB → suffix rotation; consumed by `/diag-agent-log` |
| Agent trace log | `Logs\agent.log` (segments `agent-NNN.log`) | text `[utc] [LEVEL] msg` | no | 50 MB → suffix rotation; consumed by `/diag-agent-log` |
| Crash dump + record | `Crashes\<stem>.dmp` + `.json` | MiniDump + JSON | n/a (process dying) | 5 files / 7 days |
| Startup-event gate | `State\startup-event-state.json` | JSON dict | no (atomic) | cross-reboot dedup of one-shot probes |
| Bandwidth state | `State\bandwidth-state.json` | JSON samples/counters | no (atomic) | reboot-survivor for the DO bandwidth estimate |
+1 -1
View File
@@ -146,7 +146,7 @@ snapshots) are executed by the `EffectRunner` — the reducer itself is pure. De
| `\Agent\` | deployed binaries |
| `\State\` | decision persistence (SignalLog, Journal, Snapshot), `enrollment-complete.marker`, `final-status.json`, self-update state, `.quarantine/`, `.part1-<ts>/` |
| `\Spool\` | telemetry spool (offline queue) |
| `\Logs\` | agent trace log, `crash_*.log`, `ime_pattern_matches.log` |
| `\Logs\` | agent trace log (`agent.log`), `crash-*.log`, `ime-pattern-matches.log` |
| `\Config\` | `remote-config.json` cache |
| `\Agent-Update\`, `\Updates\` | self-update staging/download |
+1
View File
@@ -2,6 +2,7 @@
## 2026-08-18
* **Update**: `agent/logs-and-persistence.md`, `agent/overview.md`, `agent/gather-rule-debug-log.md` — log files renamed to unified kebab-case: `agent.log` (date rotation dropped; size segments `agent-NNN.log`), `bootstrap-script.log` (was `bootstrap_agent.log`), `bootstrap-msi.log` (was `msi-bootstrap.log`), `crash-*.log`, `gather-rules-debug.log`, `ime-pattern-matches.log`. Old names still appear in archives from pre-rename agents.
* **New**: `agent/registry-app-state-pillar.md` — the registry second pillar for IME app state (audit follow-up): snapshot-and-diff watcher on `HKLM\...\IntuneManagementExtension` (Win32Apps / EspTrackingWin32Apps / StatusServiceReports), `registry_app_state` + `app_state_reconciliation` events, silent startup baseline, WDP reuse notes.
## 2026-08-17
+1 -1
View File
@@ -629,7 +629,7 @@
"category": "always",
"pattern": "Failed to get AAD token\\..*?errorCode = (?<errorCode>.*?)$",
"action": "imeTokenFailure",
"description": "IME token acquisition failure ('Failed to get AAD token. len = N using client id ... and resource id ..., errorCode = N', IntuneTokenManager, IntuneManagementExtension.log). Complements IME-TOKEN-SUCCESS: an auth outage (WAM broken, conditional access, network) previously left every check-in silently doing nothing with no timeline evidence. errorCode can be empty when IME logged none; the agent dedups to one Warning per distinct code. Verified against IME 1.97 and 1.104 decompiled sources.",
"description": "IME token acquisition failure ('Failed to get AAD token. len = N using client id ... and resource id ..., errorCode = N', IntuneTokenManager, IntuneManagementExtension.log). This line appears ROUTINELY during the device phase (no user token exists yet), so the agent holds the failure pending: any IME-TOKEN-SUCCESS match clears it, and the ime_token_failure Warning fires only when no success follows for the whole 30-min grace window (then deduped to one Warning per distinct errorCode). A genuine auth outage (WAM broken, conditional access, network) runs long enough to hit the window. Verified against IME 1.97 and 1.104 decompiled sources.",
"enabled": true
},
{
@@ -4,6 +4,6 @@
"category": "always",
"pattern": "Failed to get AAD token\\..*?errorCode = (?<errorCode>.*?)$",
"action": "imeTokenFailure",
"description": "IME token acquisition failure ('Failed to get AAD token. len = N using client id ... and resource id ..., errorCode = N', IntuneTokenManager, IntuneManagementExtension.log). Complements IME-TOKEN-SUCCESS: an auth outage (WAM broken, conditional access, network) previously left every check-in silently doing nothing with no timeline evidence. errorCode can be empty when IME logged none; the agent dedups to one Warning per distinct code. Verified against IME 1.97 and 1.104 decompiled sources.",
"description": "IME token acquisition failure ('Failed to get AAD token. len = N using client id ... and resource id ..., errorCode = N', IntuneTokenManager, IntuneManagementExtension.log). This line appears ROUTINELY during the device phase (no user token exists yet), so the agent holds the failure pending: any IME-TOKEN-SUCCESS match clears it, and the ime_token_failure Warning fires only when no success follows for the whole 30-min grace window (then deduped to one Warning per distinct errorCode). A genuine auth outage (WAM broken, conditional access, network) runs long enough to hit the window. Verified against IME 1.97 and 1.104 decompiled sources.",
"enabled": true
}
@@ -95,6 +95,8 @@
Organizations). SKIP-safe when the API is absent (< 1809).
Removed legacy Content-MD5 fallback; SHA-256 manifest verification is
now mandatory (no hash -> bootstrap aborts, no unverified install).
2026-08-18 v2.4 Renamed log file bootstrap_agent.log -> bootstrap-script.log
(unified kebab-case log naming across the agent log directory).
2026-05-09 v2.0 Generic bootstrap: agent owns its own defaults (e.g. 600 s
TenantId-wait), so the script only calls `--install` plain.
Hardened post-install with a 10 s runtime-process verify.
@@ -126,13 +128,13 @@ param(
)
# Script version (bump on meaningful changes; see .CHANGELOG above)
$ScriptVersion = "2.3"
$ScriptVersion = "2.4"
# Configuration - Everything in ProgramData for easy cleanup
$AgentBasePath = "$env:ProgramData\AutopilotMonitor"
$AgentBinPath = "$AgentBasePath\Agent"
$AgentLogPath = "$AgentBasePath\Logs"
$LogFile = "$AgentLogPath\bootstrap_agent.log"
$LogFile = "$AgentLogPath\bootstrap-script.log"
# Write-Host (NOT Write-Output): log calls happen inside functions that return
# decision objects; Write-Output would leak every log line into those return values.
@@ -15,7 +15,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Logging
/// </summary>
public sealed class AgentLoggerTests
{
private static string TodayBaseName => $"agent_{DateTime.Now:yyyyMMdd}";
private const string BaseName = "agent";
[Fact]
public void Log_writes_to_unsuffixed_base_file_when_dir_is_empty()
@@ -25,7 +25,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Logging
logger.Info("hello");
var basePath = Path.Combine(tmp.Path, TodayBaseName + ".log");
var basePath = Path.Combine(tmp.Path, BaseName + ".log");
Assert.True(File.Exists(basePath), $"expected base file at {basePath}");
Assert.Contains("hello", File.ReadAllText(basePath));
}
@@ -39,17 +39,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Logging
// Each line ~80 bytes after timestamp/severity envelope; 30 lines clears 1 KB
// even at the smallest payload. The first lines fill the base file; once the
// FileInfo.Length read sees >= 1024 the next call rotates to _002.
// FileInfo.Length read sees >= 1024 the next call rotates to -002.
for (int i = 0; i < 60; i++)
{
logger.Info($"line-{i:D4}-padded-with-some-extra-context-to-cross-1KB-quickly");
}
var files = Directory.GetFiles(tmp.Path, TodayBaseName + "*.log").OrderBy(s => s).ToArray();
var files = Directory.GetFiles(tmp.Path, BaseName + "*.log").OrderBy(s => s).ToArray();
Assert.True(files.Length >= 2, $"expected at least 2 segments, got: {string.Join(",", files.Select(Path.GetFileName))}");
// The unsuffixed file is the first segment; rotated segments use _002 onward.
var rotated = files.FirstOrDefault(p => Path.GetFileNameWithoutExtension(p).EndsWith("_002"));
// The unsuffixed file is the first segment; rotated segments use -002 onward.
var rotated = files.FirstOrDefault(p => Path.GetFileNameWithoutExtension(p).EndsWith("-002"));
Assert.NotNull(rotated);
}
@@ -64,8 +64,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Logging
logger.Info($"pad-{i:D4}-content-content-content-content-content-content");
}
var rotatedPath = Path.Combine(tmp.Path, TodayBaseName + "_002.log");
Assert.True(File.Exists(rotatedPath), "expected agent_YYYYMMDD_002.log to exist");
var rotatedPath = Path.Combine(tmp.Path, BaseName + "-002.log");
Assert.True(File.Exists(rotatedPath), "expected agent-002.log to exist");
var first = File.ReadAllLines(rotatedPath).FirstOrDefault();
Assert.NotNull(first);
Assert.Contains("AgentLogger: rotated", first);
@@ -75,23 +75,23 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Logging
public void Log_continues_existing_rotation_on_restart_without_overwriting()
{
using var tmp = new TempDirectory();
// Pre-seed: looks like an earlier process already rotated up to _003.
File.WriteAllText(Path.Combine(tmp.Path, TodayBaseName + ".log"), "older-base\n");
File.WriteAllText(Path.Combine(tmp.Path, TodayBaseName + "_002.log"), "older-002\n");
File.WriteAllText(Path.Combine(tmp.Path, TodayBaseName + "_003.log"), "older-003\n");
// Pre-seed: looks like an earlier process already rotated up to -003.
File.WriteAllText(Path.Combine(tmp.Path, BaseName + ".log"), "older-base\n");
File.WriteAllText(Path.Combine(tmp.Path, BaseName + "-002.log"), "older-002\n");
File.WriteAllText(Path.Combine(tmp.Path, BaseName + "-003.log"), "older-003\n");
var logger = new AgentLogger(tmp.Path, AgentLogLevel.Info, maxFileSizeBytes: 1024L * 1024);
logger.Info("after-restart");
// New entries must land in the highest existing segment (_003), not in a fresh
// New entries must land in the highest existing segment (-003), not in a fresh
// base file. Older content in _003 must be preserved.
var resumed = File.ReadAllText(Path.Combine(tmp.Path, TodayBaseName + "_003.log"));
var resumed = File.ReadAllText(Path.Combine(tmp.Path, BaseName + "-003.log"));
Assert.Contains("older-003", resumed);
Assert.Contains("after-restart", resumed);
// Older segments are left alone.
Assert.Equal("older-base\n", File.ReadAllText(Path.Combine(tmp.Path, TodayBaseName + ".log")));
Assert.Equal("older-002\n", File.ReadAllText(Path.Combine(tmp.Path, TodayBaseName + "_002.log")));
Assert.Equal("older-base\n", File.ReadAllText(Path.Combine(tmp.Path, BaseName + ".log")));
Assert.Equal("older-002\n", File.ReadAllText(Path.Combine(tmp.Path, BaseName + "-002.log")));
}
[Fact]
@@ -105,7 +105,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Logging
// as plain ASCII, while genuine non-ASCII payload stays untouched.
logger.Info("check failed \u2014 range 1\u20132 \u2192 done\u2026 \u2018a\u2019 \u201Cb\u201D nb\u00A0sp f\u00FCr M\u00FCller");
var text = File.ReadAllText(Path.Combine(tmp.Path, TodayBaseName + ".log"));
var text = File.ReadAllText(Path.Combine(tmp.Path, BaseName + ".log"));
Assert.Contains("check failed - range 1-2 -> done... 'a' \"b\" nb sp f\u00FCr M\u00FCller", text);
}
@@ -115,8 +115,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Logging
using var tmp = new TempDirectory();
var logger = new AgentLogger(tmp.Path, AgentLogLevel.Info, maxFileSizeBytes: 1024L * 1024);
// Hold an exclusive lock on today's file to force AppendAllText to throw.
var lockedPath = Path.Combine(tmp.Path, TodayBaseName + ".log");
// Hold an exclusive lock on the log file to force AppendAllText to throw.
var lockedPath = Path.Combine(tmp.Path, BaseName + ".log");
File.WriteAllText(lockedPath, ""); // ensure exists
using var holder = new FileStream(lockedPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
@@ -25,7 +25,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather
public void Dispose() => _tmp.Dispose();
private string LogPath => Path.Combine(_tmp.Path, "sub", "gather_rules_debug.log");
private string LogPath => Path.Combine(_tmp.Path, "sub", "gather-rules-debug.log");
[Fact]
public void Write_produces_pipe_delimited_line_and_creates_directory_lazily()
@@ -73,7 +73,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather
for (int i = 0; i < 20; i++)
log.Write("RULE-1", GatherRuleDebugLog.StageExec, new string('x', 100));
var oldPath = Path.Combine(Path.GetDirectoryName(LogPath), "gather_rules_debug.old.log");
var oldPath = Path.Combine(Path.GetDirectoryName(LogPath), "gather-rules-debug.old.log");
Assert.True(File.Exists(oldPath), "expected one .old rotation generation");
Assert.True(File.Exists(LogPath));
Assert.Contains("-- rotated", File.ReadAllText(LogPath) + File.ReadAllText(oldPath));
@@ -28,7 +28,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather
public GatherRuleExecutorDebugLogTests()
{
var logger = new AgentLogger(_tmp.Path, AgentLogLevel.Info);
_debugLogPath = Path.Combine(_tmp.Path, "gather_rules_debug.log");
_debugLogPath = Path.Combine(_tmp.Path, "gather-rules-debug.log");
_executor = new GatherRuleExecutor(
"sess", "tenant", _ => { }, logger,
debugLogPath: _debugLogPath)
@@ -32,7 +32,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Gather
{
Directory.CreateDirectory(_outsideUsers);
var logger = new AgentLogger(_tmp.Path, AgentLogLevel.Info);
_debugLogPath = Path.Combine(_tmp.Path, "gather_rules_debug.log");
_debugLogPath = Path.Combine(_tmp.Path, "gather-rules-debug.log");
_context = new GatherRuleContext(
logger, "sess", "tenant",
evt => _events.Add(evt),
@@ -119,6 +119,40 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals
Assert.Equal("success", evt.Payload["enforcementClass"]);
}
[Fact]
public void NonEmpty_baseline_emits_one_summary_with_counts()
{
var (sink, clock, observer) = CreateObserver(trackerApps: null);
var current = Snapshot((App1, 1000, 0), (App2, 5003, 101));
using (new ImeRegistryAppStateObserver.ScopedSnapshotOverride(() => current))
{
observer.Tick("baseline");
observer.Tick("periodic"); // summary is one-shot, not per tick
}
var summary = Assert.Single(Events(sink, SharedEventTypes.RegistryAppBaseline));
Assert.Equal("2", summary.Payload!["totalApps"]);
Assert.Equal("1", summary.Payload["successCount"]);
Assert.Equal("1", summary.Payload["errorCount"]);
// The baseline stays silent on the diff rail.
Assert.Empty(Events(sink, SharedEventTypes.RegistryAppState));
}
[Fact]
public void Empty_baseline_emits_no_summary()
{
var (sink, clock, observer) = CreateObserver(trackerApps: null);
var current = new ImeRegistrySnapshot();
using (new ImeRegistryAppStateObserver.ScopedSnapshotOverride(() => current))
{
observer.Tick("baseline");
}
Assert.Empty(Events(sink, SharedEventTypes.RegistryAppBaseline));
}
[Fact]
public void Unchanged_snapshot_emits_nothing()
{
@@ -53,7 +53,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
using var tmp = new TempDirectory();
var logDir = Path.Combine(tmp.Path, "logs");
Directory.CreateDirectory(logDir);
var crashPath = Path.Combine(logDir, "crash_20260421_100000.log");
var crashPath = Path.Combine(logDir, "crash-20260421-100000.log");
File.WriteAllText(crashPath, "[2026-04-21T10:00:00Z] FATAL: InvalidOperationException: simulated crash");
var summary = AutopilotMonitor.Agent.V2.Program.DetectPreviousExit(tmp.Path, logDir);
@@ -312,7 +312,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
AutopilotMonitor.Agent.V2.Program.WriteCrashLog(logDir, new InvalidOperationException("boom"));
var files = Directory.GetFiles(logDir, "crash_*.log");
var files = Directory.GetFiles(logDir, "crash-*.log");
Assert.Single(files);
var content = File.ReadAllText(files[0]);
Assert.Contains("FATAL:", content);
@@ -479,16 +479,32 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters
}
[Fact]
public void ImeTokenFailure_tracker_dedups_per_error_code()
public void ImeTokenFailure_grace_window_emits_only_without_token_success()
{
// "Failed to get AAD token" appears routinely during the device phase (no user
// token yet) — the Warning must fire only when NO success follows for the whole
// grace window.
using var f = new ImeLogTrackerAdapterFixture();
using var adapter = new ImeLogTrackerAdapter(f.Tracker, f.Ingress, f.Clock);
var t0 = DateTime.UtcNow;
// Failure resolved by a token success: never emitted, not even after the window.
f.Tracker.HandleImeTokenFailureForTest("100", "line a");
f.Tracker.HandleImeTokenFailureForTest("100", "line b"); // same code — suppressed
f.Tracker.HandleImeTokenFailureForTest("200", "line c"); // new code — emitted
f.Tracker.NoteTokenSuccessForTest();
f.Tracker.CheckPendingTokenFailure(t0 + ImeLogTracker.TokenFailureGraceWindow + TimeSpan.FromMinutes(1));
Assert.Empty(f.InfoEvents(SharedEventTypes.ImeTokenFailure));
Assert.Equal(2, f.InfoEvents(SharedEventTypes.ImeTokenFailure).Count);
// Unresolved failure: silent before the window, one Warning after it.
f.Tracker.HandleImeTokenFailureForTest("100", "line b");
f.Tracker.CheckPendingTokenFailure(t0 + TimeSpan.FromMinutes(5));
Assert.Empty(f.InfoEvents(SharedEventTypes.ImeTokenFailure));
f.Tracker.CheckPendingTokenFailure(t0 + ImeLogTracker.TokenFailureGraceWindow + TimeSpan.FromHours(1));
Assert.Single(f.InfoEvents(SharedEventTypes.ImeTokenFailure));
// Same code again: re-armed window expires but the per-code dedup suppresses.
f.Tracker.HandleImeTokenFailureForTest("100", "line c");
f.Tracker.CheckPendingTokenFailure(t0 + TimeSpan.FromHours(3));
Assert.Single(f.InfoEvents(SharedEventTypes.ImeTokenFailure));
}
[Fact]
@@ -34,9 +34,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Logging
private readonly long _maxFileSizeBytes;
private readonly string _logDirectory;
private readonly string _logFileBaseName; // "agent_20260425"
private string _logFilePath; // mutable — rotates to "_002", "_003" ...
private int _rotationSuffix; // 0 = base file, 2+ = "_002" etc.
private readonly string _logFileBaseName; // "agent"
private string _logFilePath; // mutable — rotates to "-002", "-003" ...
private int _rotationSuffix; // 0 = base file, 2+ = "-002" etc.
/// <summary>
/// When true, log entries are also written to Console.Out (same format as the log file).
@@ -55,14 +55,19 @@ namespace AutopilotMonitor.Agent.V2.Core.Logging
Directory.CreateDirectory(logDirectory);
}
_logFileBaseName = $"agent_{DateTime.Now:yyyyMMdd}";
// One canonical name, no date: the agent only lives for the duration of an
// enrollment, and a date-based split rotates at local midnight — which can fall
// mid-enrollment (local time may still be on the OOBE default timezone). Size
// rotation below bounds pathological growth. Legacy `agent_YYYYMMDD*.log` files
// from a pre-rename agent are ignored by the suffix probe and left in place.
_logFileBaseName = "agent";
_rotationSuffix = ProbeNextRotationSuffix(logDirectory, _logFileBaseName);
_logFilePath = BuildLogFilePath();
}
// Picks up the highest existing rotation suffix on the same day so a process restart
// doesn't overwrite previous segments. Returns 0 when no log file exists yet — the
// first segment uses the unsuffixed name `agent_YYYYMMDD.log`.
// Picks up the highest existing rotation suffix so a process restart doesn't
// overwrite previous segments. Returns 0 when no log file exists yet — the
// first segment uses the unsuffixed name `agent.log`.
private static int ProbeNextRotationSuffix(string dir, string baseName)
{
if (!Directory.Exists(dir)) return 0;
@@ -80,7 +85,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Logging
continue;
}
if (name.Length > baseName.Length + 1 &&
name.StartsWith(baseName + "_", StringComparison.OrdinalIgnoreCase) &&
name.StartsWith(baseName + "-", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(name.Substring(baseName.Length + 1), out var suffix) &&
suffix > max)
{
@@ -97,7 +102,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Logging
{
var name = _rotationSuffix < 2
? $"{_logFileBaseName}.log"
: $"{_logFileBaseName}_{_rotationSuffix:D3}.log";
: $"{_logFileBaseName}-{_rotationSuffix:D3}.log";
return Path.Combine(_logDirectory, name);
}
@@ -25,25 +25,70 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime
private DateTime _impersonationLastRollupUtc = DateTime.MinValue;
private static readonly TimeSpan ImpersonationRollupInterval = TimeSpan.FromSeconds(60);
// IME retries token acquisition on every check-in — dedup per distinct error
// code so an outage yields one Warning, not a timeline flood.
// "Failed to get AAD token" is logged ROUTINELY during the device phase (no user
// token exists yet — field-confirmed: the line appears en masse in every enrollment,
// Oliver 2026-08-18). A failure is therefore only meaningful when NO token success
// follows: the failure is held pending, any IME-TOKEN-SUCCESS match clears it, and
// the Warning fires only after the grace window elapses without a success. Genuine
// auth outages (WAM broken, conditional access, network) run long enough to hit it.
// Not persisted across restarts — failures keep arriving, the window re-arms.
internal static readonly TimeSpan TokenFailureGraceWindow = TimeSpan.FromMinutes(30);
private sealed class PendingTokenFailure
{
public string Code = "unknown";
public string Excerpt = string.Empty;
public DateTime FirstFailureUtc;
}
private PendingTokenFailure _pendingTokenFailure;
// Dedup per distinct error code so a persistent outage yields one Warning total.
private readonly HashSet<string> _reportedTokenFailureCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private void HandleImeTokenFailure(string errorCode, string message)
{
var code = string.IsNullOrEmpty(errorCode) ? "unknown" : errorCode.Trim();
if (!_reportedTokenFailureCodes.Add(code)) return;
if (_pendingTokenFailure != null) return; // earliest unresolved failure wins
var code = string.IsNullOrEmpty(errorCode) ? "unknown" : errorCode.Trim();
var excerpt = message ?? string.Empty;
if (excerpt.Length > 300) excerpt = excerpt.Substring(0, 300);
_logger.Warning($"ImeLogTracker: IME token acquisition failure (errorCode={code})");
OnImeTokenFailure?.Invoke(code, excerpt);
_pendingTokenFailure = new PendingTokenFailure
{
Code = code,
Excerpt = excerpt,
// Source-line time so a late-starting agent replaying an ongoing outage
// ages the window correctly; a success later in the same log still clears
// before the expiry check runs (lines are drained first, expiry after).
FirstFailureUtc = LastMatchedLogTimestamp ?? DateTime.UtcNow,
};
}
// Test seam: drives HandleImeTokenFailure without the regex pipeline.
private void ClearPendingTokenFailure()
{
if (_pendingTokenFailure == null) return;
_logger.Debug($"ImeLogTracker: token success observed — pending token failure (errorCode={_pendingTokenFailure.Code}) cleared");
_pendingTokenFailure = null;
}
/// <summary>Called once per poll cycle after the log drain (and testable directly).</summary>
internal void CheckPendingTokenFailure(DateTime nowUtc)
{
var pending = _pendingTokenFailure;
if (pending == null) return;
if (nowUtc - pending.FirstFailureUtc < TokenFailureGraceWindow) return;
_pendingTokenFailure = null; // next failure line re-arms a fresh window
if (!_reportedTokenFailureCodes.Add(pending.Code)) return;
_logger.Warning($"ImeLogTracker: IME token acquisition failing for >= {TokenFailureGraceWindow.TotalMinutes:0} min without a success (errorCode={pending.Code})");
OnImeTokenFailure?.Invoke(pending.Code, pending.Excerpt);
}
// Test seams: drive the pending/clear/expiry cycle without the regex pipeline.
internal void HandleImeTokenFailureForTest(string errorCode, string message)
=> HandleImeTokenFailure(errorCode, message);
internal void NoteTokenSuccessForTest() => ClearPendingTokenFailure();
private void HandleImeImpersonation(string user)
{
@@ -244,6 +244,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime
return;
}
// A token success resolves any pending token failure (grace-window model, see
// HandleImeTokenFailure). Matched on our own stable pattern ID — the line also
// drives espPhaseDetected, whose handler must stay unaware of token semantics.
// Placed after the staleness gate so a success replayed from a previous
// enrollment's log cannot clear a genuine current outage.
if (!isStaleReplayLine && string.Equals(pattern.PatternId, "IME-TOKEN-SUCCESS", StringComparison.OrdinalIgnoreCase))
ClearPendingTokenFailure();
try
{
var id = match.Groups["id"]?.Value;
@@ -607,6 +607,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime
// line never arrived within the grace period. Runs on this same loop
// thread (no locking) so it observes the buffer right after parsing.
FlushStalePlatformScriptResults(DateTime.UtcNow);
// Token-failure grace window: fires only when the failure stayed
// unresolved (no IME-TOKEN-SUCCESS) for the whole window. Runs after
// the drain so a success later in the same batch clears first.
CheckPendingTokenFailure(DateTime.UtcNow);
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
@@ -119,9 +119,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
if (_last == null)
{
// Silent baseline — see class doc.
// Silent baseline for the DIFF stream — see class doc. But the baseline
// itself is valuable evidence: on WDP the DPP-policy apps install in
// Batch 1 BEFORE the agent (bootstrap = Batch 2), so their terminal
// states exist ONLY here. One capped summary event closes that
// pre-agent gap without any log backfill (Oliver, 2026-08-18).
_last = snapshot;
_logger?.Info($"RegistryAppState: baseline captured ({snapshot.Entries.Count} app entries, trigger={reason})");
if (snapshot.Entries.Count > 0)
EmitBaselineSummary(snapshot, nowUtc);
return;
}
@@ -181,6 +187,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
var entry = snapshot.GetOrAdd(userName, appId);
entry.ExitCode = TryReadInt(appKey.GetValue("ExitCode"));
entry.Intent = TryReadInt(appKey.GetValue("Intent"));
entry.LastUpdatedUtc = appKey.GetValue("LastUpdatedTimeUtc") as string;
using var esm = appKey.OpenSubKey("EnforcementStateMessage");
var json = esm?.GetValue("EnforcementStateMessage") as string;
@@ -452,6 +459,64 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
occurredAtUtc: nowUtc);
}
// Baseline entries in the summary's typed payload are capped — a long-lived device
// with many historical Win32Apps entries must not produce an oversized event.
internal const int MaxBaselineSummaryApps = 50;
private void EmitBaselineSummary(ImeRegistrySnapshot snapshot, DateTime nowUtc)
{
int success = 0, error = 0, inProgress = 0, other = 0;
var apps = new List<Dictionary<string, string>>();
foreach (var entry in snapshot.Entries.Values)
{
var cls = entry.EnforcementState is int es ? ClassifyEnforcementState(es) : null;
switch (cls)
{
case "success": success++; break;
case "error": error++; break;
case "inProgress": inProgress++; break;
default: other++; break;
}
if (apps.Count >= MaxBaselineSummaryApps) continue;
var app = new Dictionary<string, string>(StringComparer.Ordinal)
{
["appId"] = entry.AppId,
["userContext"] = entry.UserContext,
};
if (entry.EnforcementState is int state)
{
app["enforcementState"] = state.ToString(CultureInfo.InvariantCulture);
app["enforcementClass"] = ClassifyEnforcementState(state);
}
if (entry.ErrorCode is long ec) app["errorCode"] = ec.ToString(CultureInfo.InvariantCulture);
if (entry.ExitCode is int xc) app["exitCode"] = xc.ToString(CultureInfo.InvariantCulture);
if (!string.IsNullOrEmpty(entry.LastUpdatedUtc)) app["lastUpdatedUtc"] = entry.LastUpdatedUtc!;
if (entry.EspTracked) app["espTracked"] = "true";
apps.Add(app);
}
var data = new Dictionary<string, string>(StringComparer.Ordinal)
{
["totalApps"] = snapshot.Entries.Count.ToString(CultureInfo.InvariantCulture),
["successCount"] = success.ToString(CultureInfo.InvariantCulture),
["errorCount"] = error.ToString(CultureInfo.InvariantCulture),
["inProgressCount"] = inProgress.ToString(CultureInfo.InvariantCulture),
["otherCount"] = other.ToString(CultureInfo.InvariantCulture),
};
if (snapshot.Entries.Count > MaxBaselineSummaryApps)
data["appListTruncated"] = "true";
_post.Emit(
eventType: SharedEventTypes.RegistryAppBaseline,
source: SourceLabel,
message: $"Registry app baseline at agent start: {snapshot.Entries.Count} apps ({success} success, {error} error, {inProgress} in progress) — state that predates this agent run",
severity: EventSeverity.Info,
data: data,
occurredAtUtc: nowUtc,
typedPayload: new { apps });
}
private void TrackTerminalTransitions(ImeRegistrySnapshot snapshot, DateTime nowUtc)
{
foreach (var kv in snapshot.Entries)
@@ -552,6 +617,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
public string Key { get; }
public string UserContext { get; }
public string AppId { get; }
public string? LastUpdatedUtc { get; set; }
public int? EnforcementState { get; set; }
public long? ErrorCode { get; set; }
public int? ExitCode { get; set; }
@@ -244,7 +244,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
result.DurationMs = (int)(DateTime.UtcNow - started).TotalMilliseconds;
// Always log locally for diagnostics — the agent_YYYYMMDD.log is the always-on observability.
// Always log locally for diagnostics — agent.log is the always-on observability.
var anomaliesStr = result.Anomalies.Count > 0
? $"anomalies=[{string.Join(",", result.Anomalies)}]"
: "quiet";
@@ -972,7 +972,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime
/// <summary>
/// Appends a log line to the main agent log file (logger isn't initialized yet at update time).
/// Uses the same date-based naming and format as AgentLogger: agent_YYYYMMDD.log
/// Uses the same naming and format as AgentLogger: agent.log
/// </summary>
private static void LogToFile(string message)
{
@@ -980,8 +980,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime
{
var logDir = Environment.ExpandEnvironmentVariables(Constants.LogDirectory);
Directory.CreateDirectory(logDir);
var logFileName = $"agent_{DateTime.Now:yyyyMMdd}.log";
var logPath = Path.Combine(logDir, logFileName);
var logPath = Path.Combine(logDir, "agent.log");
File.AppendAllText(logPath, $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff}] [INFO] {message}{Environment.NewLine}");
}
catch
@@ -26,7 +26,7 @@ namespace AutopilotMonitor.Agent.V2
/// cleanup and exit.</item>
/// <item><b>Session-age emergency break</b> — absolute cap (<c>AbsoluteMaxSessionHours</c>)
/// across restarts prevents zombie agents from persisting forever.</item>
/// <item><b>Crash log writer</b> — writes a last-resort crash_*.log next to the agent log
/// <item><b>Crash log writer</b> — writes a last-resort crash-*.log next to the agent log
/// directory when an uncaught exception escapes <c>Main</c>.</item>
/// </list>
/// </summary>
@@ -34,7 +34,10 @@ namespace AutopilotMonitor.Agent.V2
{
internal const string CleanExitMarkerFileName = "clean-exit.marker";
internal const string EnrollmentCompleteMarkerFileName = "enrollment-complete.marker";
private const string CrashLogPrefix = "crash_";
private const string CrashLogPrefix = "crash-";
// Pre-rename agents (< 2026-08-18) wrote crash_*.log; still detected once so an
// in-place self-update does not lose a pending crash classification.
private const string LegacyCrashLogPrefix = "crash_";
// ----------------------------------------------------------------- Multi-instance guard
@@ -102,7 +105,7 @@ namespace AutopilotMonitor.Agent.V2
}
/// <summary>
/// Reads <c>clean-exit.marker</c> + <c>crash_*.log</c> + session files + (if needed) the
/// Reads <c>clean-exit.marker</c> + <c>crash-*.log</c> + session files + (if needed) the
/// Windows event log to classify the previous exit. Deletes the marker and crash logs so
/// the next cycle starts fresh. Legacy parity — same rules, same outputs.
/// </summary>
@@ -114,6 +117,8 @@ namespace AutopilotMonitor.Agent.V2
var hadCleanExit = File.Exists(cleanExitMarker);
var crashLogs = Directory.Exists(logDirectory)
? Directory.GetFiles(logDirectory, CrashLogPrefix + "*.log")
.Concat(Directory.GetFiles(logDirectory, LegacyCrashLogPrefix + "*.log"))
.ToArray()
: Array.Empty<string>();
if (hadCleanExit)
@@ -406,7 +411,7 @@ namespace AutopilotMonitor.Agent.V2
try
{
Directory.CreateDirectory(logDirectory);
var crashPath = Path.Combine(logDirectory, $"{CrashLogPrefix}{DateTime.UtcNow:yyyyMMdd_HHmmss}.log");
var crashPath = Path.Combine(logDirectory, $"{CrashLogPrefix}{DateTime.UtcNow:yyyyMMdd-HHmmss}.log");
File.WriteAllText(crashPath, $"[{DateTime.UtcNow:u}] FATAL: {ex}");
}
catch { /* nowhere left to log */ }
@@ -7,14 +7,14 @@
# server-updatable without re-shipping the MSI.
#
# Fail-soft by design: the runner always exits 0 so a transient network failure never
# marks the LOB install as failed; diagnostics go to msi-bootstrap.log.
# marks the LOB install as failed; diagnostics go to bootstrap-msi.log.
# ASCII only - no special characters in this file.
$ErrorActionPreference = 'Stop'
$baseDir = Join-Path $env:ProgramData 'AutopilotMonitor'
$logDir = Join-Path $baseDir 'Logs'
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$log = Join-Path $logDir 'msi-bootstrap.log'
$log = Join-Path $logDir 'bootstrap-msi.log'
function Write-Log([string]$message) {
Add-Content -Path $log -Value ("[{0}] {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $message)
@@ -13,7 +13,7 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package Name="Autopilot Monitor Bootstrap"
Manufacturer="Autopilot Monitor"
Version="1.0.0"
Version="1.0.2"
UpgradeCode="1fbe5515-d346-445c-b8b8-14ee79f97522"
Scope="perMachine"
Compressed="yes">
@@ -39,7 +39,7 @@
<!-- Deferred + no-impersonate: runs as SYSTEM after files land. Return=ignore keeps
the LOB install green even if the runner hits a transient error (the runner is
additionally fail-soft itself); diagnostics live in msi-bootstrap.log. -->
additionally fail-soft itself); diagnostics live in bootstrap-msi.log. -->
<CustomAction Id="RunBootstrap"
Directory="INSTALLFOLDER"
ExeCommand="&quot;[System64Folder]WindowsPowerShell\v1.0\powershell.exe&quot; -NoProfile -ExecutionPolicy Bypass -File &quot;[INSTALLFOLDER]Invoke-BootstrapDownload.ps1&quot;"
+4 -4
View File
@@ -24,7 +24,7 @@
"@types/compression": "^1.8.1",
"@types/express": "^5.0.6",
"@types/node": "^22.20.1",
"tsx": "^4.23.11",
"tsx": "^4.23.12",
"typescript": "^5.0.0",
"vitest": "^4.1.10"
},
@@ -3823,9 +3823,9 @@
"optional": true
},
"node_modules/tsx": {
"version": "4.23.11",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.11.tgz",
"integrity": "sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==",
"version": "4.23.12",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz",
"integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -32,7 +32,7 @@
"@types/compression": "^1.8.1",
"@types/express": "^5.0.6",
"@types/node": "^22.20.1",
"tsx": "^4.23.11",
"tsx": "^4.23.12",
"typescript": "^5.0.0",
"vitest": "^4.1.10"
},
@@ -28,8 +28,8 @@ export const DIAG_ZIP_MAP = {
appliesTo: 'v2',
},
{
path: 'AgentLogs/agent_YYYYMMDD.log',
what: 'Full client chronology: startup → config → collectors → enrollment tracking → completion → upload. Rotated per day on multi-day enrollments — read every dated file.',
path: 'AgentLogs/agent.log',
what: 'Full client chronology: startup → config → collectors → enrollment tracking → completion → upload. Size-rotated segments agent-002.log onward. Pre-2026-08-18 agents wrote date-rotated agent_YYYYMMDD.log instead — glob agent*.log to catch both.',
read: 'grep-errors-first',
grepHints: ['"\\[WARN\\]\\|\\[ERROR\\]"', '-A 5 -B 2 "\\[ERROR\\]"', '"DecisionStep: ord="', '"EventUploadService"', '"EnrollmentTracker"', '"DiagnosticsCollector"'],
priority: 2,
@@ -57,7 +57,7 @@ export const DIAG_ZIP_MAP = {
appliesTo: 'v2',
},
{
path: 'AgentLogs/ime_pattern_matches.log (v2) / ime-matches.log (v1)',
path: 'AgentLogs/ime-pattern-matches.log (older v2 agents: ime_pattern_matches.log; v1: ime-matches.log)',
what: 'IME pattern matches. Format: [sourceFileName] [patternId] rawLogLine. Read when the question is about apps / ESP phases / IME patterns.',
read: 'full',
priority: 6,
@@ -113,5 +113,6 @@ export const DIAG_ZIP_MAP = {
'V1 agent archives have NO AgentState/ or AgentSpool/ folders (DecisionCore is v2-only).',
'Pre-provisioned (White Glove) blobs carry a -preprov suffix — that is normal.',
'No archive ≠ a problem: upload mode may be Off or OnFailure (so successful sessions have none).',
'Log naming is kebab-case since 2026-08-18: agent.log, bootstrap-script.log (was bootstrap_agent.log), bootstrap-msi.log (was msi-bootstrap.log), crash-*.log (was crash_*.log), gather-rules-debug.log, ime-pattern-matches.log. Archives from older agents/bootstraps carry the old names.',
],
} as const;
@@ -148,6 +148,7 @@ export const EVENT_TYPES_CATALOG = {
'ime_agent_version',
'ime_token_failure',
'registry_app_state',
'registry_app_baseline',
'app_state_reconciliation',
'ime_user_session_completed',
'ime_session_change',
@@ -60,14 +60,14 @@ namespace AutopilotMonitor.Shared
/// <summary>
/// Default path for the IME pattern match log file (debugging/diagnostics)
/// </summary>
public const string ImeMatchLogPath = @"%ProgramData%\AutopilotMonitor\Logs\ime_pattern_matches.log";
public const string ImeMatchLogPath = @"%ProgramData%\AutopilotMonitor\Logs\ime-pattern-matches.log";
/// <summary>
/// Default path for the gather rule debug trace file (debugging/diagnostics).
/// Written when the tenant enables EnableGatherRuleDebugLog; explains every
/// gather-rule evaluation decision, including outcomes that produce no event.
/// </summary>
public const string GatherRuleDebugLogPath = @"%ProgramData%\AutopilotMonitor\Logs\gather_rules_debug.log";
public const string GatherRuleDebugLogPath = @"%ProgramData%\AutopilotMonitor\Logs\gather-rules-debug.log";
/// <summary>
/// Scheduled Task name used to run the agent as SYSTEM
@@ -421,6 +421,7 @@ namespace AutopilotMonitor.Shared
// per-app state read straight from HKLM\...\IntuneManagementExtension. Observability only —
// never consumed by the DecisionEngine.
public const string RegistryAppState = "registry_app_state"; // Info, state-change-only (snapshot-diff vs silent startup baseline; capped 200/session, cap announced once as Warning): per-app EnforcementState/ErrorCode/ExitCode/StatusService status/ESP-tracking from the registry.
public const string RegistryAppBaseline = "registry_app_baseline"; // Info, one-shot at agent start (only when non-empty): summary of the Win32Apps state that PREDATES this agent run. Closes the WDP pre-agent gap — DPP-policy apps install in Batch 1 before the bootstrap-script agent exists, so their outcomes live only in this baseline (typed payload capped at 50 apps).
public const string AppStateReconciliation = "app_state_reconciliation"; // Warning, once per app, immediate upload: registry terminal outcome (settled >= 90 s) diverges from the IME-log-derived app state — the built-in log-pattern drift alarm (registry = truth, logs = narrative).
public const string ImeUserSessionCompleted = "ime_user_session_completed";
public const string DoTelemetry = "do_telemetry";
@@ -194,7 +194,7 @@ export default function AgentSettingsSection({
<p className="font-medium text-gray-900">IME Pattern Match Log</p>
<p className="text-sm text-gray-500">
Write every matched IME log line to a local file for diagnostics
{enableImeMatchLog && <span className="block text-xs text-gray-400 mt-0.5 font-mono">%ProgramData%\AutopilotMonitor\Logs\ime_pattern_matches.log</span>}
{enableImeMatchLog && <span className="block text-xs text-gray-400 mt-0.5 font-mono">%ProgramData%\AutopilotMonitor\Logs\ime-pattern-matches.log</span>}
</p>
</div>
<button onClick={() => setEnableImeMatchLog(!enableImeMatchLog)}
@@ -209,7 +209,7 @@ export default function AgentSettingsSection({
<p className="font-medium text-gray-900">Gather Rule Debug Log</p>
<p className="text-sm text-gray-500">
Trace gather rule evaluation to a local file to diagnose rules that produce no timeline events. The trace never leaves the device.
{enableGatherRuleDebugLog && <span className="block text-xs text-gray-400 mt-0.5 font-mono">%ProgramData%\AutopilotMonitor\Logs\gather_rules_debug.log</span>}
{enableGatherRuleDebugLog && <span className="block text-xs text-gray-400 mt-0.5 font-mono">%ProgramData%\AutopilotMonitor\Logs\gather-rules-debug.log</span>}
</p>
</div>
<button onClick={() => setEnableGatherRuleDebugLog(!enableGatherRuleDebugLog)}
+64 -64
View File
@@ -13,25 +13,25 @@
"@microsoft/applicationinsights-web": "^3.4.3",
"@microsoft/signalr": "^10.0.11",
"@types/leaflet": "^1.9.22",
"@xyflow/react": "^12.11.2",
"@xyflow/react": "^12.11.3",
"dagre": "^0.8.5",
"fflate": "^0.8.3",
"leaflet": "^1.9.4",
"next": "^16.3.0",
"next": "^16.3.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-leaflet": "^5.0.0",
"recharts": "^2.15.4"
},
"devDependencies": {
"@next/bundle-analyzer": "^16.3.0",
"@next/bundle-analyzer": "^16.3.1",
"@types/dagre": "^0.7.54",
"@types/node": "^22.20.1",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.5.4",
"eslint": "^9.39.5",
"eslint-config-next": "^16.3.0",
"eslint-config-next": "^16.3.1",
"postcss": "^8.5.12",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
@@ -1358,9 +1358,9 @@
"license": "MIT"
},
"node_modules/@next/bundle-analyzer": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.3.0.tgz",
"integrity": "sha512-o8OiXKIATcSC6kHm5BIEmaDEDTFUDhpy9POQIbzAQTlb8NWwFhEQheTLFSbyQXxH2ywhjZ/e8EUlbOjj2k22Yg==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.3.1.tgz",
"integrity": "sha512-/6XQeYPHM6jF1gTeJ82Mu6yXOHQIFVxzAn3DkPtHDp5v6SDXoCicBMTHloN+2h4WKFCQujSLCgLZHItJ3jN1uw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1368,15 +1368,15 @@
}
},
"node_modules/@next/env": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.0.tgz",
"integrity": "sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.1.tgz",
"integrity": "sha512-35G3xwkQUb2oETSDjFXGrVugknoayLFBh7vSE+yAcl9IP2zT9wyGwq7297AYHR11kJld807t5f8AJBs6WBzXsQ==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.0.tgz",
"integrity": "sha512-OqgJ8PN0d04KcPhDX/PTY5tJUJZxlbrt7O7FBsm4XE0XW2JDrKnDXsc9uo9WUimJGPoo2j+JRGhyXApC//mvbw==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.1.tgz",
"integrity": "sha512-B4SznlXwVpaLDa7Tbi6zLuueria2d/PmFDhXyDymPGrk2r1n/RMJmcn5FZq1L64k+Jsyte1lKvOr7lP9Xo80mQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1385,9 +1385,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.0.tgz",
"integrity": "sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.1.tgz",
"integrity": "sha512-ABMIu2zQ7cnNIHm5ivKGwZwUrm0pAai3yiJ/gK/rF1c1VP9UOnj7XECbMKFdVKp9I9eMYq9NoDs1WXOoowxzJw==",
"cpu": [
"arm64"
],
@@ -1401,9 +1401,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.0.tgz",
"integrity": "sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.1.tgz",
"integrity": "sha512-gNG21e/UnrroeScbY/QndUEdl0mF1FRibW7BBeYUz/5ABCepjqDdEdgr592vpzMtCn/m7FTjYq3TN4TpyDnutw==",
"cpu": [
"x64"
],
@@ -1417,9 +1417,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.0.tgz",
"integrity": "sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.1.tgz",
"integrity": "sha512-6B6Lw016iwNUQuaJoraMMTLh6TwHzFUtxipSScD1F3YyymcrRWkobodRS2ftIOkF5vrs4zNlyUrTC5YZQ9Lz5w==",
"cpu": [
"arm64"
],
@@ -1436,9 +1436,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.0.tgz",
"integrity": "sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.1.tgz",
"integrity": "sha512-JUiPXZKK9wOhjf4MgDiH29GZLxfqOesbLtHq2pDxwH/WwscTRV2ToymnOTh1egzaZf0ueUf8T2+CeYTGHjW0Iw==",
"cpu": [
"arm64"
],
@@ -1455,9 +1455,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.0.tgz",
"integrity": "sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.1.tgz",
"integrity": "sha512-Uog9jsrmIRIL/lfvIp9htmskSNC7JcQsMVucXL2V2YY1y/D9IUN3LPEafqy0zRJ2cIU1SQ0V6F6TlffQ+pLAGg==",
"cpu": [
"x64"
],
@@ -1474,9 +1474,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.0.tgz",
"integrity": "sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.1.tgz",
"integrity": "sha512-6yy3FT13KgUFOj5H8bl8w/6nKiJwHIvbtwh1V+1acsu+7y4tJjnemSa6mhsh53BeoVrlozE+fMgZhXH46WmjMA==",
"cpu": [
"x64"
],
@@ -1493,9 +1493,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.0.tgz",
"integrity": "sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.1.tgz",
"integrity": "sha512-iOoN1QecUoGNZik536U/vtK43YwgyrCsGIkth52yIkl612n+0C9MjSnJbQAikISpb+WYRooBVhaDlUW7iZoKog==",
"cpu": [
"arm64"
],
@@ -1890,9 +1890,9 @@
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
@@ -2762,12 +2762,12 @@
}
},
"node_modules/@xyflow/react": {
"version": "12.11.2",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz",
"integrity": "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==",
"version": "12.11.3",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.3.tgz",
"integrity": "sha512-G3jogHz2GWUtIOkhavUGno2YzY9u6fILIJBttfsBendb0/HWB90JG+sOTAvlIMEwyvq9zgy9V9ZQSwyQjR5QzQ==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.79",
"@xyflow/system": "0.0.80",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
@@ -2787,9 +2787,9 @@
}
},
"node_modules/@xyflow/system": {
"version": "0.0.79",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.79.tgz",
"integrity": "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==",
"version": "0.0.80",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.80.tgz",
"integrity": "sha512-ywc3ZqG91brzWrH1WlwMdIX4goOfrpBy6AbLdVSaof/Xx9l138ijIKRExM6EkMro2F+OImGmSiA/WKcXvKVcfA==",
"license": "MIT",
"dependencies": {
"@types/d3-drag": "^3.0.7",
@@ -4216,13 +4216,13 @@
}
},
"node_modules/eslint-config-next": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.0.tgz",
"integrity": "sha512-lPrf1kHsMJEZqO0uXkNB400c5MGrhrTk3BNX7P0ol4gt61+iUlQfjy9TyIOEA9eOXrf+5+mYbT/JsY8+zqUByQ==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.1.tgz",
"integrity": "sha512-0vtrpwFVHFEkycUgV/DyrG29OS+HSRdah5Yu8YuZoiBMtlAT6NIiWzaLwDkJZxr2kGfx+9LIvfQ7KHAlEs0VsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/eslint-plugin-next": "16.3.0",
"@next/eslint-plugin-next": "16.3.1",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.32.0",
@@ -6308,13 +6308,13 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/next/-/next-16.3.0.tgz",
"integrity": "sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/next/-/next-16.3.1.tgz",
"integrity": "sha512-hsAp0i7Rh+/dhe7DGIeN2YlpLM1DP4MNxti9EtDMtqcO612X81MvvEj388/oTce9U1EcEIOWDlGq0zRwrBKvuA==",
"license": "MIT",
"dependencies": {
"@next/env": "16.3.0",
"@swc/helpers": "0.5.15",
"@next/env": "16.3.1",
"@swc/helpers": "0.5.23",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.5.23",
@@ -6327,14 +6327,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.3.0",
"@next/swc-darwin-x64": "16.3.0",
"@next/swc-linux-arm64-gnu": "16.3.0",
"@next/swc-linux-arm64-musl": "16.3.0",
"@next/swc-linux-x64-gnu": "16.3.0",
"@next/swc-linux-x64-musl": "16.3.0",
"@next/swc-win32-arm64-msvc": "16.3.0",
"@next/swc-win32-x64-msvc": "16.3.0",
"@next/swc-darwin-arm64": "16.3.1",
"@next/swc-darwin-x64": "16.3.1",
"@next/swc-linux-arm64-gnu": "16.3.1",
"@next/swc-linux-arm64-musl": "16.3.1",
"@next/swc-linux-x64-gnu": "16.3.1",
"@next/swc-linux-x64-musl": "16.3.1",
"@next/swc-win32-arm64-msvc": "16.3.1",
"@next/swc-win32-x64-msvc": "16.3.1",
"sharp": "^0.35.3"
},
"peerDependencies": {
@@ -6361,9 +6361,9 @@
}
},
"node_modules/next/node_modules/@next/swc-win32-x64-msvc": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.0.tgz",
"integrity": "sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.1.tgz",
"integrity": "sha512-d/k+PpAriUPaeMJJOG7HUSdqfEX46FEPWU1p3/nm2ACmXhj9hFEWdFODUBIpkuijXYkfL90qZzTqVPRp4BW/hw==",
"cpu": [
"x64"
],
+4 -4
View File
@@ -20,25 +20,25 @@
"@microsoft/applicationinsights-web": "^3.4.3",
"@microsoft/signalr": "^10.0.11",
"@types/leaflet": "^1.9.22",
"@xyflow/react": "^12.11.2",
"@xyflow/react": "^12.11.3",
"dagre": "^0.8.5",
"fflate": "^0.8.3",
"leaflet": "^1.9.4",
"next": "^16.3.0",
"next": "^16.3.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-leaflet": "^5.0.0",
"recharts": "^2.15.4"
},
"devDependencies": {
"@next/bundle-analyzer": "^16.3.0",
"@next/bundle-analyzer": "^16.3.1",
"@types/dagre": "^0.7.54",
"@types/node": "^22.20.1",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.5.4",
"eslint": "^9.39.5",
"eslint-config-next": "^16.3.0",
"eslint-config-next": "^16.3.1",
"postcss": "^8.5.12",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
@@ -604,6 +604,7 @@ export const SHARED_MANIFEST = {
"realmjoin_resolved",
"realmjoin_timeout",
"reboot_triggered",
"registry_app_baseline",
"registry_app_state",
"remote_config_fetch_failed",
"script_completed",
@@ -600,6 +600,7 @@
"realmjoin_resolved",
"realmjoin_timeout",
"reboot_triggered",
"registry_app_baseline",
"registry_app_state",
"remote_config_fetch_failed",
"script_completed",