diff --git a/docs/native-hosts.md b/docs/native-hosts.md
index 676906b..2b6fdc1 100644
--- a/docs/native-hosts.md
+++ b/docs/native-hosts.md
@@ -162,6 +162,13 @@ headroom**. Each calculation considers up to the latest 10,000 minute peaks.
Host-local evidence is preferred to account for different hardware; new hosts
can borrow the fleet's learned allowance. Startup allowances are never lower
than running allowances. The host page shows both allowances and their source.
+Expand **View samples** in Learned server capacity to inspect retained minute
+peaks for the last 24 hours. Filter by startup/running phase or switch from this
+host to the entire fleet. Rows show time, server and generation, CPU cores, and
+memory in MiB before headroom; CPU and memory peaks may come from different
+heartbeats. Results are paginated, and **Refresh samples** loads the latest stored
+measurements without contacting agents. The allowance count may lag the table
+until the next calculation or reflect a different source.
Lower allowances require at least three startup minute samples spanning two
minutes, or twenty running minute samples spanning five minutes. Until then,
diff --git a/src/lib/hosts.ts b/src/lib/hosts.ts
index 303fbcf..cb2f8cb 100644
--- a/src/lib/hosts.ts
+++ b/src/lib/hosts.ts
@@ -79,6 +79,13 @@ export interface ResourceAllowance {
samples: number; observedAt: string | null;
}
export interface CapacityCalibration { running: ResourceAllowance; startup: ResourceAllowance }
+export interface CapacitySample {
+ hostId: string; hostName: string | null; instanceId: string; server: string | null;
+ generation: number; phase: 'startup' | 'running'; timestamp: string; cpuCores: number; memoryBytes: number;
+}
+export interface CapacitySamplePage {
+ samples: CapacitySample[]; total: number; page: number; pageSize: number;
+}
export interface Host {
id: string; name: string; hostname: string; mode: HostMode; portMin: number; portMax: number;
bindAddress: string; publicAddress: string; minimumFreeBytes: number; createdAt: string;
diff --git a/src/lib/server/hosts/calibration.ts b/src/lib/server/hosts/calibration.ts
index ef948c8..89db95c 100644
--- a/src/lib/server/hosts/calibration.ts
+++ b/src/lib/server/hosts/calibration.ts
@@ -1,6 +1,6 @@
import { db } from '../db/index.js';
import { ensureHostSchema } from './schema.js';
-import type { CapacityCalibration, HostInventory, ResourceAllowance } from '../../hosts.js';
+import type { CapacityCalibration, CapacitySample, CapacitySamplePage, HostInventory, ResourceAllowance } from '../../hosts.js';
const BOOTSTRAP_CPU = 0.5;
const BOOTSTRAP_MEMORY = 1024 ** 3;
@@ -14,6 +14,27 @@ interface AllowanceRow {
}
function database() { const handle = db(); ensureHostSchema(handle); return handle; }
+/** Read retained minute peaks without contacting agents or recalculating allowances. */
+export function capacitySamples(hostId: string, options: { scope: 'host' | 'fleet'; phase: 'all' | Phase; page: number }): CapacitySamplePage {
+ const handle = database();
+ const clauses = ['s.bucket >= ?'];
+ const values: (string | number)[] = [new Date(Date.now() - WINDOW_MS).toISOString()];
+ if (options.scope === 'host') { clauses.push('s.host_id = ?'); values.push(hostId); }
+ if (options.phase !== 'all') { clauses.push('s.phase = ?'); values.push(options.phase); }
+ const where = clauses.join(' AND ');
+ const total = (handle.prepare(`SELECT COUNT(*) AS count FROM ec_instance_resource_samples s WHERE ${where}`).get(...values) as { count: number }).count;
+ const pageSize = 50;
+ const page = Math.min(options.page, Math.max(1, Math.ceil(total / pageSize)));
+ const samples = handle.prepare(`SELECT s.host_id AS hostId, h.name AS hostName, s.instance_id AS instanceId,
+ i.server, s.generation, s.phase, s.bucket AS timestamp, s.cpu_cores AS cpuCores, s.memory_bytes AS memoryBytes
+ FROM ec_instance_resource_samples s
+ LEFT JOIN ec_hosts h ON h.id=s.host_id
+ LEFT JOIN native_instances i ON i.id=s.instance_id AND i.host_id=s.host_id
+ WHERE ${where} ORDER BY s.bucket DESC, s.host_id, s.instance_id, s.generation, s.phase LIMIT ? OFFSET ?`)
+ .all(...values, pageSize, (page - 1) * pageSize) as CapacitySample[];
+ return { samples, total, page, pageSize };
+}
+
export function validateInstanceResources(inventory: HostInventory): void {
if (inventory.instanceResourcesError !== undefined && (typeof inventory.instanceResourcesError !== 'string'
|| inventory.instanceResourcesError.length > 500)) throw new Error('Invalid instance resource sampling error.');
diff --git a/src/lib/ui/components/CapacitySamples.svelte b/src/lib/ui/components/CapacitySamples.svelte
new file mode 100644
index 0000000..dc5acd2
--- /dev/null
+++ b/src/lib/ui/components/CapacitySamples.svelte
@@ -0,0 +1,84 @@
+
+
+
+ View samples
+ {#if open}
+
Retained minute peaks from the last 24 hours, before the 25% allowance headroom. CPU and memory peaks may come from different heartbeats. Choose the fleet to inspect measurements from other hosts used by fleet allowances.
+
+
+
+
+
+ {#if error}
{error}
{/if}
+ {#if loading}
Loading samples…
+ {:else if result}
+ {#if result.total === 0}
No {phase === 'all' ? '' : phase === 'startup' ? 'startup ' : 'running '}samples recorded for this source in the last 24 hours.
+ {:else}
+
{result.total.toLocaleString()} samples · newest first. Counts can differ from the allowance above, which is recalculated periodically.
+
+
+
CPU and memory peaks per container and minute; times are local.
Use agent 1.4.6 or later for resource sampling. Measurements arrive with agent heartbeats and are combined into one peak sample per container per minute. The first five minutes after a container starts count toward startup; later measurements count toward running. Temporary allowances stay in use until enough measurements are available.