mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
71282f3556
Backend/Shared:
- McpQuotaService: ResolveHomeAsync reads definitions, entitlements, plan name and purchased slots once per check; Grow adds slots × per-slot value to every window (a lifted window stays unlimited); the decision and McpPlanLimits carry the breakdown; CheckManyAsync, the charge scope and TargetTenantId are gone
- McpQuotaEnforcementMiddleware: one home charge for every caller; IsExempt = Global Admin only (counted, never blocked; the Global Reader stays quota'd); two 429 texts (organization / user)
- TenantEntitlementService.GetPurchasedDelegatedSlots = max(0, effective slot limit − catalog count), 0 without the delegation right; FeatureEntitlementCatalog gains McpSlot*RequestLimit (Pro 300/6000 per account, 900/18000 per organization = 30 % of the Pro windows; Community and conferred Pro 0)
- PlanTierDefinition gains four nullable slot fields; GET/PUT global/config/plan-tiers return the catalog (UsagePlanCatalogDefaults per edition) so a blank field is explained
- McpUsageQuotaNode / McpOrganizationQuotaNode carry purchasedDelegatedSlots and the slot values (absent without purchased slots)
- Removed: the Tenant Group flag ChargeHomeTenantQuota with its scope plumbing (HomeChargedTenantIds, IsDelegatedAggregate, CatalogDecisionResult), Narrow/quotaExcludedTenants on the session envelopes and the X-MCP-Quota-Excluded-Tenants header, targetTenantId on the 429 body, the foreign-home attribution on McpTenantUsage (HomeTenantId, delegated/homeTenantId on the organization report) and the managed tenant's budget on the manager's managed-tenant list; PATCH global/tenant-groups/{id} is a rename
Web: SectionUsagePlans with four slot inputs, catalog placeholders and a built-in-defaults line; SectionMcpUsage renders "base + slots × per-slot" on both cards and drops the delegated badge; SectionTenantGroups without the charge toggle; SectionDelegatedAccess, the accept page and the privacy page state the new model (privacy last updated 2026-09-11).
MCP: get_fleet_overview no longer merges an exclusion list, the 429 renderer names only the caller's organization, the delegated-scope instruction states the home-charge model; wire types regenerated.
Tests: quota service slot-growth suite, entitlement purchased-slot matrix, conferred-Pro self-service writes (403/409), parity facts for the new and the removed fields; the radar analytics fixture is dated off the real clock (it had aged out of the 30-day window).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
279 lines
13 KiB
C#
279 lines
13 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 HOME tenant (shared by every member; 0 = unlimited). Every
|
||
/// MCP request the caller makes — reads into managed tenants included — draws on these windows. The
|
||
/// limits already contain the growth from purchased delegation slots; the slot fields let a client show
|
||
/// the breakdown (base = limit − slots × slot value) and are absent when the tenant has no purchased slot.
|
||
/// </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>Delegation slots bought beyond the edition's included ones; absent when zero.</summary>
|
||
public int? PurchasedDelegatedSlots { get; set; }
|
||
/// <summary>Per-user daily growth per purchased slot; absent when no slot was purchased.</summary>
|
||
public int? SlotDailyLimit { get; set; }
|
||
public int? SlotMonthlyLimit { get; set; }
|
||
/// <summary>Organization daily growth per purchased slot; absent when no slot was purchased.</summary>
|
||
public int? SlotTenantDailyLimit { get; set; }
|
||
public int? SlotTenantMonthlyLimit { 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 ONE tenant — the caller's own (GetMcpOrganizationUsage) or the
|
||
/// tenant a Global Admin / Global Reader names (GetGlobalMcpOrganizationUsage): every account of the
|
||
/// tenant whose requests were charged to its organization budget (a member's reads into managed tenants
|
||
/// included), plus the tenant's organization windows. 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>The tenant's organization windows (plan, limits, today / this month) from the same counters.</summary>
|
||
public McpOrganizationQuotaNode Quota { get; set; } = default!;
|
||
/// <summary>Requests charged to the tenant per day inside the range, oldest first; days without requests are absent.</summary>
|
||
public IReadOnlyList<McpOrganizationDailyItem> Daily { get; set; } = default!;
|
||
}
|
||
|
||
/// <summary>One day of the tenant's organization budget, nested in <see cref="GetMcpOrganizationUsageResponse"/>.</summary>
|
||
// Declaration order == wire order.
|
||
public class McpOrganizationDailyItem
|
||
{
|
||
/// <summary>yyyyMMdd.</summary>
|
||
public string Date { get; set; } = default!;
|
||
public long Requests { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// The organization-wide windows of the tenant in <see cref="GetMcpOrganizationUsageResponse"/>: its tenant
|
||
/// plan (tenant-wide override, else edition) with the limits (0 = unlimited; purchased-slot growth included)
|
||
/// and the counters over every account charged to the tenant. The slot fields carry the breakdown and are
|
||
/// absent when the tenant has no purchased delegation slot.
|
||
/// </summary>
|
||
// Declaration order == wire order.
|
||
public class McpOrganizationQuotaNode
|
||
{
|
||
public string TenantPlan { get; set; } = default!;
|
||
public int DailyLimit { get; set; }
|
||
public int MonthlyLimit { get; set; }
|
||
public long DailyUsed { get; set; }
|
||
public long MonthlyUsed { get; set; }
|
||
/// <summary>Delegation slots bought beyond the edition's included ones; absent when zero.</summary>
|
||
public int? PurchasedDelegatedSlots { get; set; }
|
||
/// <summary>Organization daily growth per purchased slot; absent when no slot was purchased.</summary>
|
||
public int? SlotDailyLimit { get; set; }
|
||
public int? SlotMonthlyLimit { get; set; }
|
||
}
|
||
|
||
/// <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; }
|
||
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; }
|
||
}
|
||
}
|