The XML summary added on TenantManagerItem.Name in ef280b7b rides into
shared-manifests.json as the field's doc; CI run 35701895623 caught the drift.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GET delegations/managers no longer exposes the name of an operator-created
Tenant Group to the customer; both operator shapes (groups and direct grants)
carry the neutral label. Assignees, reader count and source stay visible.
Web badge reads "Provisioned by operators". D-267.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
network_state_change and network_connectivity_check join the non-activity
list next to power_state_change. A flapping dock or access point kept the
idle-stopped periodic collectors running for the whole episode and reset
the stall clock of the enrollment it was stalling. The events are emitted
unchanged; the device-info network snapshots stay activity.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
IME re-runs remediation policies on its own schedule for as long as the
device is up, so an enrollment that waits for its user reported every run
of every policy. The tracker now emits a policy's first run and every run
whose result differs from the last reported one (compliance, exit code,
stderr, remediation status, error code); repeats are counted and surface
as one script_recurrence_summary per policy when the tracker stops.
The start signal of a known policy is held until its result is known,
because the running indicators pair a start with a later result; a start
without any result is still emitted after a grace. The counters live in
the tracker state file next to the file positions, so a hard kill rolls
both back together. Platform scripts are untouched. The summary does not
count as enrollment activity.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The stored EventCount doubles on replayed upload batches and was only
corrected at terminal transitions, so the sweep judged running sessions on
an inflated number. It now recounts a candidate before it records the ops
event or blocks the device, skips candidates it cannot verify, and limits
the recount to candidates with a pending action. The warn text no longer
claims an agent loop bug.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
73 sites in 68 files built the query collection with
HttpUtility.ParseQueryString(req.Url.Query), 15 files used req.Query and five mixed
both. HttpRequestData.Query is that same call, cached per request, and the ASP.NET
Core request type does not override it, so the change is behaviour-neutral.
- QueryParamGuardTests: ParseQueryString in the Functions project is limited to a
named baseline of three sites that parse something other than the request query.
- A fact pins req.Query against HttpUtility.ParseQueryString (repeated keys, encoded
values, caching) so a worker update that changes the implementation fails a test.
- Test mocks that set Url answer Query through CallBase; a loose mock returns null.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Six functions wrote their responses through a private wrapper taking the body as
object, which no typed-response guard could see. Their error bodies carried the code
in the error slot, no correlation id, and RestoreRow returned the raw exception text
on a 500.
- Backup job status, row restore and backup trigger answer through ResponseHelper:
errors are the envelope, the 500 is sanitized, the 202 body is BackupTriggerResponse.
- Same for the session-deletion maintenance trigger, RestoreSession's three error
sites and the delegation acknowledgements (MessageResponse).
- Backup codes are constants (Constants.BackupErrorCodes) and part of apiErrorCodes;
RunAlreadyActive and EnqueueFailed join ApiErrorCodes.
- TypedResponseGuardTests gains guard F: no HttpResponseData-returning method with an
object parameter. BackupWireParityTests pins that the move from the backup
serializer options to the worker pipeline leaves the wire unchanged.
- Web reads BackupTriggerResponse from the generated wire types.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A version whose installer was already in the archive could only turn
Archived through the 409 on the write-once upload, which needs a
distribution host that still serves the build. For a version no host
serves any more the row stayed Failed:VersionMismatch next to its own
blob and re-walked every host once a day.
The archiver now checks the archive first: an entry whose ProductVersion
is the observed version ends the job as Archived with the hash and size
of the archived bytes and no download; a missing provenance sidecar is
written without a source URL. An entry holding another build, or an
unreadable one, is not accepted and the walk runs as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The browser loads the photo straight from Microsoft Graph with the User.Read
scope already consented at sign-in; it never reaches the backend. Any failure
(no photo, token, network) falls back to the initials. One request per account
and tab, cached in sessionStorage including the no-photo answer.
The four copied avatar blocks in the navbar become one UserAvatar component.
Graph host added to the URL registry and to connect-src in both CSPs; the
privacy page states where the photo comes from and that it is not stored.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A process name is not a protected namespace: any user session can keep a
binary named like the agent resident, which made every agent start during
that session exit 1 before registration and let install mode report a
runtime launch that never happened.
The guard and install mode's launch verification now share one rule
(FindSiblingAgentPid): a name match counts only in session 0 and never for
the caller itself. Every runtime start lands there (Scheduled Task, WMI
launch from the SYSTEM install chain, self-update restart), so behaviour
for real agents is unchanged. The session id comes from the process
snapshot, so no handle is opened; a process that cannot be read does not
count, which fails open into two agents rather than none.
Two interactive --console runs in the same user session no longer block
each other; an interactive run next to the session-0 runtime still does.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Banner and the success-rate/duration gauges read the evaluation period
(last 30 days) instead of the current ISO week and name the window in their
labels; the app install gauge says "this week". A target whose period holds
no finished enrollment renders a neutral "No data" card and is left out of
the banner count; with nothing to judge the banner is neutral.
Banner logic moved to lib/slaStatus.ts (pure, tested).
Needs the backend of the previous commit: the page reads
evaluationPeriod / evaluationWindowDays.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The SLA breach alert judged the calendar month while the dashboard headline
showed the current ISO week, so an alert could report 78 % while the page it
links to showed 0 everywhere - and an empty week rendered as 0.0 % "Breached".
- SlaEvaluationWindow is the single definition of the SLA populations:
success rate and P95 duration on terminal sessions of the last 30 days
(rolling), app installs on the install attempts of the current ISO week.
The breach evaluator and SlaMetricsService both read it, including the
one-decimal rounding, so the value shown is the value judged.
- SlaMetricsResponse carries EvaluationPeriod and EvaluationWindowDays;
SlaSnapshot carries Period and HasData. A period without a finished
enrollment meets every target flag and is marked HasData = false.
- The dashboard reads from whichever starts earlier, the selected months or
the evaluation window; trend and violators stay on the selected months.
- A lasting breach is notified again only after the cooldown AND once
something finished since the last notification. A new breach episode after
a resolve still notifies. Resolve notifications are unchanged.
- Alert period fact and telemetry tag follow the window constant.
- SlaMetricsService takes a nowProvider; the week-boundary workaround in its
tests is gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A configured diagnostics entry is resolved once to folder, pattern and recursion, and
that target is what gets validated and enumerated; recursion asks the guard before
entering a subfolder, and the user-profile token is expanded before validation.
Security, PowerShell and Sysmon event logs are refused per file, by name and by
resolved channel, in every mode; the portal flags such a path before it is saved.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Directly under Dashboard the Progress Portal collected misclicks: 418 of 1,050
admin page views in 90 days went on to the dashboard within 5 seconds, from
every origin page. It is the end-user page; regular users keep it as their only
entry via REGULAR_USER_ITEMS. Annotations showed the same walk-through pattern
and moves down with it. Fleet Health, Geographic Perf. and Software lead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
All five cards showed the same bar-chart glyph on a differently coloured
block, so the colour carried no meaning. Each card now has a small grey
outline icon for its metric (pulse, check, stopwatch, calendar, x) in the
top-right corner, and the label gets the freed width. Failed Today turns
its value and icon red only when the count is above zero and reads
"No failures" at zero.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The analyze and gather rule pages loaded the community submissions list for
every role. rules/submissions is TenantAdminOrGA, so an Operator or Viewer got
a 403 and a red "Failed to load your community submissions" banner on every
page visit (16 denials in 24 h from two operators of one tenant).
The pages now skip the list for callers who cannot read it and the
contribution box says that only tenant admins can contribute and follow the
tenant's submissions. Tenant admins, Global Admins and a global-scope override
are unchanged; the backend policy stays as it is.
Bumps the internal and .claude/commands gitlinks (platform-health marker
catalog, backlog).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A retry after a poisoned offboarding is a new envelope with a new history
row, and nothing ever deleted from a poison queue. The dead letter of the
failed run therefore stayed until its 7-day TTL and kept the poison backlog
alert firing daily for a tenant that was already gone.
TenantOffboardingHandler.HandleAsync now reports a TenantOffboardingOutcome.
On Completed the worker receives tenant-offboarding-poison in batches and
deletes the envelopes of that tenant; other tenants' and malformed messages
reappear untouched. The sweep never throws: a failure there would keep the
completed envelope on the main queue and end with a Completed run in poison.
A re-pickup of a completed envelope sweeps again.
Other poison queues stay terminal on purpose; their failures have no later
success that supersedes the dead letter.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The trigger ran the whole maintenance inside the HTTP request (about three
minutes against a 230 s gateway limit): the portal reported a timeout while the
run went on, a second click started a second run, and nothing serialized those
runs against each other or against the 2h timer.
Backend
- MaintenanceRunGate: the timer and the manual run enter through one blob
lease. A held lease writes MaintenanceSkippedLocked and runs nothing;
MaintenanceStarted is written after the lease is held, the lease is released
last.
- POST maintenance/trigger validates the date, probes the lease (409 while a
run is active), enqueues on maintenance-trigger (fail-hard) and answers 202
with the typed TriggerMaintenanceResponse. MaintenanceTriggerQueueWorker
(self-polling) runs it; a failing or skipped run is not retried.
- The run report travels in the details of MaintenanceCompleted.
- BlobLeaseLockStore is the shared base of the lease lock stores;
SessionDeletionMaintenanceLockStore derives from it, behaviour unchanged.
- The probe lets a failed lease release surface as 5xx: a probe lease still
held at worker pickup would turn the queued run into a silent skip.
Web
- useOpsRunStatus: run status from ops events (latest Started newer than the
latest Completed/Failed = active), used by the session-cleanup banner
(rendering unchanged) and the maintenance trigger section.
- Trigger section: queued / already-active messages, status line, polls every
15 s while a run is active, button disabled meanwhile. The 230 s fetch
timeout is gone.
Shared manifest, generated wire types and the MCP ops-event vocabulary
regenerated (two new ops event types, reshaped response).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A Failed offboarding was a dead end: the worker returns early on a Failed
history, the offboard re-click refuses to re-enqueue on a Failed marker, and
the marker cleanup only warns. Recovery meant deleting the marker row in Table
Storage by hand, while the tenant sat in the list as a tombstone counted on the
Waitlist (its whitelist row is wiped early in the cascade).
- GET global/tenants/{tenantId}/offboarding returns the offboarding record
(marker status, failed phase, attempts, history details) as the typed
TenantOffboardingStatusResponse; GA + Reader, never delegated.
- POST global/tenants/{tenantId}/offboarding/retry re-drives a Failed run: the
History row keeps its RowKey and DrainCompletedAt (the worker's resume anchor)
and counts the retry, marker and history go back to Initiated, the ordinary
resume path re-affirms the Disabled-gate, waits out the remaining drain
barrier and enqueues the envelope. Completed / in-flight answer 409, no
marker 404, a missing history row 500 before any write. Global audit row
RETRY/TenantOffboarding. Nothing retries on its own.
- Tenant editor: an "Offboarding" card replaces the Offboard action while the
cascade owns the row (status, started by, attempts, failed phase and error);
Failed offers "Retry offboarding…" behind the type-to-confirm dialog in its
retry variant.
- Tenant list: tombstone rows carry an Offboarding badge (own Status facet)
instead of Suspended, and no longer count as Waitlist.
- Tests: retry decision (reset order, drain anchor, conflicts, not found,
enqueue failure, remaining-barrier delay), status projection, wire parity
facts, filter facet. Shared manifest and generated wire types regenerated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPG5WCmSwfNAv4xkK62n5r
The run answers when it is done (about three minutes); the default fetch
timeout reported "signal timed out" although the run completed, which invites
a second click and a second run. Wait up to the gateway limit of 230 s.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The landing-page counters stood still since D-198: the two-hourly recompute
was their only writer and merged Max(live figure, stored figure), while
retention prunes the live figure below the stored one - so new enrollments
never showed up again.
- Hot path increments the per-tenant counters only (CAS): enrollments at the
fresh registration, successes at the one status seam every Succeeded writer
passes (transitioned && Succeeded), events per ingest batch. No batch
touches the global row.
- The maintenance run floors the tenant counters at the live figures (one
multi-field write per tenant), sums the tenant rows and adds the growth
since the previous run to the platform row. The previous sums live in a
baseline row written in one transaction with the platform row. A total is
never derived from an absolute figure again: a shrinking source (retention,
offboarding) costs the growth of one interval instead of freezing the
counter. Without a baseline the sum is a lower bound only.
- TotalTenants rolls up the tenant rows with enrollments, UniqueDeviceModels
a persisted set of seen models. TotalUsers stays a high-water-mark (a
persisted user set would outlive the user-activity retention).
- The platform row is merged with its read ETag and never carries
IssuesDetected: the former full-row upsert undid every increment that
landed during the run.
- Reads that feed the baseline are not fail-soft, and a floor that could not
be written skips the rollup of that run - a swallowed zero would be added
as growth a second time.
Shared manifest and generated wire types: doc comment of PlatformStats only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The wipe of the tenant's own self-service Tenant Group handed the derived
group id ("msp-{tenantId}") to the tenant-GUID entry points of SafeWipeService.
Their GUID guard threw on every attempt, so each offboarding since the
self-service delegation change ran into max_dequeue and ended Failed after the
tenant tables were already wiped.
- SafeWipeService: WipeByOwnedGroupPartitionAsync / WipeByOwnedGroupRowKeyAsync
take the tenant GUID and derive the group id themselves; matching filters in
OffboardingFilters.
- The public wipe entry points are no longer virtual: they validate and hand
over to protected virtual cores. Test fakes override the cores only, so the
guard runs under every fake. With the old handler code 16 handler tests fail
on the guard, which is how this shipped unnoticed.
- Tests: handler regression on the owned-group anchors, owned-group variants
incl. verify-abort on a foreign group, filter shapes and guards.
Deploy and recovery of the stuck offboarding: B-5g6.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replaces the two Ready/Waitlist toggles with a Filter popover of five
facets (Plan, Paying, Trial, App registration, Status): OR within a
facet, AND across facets, each option carrying the count it would match
under the other facets, active selections as removable chips, and a
plan breakdown (Community / Pro with paying / Trial / MSP) over all
tenants in the statistics line.
The list badge, the filter and the edit modal now derive the effective
edition through one function; a trial-only tenant reads "Pro (Trial)"
in the list instead of "Community". The App facet is bound to the
dual-app parallel window (legacyConfigured), like the badge.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The DPA is published in the customer docs and accepted on the Get started
page before the first sign-in (D-251); the onboarding records the accepted
version on the tenant (D-252). The legal pages still described it as
available on request and as a Pro-only signed document.
- privacy: the Roles section says the DPA is published, applies to both
plans, is accepted at sign-up with the version recorded, and that an
individually signed DPA prevails; External Services links the DPA and its
sub-processor list; Changes no longer hinges on "if you hold a signed
DPA"; Contact no longer offers to send one.
- terms: the DPA is no longer listed as a Pro differentiator; Security and
Transparency links the DPA as applying to both plans. Last updated
11 September 2026.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>