9 Commits

Author SHA1 Message Date
dependabot[bot] 7250baca02 chore(deps-dev): bump jscpd (#189)
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.4.0 to 5.4.1
- [Release notes](https://github.com/kucherenko/jscpd/releases)
- [Commits](https://github.com/kucherenko/jscpd/commits/v5.4.1/rust/jscpd)

---
updated-dependencies:
- dependency-name: jscpd
  dependency-version: 5.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  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-20 10:02:53 +02:00
Oliver Kieselbach a7c3c16054 Global Admin sees new customers per day and the growth over time under Metrics > Customers (D-365)
The section reads the tenant configurations from config/all and derives everything in the browser: no endpoint, no counter, no backfill.

- Customers: new tenants per UTC day (per Monday-anchored week beyond 90 days), the window's best day, weekly average, the last 7 days, the total and the cumulative gain; 7/30/90/365 days in ?days=; clicking a column lists that bucket's tenants with their edition. A signup is a configuration row with a domain. The section is the first Metrics entry and the target of /admin/metrics.
- Charts: AppBarChart takes a category formatter, a custom tooltip, a column click and whole-number ticks; AppLineChart can hide its legend and use whole-number ticks. Existing callers get the same props as before.
- The Monday-of-week helper moves from the Fleet Health timeline into lib/timeWindow; the Stat tile duplicated in two metrics sections becomes one component, and the duplicate baseline loses that entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:15:29 +02:00
Oliver Kieselbach 17fc23f8cb feat: usage metrics split enrollments by type, join and deployment mode (D-361)
Usage Metrics (tenant page and the platform section) shows the window's
sessions in three groups that each sum to the session count:
Autopilot v1 or Device Preparation, Entra or hybrid joined, and the
Autopilot deployment mode (user-driven, self-deploying, pre-provisioned).
Type and mode follow the fleet enrollment class; user-driven no longer
counts the IsUserDriven flag, which every registration sets. The counts
come from the session rows on each request, so every retained session is
covered without a counter.

- DeploymentTypeMetrics gains the new counts and shares; the usage
  projection reads EnrollmentType, IsSelfDeployingProfile, IsHybridJoin
- platform and tenant compute share one builder per block
- Platform Usage takes ?days= like the other analytics pages
- the usage pages share their blocks (components/UsageMetricsBlocks.tsx);
  enrollment class labels and the time-attribution segments each live in
  one place, the duplication baseline shrinks by seven clones

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:19:48 +02:00
Oliver Kieselbach caeb7a3e58 feat: every alert links to its subject; ops alerts open their own event (D-360)
A push carries only category, event and severity for an ops alert, so the
portal link is the way to the details, and ops alerts had none: the event
id was minted inside the save and never reached the dispatch.

Backend: SaveOpsEventAsync sets OpsEventEntry.Id (OpsEventId,
{Category}_{RowKey}); WriteAsync hands it to the dispatch, and BuildAlert
adds "Open in the portal" -> /admin/ops/?eventId=<id> on the plain and the
payload path, so every ops channel (push, Telegram, Teams, Slack, Discord,
e-mail, JSON) gets it. GET global/ops-events?id= is a point read (400 for a
malformed id, an empty list for a deleted one). hardware_rejected links to
the hardware whitelist (one path constant, also for the bell); the session
watch link names the session's tenant; TenantSignup's portalPath keeps its
tenant query. Only a session link makes a push session-bound: tag and Topic
no longer key on any "id=" in the link.

Web: /admin/ops?eventId= opens that event's detail from the point read
(matched by id), and the detail links to the tenant and to the event's
related page (same-origin paths only) besides the session, which now
carries the tenant. CopyButton and the cursor pager footer are shared
components instead of four and three copies; the duplicate baseline shrinks
by five entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:19:22 +02:00
Oliver Kieselbach 127d5c1173 Diagnosis: the page answers what happened, for every status with a verdict (D-356, D-357)
The diagnosis page is rebuilt on the server headline instead of rule
confidence: a verdict card per status and cause (culprit tiles, ESP
phases, what an unfinished enrollment still waits for), the
circumstances around it, the steps of the finding that explains it,
where the time went, all open findings grouped by role, team notes and
the deduplicated errors and warnings. It reuses the session page's hook
set, refetches the headline on live activity and opens for Failed,
Stalled, AwaitingUser, Incomplete and Succeeded with open findings, also
from read-only and cross-tenant views.

Rule text: lib/ruleText.ts prunes a finding's explanation and steps like
the notifications do (shared cases in tests/fixtures/rule-text-display,
also run by RuleTemplateInterpolator.InterpolateForNotification), then
parses the markdown and fills values into text nodes only. The session
page's analysis section, the rule card and the dry run render through it;
components/findings/EvidenceChips replaces the two evidence-link copies
and covers correlation pairs.

Vocabularies: DiagnosisPrerequisites names the engine's completion
prerequisites (manifest diagnosis.prerequisites, parity fact against
DecisionEngine.CompletionPrerequisites); the page's texts for every
diagnosis vocabulary are exhaustive over the manifest types.

Fixed on the way: useSessionSignalR held the first render's callbacks,
so ruleResultsReady was lost on an already connected hub; a deep link to
an event dropped its hash before the event loaded; the global analyze
rule update answered 500 for an invalid presentation (now 400); the
rule editor kept the presentation block on create and in JSON mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:30:29 +02:00
Oliver Kieselbach 516cdc71a7 Rules: optional presentation block, {{signal.key}} tokens, one resolution for web and MCP (D-356)
Analyze rules can describe how a finding is shown: a role (outcome, cause, context,
advisory), a one-line summary, the subject it is about and up to six key facts. Like the
explanation, the block is stored raw on the result and resolved where it is rendered.

- Templates: a qualified token {{signal.key}} reads one key of one condition's evidence,
  so counts, durations and correlation fields are reachable. RuleEvidenceKeys (Shared,
  exported to the manifest) lists the keys each condition source records; tests pin it
  against the evaluators.
- Backend: PresentationJson on AnalyzeRules and RuleResults, RuleVersion on results,
  ValidatePresentation on create/update, and results stored without a presentation get
  the current rule's at read time.
- Web + MCP: lib/rulePresentation.ts resolves the block (unrecorded facts left out, summary
  falls back to the explanation). The MCP now runs generated copies of the web modules
  instead of a hand-kept mirror (npm run generate:mcp-logic, freshness tests in both
  suites); the duplicate baseline shrinks by one.
- MCP output: get_session and get_session_summary return the resolved block (issues gain
  ruleId, role, summary, subject, facts); test_analyze_rule previews it; validate_rule
  checks qualified tokens against the key catalog and lints the subject.
- Shared case files: rule-template-interpolation gains the qualified-token cases,
  rule-presentation is new.

No rule carries a presentation yet; renderers behave as before until rules gain one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:48:46 +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
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 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