mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
12a75ef13d
Phase A — the budget follows the data
- A delegated (MSP) read is charged to the MANAGED tenant's organization MCP budget and checked against
its plan; the caller's own budget still follows the home tenant. 429 names the managed tenant (domain)
and the Pro upgrade path; new McpQuotaExceededResponse.targetTenantId.
- Bounded fleet aggregate (global/sessions, global/stats/sessions): exhausted managed tenants are excluded
softly — RequestContext.AllowedTenantIds is narrowed (never widened, never null; pinned) and echoed as
quotaExcludedTenants. Global Admins stay exempt and are tracked on their home tenant only.
- Tenant Group flag ChargeHomeTenantQuota (PATCH global/tenant-groups/{id}, audited per tenant): reads
through the group are charged to the assignee's home tenant (operator-managed service groups).
- McpTenantUsage rows carry UserPrincipalName / HomeTenantId / LastRequestAt; new
GET metrics/mcp-usage/organization (budget by account incl. delegated readers) rendered on MCP Usage.
- MCP: get_fleet_overview (delegated + platform) over the two subset-tier routes; error handler names
the managed tenant. Other delegated tools stay tenant-mandatory.
Phase B — delegated tenant slots
- EditionEntitlements.MaxDelegatedTenants (Community 0 / Pro 2), TenantConfiguration.MaxDelegatedTenantsOverride
(GA-only via PATCH config/{tenantId}/plan { maxDelegatedTenants }), DelegatedSlotService counting distinct
managed tenants per home tenant through identity bindings (+ pending invitations + 24 h holds), enforced
fresh on grant / group assign / add-tenant with 409 DelegatedSlotLimitReached; GA UI offers
"raise to N & retry"; GET global/delegated-slots/{tenantId}; feature-flags entitlements.maxDelegatedTenants.
Phase C — self-service delegation
- One implicit Tenant Group msp-{tenantId} per managing tenant (OwnerTenantId on the meta row);
DelegationInvitations table (invitations + slot release holds); DelegationInviteTicket (HMAC, 7 days,
locates the row only — the accepting tenant is always the JWT tenant; one-shot via ETag).
- DelegationSelfService: ordered accept chain (distinct codes), remove/revoke with 24-hour slot hold, dual
audit and live-stream disconnect, assignees bound to tenant membership, "who manages my tenant" view;
GA release-hold escape hatch. No delegations/* route carries {tenantId} (pinned).
- Web: Settings → Tenant → Delegated Access, /delegations/accept?token=, AuthGate keeps the deep link
across the activation detour, Tenant Management holds + release, owned groups read-only in GA UI.
- Offboarding wipes both sides (documented two-bucket membership); 90-day sweep of terminal rows.
- Trust pages (terms, privacy) and generated wire types updated; docs submodule bumped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
239 lines
11 KiB
C#
239 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using AutopilotMonitor.Shared.DataAccess;
|
|
|
|
namespace AutopilotMonitor.Shared.Models
|
|
{
|
|
// -------------------------------------------------------------------------------------
|
|
// Typed wire DTOs for the Metrics function folder (anonymous-object → typed migration).
|
|
// Envelope classes implement IApiResponse; nested item classes stay flat and carry no
|
|
// marker interface. Property declaration order IS the JSON key order.
|
|
// DTOs of the two GeographicLocationSessions endpoints live in GeographicApiModels.cs.
|
|
// -------------------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Device enrollment-history envelope (GetDeviceHistory). <see cref="History"/> is absent
|
|
/// as a NORMAL outcome (unknown device, junk serial, or every chain ref pruned).
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class GetDeviceHistoryResponse : IApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
|
|
/// <summary>Absent when the device has no history row — the banner simply stays hidden.</summary>
|
|
public DeviceHistory? History { get; set; }
|
|
|
|
/// <summary>
|
|
/// The requesting session's attempt number within its journey; absent without a
|
|
/// <c>?sessionId=</c> parameter, when the session is unknown, or when the position
|
|
/// cannot be computed (fail-soft, never a guessed position).
|
|
/// </summary>
|
|
public int? AttemptNumber { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Landing-page platform stats envelope (GetPlatformStats, unauthenticated). When no
|
|
/// stats row has been computed yet, the zero shape is served: all counters 0 with
|
|
/// <see cref="TotalSignedUpTenants"/> and <see cref="LastUpdated"/> absent.
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class GetPlatformStatsResponse : IApiResponse
|
|
{
|
|
public long TotalEnrollments { get; set; }
|
|
public long TotalUsers { get; set; }
|
|
public long TotalTenants { get; set; }
|
|
|
|
/// <summary>Absent on the not-yet-computed zero shape.</summary>
|
|
public long? TotalSignedUpTenants { get; set; }
|
|
|
|
public long UniqueDeviceModels { get; set; }
|
|
public long TotalEventsProcessed { get; set; }
|
|
public long SuccessfulEnrollments { get; set; }
|
|
public long IssuesDetected { get; set; }
|
|
|
|
/// <summary>Always absent today: the computed shape never sets it, the zero shape sets null.</summary>
|
|
public DateTime? LastUpdated { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-session time-attribution envelope (GetSessionTimeAttribution). A missing breakdown
|
|
/// is a NORMAL outcome (pre-feature session, non-terminal, Incomplete — no wall clock).
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class GetSessionTimeAttributionResponse : IApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
|
|
/// <summary>Absent when no breakdown row exists — the UI simply omits the lane.</summary>
|
|
public SessionTimeBreakdown? Breakdown { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Self-service MCP/API usage envelope (GetMyMcpUsage): the caller's own usage records
|
|
/// plus resolved plan and quota state. The MCP server paginates over the
|
|
/// <c>records</c> key — its name is wire-critical.
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class GetMyMcpUsageResponse : IApiResponse
|
|
{
|
|
public string UserId { get; set; } = default!;
|
|
|
|
/// <summary>Absent when the token carries no UPN claim.</summary>
|
|
public string? Upn { get; set; }
|
|
|
|
/// <summary>Per-user plan override from the caller's own whitelist row; absent without one.</summary>
|
|
public string? UsagePlan { get; set; }
|
|
|
|
/// <summary>Resolved effective plan (per-user override → tenant edition).</summary>
|
|
public string EffectivePlan { get; set; } = default!;
|
|
|
|
public McpUsageQuotaNode Quota { get; set; } = default!;
|
|
public IReadOnlyList<UserUsageRecord> Records { get; set; } = default!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Effective quota state nested in <see cref="GetMyMcpUsageResponse"/>: the caller's own windows and
|
|
/// the organization-wide windows of their tenant (shared by every member; 0 = unlimited). For a
|
|
/// delegated (MSP) caller the tenant windows are those of their HOME tenant — reads into managed
|
|
/// tenants are charged to the managed tenant per request and never appear here.
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class McpUsageQuotaNode
|
|
{
|
|
public int DailyLimit { get; set; }
|
|
public int MonthlyLimit { get; set; }
|
|
public long DailyUsed { get; set; }
|
|
public long MonthlyUsed { get; set; }
|
|
public DateTime ResetUtc { get; set; }
|
|
public string TenantPlan { get; set; } = default!;
|
|
public int TenantDailyLimit { get; set; }
|
|
public int TenantMonthlyLimit { get; set; }
|
|
public long TenantDailyUsed { get; set; }
|
|
public long TenantMonthlyUsed { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-user MCP/API usage envelope (GetMcpUserUsage). Non-global callers only receive
|
|
/// the records attributed to their own tenant; a foreign oid and an unknown oid are
|
|
/// indistinguishable (both 200 with empty <c>records</c>). The MCP server paginates over
|
|
/// the <c>records</c> key — its name is wire-critical.
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class GetMcpUserUsageResponse : IApiResponse
|
|
{
|
|
public string UserId { get; set; } = default!;
|
|
public IReadOnlyList<UserUsageRecord> Records { get; set; } = default!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Organization-wide MCP usage by user for the caller's OWN tenant (GetMcpOrganizationUsage): every
|
|
/// account whose requests were charged to this tenant's organization budget — its own members and any
|
|
/// delegated (MSP) administrators reading the tenant. Built from the tenant's organization counters.
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class GetMcpOrganizationUsageResponse : IApiResponse
|
|
{
|
|
public string TenantId { get; set; } = default!;
|
|
/// <summary>Effective range start (yyyyMMdd).</summary>
|
|
public string DateFrom { get; set; } = default!;
|
|
/// <summary>Effective range end (yyyyMMdd, inclusive).</summary>
|
|
public string DateTo { get; set; } = default!;
|
|
public IReadOnlyList<McpOrganizationUsageItem> Users { get; set; } = default!;
|
|
}
|
|
|
|
/// <summary>One account's share of the organization budget, nested in <see cref="GetMcpOrganizationUsageResponse"/>.</summary>
|
|
// Declaration order == wire order.
|
|
public class McpOrganizationUsageItem
|
|
{
|
|
public string UserId { get; set; } = default!;
|
|
/// <summary>Absent for rows written before the UPN was recorded.</summary>
|
|
public string? UserPrincipalName { get; set; }
|
|
/// <summary>True when the account is a delegated (MSP) administrator homed in another tenant.</summary>
|
|
public bool Delegated { get; set; }
|
|
/// <summary>The delegated administrator's home tenant; absent for the tenant's own members.</summary>
|
|
public string? HomeTenantId { get; set; }
|
|
public long RequestsToday { get; set; }
|
|
public long RequestsThisMonth { get; set; }
|
|
public long RequestsInRange { get; set; }
|
|
/// <summary>Absent for rows written before the timestamp was recorded.</summary>
|
|
public DateTime? LastRequestAt { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Global per-tenant MCP/API usage envelope (GetGlobalMcpUsage). The MCP server paginates
|
|
/// over the <c>records</c> key — its name is wire-critical.
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class GetGlobalMcpUsageResponse : IApiResponse
|
|
{
|
|
/// <summary>Echo of the request filter; absent when the caller passed no tenantId.</summary>
|
|
public string? TenantId { get; set; }
|
|
|
|
public IReadOnlyList<UserUsageRecord> Records { get; set; } = default!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Global daily MCP/API usage summaries envelope (GetGlobalMcpUsageDaily). The MCP server
|
|
/// paginates over the <c>summaries</c> key — its name is wire-critical.
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class GetGlobalMcpUsageDailyResponse : IApiResponse
|
|
{
|
|
/// <summary>Echo of the request filter; absent when the caller passed no tenantId.</summary>
|
|
public string? TenantId { get; set; }
|
|
|
|
public IReadOnlyList<UserUsageDailySummary> Summaries { get; set; } = default!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-tenant session-status tally envelope shared by MetricsSummary and
|
|
/// MetricsSummaryGlobal.
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class MetricsSummaryResponse : IApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public IReadOnlyList<MetricsSummaryTenantItem> Summary { get; set; } = default!;
|
|
public int WindowDays { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// One per-tenant status tally in <see cref="MetricsSummaryResponse"/>. WindowDays is
|
|
/// repeated per item (envelope carries it too — historical wire shape, kept for parity).
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class MetricsSummaryTenantItem
|
|
{
|
|
public string TenantId { get; set; } = default!;
|
|
public int TotalSessions { get; set; }
|
|
public int Succeeded { get; set; }
|
|
public int Failed { get; set; }
|
|
public int InProgress { get; set; }
|
|
public int Pending { get; set; }
|
|
public int Stalled { get; set; }
|
|
public int AwaitingUser { get; set; }
|
|
public int Incomplete { get; set; }
|
|
public int Other { get; set; }
|
|
|
|
/// <summary>Failed over the TERMINAL outcomes only (Succeeded + Failed), percent rounded to one decimal; 0 without terminal sessions.</summary>
|
|
public double FailureRate { get; set; }
|
|
|
|
public int WindowDays { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Session ids where a rule produced a result within the window (GetRuleHitSessions) —
|
|
/// powers the dashboard's <c>?ruleId=</c> deep link.
|
|
/// </summary>
|
|
// Declaration order == wire order.
|
|
public class GetRuleHitSessionsResponse : IApiResponse
|
|
{
|
|
public string RuleId { get; set; } = default!;
|
|
public int Days { get; set; }
|
|
public IReadOnlyList<string> SessionIds { get; set; } = default!;
|
|
|
|
/// <summary>True when the result hit the hit-set cap (2000) — the list is a lower bound.</summary>
|
|
public bool Truncated { get; set; }
|
|
}
|
|
}
|