@littlebearapps/platform-admin-sdk 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -428,9 +428,18 @@ curl -X POST ${PLATFORM_USAGE_URL}/usage/gaps/backfill -H 'Content-Type: applica
428
428
  // =============================================================================
429
429
 
430
430
  /**
431
- * Threshold for per-project coverage alerts (percentage)
431
+ * Default threshold for per-project coverage alerts (percentage).
432
+ * Can be overridden at runtime via PlatformSettings.gapCoverageThresholdPct.
432
433
  */
433
- const PROJECT_COVERAGE_THRESHOLD = 90;
434
+ const DEFAULT_PROJECT_COVERAGE_THRESHOLD = 90;
435
+
436
+ /**
437
+ * KV cache key and TTL for gap detection results.
438
+ * Gap detection doesn't need 15-minute freshness — 1-hour cache prevents
439
+ * 96 expensive D1 scans/day from becoming 24.
440
+ */
441
+ const PROJECT_GAP_CACHE_KEY = 'sentinel:project-gaps:result';
442
+ const PROJECT_GAP_CACHE_TTL = 3600;
434
443
 
435
444
  /**
436
445
  * On-demand resources excluded from coverage "expected" denominator.
@@ -440,6 +449,7 @@ const PROJECT_COVERAGE_THRESHOLD = 90;
440
449
  * TODO: Customise this list for your projects' on-demand resources
441
450
  */
