Commit Graph

3176 Commits

Author SHA1 Message Date
Oliver Kieselbach 4e519fa4b7 Backend: the correlation-id gate anchors with \z
.NET's $ also matches before a final newline, so "7c9e6679\n" passed the allow-list that guards
the inbound X-Correlation-ID against CRLF and log forgery and was echoed. Such a value does not
arrive over HTTP, but the gate now holds what its comment promises; a new test case pins it
(red with $).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:55:26 +02:00
Oliver Kieselbach 52f6a177f9 bump pointers: internal, .claude/commands (client-cert interception, D-349)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:45:02 +02:00
Oliver Kieselbach caafaeafd4 Tests: the push recording handler survives the parallel fan-out
PushDeliveryService sends with Parallel.ForEachAsync, and RecordingHandler appended to plain
lists without a lock: two concurrent sends could lose a request (Delivered 2, recorded 1), so
Tenant_alert_also_reaches_platform_devices_whose_owners_home_tenant_it_is failed now and then
(once in six full runs while it was traced). A request and its body are now recorded together
under the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:43:52 +02:00
Oliver Kieselbach bfb6c12384 Agent: an intercepted connection is no longer an auth failure
A security client that inspects TLS strips the device certificate on the way to the backend;
the platform then answers 403 "Client Certificate Required" itself, or a proxy answers with
its own page. The agent counted these like backend rejections: five in a row shut it down,
the scheduled task restarted it at every boot, and only the 48-hour emergency break removed it.

- Origin of every 401/403: only a response that echoes the request's X-Correlation-ID is the
  backend's verdict and counts toward the auth shutdown; the stopped-app page still counts;
  anything else is foreign (client_certificate_required or foreign_response) and never counts.
- Telemetry: a foreign answer keeps the batch and pauses uploads with a backoff from 30 s up to
  15 min, without a timer; collectors and the decision engine keep running, the terminal phase
  gets one upload attempt, and the agent still leaves the device when the enrollment ends.
- Distress type ClientCertificateNotReceived (9), once per process, with operation, status,
  page title and the issuer of the server certificate; both handlers observe it through a
  validation callback that returns exactly sslPolicyErrors == None.
- Registration: an intercepted start retries on at most InterceptedRegistrationRetryBoots
  further boots (config channel, default 1, agent clamp 0..5, ConfigVersion 41), then ends
  monitoring like the emergency break. The break, this give-up and the config kill signal now
  share Program.EndMonitoringAtStart.
- Evidence: telemetry_upload_blocked carries reason, operation, statusCode, pageTitle,
  serverCertIssuer and firstBlockedAtUtc; agent_started carries priorRegistration* after failed
  starts (a local network fingerprint; only whether it changed leaves the device);
  agent_metrics_snapshot counts 2xx responses without the echo.
- Backend: distress type 9, the buffer default, InterceptedRuns on the registration-failure
  summary; tests pin the correlation echo on 401/403 and the middleware's place in the pipeline.
- Web: distress labels for TPM Incompatible and Cert Not Received; config report row.
- MCP: session coverage names intercepted uploads and the failed registrations before the start.

D-349

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:42:59 +02:00
Oliver Kieselbach cd4199b1d1 bump pointer: internal (about page in the evidence-loop plan)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:53:02 +02:00
Oliver Kieselbach d203ceee32 About: the evidence loop and AI investigation
- What is Autopilot Monitor?: the evidence of every enrollment is recorded while it runs;
  analyze rules flag the patterns they know, and for the rest the AI assistant over the
  built-in MCP server investigates the session (clients that work with files down to the
  raw IME logs of an uploaded diagnostics package) and drafts the rule for the next one
- Key Features: "AI Investigation & Rule Authoring", full width as the seventh card
- Technology & Platform → Integrations: the MCP server (read-only)

