feat: Quota enforcement UI and various improvements

- Add quota limit warnings to Resources, Services, and OwnerScheduler pages
- Add quotaUtils.ts for checking quota limits
- Update BusinessLayout with quota context
- Improve email receiver logging
- Update serializers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-03 15:47:48 -05:00
parent fd751f02f8
commit 4f515c3710
8 changed files with 281 additions and 36 deletions

View File

@@ -0,0 +1,116 @@
/**
* Quota Utilities
*
* Helpers for identifying resources/services that are over quota and will be
* auto-archived when the grace period expires.
*/
import { Resource, Service, QuotaOverage } from '../types';
/**
* Get the IDs of resources that are over quota and will be auto-archived.
* These are the oldest resources (by created_at) that exceed the limit.
*
* @param resources - All resources
* @param quotaOverages - Active quota overages
* @returns Set of resource IDs that are over quota
*/
export function getOverQuotaResourceIds(
resources: Resource[],
quotaOverages?: QuotaOverage[]
): Set<string> {
const overQuotaIds = new Set<string>();
if (!quotaOverages || quotaOverages.length === 0) {
return overQuotaIds;
}
// Find MAX_RESOURCES overage
const resourceOverage = quotaOverages.find(o => o.quota_type === 'MAX_RESOURCES');
if (!resourceOverage) {
return overQuotaIds;
}
// Filter out already-archived resources and sort by created_at (oldest first)
const activeResources = resources
.filter(r => !r.is_archived_by_quota)
.sort((a, b) => {
const aDate = a.created_at ? new Date(a.created_at).getTime() : 0;
const bDate = b.created_at ? new Date(b.created_at).getTime() : 0;
return aDate - bDate;
});
// The first N resources (where N = overage_amount) are over quota
const overageCount = resourceOverage.overage_amount;
for (let i = 0; i < Math.min(overageCount, activeResources.length); i++) {
overQuotaIds.add(activeResources[i].id);
}
return overQuotaIds;
}
/**
* Get the IDs of services that are over quota and will be auto-archived.
* These are the oldest services (by created_at) that exceed the limit.
*
* @param services - All services
* @param quotaOverages - Active quota overages
* @returns Set of service IDs that are over quota
*/
export function getOverQuotaServiceIds(
services: Service[],
quotaOverages?: QuotaOverage[]
): Set<string> {
const overQuotaIds = new Set<string>();
if (!quotaOverages || quotaOverages.length === 0) {
return overQuotaIds;
}
// Find MAX_SERVICES overage
const serviceOverage = quotaOverages.find(o => o.quota_type === 'MAX_SERVICES');
if (!serviceOverage) {
return overQuotaIds;
}
// Filter out already-archived services and sort by created_at (oldest first)
const activeServices = services
.filter(s => !s.is_archived_by_quota)
.sort((a, b) => {
const aDate = a.created_at ? new Date(a.created_at).getTime() : 0;
const bDate = b.created_at ? new Date(b.created_at).getTime() : 0;
return aDate - bDate;
});
// The first N services (where N = overage_amount) are over quota
const overageCount = serviceOverage.overage_amount;
for (let i = 0; i < Math.min(overageCount, activeServices.length); i++) {
overQuotaIds.add(activeServices[i].id);
}
return overQuotaIds;
}
/**
* Check if a specific resource is over quota (will be archived)
*/
export function isResourceOverQuota(
resourceId: string,
resources: Resource[],
quotaOverages?: QuotaOverage[]
): boolean {
const overQuotaIds = getOverQuotaResourceIds(resources, quotaOverages);
return overQuotaIds.has(resourceId);
}
/**
* Check if a specific service is over quota (will be archived)
*/
export function isServiceOverQuota(
serviceId: string,
services: Service[],
quotaOverages?: QuotaOverage[]
): boolean {
const overQuotaIds = getOverQuotaServiceIds(services, quotaOverages);
return overQuotaIds.has(serviceId);
}