442
451
  const ON_DEMAND_RESOURCE_EXCLUSIONS = new Set([
452
+ 'do:platform-notifications', // Durable Object, on-demand only
443
453
  'worker:platform-settings', // Admin-only, on-demand API
444
454
  'worker:platform-search', // Admin-only, on-demand API
445
455
  'worker:platform-alert-router', // Only invoked by Gatus/GitHub webhooks
@@ -474,14 +484,30 @@ export interface ProjectGap {
474
484
  /**
475
485
  * Detect gaps in per-project data coverage.
476
486
  * Queries resource_usage_snapshots table for projects with less than
477
- * PROJECT_COVERAGE_THRESHOLD% coverage in the last 24 hours.
487
+ * the configured coverage threshold in the last 24 hours.
488
+ *
489
+ * Results are cached in KV for 1 hour to prevent excessive D1 reads.
478
490
  *
491
+ * @param coverageThreshold - Coverage percentage threshold (default from PlatformSettings)
479
492
  * @returns Array of projects with low coverage, including their repo mapping
480
493
  */
481
494
  export async function detectProjectGaps(
482
495
  env: GapDetectionEnv,
483
- log: Logger
496
+ log: Logger,
497
+ coverageThreshold: number = DEFAULT_PROJECT_COVERAGE_THRESHOLD
484
498
  ): Promise<ProjectGap[]> {
499
+ // Check KV cache first — gap detection doesn't need 15-minute freshness
500
+ try {
501
+ const cached = await env.PLATFORM_CACHE.get(PROJECT_GAP_CACHE_KEY);
502
+ if (cached) {
503
+ const parsed = JSON.parse(cached) as ProjectGap[];
504
+ log.debug('Using cached project gap results', { count: parsed.length });
505
+ return parsed;
506
+ }
507
+ } catch {
508
+ // Cache miss or parse error — proceed with fresh query
509
+ }
510
+
485
511
  const gaps: ProjectGap[] = [];
486
512
 
487
513
  try {
@@ -500,9 +526,10 @@ export async function detectProjectGaps(
500
526
  AND project NOT IN ('unknown', 'all')
501
527
  ),
502
528
  known AS (
503
- SELECT project, resource_type, resource_id
529
+ SELECT DISTINCT project, resource_type, resource_id
504
530
  FROM resource_usage_snapshots
505
- WHERE project IS NOT NULL
531
+ WHERE snapshot_hour >= datetime('now', '-7 days')
532
+ AND project IS NOT NULL
506
533
  AND project NOT IN ('unknown', 'all')
507
534
  AND (resource_type || ':' || resource_id) NOT IN (${exclusionPlaceholders})
508
535
  )
@@ -525,7 +552,7 @@ export async function detectProjectGaps(
525
552
  HAVING coverage_pct < ?
526
553
  `
527
554
  )
528
- .bind(...exclusionKeys, PROJECT_COVERAGE_THRESHOLD)
555
+ .bind(...exclusionKeys, coverageThreshold)
529
556
  .all<{
530
557
  project: string;
531
558
  expected_resources: number;
@@ -547,6 +574,7 @@ export async function detectProjectGaps(
547
574
  SELECT DISTINCT resource_type || ':' || resource_id as resource_key
548
575
  FROM resource_usage_snapshots
549
576
  WHERE project = ?
577
+ AND snapshot_hour >= datetime('now', '-7 days')
550
578
  AND resource_type || ':' || resource_id NOT IN (
551
579
  SELECT DISTINCT resource_type || ':' || resource_id
552
580
  FROM resource_usage_snapshots
@@ -585,9 +613,10 @@ export async function detectProjectGaps(
585
613
  AND project = ?
586
614
  ),
587
615
  known AS (
588
- SELECT resource_type, resource_id
616
+ SELECT DISTINCT resource_type, resource_id
589
617
  FROM resource_usage_snapshots
590
618
  WHERE project = ?
619
+ AND snapshot_hour >= datetime('now', '-7 days')
591
620
  )
592
621
  SELECT
593
622
  k.resource_type,
@@ -636,6 +665,17 @@ export async function detectProjectGaps(
636
665
  projectCount: gaps.length,
637
666
  projects: gaps.map((g) => `${g.project}:${g.coveragePct}%`),
638
667
  });
668
+
669
+ // Cache results in KV to avoid repeated expensive D1 queries
670
+ try {
671
+ await env.PLATFORM_CACHE.put(
672
+ PROJECT_GAP_CACHE_KEY,
673
+ JSON.stringify(gaps),
674
+ { expirationTtl: PROJECT_GAP_CACHE_TTL }
675
+ );
676
+ } catch {
677
+ // Cache write failure is non-fatal
678
+ }
639
679
  } catch (error) {
640
680
  log.error('Failed to detect project gaps', error);
641
681
  }
@@ -525,8 +525,8 @@ async function fetchCostsFromD1(env: Env, log: Logger): Promise<CostBreakdown |
525
525
  SUM(COALESCE(workflows_cost_usd, 0)) as workflows,
526
526
  SUM(COALESCE(total_cost_usd, 0)) as total
527
527
  FROM hourly_usage_snapshots
528
- WHERE project = 'all' AND DATE(snapshot_hour) >= ?
529
- `).bind(billing.start).first<CostBreakdown>();
528
+ WHERE project = 'all' AND snapshot_hour >= ?
529
+ `).bind(billing.start + 'T00:00:00Z').first<CostBreakdown>();
530
530
 
531
531
  if (!result) {
532
532
  log.warn('D1 fallback returned no data');
@@ -805,10 +805,10 @@ async function queryAllowanceStatus(
805
805
  try {
806
806
  // Build a single query for all metrics of this service
807
807
  const selectExprs = metricDefs.map((m, i) => `${m.sqlExpr} as metric_${i}`).join(', ');
808
- const sql = `SELECT ${selectExprs} FROM hourly_usage_snapshots WHERE project = 'all' AND DATE(snapshot_hour) >= ?`;
808
+ const sql = `SELECT ${selectExprs} FROM hourly_usage_snapshots WHERE project = 'all' AND snapshot_hour >= ?`;
809
809
 
810
810
  const result = await env.PLATFORM_DB.prepare(sql)
811
- .bind(billingStart)
811
+ .bind(billingStart + 'T00:00:00Z')
812
812
  .first<Record<string, number>>();
813
813
 
814
814
  if (!result) {