diff --git a/src/lib/ui/autoscaling.ts b/src/lib/ui/autoscaling.ts
index e724cc8..e1cc26f 100644
--- a/src/lib/ui/autoscaling.ts
+++ b/src/lib/ui/autoscaling.ts
@@ -1,3 +1,4 @@
+import { freshStatusTimestamp } from '../server-freshness.js';
import type { ScalingHistory, ScalingMember, ScalingView } from '../autoscaling.js';
import { parseServerName, zombieMapBySlug, zombieMaps } from '../map-aliases.js';
@@ -79,3 +80,14 @@ export function scalingActivity(view: ScalingView): ScalingActivity[] {
}
return [...groups.values()].sort((a, b) => b.lastSeenAt - a.lastSeenAt);
}
+
+/** Advance observation ages even when polling fails, using the fleet freshness window. */
+export function observationAge(at: number, now: number): { label: string; stale: boolean } {
+ const age = now - at;
+ if (!Number.isFinite(at) || !Number.isFinite(age) || age < -1000) return { label: 'Check observation time', stale: true };
+ const seconds = Math.max(0, Math.floor(age / 1000));
+ const elapsed = seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m`
+ : seconds < 86400 ? `${Math.floor(seconds / 3600)}h` : `${Math.floor(seconds / 86400)}d`;
+ const stale = !freshStatusTimestamp(new Date(at).toISOString(), now);
+ return { label: `${stale ? 'Stale · ' : ''}${elapsed} ago`, stale };
+}
diff --git a/src/lib/ui/components/autoscaling/ObservationTime.svelte b/src/lib/ui/components/autoscaling/ObservationTime.svelte
new file mode 100644
index 0000000..ad43185
--- /dev/null
+++ b/src/lib/ui/components/autoscaling/ObservationTime.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/src/lib/ui/components/autoscaling/PolicyCard.svelte b/src/lib/ui/components/autoscaling/PolicyCard.svelte
index c35be4e..861abd4 100644
--- a/src/lib/ui/components/autoscaling/PolicyCard.svelte
+++ b/src/lib/ui/components/autoscaling/PolicyCard.svelte
@@ -1,6 +1,7 @@