Commit Graph

3080 Commits

Author SHA1 Message Date
Oliver Kieselbach 2517e8eb28 Push receiver: delete entries, user-set retention, self-migrating VAPID key (#180)
* Push receiver: delete entries, user-set retention, self-migrating VAPID key (B-y4z)

- History: a long press (or right-click) on an entry opens a small menu — Delete (local only;
  the history never leaves the device) and Open in portal when the entry carries a link.
- Retention: the status page lets the user keep the history 0-365 days (default 30 as before,
  0 = until the entry cap of 200, which is unchanged and stated); stored in the receiver's
  IndexedDB meta, honoured by the worker and the pages when pruning.
- Key rotation is a re-registration, not a re-pair campaign: GET push/device names the active
  kid and public key; a receiver whose stored kid differs subscribes again with the new key on
  its next open and PUTs the new kid (reconcile action "rekey"); a Stale vapid_key_mismatch row
  re-arms the same way. Operator texts (settings template, generator script) follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* bump pointer: internal, infra (push-relay addendum: delete, retention, key migration)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Push receiver: name a failed subscription repair; the worker re-subscribes with the server's active key

- reconcileDevice returns repairError when the subscribe() or the PUT after it failed (400
  InvalidSubscription, 429, network); the status page shows "present, server not updated" plus
  the reason instead of a silent "present". Retried on the next open as before.
- pushsubscriptionchange (Chrome, Firefox) asks GET push/device for the active key before it
  re-subscribes (planSubscriptionKeys: active key when it differs from the stored kid or no key
  is stored, else the stored key, else nothing), drops the browser's replacement subscription
  after a rotation, PUTs the kid it used and stores kid + key; never a PUT with an empty kid.
  Worker fetches are bounded to 10 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* bump pointer: internal (push-relay: worker key rule)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 15:45:33 +02:00
Oliver Kieselbach d378fabc01 Push receiver: read the declarative notification from the push event (Safari history fix) (#179)
* Push receiver: read the declarative notification from the push event (Safari history fix)

On an iPhone the pushes arrived with the right content, but every history entry was the generic
placeholder: with "mutable": true Safari (iOS >= 18.4) dispatches the regular push event with the
proposed Notification in event.notification and event.data null, and the worker read only
event.data. The push handler now takes event.notification first and shows the same content
itself (one notification, entry written at arrival); a tap on a notification the platform
displayed without the worker rebuilds the entry from the Notification's data, and the click
path's history write is bounded like the push path's. Tests pin both paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* bump pointer: internal (push-relay: Safari declarative-push finding)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 14:59:44 +02:00
Oliver Kieselbach be0e37008a Action pins: exact release tag in every version comment (D-331) (#178)
* Action pins: exact release tag in every version comment (D-331)

setup-node v7.1.0 and upload-artifact v7.0.2 moved the floating v7 tag, so the
"# v7" comments on our still-correct pins (v7.0.0, v7.0.1) stopped naming the
pinned commit and zizmor's ref-version-mismatch raised 11 code-scanning
alerts on main. The other five major-only comments (checkout, setup-dotnet,
cache, attest-build-provenance, functions-action) were one upstream release
away from the same alert.

All 38 major-only comments now carry the tag their unchanged SHA resolves to
(checkout v7.0.1, setup-dotnet v6.0.0, cache v6.1.0, setup-node v7.0.0,
upload-artifact v7.0.1, attest-build-provenance v4.2.2, functions-action
v1.5.7); no SHA moved, Dependabot keeps advancing SHA and comment together.
verify-pins now accepts only a vX.Y.Z comment and names the floating-major
case in its own error.

Verified: zizmor online (GH_TOKEN) reproduced the 11 findings before and
reports 0 ref-version-mismatch after, --min-severity low exits 0; the
verify-pins script passes all 51 uses lines and rejects a major-only comment,
a missing comment and a tag reference in a scratch workflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* bump pointer: internal (D-331 action pin comments)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:45:34 +02:00
Oliver Kieselbach 04fae4fe86 Web Push channel: alerts to paired phones without a third-party messenger (#177)
* bump pointer: internal (ingest-session-row-reads RELEASED, measurement)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Web Push channel: alerts to paired phones without a third-party messenger (D-327 to D-330)

Provider WebhookProviderType.Push = 50 in both channel scopes. It carries no destination;
NotificationChannelDispatcher takes a NotificationScope from every caller and the push
transport resolves the scope's paired devices at send time (table-backed Admin/Operator,
GlobalAdmin with matching identity binding; Viewer and Global Reader never).

The receiver never signs in: a signed-in Admin/Operator creates a one-shot pairing code
(11 Crockford chars, hashed, 10 min, ETag one-shot, five-failure burn), the phone redeems
it anonymously, the portal session confirms. The phone's only credential is the device
token in X-Push-Device-Token, good for status, re-subscribe and unpair of its own row
(10 calls per minute per device; the anonymous pairing routes 10 per minute per IP).

Payloads are RFC 8291 per device (new BCL-only library src/Push with the RFC vector pinned
byte for byte) with a VAPID key ring and the kid on every device row; the projection reads
an allow-list only (never Sections, DataJson, Failure Reason, Last Failure or the ops
message), serial masked, 2 KB. 404/410 mark a device Stale instead of deleting it and the
receiver re-arms it on its next open (the same PUT lifts a delivery-failures pause); devices
pause when their owner stops signing in (PushOwners stamp in auth/me) through one pause path
and resume by themselves; an owner without a stamp is paused, never deleted outright; role
loss revokes with a wipe push and deletes the row (there is no Revoked state). PushDevices
and PushPairingGrants are credential-bearing: raw-table deny-list, no backup, redacted
dependency rows.

Receiver app under /push/ (module service worker, Declarative Web Push shape, local history,
pairing and status pages, manifest + icons), portal UI (Push provider in the channel editor,
pairing dialog with QR code, device lists for tenant and platform scope, "Notify me when
done" on the session page — the watch row records the watcher's own scopes, so a Global
Admin watching a customer session is notified on the platform-paired phone), 22 catalogued
routes, ops events PushDeliveryFailed and PushEndpointRefused (deduplicated per instance),
maintenance sweep inside the existing run, settings template.

The tenant-scope provider stays Global-Admin-only until the customer release (same
ValidateModel gate as Telegram). Backend 7371, library 125, MCP 997, web 2061 tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* bump pointer: internal, .claude/commands, infra (push-relay: plan, D-327 to D-330, markers, VAPID generator)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* bump pointer: internal (push-relay: commit step ticked)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Drop sprintf-js from both lockfiles (Dependabot #114, #117)

GHSA-hp3w-g68c-fv3c (sprintf-js <= 1.1.3, denial of service through unbounded
precision specifiers) has no patched release, so both chains are cut instead.

MCP (#117): sprintf-js arrived via @huggingface/transformers -> onnxruntime-node
-> global-agent 3 -> roarr. global-agent is only loaded by onnxruntime-node's
CUDA install script, which the image never runs (npm ci --ignore-scripts). The
override global-agent ^4.1.3 drops roarr; 4.x keeps the bootstrap export that
script calls, and onnxruntime-node 1.30 depends on the same range.

Rules tooling (#114): ajv-cli 5.0.0 (no release since 2023) pulled js-yaml 3
-> argparse 1 -> sprintf-js. rules/scripts/validate.js now checks the four rule
sets against their schemas with ajv (draft 2020-12) and ajv-formats directly,
and combine-rules.yml runs it in one step instead of four ajv-cli loops. The
lockfile shrinks from 26 to 6 packages; the fast-json-patch override and the
legacy-peer-deps .npmrc (only there for ajv-cli's ts-node peer) are gone.

Verified: validate.js accepts all 149 rule files plus guardrails.json exactly
as ajv-cli did and rejects a corrupted copy (missing field, broken JSON, U+2028
in a guardrail value) with exit 1; clean npm ci + combine.js leave the
generated outputs unchanged; MCP build and 997 tests green; zizmor clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* bump pointer: internal, .claude/commands (validate.js replaces ajv-cli: B-gzv, rule-builder fallback)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bump sharp, proxy-addr and source-map-js past fresh advisories (MCP, Web)

- sharp 0.35.4 -> 0.35.5 (GHSA-wq5f-xc86-pv6w, librsvg): the existing sharp
  override in both projects now floors at ^0.35.5 so the fixed build cannot be
  undercut by a parent range.
- proxy-addr 2.0.7 -> 2.0.8 (GHSA-jqcg-44mw-7w3h, via express; MCP only) and
  source-map-js 1.2.1 -> 1.2.2 (GHSA-68fv-2mgg-jv7q, via postcss) are
  lockfile-only bumps inside the ranges their parents declare.

No other lockfile entry moved. npm audit in the MCP server is clean.
Verified: MCP build and 997 tests green; Web tsc clean, 2061 tests green,
next build (static export) succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* bump pointer: internal (push-relay: CORS verified, cert-exclusion prerequisite, EU app facts)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Web: Next.js 16.3.6 -> 16.3.8 (six advisories)

16.3.8 closes GHSA-3w37-wq28-93x7, GHSA-4jqv-mc3x-m676, GHSA-39w2-rjm5-chcv,
GHSA-f87g-xv8r-7p7x, GHSA-mcj8-r9mp-w47p and GHSA-cjq9-62q9-8jv4 (affected
16.0.0 - 16.3.7). Under output:"export" the cache, ISR and image-optimization
paths never run on the SWA; the dev-server disclosure is local-only. The patch
is taken anyway. next, eslint-config-next and @next/bundle-analyzer stay
aligned and floor at ^16.3.8; the lockfile is pinned to 16.3.8, not the 16.4.0
minor the caret would resolve to.

What npm audit still reports (braces, micromatch, chokidar, fast-glob,
postcss-selector-parser) comes through Tailwind 3 and eslint-config-next,
build-time only; braces has no fix, the rest needs Tailwind 4 (backlog).

Verified: tsc clean, eslint 0 findings, 2061 tests green, next build
(16.3.8, 102 static pages) succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* bump pointer: internal (B-dzs lockfile merge note, B-hxz braces chain)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 13:48:42 +02:00
dependabot[bot] 2b29019b59 chore(deps): bump sharp (#175)
Bumps the mcp-security group with 1 update in the /src/McpServer/autopilot-monitor-mcp directory: [sharp](https://github.com/lovell/sharp).


Updates `sharp` from 0.35.4 to 0.35.5
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.4...v0.35.5)

---
updated-dependencies:
- dependency-name: sharp
  dependency-version: 0.35.5
  dependency-type: indirect
  dependency-group: mcp-security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-07 16:47:50 +02:00
dependabot[bot] aa578def0e chore(deps): bump sharp (#176)
Bumps the web-security group with 1 update in the /src/Web/autopilot-monitor-web directory: [sharp](https://github.com/lovell/sharp).


Updates `sharp` from 0.35.4 to 0.35.5
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.4...v0.35.5)

---
updated-dependencies:
- dependency-name: sharp
  dependency-version: 0.35.5
  dependency-type: indirect
  dependency-group: web-security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-07 16:47:38 +02:00
dependabot[bot] 7eceed16e1 chore(deps): bump the web-security group across 1 directory with 2 updates (#173)
Bumps the web-security group with 2 updates in the /src/Web/autopilot-monitor-web directory: [postcss-selector-parser](https://github.com/postcss/postcss-selector-parser) and [source-map-js](https://github.com/7rulnik/source-map-js).


Removes `postcss-selector-parser`

Updates `source-map-js` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/7rulnik/source-map-js/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: postcss-selector-parser
  dependency-version:
  dependency-type: indirect
  dependency-group: web-security
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
  dependency-group: web-security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-07 16:02:23 +02:00
dependabot[bot] 27454d5744 chore(deps): bump the mcp-security group across 1 directory with 2 updates (#174)
Bumps the mcp-security group with 2 updates in the /src/McpServer/autopilot-monitor-mcp directory: [proxy-addr](https://github.com/jshttp/proxy-addr) and [source-map-js](https://github.com/7rulnik/source-map-js).


Updates `proxy-addr` from 2.0.7 to 2.0.8
- [Release notes](https://github.com/jshttp/proxy-addr/releases)
- [Changelog](https://github.com/jshttp/proxy-addr/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/proxy-addr/compare/v2.0.7...v2.0.8)

Updates `source-map-js` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/7rulnik/source-map-js/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: proxy-addr
  dependency-version: 2.0.8
  dependency-type: indirect
  dependency-group: mcp-security
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
  dependency-group: mcp-security
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-07 16:02:09 +02:00
Oliver Kieselbach 46b8503207 bump pointer: internal (plan ingest-session-row-reads, D-325)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 15:33:38 +02:00
Oliver Kieselbach 50bb09eb92 Read the Sessions row projected on the agent hot path
Backend 1.5.1256 raised IngestTelemetry p50 from 66 to ~100 ms and
RegisterSession from 90 to ~110 ms. Storage metrics per API located it:
server latency of point reads unchanged, end-to-end latency of QueryEntity
alone up from 3.7 to 6.4 ms, every other API flat. The excess times the
point reads per upload is the whole regression, and the request histogram
gained a second mode 46 ms above the first. The only point-read response
that had changed was the Sessions row (~400 bytes of tracker state and
twins), which every upload read twice in full.

- SessionRowProjections: the guard row for the agent routes (Status,
  SerialNumber, owner columns) and the counter merge's slice (counters,
  anchor, index key, tracker state and twins, the fields non-terminal
  batches consume). The guard takes a select and adds its lock columns;
  the inventory reader gets a projected overload, the deletion flows keep
  their full reads.
- The counter merge returns SessionIngestSnapshot, a narrow type instead
  of a SessionSummary built from a projected row. The processor reads the
  whole row only on terminal, diagnostics and transition batches or when
  the merge returned nothing, and derives the snapshot there. The SignalR
  delta and the control signals carry the same fields as before.
- The signal-only upload reads the same slice; registration takes its
  lifecycle entry from the guard's row instead of a second read.
- SessionRowProjectionTests: a select-strict harness pins each list. The
  merge's write, index mirror and snapshot, the slice mapper, the guard
  row's readers and the owner policy come out identical whole or
  projected; a dropped column fails there (verified by mutation).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 15:33:37 +02:00
dependabot[bot] aaf9b74426 chore(deps-dev): bump the web group (#172)
Bumps the web group in /src/Web/autopilot-monitor-web with 2 updates: [@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 `@next/bundle-analyzer` from 16.3.5 to 16.3.6
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.6/packages/next-bundle-analyzer)

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

---
updated-dependencies:
- dependency-name: "@next/bundle-analyzer"
  dependency-version: 16.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web
- dependency-name: eslint-config-next
  dependency-version: 16.3.6
  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-10-06 13:57:27 +02:00
dependabot[bot] e31a5d2875 chore(deps): bump the mcp group (#171)
Bumps the mcp group in /src/McpServer/autopilot-monitor-mcp with 3 updates: [@modelcontextprotocol/node](https://github.com/modelcontextprotocol/typescript-sdk), [@modelcontextprotocol/server](https://github.com/modelcontextprotocol/typescript-sdk) and [@modelcontextprotocol/client](https://github.com/modelcontextprotocol/typescript-sdk).


Updates `@modelcontextprotocol/node` from 2.0.0 to 2.1.0
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/@modelcontextprotocol/node@2.0.0...@modelcontextprotocol/node@2.1.0)

Updates `@modelcontextprotocol/server` from 2.0.0 to 2.2.0
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/@modelcontextprotocol/server@2.0.0...@modelcontextprotocol/server@2.2.0)

Updates `@modelcontextprotocol/client` from 2.0.0 to 2.2.0
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/@modelcontextprotocol/client@2.0.0...@modelcontextprotocol/client@2.2.0)

---
updated-dependencies:
- dependency-name: "@modelcontextprotocol/node"
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mcp
- dependency-name: "@modelcontextprotocol/server"
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mcp
- dependency-name: "@modelcontextprotocol/client"
  dependency-version: 2.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: mcp
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 13:56:30 +02:00
Oliver Kieselbach 5dc17377f6 bump pointer: internal (device-clock-server-frame P0+P1 live; B-dhu)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 23:47:39 +02:00
Oliver Kieselbach e2e12055bf Show the NTP clock skew in the right direction
The agent's ntp_time_check reports offsetSeconds = NTP time - device time,
so a positive value means the device clock is BEHIND. The session card
said "ahead of UTC" for it (a device 10 h behind showed as 10 h ahead),
and ANALYZE-DEV-007 named its two signals the wrong way round.

- SessionInfoCard: the direction comes from deviceClockDirection
  (sessionInfoLogic.ts, pinned by a test with both signs).
- ANALYZE-DEV-007 1.1.1: offsetSeconds > 120 is clock_behind, < -120 is
  clock_ahead. Conditions, thresholds and text are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 22:58:46 +02:00
Oliver Kieselbach 134ea71d63 bump pointer: internal, .claude/commands (plan device-clock-server-frame P1; B-r67; health marker)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 22:54:36 +02:00
Oliver Kieselbach 2f9a404323 Record session start and end on the server clock (no reader yet)
Session durations come from device times, and a device clock can be off by
hours for a whole session or change in the middle of one. Every upload
carries the device send time next to the server receive time; their
difference is the device clock error. This change records the session's
start, WhiteGlove Part 2 start and end on the server clock next to the
device values. Nothing reads them yet, so no stored value and no display
changes.

- ServerTime: the rule, defined by tests/fixtures/server-time/cases.json and
  run by both the backend (ServerTime.cs) and the web (lib/serverTime.ts).
- Ingest: ServerTime.Tracker, the rule fed one upload at a time, keeps its
  state in the primary-only column ServerTimeState and writes
  StartedAtServer and ResumedAtServer in the counter merge (no extra read
  or write). It follows a session from its first upload only, and only when
  that upload carries agent_started; sessions already under way when this
  build arrives get their values at completion. On every upload prefix of
  every followed case it gives the rule's start and Part 2 start; a retried
  upload with agent_started is not counted as a restart.
- Terminal: the counter reconcile, which every terminal path runs, also
  computes the rule over the same event scan and writes StartedAtServer,
  ResumedAtServer and CompletedAtServer. Fail-soft: a rule failure never
  costs the counter reconcile or the ingest write.
- Re-registration carries the four columns from the fresh CAS read through
  the Replace (the agent's first upload often races its registration).
- SessionSummary gets the three fields; the index mirrors them, the index
  merge drops primary-only columns; manifests and wire types regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 22:53:35 +02:00
Oliver Kieselbach 964a548e81 bump pointer: internal (D-324 live; B-xf5 field verify)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 19:45:44 +02:00
Oliver Kieselbach 5d5a893557 Serve the OOBE update phase as Active by default (D-324)
The backend default of CollectorConfiguration.OobeUpdatePhaseMode is now Active. While Windows
installs its OOBE quality update, the decision engine holds the post-ESP completion window (at most
3 h) and then waits 60 min for the sign-in; a session nobody signs in to ends as awaiting the user
or incomplete instead of failed. Agents still count a missing or unknown value as Shadow. Shared
manifest and generated wire types regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 19:23:19 +02:00
Oliver Kieselbach d4e4170813 bump pointer: internal (D-323, plan teams-legacy-connector-removal; B-f5b, B-wkw, B-nuz)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 18:23:05 +02:00
Oliver Kieselbach 6d313d2832 Remove the Teams Office 365 Connector notification provider (D-323)
Microsoft switched off the Office 365 connectors in Teams in May 2026. Provider 1,
its MessageCard renderer, the channel editor option and the flat TeamsWebhookUrl /
TeamsNotifyOn* fields with their fallback branch are gone; the value stays retired.

NotificationChannel.IsSupportedProviderType is now the one rule for channel parsing,
channel validation and the flat WebhookProviderType, so a retired or unknown provider
can no longer be stored and come back as a synthesized channel. The channel editor
looks up the placeholder by value instead of by list index. The header-parsing tests
move to NotificationChannel.GetCustomHeaders; manifests and wire types regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 18:16:16 +02:00
Oliver Kieselbach 0618df3c56 bump pointer: internal (D-322 live; B-vq3 live verify)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 17:48:02 +02:00
Oliver Kieselbach c6fda24843 Show the OOBE quality update live on the session page and in the Progress Portal (D-322)
The time attribution shows the OOBE quality update only once a session ended. While it
runs, the web now derives it from the events it already holds:

- lib/osUpdateLive.ts ports the calculator's update interval for the current window and
  labels it from positive evidence that the update finished: running (restarting),
  waiting for sign-in from 2 min after its end, or failed/skipped without a restart.
  A package waiting for its restart keeps the update running whatever the page reported.
- Session page: a line under the phase timeline, shown after the first complete walk of
  the event pages, with a note when the device has been silent for 15 minutes.
- Progress Portal: the headline says that Windows is installing updates and asks the user
  to sign in after the update's restart.
- OobeUpdateVocabulary (Shared) is the one vocabulary of the agent, the calculator and the
  web (manifest section oobeUpdate); same strings on the wire, no behaviour change.
- tests/fixtures/os-update-live/cases.json pins the calculator and the port
  (TimeAttributionOsUpdateLiveParityTests, lib/__tests__/osUpdateLive.test.ts).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 17:37:28 +02:00
Oliver Kieselbach 32bfa8b5b7 bump pointer: internal (feedback-bubble-ask-twice live)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:55:20 +02:00
Oliver Kieselbach c7bfc1e2f1 bump pointer: internal (plan feedback-bubble-ask-twice; B-yzf solved)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:45:33 +02:00
Oliver Kieselbach 2fb533ab92 Rating bubble asks a person at most twice and keeps one answer per person
- An X click counts a dismissal (DismissCount on the person's row); the second ask waits for the
  configured pause (FeedbackCooldownDays), the second dismissal ends the asking. A rating ends it at
  once. Ignoring the bubble records nothing, as before.
- A rated row is final: a later dismissal or rating (another tab) changes nothing and sends no second
  ping. Answers are written conditionally on what was read (ETag), so two tabs cannot overwrite a rating.
- Rows from before the count read as one dismissal when dismissed; the person keeps UPN and name.
- Rules in FeedbackPromptRules; the admin Ratings tab shows "dismissed twice".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:44:41 +02:00
Oliver Kieselbach b0b38c1cfa bump pointer: internal (D-321 live; B-az6 alert rule, B-yre live verify)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 15:59:51 +02:00
Oliver Kieselbach 34c8715c5a bump pointer: internal (D-321, plan in-app-feedback-and-upload-budget), .claude/commands (health markers and events for D-321)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 15:47:55 +02:00
Oliver Kieselbach 5e240d7b81 Send feedback from the help menu; Submit Logs and Report Session share one upload budget (D-321)
- POST feedback/general (MemberRead): text up to 4096 characters, optional reply address, at most
  10 per user in 24 hours; one Feedback row per message (PK General), shown in a new admin tab.
- FeedbackReceived ops event without text or sender, plus a bell entry. The help menu gets one
  "Send feedback" entry; the dialog links to Submit Logs for Admins and Operators.
- Report uploads: logs are zipped before sending and both forms share one budget derived from the
  20 MiB body cap (Constants.SubmissionLimits through the shared manifest). The session exports go
  along when they fit next to the attachments.
- Report Session opens to Operators. Comment and contact address are validated; a report whose row
  cannot be written fails and its uploaded blobs are removed.
- feedback/status no longer creates a tenant configuration row for an unknown tenant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 15:47:37 +02:00
Oliver Kieselbach a621a00252 bump pointer: internal (D-320 released, agent 2.0.1478; B-ywb for the field verify)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 12:14:36 +02:00
Oliver Kieselbach d1063f17cb bump pointer: internal (D-320, concept docs, plan oobe-update-outcome-and-late-start)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
v2.0.1478
2026-10-05 11:27:58 +02:00
Oliver Kieselbach ed517b5b40 Agent: the first run reads the OOBE update page back an hour; live and backfilled names have their own budgets (D-320)
The first run has no Shell-Core replay (the restart replay re-raises the Hello-wizard start), so an
agent installed late during the ESP missed the update page's start and first half (6a7d241d).

- BackfillUpdatePageTelemetry: on the first run the last 60 min of Shell-Core become oobe_update_page
  rows and nothing else — no Hello replay, no ESP exit or failure, no agent_trace, no registry read.
- Live and backfilled names have budgets of 150 each; the first name over a budget sends a
  names_capped marker, so a missing name is never read as "did not happen".
- A backfill batch keeps its own last page and moves the live watcher's only with a newer RecordId:
  a restart replay's older page start no longer takes over the next live stop, and a first run's
  live stop finds the page that started before the agent.

Agent suite 3,436 green; the agent stays on net48.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 11:27:08 +02:00
Oliver Kieselbach 46058ffb90 OOBE quality update: how it ended, a begin without the page start, three rules for the update that did not happen (D-320)
Field measurement 3: a first-run agent that started after the update page (6a7d241d) left the whole
update in Apps (ESP), failed and skipped updates were reported by nothing, and the lane listed a
failed update's staged servicing stack as an installed KB.

- Time attribution v5: update names before the first observed OobeNDUP start begin the update at the
  earlier of the first name and the update's first servicing step (after the window's ESP exit when
  one was seen before the name, else the first step of a package the update also serviced later).
  The fallback's ESP exit is per window, so a WhiteGlove part-1 exit no longer begins an update in
  part 2. OsUpdateSpan carries Outcome (installed/failed/skipped/unknown, the latest evidence wins)
  and NotInstalledKbs; Kbs lists only packages that reached Installed. The 2-h sweep recomputes the
  last 30 days.
- ANALYZE-DEV-013 (update failed), DEV-014 (not offered after an earlier failure), DEV-015 (update
  check ended without a result): warning, never KO; a failure only from explicit names, every
  absence condition gated on the agent's names_capped marker; never two at once.
- ShellCoreTracker rows never set the session start (D-315 list), ahead of the agent's first-run
  update page backfill.
- The web lane names the outcome and the KBs not installed; MCP get_time_attribution mentions both.

Real-event replay: 6a7d241d os_update 2358 s installed (was 55 min in esp_apps), e7551b82 failed with
KB5124007 not installed. Rule dry runs and a sweep over all 97 update-page sessions since 2026-10-02
hit exactly the five field sessions. Functions 6,918, web 1,701, MCP 997 green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 11:26:57 +02:00
Oliver Kieselbach 81d0fe6ed3 Row accents survive the list divider on every row, not just the first
Tailwind 3's divide-<color> rule (`> :not([hidden]) ~ :not([hidden])`,
specificity 0,3,0, plus the dark-mode !important overrides in globals.css)
recolours every child but the first on all sides, so a row's own border
accent only ever showed on the first row.

- Notification dropdown: the purple GA and blue tenant bars are inset
  shadows now, so every such row keeps its bar.
- Geographic performance: the red outlier bar moves from the row to its
  first cell; it was grey on every outlier but the first table row.
- Tenant config report: the heavier separator between notification
  channels and between self-hosted AI clients moves to the cell
  (GROUP_SEPARATOR); the divider had shrunk it to a plain 1px line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 09:33:47 +02:00
Oliver Kieselbach a26cd5edf7 bump pointer: internal (D-319 released, agent 2.0.1477; B-bmk narrowed to the field verify)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 09:23:51 +02:00
Oliver Kieselbach 48e9702fd2 bump pointer: internal (D-319, concept docs, plan rewound-line-freshness, B-bmk)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
v2.0.1477
2026-10-05 09:07:10 +02:00
Oliver Kieselbach b560cb13b6 A re-processed IME log entry anchors itself when the bytes it replaced were seen moments ago (D-319)
A concurrent IME writer overwrites lines behind the bookmark, and the overwrite check recovers them a
pass later. Recovered lines were never fresh, so they resolved in the agent's own cached zone: in a lab
the zone was stale (+60, IME wrote +120), recovered result lines landed an hour ahead of the clock and
the platform-script durations were omitted ("n/a" on Install Autopilot Monitor, session d29e27e3, also
on 2.0.1474/2.0.1467). Bytes that read differently at a position were written after that position was
last seen, and IME stamps a line when it writes it, so the last sighting bounds the new line's age.

- Ledger entries carry SeenUtc: set when read or seeded, renewed whenever a verification finds them
  unchanged.
- A changed entry below the old bookmark is fresh when the pass is, the ledger held the entry at the
  rewind point, and the replaced bytes were seen 0-30 s ago; otherwise the fallback as before.
  Re-processed entries are never calibration anchors.

Field replay and longer-running script red before; six mutation proofs; agent suite 3,426 green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 09:07:09 +02:00
Oliver Kieselbach b0f866dc7d bump pointer: internal (OOBE update field measurement 3, D-315 verified)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 08:57:03 +02:00
Oliver Kieselbach f3460ea828 bump pointer: internal (D-318 released, agent 2.0.1476; B-gc8 narrowed to the field verify)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:12:25 +02:00
Oliver Kieselbach d88d4a143b bump pointer: internal (D-318, concept docs, plan platform-script-retry-pairing, B-gc8)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
v2.0.1476
2026-10-04 18:29:50 +02:00
Oliver Kieselbach 913dfa13fe One platform run per executor start; IME results pair with the run that started last before them, the fallback judges by IME's rule (D-318)
IME re-runs a failed platform script on later check-ins, and the agent's first pass reads all of
AgentExecutor.log before the IME log, so several runs of one policy shared one slot: the first IME
result took the LAST run's exit code and output (session eff1b413: the bootstrap script "Failed
(exit 0)" with the stdout of its successful retry), every later result went out without an end
block. The exit-code fallback said Success for exit 0 with stderr and Failed for exit 1 without,
which IME never does.

- A later timestamped executor start parks the previous run of its policy (persisted, max 64); an
  IME result pairs with the run of its policy and user that started last before the result line;
  earlier parked runs of the same user resolve then, so emits keep run order.
- Invocation markers carry the run id; the emitted marker only rises; no dedup path deletes a live
  run; a parked emit never takes the later run's D-316 register entry.
- Fallback: Success exactly when the stderr is empty, the exit code only when no stderr line was
  read; "write output done … error =" replaces the "error from script" snapshot, empty stdout too.

Backfill replays fail on the old code; nine mutation proofs; agent suite 3,416 green.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 18:29:33 +02:00
Oliver Kieselbach 51345718c6 bump pointer: internal (D-316/D-317 released, agent 2.0.1475; B-qrz narrowed to the field verify)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 15:46:47 +02:00
Oliver Kieselbach 6b92e4e28c bump pointer: internal (D-316/D-317, concept docs, plan review, B-qrz/B-cwy/B-shx/B-96m), .claude/commands (platform-health marker)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
v2.0.1475
2026-10-04 15:20:45 +02:00
Oliver Kieselbach f392819b97 Portal, MCP and backend apply script_output_reconciliation: a corrected platform run shows its own output and "exit n/a" (D-316)
Web: the script panel applies the correction to the run its runId names: the saved output replaces the emitted one, a foreign end block's exit code shows as n/a with an explanation, the state follows the corrected data instead of the emitted event type, and the output carries a short "from IME's saved result" note. The timeline marks the original row as corrected later, and the correction event falls under the script-output setting like the other script events. The session page passes the event to the panel; the gather-rule event list knows the type.

MCP: get_session_summary no longer counts a script_failed as an error, ranks it as one or attaches the exit code's catalog text when a foreign correction with IME result Success names its run.

Backend: the correction never heals a stalled session and never counts as a script run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 15:15:07 +02:00
Oliver Kieselbach 672570df04 Platform script output is checked against IME's saved result and corrected when the log attributed another script's end block (D-316)
After reporting a script batch, IME saves every run under HKLM\SOFTWARE\Microsoft\IntuneManagementExtension\Policies\<user>\<policy>; ResultDetails carries the stdout IME read from that policy's own result file (the stderr when it failed with error code 99). The agent now checks each emitted platform run against it: a match confirms the output and the exit code of the same end block, a contradiction emits script_output_reconciliation (Warning, outcome "foreign": the saved output replaces the emitted one, the exit code is unverified), and output the log delivered incomplete is completed (Info, outcome "repaired").

A save is paired with the latest run of its user and policy only after the tracker completed a pass that started two seconds after the save was seen, so a stalled poll loop cannot pair it with an older run. Platform completions carry a runId the correction names. Two restart-safe counters (verified, corrected) ride agent_metrics_snapshot and ime_pattern_hits. The new event type is registered in the shared constants, the generated manifests and the MCP event catalog.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 15:08:22 +02:00
Oliver Kieselbach 6f7ececa1f Platform scripts no longer take another executor's output and exit code: a cut line in AgentExecutor.log ends the invocation in front of it (D-317)
Two AgentExecutor processes that open the log at the same end write their start blocks over each other. The end block of the one whose start block lost then sat behind the other executor's start block, and position-based ownership handed a Win32 detection script's stdout and exit code to the platform script.

An AgentExecutor.log entry that is not exactly one CMTrace record (a fragment, a merged line or an empty line) is where one writer's bytes end and another's begin: it now closes the platform invocation in front of it and is never matched. Invocation markers live as long as the bytes of their entry: a rewind sets them aside and gives an entry that reads unchanged its markers back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 15:08:21 +02:00
Oliver Kieselbach af7e9d3650 bump pointer: internal (D-315 live, B-crw field verify)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 13:51:00 +02:00
Oliver Kieselbach b3b122a23c bump pointer: internal (session start anchor D-315, plan, B-crw)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 13:41:19 +02:00
Oliver Kieselbach 064b670c75 Update and servicing rows backfilled from before the agent no longer set the session start (D-315)
Since agent 2.0.1473 the Windows Update and servicing watchers backfill rows with their event-log
time up to 60 min before the agent started, and the session anchor only skipped the system
timeline watcher, so session starts and durations grew by 22 min at the median.

- Shared: Constants.EventSources.SessionAnchorIneligible and IsSessionAnchorEligible are the one
  rule; the agent writes the same constants as Source.
- Backend: the ingest batch (ClassifyEvents, now internal static) and the Events-table probe use it.
- Web: the session duration reads the list from shared-manifests.json.
- Recorded sessions keep their stored start; the rule applies to new ingests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 13:40:54 +02:00
Oliver Kieselbach e5eb6dc4fb bump pointer: internal (OOBE update field measurement 2, B-cqa session start regression, B-93v)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 13:05:18 +02:00
Oliver Kieselbach 9f46adcdfa ci: the storage auth helper masks its token in the run log, like the Key Vault token
GitHub masks only secrets.* by itself. The bearer token Get-StorageAuthHeaders.ps1
mints at run time was never registered as a mask, unlike the Key Vault signing token,
which build-agent, build-msi and publish-scripts mask before exporting it. No caller
prints it today -- every call site captures the hashtable and passes it only as
-Headers -- but this repository's run logs are public, and a single debugging line
would have published it: an uncaptured call of the helper, or Get-Error /
Format-List on a failed request, whose TargetObject is the HttpRequestMessage
including the Authorization header (reproduced locally with a dummy token).

The helper now emits ::add-mask:: for every token it hands out, which covers all six
workflows in one place. Write-Host, never Write-Output: the output stream is the
script's return value. Verified with a mocked az that the result stays one hashtable
and that a nested caller in the Request-BuildNumber.ps1 pattern still returns only
its build number. Only under GITHUB_ACTIONS -- locally the line would print the token
instead of hiding it.

Prompted by an external AI review of the public scripts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 12:52:01 +02:00