diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index ff3a6a0..4b81be7 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -221,14 +221,20 @@

{deployment.name}

{getStatusBadge(deployment.status)} - {/* B6 expiry icon — only when warning/expired. Tooltip via - native title attribute keeps the dependency footprint - tiny on a hot list. */} + {/* B6/#180 expiry icon — only when approaching expiry. + Yellow ≤3 months out, red ≤6 weeks out, red octagon + once expired. Tooltip via native title attribute keeps + the dependency footprint tiny on a hot list. */} {expiryState === 'warning' && ( )} + {expiryState === 'critical' && ( + + + + )} {expiryState === 'expired' && ( diff --git a/src/pages/DeploymentDetails.tsx b/src/pages/DeploymentDetails.tsx index 0e56dc1..c513f56 100644 --- a/src/pages/DeploymentDetails.tsx +++ b/src/pages/DeploymentDetails.tsx @@ -733,11 +733,20 @@ export function DeploymentDetails({ deployment, onBack, onDelete, onRetry }: Dep
- {/* Expiry banner */} - {expiryState !== "ok" && deployment.expires_at && ( + {/* Expiry banner (#180): red theme once expiry is imminent (≤6 weeks, + "critical") or past ("expired"), amber while merely approaching + (≤3 months, "warning"). */} + {expiryState !== "ok" && deployment.expires_at && (() => { + const isRed = expiryState === "critical" || expiryState === "expired"; + const expiryDateLabel = new Date(deployment.expires_at).toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + return (
{expiryState === "expired" ? ( ) : ( - + )}
-

+

{expiryState === "expired" ? "Dieses Deployment ist abgelaufen und wird in Kürze gelöscht." - : `Läuft am ${new Date(deployment.expires_at).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" })} ab`} + : expiryState === "critical" + ? `Läuft bald ab — am ${expiryDateLabel}` + : `Läuft am ${expiryDateLabel} ab`}

-

+

{expiryState === "expired" ? "Verlängern Sie es jetzt, falls die nächtliche Bereinigung noch nicht gelaufen ist." : "Verlängern Sie es, bevor der Cleanup-Job es entfernt."}

- )} + ); + })()} {/* Progress for active deployments */} {deployment.status === 'deploying' && ( diff --git a/src/utils/deployment.ts b/src/utils/deployment.ts index c8e5520..a98228a 100644 --- a/src/utils/deployment.ts +++ b/src/utils/deployment.ts @@ -1,26 +1,39 @@ import type { DeploymentDto } from "../api/deployments"; /** - * Lifecycle state of a deployment based on its `expires_at` and - * `expiry_warning_at` timestamps (set by the backend at create-time). + * Lifecycle state of a deployment based on how much runtime is left before its + * `expires_at` timestamp (set by the backend at create-/extend-time). * - * - "ok" : not expiring soon — no banner, no icon - * - "warning" : within the warning window (e.g. ≤14 days before expiry) - * - "expired" : past expires_at, will be hard-deleted by the next Beat sweep + * - "ok" : more than 3 months left — no banner, no icon + * - "warning" : within 3 months of expiry (yellow) — expiry approaching + * - "critical" : within 6 weeks of expiry (red) — expiry imminent + * - "expired" : past `expires_at`, will be hard-deleted by the next Beat sweep * - * Legacy deployments without expiry timestamps stay "ok" forever so the - * UI never warns about something the backend isn't actually tracking. + * The 6-week / 3-month windows (issue #180) are fixed frontend constants for + * now. The issue discussion raised whether these durations should instead come + * from the backend — if that happens, replace the constants below with values + * read off the DTO. Whether every extension is actually accepted is a separate + * concern tracked in #184. + * + * Legacy deployments without an `expires_at` stay "ok" forever so the UI never + * warns about something the backend isn't actually tracking. */ -export type ExpiryState = "ok" | "warning" | "expired"; +export type ExpiryState = "ok" | "warning" | "critical" | "expired"; + +// Pre-expiry thresholds, expressed in ms. Days-based (not calendar months) to +// mirror the backend's DAYS_PER_MONTH=30 approximation in deployment_expiry.py. +const SIX_WEEKS_MS = 42 * 24 * 60 * 60 * 1000; // "critical" — red +const THREE_MONTHS_MS = 90 * 24 * 60 * 60 * 1000; // "warning" — yellow export function getExpiryState( - deployment: Pick + deployment: Pick ): ExpiryState { - if (!deployment.expires_at || !deployment.expiry_warning_at) return "ok"; - const now = Date.now(); + if (!deployment.expires_at) return "ok"; const expiresAtMs = new Date(deployment.expires_at).getTime(); - const warnAtMs = new Date(deployment.expiry_warning_at).getTime(); - if (now >= expiresAtMs) return "expired"; - if (now >= warnAtMs) return "warning"; + if (Number.isNaN(expiresAtMs)) return "ok"; + const remaining = expiresAtMs - Date.now(); + if (remaining <= 0) return "expired"; + if (remaining <= SIX_WEEKS_MS) return "critical"; + if (remaining <= THREE_MONTHS_MS) return "warning"; return "ok"; }