The claims carry the same qualifiers as the landing and the docs (D-348).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:52:03 +02:00
Oliver Kieselbach 59c012b42e chore 2026-10-12 00:49:40 +02:00
Oliver Kieselbach 86a122f31f bump pointer: internal (evidence loop released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:46:13 +02:00
Oliver Kieselbach 14af0a05cd bump pointers: internal, .claude/commands (evidence loop, D-348)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:37:03 +02:00
Oliver Kieselbach ee41ea80cf Landing, /ai and portal: the evidence loop
- Landing: section evidence_loop right after the stats ("Never debug the same failure
  twice." — capture, detect, investigate, learn), hero sub-line, Compare row "Unknown
  failures", capabilities tile. The terminal demo continues with the rule step: validate_rule
  and both test_analyze_rule verdicts are the real results of a dry-run against the demo's
  two sessions, anonymized like the rest; the window scrolls with the newest line.
- /ai: "Facts first. Raw logs when it matters." with the way back to the raw line; new
  section rules ("From finding to rule") with the anonymized trace of the same dry-run;
  the step card is shared with Connect.
- Portal: a failed session without any rule finding offers the AI investigation in Analysis
  and Guided Diagnosis (the debug-session prompt for clients with MCP prompts, a copyable
  full prompt for every other client). A failed session without a diagnostics package on a
  tenant without a usable upload links to the diagnostics settings. The analyze-rule editor
  tests a draft against a session on the existing dry-run route; create, save and test now
  share one rule body.
- llms.txt (evidence loop, two docs links without a redirect), about FAQ, marketing guard
  sections, three tracking events; jscpd baseline -1 for the removed payload clone.

D-348

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:37:03 +02:00
Oliver Kieselbach 20ae3d8c09 MCP: list the rule dry-run tools only for roles that can run them
GET /api/auth/mcp reports the caller's client-capped member role in the token's tenant
(tenantRole; reported, never a grant). The MCP server registers test_analyze_rule and
test_log_pattern only for a platform role or a Tenant Admin: their routes are
TenantAdminOrGlobalReader, so Operators and Viewers only ever got a 403. validate_rule,
author-rule and the authoring guide hand a draft to a Tenant Admin otherwise, and
investigate-failed-session and debug-session end by drafting a rule for a cause that can
recur. Catalog ratchet lowered for tenant users (51,000) and delegated callers (55,500).

D-348

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:37:03 +02:00
Oliver Kieselbach d0f62a6aaf web(geo): sign-in before the loading card on Geographic Performance
Both geo pages returned their CalculatingCard before rendering their own
ProtectedRoute. MSAL keeps the account in sessionStorage, so a new tab,
bookmark or typed URL starts signed out: the tenant scope never settled,
the fetch never started, and ProtectedRoute never mounted to fire the
login redirect. The page spun on "Calculating geographic metrics… 0s
elapsed" forever with no API call.

ProtectedRoute now wraps each page's default export, so the content only
mounts once signed in. Verified in a signed-out headless Edge: production
stays on the card, the fix redirects to login.microsoftonline.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:28:08 +02:00
Oliver Kieselbach 683142dd27 bump pointer: internal (docs meta descriptions live)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:24:56 +02:00
Oliver Kieselbach e3f9ad5704 bump pointer: internal (CI endpoint registry released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:56:41 +02:00
Oliver Kieselbach 0f98d9ca17 CodeQL: ignore action-pins.yml like ci.yml does
The workflow gates file was missing from both paths-ignore lists, so every gate
change re-ran the full CodeQL analysis for nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:56:40 +02:00
Oliver Kieselbach bfe345ca93 bump pointer: internal (CI endpoint registry: redirect URIs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:39:50 +02:00
Oliver Kieselbach 587ed74d96 deploy-web: Entra redirect URIs from the endpoint registry
Both were repository variables holding the www origin; lib/msalConfig.ts uses them
only as the static-render fallback and window.location.origin in the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:39:49 +02:00
Oliver Kieselbach d5222fcb93 bump pointer: infra, internal (CI endpoint registry: D-347)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:32:38 +02:00
Oliver Kieselbach 8d24c5680b CI: one endpoint registry for every host the workflows use
.github/scripts/Get-Endpoints.ps1 defines the storage account (blob and table
derived from its name), the containers, the AdminConfiguration row, the legacy
mirror, the download alias, API, MCP, portal, www, docs and the timestamp
server. Workflows load it once per job; the script publisher and the local
release build call it; the dev-render anchors are built from the alias entry.

deploy-web takes NEXT_PUBLIC_API_BASE_URL and NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL
from the registry instead of repository variables and refuses an empty value.
A new Workflow gates job fails on any host spelled out in a workflow or CI
helper; the enforced list is derived from the registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:32:36 +02:00
Oliver Kieselbach 818f5f1091 bump pointer: internal (CI credential zones verified)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:56:56 +02:00
Oliver Kieselbach 485bda3658 bump pointer: internal (CI credential zones: 2b on 2026-10-19, B-2s7 done)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:44:56 +02:00
Oliver Kieselbach 0b82faa7bd bump pointer: internal (CI credential zones: D-346, deploy identity on environments)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:21:58 +02:00
Oliver Kieselbach c26542776e CI: credential-free build jobs, trust jobs that never build
Every job that runs third-party code (restore, build, tests, npm ci, next build,
wix) now holds only a read-only token and hands its output on as one archive.
Signing, publishing and deploying happen in separate jobs on a fresh runner that
run only this repository's scripts and pinned actions, unpack artifacts outside
the checkout and take no build-job output into a script.

- build-agent: reserve -> build -> release
- build-msi: sign-loader -> build -> release
- deploy-backend: reserve -> build -> deploy (publish output as one tar.gz)
- deploy-web: build -> deploy (SWA token only, no id-token) -> record
- combine-rules: validate (read-only, PRs too) -> publish (push to main only)
- deploy-mcp: same shape; documents docker build as its boundary

Deploy-only jobs run in the production environment. The jobs that sign stay
without an environment for now: a job carries one OIDC subject, and the signing
identity still trusts the ref. oidc-login-test.yml is removed; combine-rules no
longer triggers on the dev branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:21:04 +02:00
Oliver Kieselbach 80cd6c52f4 bump pointer: internal (B-vky)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:46:09 +02:00
Oliver Kieselbach e40471e61a Agent release: run all agent test suites before the release build
build-agent.yml tested only DecisionCore (648 of 4,154 agent tests). It now runs
the DecisionCore, Agent.V2.Core and SummaryDialog suites before building the
release binaries, with the reserved build number.

The order matters: Agent.V2.Core.Tests references the agent exe, so its build
compiles the agent. Run after the release build or without BUILD_NUMBER, it
rewrites the exe with another version (a fresh checkout stamped 2.0.1 in a local
simulation). Run first with the number, it stamps the release version and the
Build Agent step stays an up-to-date no-op (same FileVersion, same write time).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:39:01 +02:00
Oliver Kieselbach 7908d54999 bump pointer: internal (dead-code and duplicate gates released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:17:04 +02:00
dependabot[bot] 51aaa2e80a chore(deps): bump the mcp group (#183)
Bumps the mcp group in /src/McpServer/autopilot-monitor-mcp with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@modelcontextprotocol/node](https://github.com/modelcontextprotocol/typescript-sdk) | `2.1.0` | `2.1.1` |
| [@modelcontextprotocol/server](https://github.com/modelcontextprotocol/typescript-sdk) | `2.2.0` | `2.3.0` |
| [@modelcontextprotocol/client](https://github.com/modelcontextprotocol/typescript-sdk) | `2.2.0` | `2.3.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.20.4` | `22.20.5` |
| [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `6.37.0` | `6.39.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.65.0` | `8.71.0` |


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

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

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

Updates `@types/node` from 22.20.4 to 22.20.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `knip` from 6.37.0 to 6.39.0
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.39.0/packages/knip)

Updates `typescript-eslint` from 8.65.0 to 8.71.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.71.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@modelcontextprotocol/node"
  dependency-version: 2.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: mcp
- dependency-name: "@modelcontextprotocol/server"
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mcp
- dependency-name: "@modelcontextprotocol/client"
  dependency-version: 2.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: mcp
- dependency-name: "@types/node"
  dependency-version: 22.20.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: mcp
- dependency-name: knip
  dependency-version: 6.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: mcp
- dependency-name: typescript-eslint
  dependency-version: 8.71.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>
v2.0.1481
2026-10-10 18:59:41 +02:00
dependabot[bot] 0a6779cd26 chore(deps-dev): bump the web group (#182)
Bumps the web group in /src/Web/autopilot-monitor-web with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip).


Updates `@types/node` from 22.20.4 to 22.20.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `knip` from 6.37.0 to 6.39.0
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.39.0/packages/knip)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.20.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web
- dependency-name: knip
  dependency-version: 6.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  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-10 18:59:27 +02:00
dependabot[bot] 1a1c2b413e chore(deps-dev): bump jscpd (#181)
Bumps the quality-tooling group in /tools/quality with 1 update: [jscpd](https://github.com/kucherenko/jscpd/tree/HEAD/rust/jscpd).


Updates `jscpd` from 5.3.0 to 5.4.0
- [Release notes](https://github.com/kucherenko/jscpd/releases)
- [Commits](https://github.com/kucherenko/jscpd/commits/v5.4.0/rust/jscpd)

---
updated-dependencies:
- dependency-name: jscpd
  dependency-version: 5.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: quality-tooling
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-10 18:59:15 +02:00
Oliver Kieselbach 5e763375b4 bump pointer: internal, .claude/commands (dead-code and duplicate gates built: D-343..D-345)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:33:20 +02:00
Oliver Kieselbach 9f4f456dc5 CLAUDE.md: search for an existing helper by behaviour; run the dead-code and duplicate gates
Names the helper homes (web lib/, backend Functions/Helpers/) and asks for a
search by what a helper does before writing a new one, which no tool can judge.
Lists the local commands for the new gates, since CI only reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:10:32 +02:00
Oliver Kieselbach af2f2c7abe CI: duplicate-code check over src/ with a baseline that only shrinks
tools/quality runs jscpd (5.3.0, pinned with a lockfile like rules/scripts)
over the C# and TypeScript sources, tests and generated files excluded, at 100
tokens. The 95 existing clones are frozen in jscpd-baseline.json. A clone that
is not in the baseline fails, and so does a baseline entry whose clone is gone:
jscpd only reports missing fingerprints, and a stale one would let the same
copy come back unnoticed.

jscpd scans an LF-normalized copy of the tracked and new files under src/,
because with CRLF in a Windows checkout some JSX clones hash differently than
on the Linux runner; local runs and CI now see the same 95 clones.

At 70 tokens a sixth of all clones were identical using blocks, hence 100.
Gate proven both ways with an untracked probe file: a third copy of an existing
helper fails with its location, and a baseline entry without its clone fails.

New CI job "Duplicate code"; Dependabot watches tools/quality with the usual
7-day cooldown. Locally: npm run dup (check), npm run dup:baseline (shrink).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:10:07 +02:00
Oliver Kieselbach 98d87a8579 Build: unused private members, unread fields and unused parameters fail the .NET build
The root .editorconfig raises IDE0051, IDE0052 and IDE0060 (non-public
methods) to warning, and Directory.Build.props sets EnforceCodeStyleInBuild,
so the warning freeze turns them into build errors in every project, the net48
agent included.

Cleanup to start at zero, each finding checked for leftover vs. missing
behaviour (none was a feature that silently never ran):
- 38 injected dependencies nobody read: loggers, configuration, and services
  whose last read had moved to a live replacement (the IME reseed writes
  through the rule repository, the offboarding poison OpsEvent comes from the
  handler, the vulnerability gate sits in the correlate handler). Removed with
  their constructor parameters and the argument at every construction site.
- unused private members: SelfUpdater.IsNewerVersion, a GatherRuleExecutor
  wrapper, test constants and an unused test overload.
  RjPhaseCompletedFirstDeployment now replaces the two literal 110s it names.
- unused parameters removed together with their arguments; test fakes bound as
  method groups use discards.
- the commented-out backend-URL connectivity check and its plumbing are gone;
  the reason the backend stays out remains as a comment.
- DiagnosticsPackageService names the blob in its upload-failure warnings, as
  its doc already said.
- stale docs fixed on the way: OnboardedAt no longer claims a maintenance
  backfill (shared manifest and wire types regenerated), the vulnerability
  functions' policy comments match the catalog, a misplaced summary moved to
  its method.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:03:00 +02:00
Oliver Kieselbach 0709ce2f76 Web, MCP: knip gates unused files, exports and dependencies
knip (6.37.0) runs in both TypeScript projects as its own CI step
(npm run knip) next to ESLint. It builds the module graph, so "nothing
imports this" is checked against every reference instead of a search.
Generated files stay out of scope (lib/generated, src/generated); the
MCP's Dockerfile-run precompute and its manual scripts are entries, and so
is the web's compile-time manifest check.

Cleanup to start at zero:
- web: four orphaned components (AdminPageSections, SettingsMenu, UserMenu
  (the navbar renders its own menu) and MarkFailedModal), 17 unused sidebar
  icons, four unused config constants (the agent-download host allowlist lost
  its only reader with the /go route in July), the push device-status union
  that existed twice and was used nowhere, an uncalled push error helper,
  unused barrel re-exports, default exports duplicating named ones, four
  superseded DTO interfaces, and a JSDoc type import of an undeclared package.
- mcp: two test helpers nothing calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:56:23 +02:00
Oliver Kieselbach 69d876b0a6 MCP: lint and type-check tests and scripts like the web app
The MCP server had no ESLint, and since its tests left the tsconfig (to keep
them out of dist/) nothing type-checked them; scripts/ never was. tsconfig.json
now type-checks sources, tests and scripts (noEmit); tsconfig.build.json emits
the same dist/ as before (byte-identical) for npm run build and the Docker
image. ESLint (the web's versions: eslint 9.39.5, typescript-eslint 8.65.0)
runs the recommended rules plus the typed promise rules, and CI's mcp job runs
it with --max-warnings 0.

Findings fixed: 16 type errors in tests (a fixture missing three wire fields,
unknown catch results, untyped mocks); gracefulShutdown was async without
awaiting anything, so the signal handlers returned a promise; two dead imports;
a Function cast replaced by the exact pipeline() signature; an unused test
variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:42:13 +02:00
Oliver Kieselbach 224eb668a1 Web: helpers live in lib/, generated files in lib/generated/; utils/ is gone
The web app kept helpers in two root folders (lib/ and utils/) and named
route-local helper folders either utils/ or lib/. Shared helpers now live in
lib/, route-only helpers in a lib/ next to their route (app/dashboard/lib,
app/sessions/lib). Generated wire types, manifests, guardrails, error codes and
the lastmod file move to lib/generated/, like the MCP server's src/generated/.
The session-search parity cases sit next to sessionSearchQuery.ts.

Pure move, no behaviour change. Imports, the generators (combine.js,
generate-shared-manifest-types.js, sync-error-codes.js, generate-lastmod.js),
the CI drift paths, the two backend tests and the MCP freshness test that read
these files, and the guard tests' registry path and scan list follow the new
locations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:29:57 +02:00
Oliver Kieselbach a6980a93f6 bump pointer: internal (notification channel health released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:06:37 +02:00
Oliver Kieselbach eb57980b53 bump pointer: internal, .claude/commands (channel health orphan sweep)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:49:28 +02:00
Oliver Kieselbach c4c2415de8 Notification channel health: the maintenance run deletes the rows of deleted channels (D-342)
A health row goes once its channel is no longer in the stored channel list of its scope and it
saw no send for 30 days. The channel lists are read strictly, so a failed read skips the scope
instead of reading as "no channels"; the delete is ETag-conditional, so a row a send just
rewrote survives. Runs on the 2-hourly maintenance timer next to the push retention.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:49:27 +02:00
Oliver Kieselbach 9331366025 bump pointer: internal, .claude/commands (notification channel health: D-342, marker)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:23:44 +02:00
Oliver Kieselbach 85a0cb3c31 Notification channels: per-channel delivery status and a bell when a channel keeps failing (D-342)
Every non-Push send, real or test, is recorded in a NotificationChannelHealth row per channel
(own table, server-side status rule, destination fingerprint resets the history). The settings
show a status dot per channel: Operating normally, Failure rate x%, Error with the cause, or No
deliveries yet. Two failed deliveries in a row raise one bell for the tenant admins (or the GA
bell for ops channels) per failing episode; a failing channel keeps receiving deliveries.

Each transport now has one send path that reports its outcome (WebhookTestResult is
NotificationSendResult, the dispatcher's test entry is SendTestAsync); failures are logged with
tenant and channel id. New routes GET config/{tenantId}/notification-channel-health
(TenantAdminOrGA) and GET global/config/ops-channel-health (GlobalAdminOnly).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:23:43 +02:00
Oliver Kieselbach 5efa7d078c bump pointer: internal (global fan-out released, first measurements)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 14:18:18 +02:00
Oliver Kieselbach 35d1445ebb bump pointer: internal, .claude/commands (global fan-out built: D-340/D-341, health markers)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 14:01:30 +02:00
Oliver Kieselbach 23cd9050ea Global fan-out: session-derived cross-tenant reads run over enrolled tenants, GA follow-up pages fetch 100 (D-340)
- TableStorageService.GetEnrolledTenantIdsCachedAsync: configured tenants with a PlatformStats counter row (RowKey 'current', TotalEnrollments > 0), 60 s single-flight; fail-open to the configured list with a Warning when the counter scan fails
- Session list pages, cross-tenant stats drain, geo window and app-summary snapshot fan out over that subset (353 -> 127 partition queries per page live); fresh-install fallbacks key on the configured list, a configured set nobody enrolled in is an empty page; audit logs, QuerySessionsByDateRangeAsync and maintenance keep the full list
- Web: cross-tenant follow-up pages fetch max(pageSize, 100) (resolveSessionsFetchPageSize); first page and own-tenant list unchanged
- Tests: CrossTenantFanOutTests (enrolled subset, counter 0 / no row, fail-open, unconfigured counter row, empty enrolled set never the legacy scan, bound after the filter, geo + apps), DelegatedBoundedAggregateTests harness, sessionsPageSize.test.ts

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 14:00:24 +02:00
Oliver Kieselbach 8615fb458f bump pointer: internal (global fan-out audit for B-gdd)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 13:20:03 +02:00
Oliver Kieselbach 645f747191 bump pointer: internal (orphan sweep released, first measurements)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 00:42:32 +02:00
Oliver Kieselbach 5934ae9fc4 bump pointer: internal, .claude/commands (orphan sweep built: D-339, concept doc, health markers)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
v2.0.1480
2026-10-09 22:56:30 +02:00
Oliver Kieselbach 3d10011460 Orphan sessions: ingest refuses sessions without a row, the sweep becomes their cascade in its own function (D-339)
A session without a Sessions row can never be reached by the cascade, so every row written for it was an orphan by construction. Three layers now keep the platform clean:

- Prevent: IngestTelemetry answers a batch for an unknown session with 410 (same status and body as the cascade lock; request-row marker IngestRefusal = session_unknown | cascade_locked). The derived writers - vulnerability correlation, analyze, manual rescan - stop on SessionDeletionGuard.GetLivenessAsync != Live. The agent treats every 410 as terminal: one TelemetryRejectedSessionUnknown report (exempt from the reporter cooldown), then terminate_session with forced self-destruct (origin session_gone, no final drain, no diagnostics upload); spool items of a foreign session id are dropped at startup and never terminate the current session.
- Detect: OrphanSessionSweepFunction (every 4h at :45, own lease) diffs the Sessions keys against the EventSessionIndex handles - two range drains instead of ~16.8k Sessions point reads per 2h maintenance run - and re-checks every candidate (handle unchanged, row still absent, tombstone) moments before deleting. The in-maintenance sweep raced the retention cascade on their shared 00:00/12:00 tick and reported false orphans; it is gone.
- Clean: every table of the cascade manifest minus the tombstone pair (ratchet in TableLifecycleBucketTests), the inventory counters with the stamp on the handle (claim, then decrement), the handle last with If-Match. A weekly read-only reconciliation of EventTypeIndex against the Sessions keys (Sunday mornings UTC, and on every manual maintenance run) reports residue.

MaintenanceCompleted now carries stepsMs. MCP search_sessions_by_event auto-exhausts empty-but-continuable pages. Manifests regenerated for the new ops event types and AdminConfiguration.OrphanDeepCheckLastRunUtc.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 22:55:27 +02:00
Oliver Kieselbach edad842a0b bump pointer: internal (inventory-key-control-chars released)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:06:17 +02:00
Oliver Kieselbach be239b74b8 bump pointer: internal (lessons: analysis vs build plan)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v2.0.1479
2026-10-09 21:35:16 +02:00