diff --git a/spec/openapi.dashboard-api.yaml b/spec/openapi.dashboard-api.yaml index 3e9e4b1d5..09ddf6d70 100644 --- a/spec/openapi.dashboard-api.yaml +++ b/spec/openapi.dashboard-api.yaml @@ -565,6 +565,7 @@ components: - cpuCount - memoryMB - diskSizeMB + - retentionExpired properties: templateID: type: string @@ -594,6 +595,9 @@ components: $ref: "#/components/schemas/MemoryMB" diskSizeMB: $ref: "#/components/schemas/DiskSizeMB" + retentionExpired: + type: boolean + description: Whether the sandbox ended more than the retention window ago, so its monitoring, events, and logs data is no longer available HealthResponse: type: object diff --git a/src/core/modules/sandboxes/models.ts b/src/core/modules/sandboxes/models.ts index 253eb1c94..a359f378b 100644 --- a/src/core/modules/sandboxes/models.ts +++ b/src/core/modules/sandboxes/models.ts @@ -20,6 +20,9 @@ interface SandboxDetailsBaseModel { cpuCount: number memoryMB: number diskSizeMB: number + // True when the sandbox ended more than the retention window ago, so its + // monitoring, events, and logs data is no longer available. + retentionExpired: boolean lifecycle?: SandboxLifecycleModel } @@ -169,6 +172,8 @@ export function mapInfraSandboxDetailsToModel( diskSizeMB: sandbox.diskSizeMB, metadata: sandbox.metadata, state: sandbox.state, + // Infra only serves live sandboxes, whose data is never past retention. + retentionExpired: false, } } @@ -189,5 +194,6 @@ export function mapApiSandboxRecordToModel( memoryMB: sandbox.memoryMB, diskSizeMB: sandbox.diskSizeMB, state: 'killed', + retentionExpired: sandbox.retentionExpired, } } diff --git a/src/core/server/api/routers/sandbox.ts b/src/core/server/api/routers/sandbox.ts index b85b46e67..7a50ba0a3 100644 --- a/src/core/server/api/routers/sandbox.ts +++ b/src/core/server/api/routers/sandbox.ts @@ -20,6 +20,17 @@ import { SandboxIdSchema } from '@/core/shared/schemas/api' import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants' import { TERMINAL_SANDBOX_TIMEOUT_MS } from '@/features/dashboard/terminal/constants' +function isPastRetention(timestamp: string | null): boolean { + if (!timestamp) { + return false + } + const timestampMs = new Date(timestamp).getTime() + if (Number.isNaN(timestampMs)) { + return false + } + return Date.now() - timestampMs > SANDBOX_MONITORING_METRICS_RETENTION_MS +} + const sandboxRepositoryProcedure = protectedTeamProcedure.use( withTeamAuthedRequestRepository( createSandboxesRepository, @@ -67,11 +78,21 @@ export const sandboxRouter = createTRPCRouter({ ? (mappedDetails.stoppedAt ?? mappedDetails.endAt) : null + const pausedAt = derivedLifecycle.pausedAt ?? fallbackPausedAt + + // A paused sandbox keeps no fresh data, so once it has been paused longer + // than the retention window its monitoring/events/logs are gone too. The + // killed case is already flagged by the backend (mappedDetails). + const retentionExpired = + mappedDetails.retentionExpired || + (mappedDetails.state === 'paused' && isPastRetention(pausedAt)) + return { ...mappedDetails, + retentionExpired, lifecycle: { createdAt: derivedLifecycle.createdAt ?? mappedDetails.startedAt, - pausedAt: derivedLifecycle.pausedAt ?? fallbackPausedAt, + pausedAt, endedAt: derivedLifecycle.endedAt ?? fallbackEndedAt, events: derivedLifecycle.events, }, diff --git a/src/core/shared/contracts/dashboard-api.types.ts b/src/core/shared/contracts/dashboard-api.types.ts index 0f7821b27..022031f81 100644 --- a/src/core/shared/contracts/dashboard-api.types.ts +++ b/src/core/shared/contracts/dashboard-api.types.ts @@ -1280,6 +1280,8 @@ export interface components { cpuCount: components['schemas']['CPUCount'] memoryMB: components['schemas']['MemoryMB'] diskSizeMB: components['schemas']['DiskSizeMB'] + /** @description Whether the sandbox ended more than the retention window ago, so its monitoring, events, and logs data is no longer available */ + retentionExpired: boolean } HealthResponse: { /** @description Human-readable health check result. */ diff --git a/src/features/dashboard/sandbox/common/data-retention-expired.tsx b/src/features/dashboard/sandbox/common/data-retention-expired.tsx new file mode 100644 index 000000000..417120d01 --- /dev/null +++ b/src/features/dashboard/sandbox/common/data-retention-expired.tsx @@ -0,0 +1,19 @@ +import { LOG_RETENTION_MS } from '@/configs/logs' +import { ExpiryIcon } from '@/ui/primitives/icons' + +const RETENTION_DAYS = LOG_RETENTION_MS / 24 / 60 / 60 / 1000 + +export function DataRetentionExpired() { + return ( +
Data retention is over
++ This sandbox ended more than {RETENTION_DAYS} days ago, so its + monitoring, events, and logs data is no longer available. +
+