diff --git a/kiloclaw/src/app/(app)/claw/components/changelog-data.ts b/kiloclaw/src/app/(app)/claw/components/changelog-data.ts new file mode 100644 index 00000000..4d730db8 --- /dev/null +++ b/kiloclaw/src/app/(app)/claw/components/changelog-data.ts @@ -0,0 +1,77 @@ +type ChangelogCategory = 'feature' | 'bugfix'; +type ChangelogDeployHint = 'redeploy_suggested' | 'redeploy_required' | null; + +export type ChangelogEntry = { + date: string; // ISO date string, e.g. "2026-02-18" + description: string; + category: ChangelogCategory; + deployHint: ChangelogDeployHint; +}; + +// Newest entries first. Developers add new entries to the top of this array. +export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ + { + date: '2026-02-24', + description: + 'Improve OpenClaw restart handling when restarts are triggered via the OpenClaw Control UI.', + category: 'bugfix', + deployHint: 'redeploy_suggested', + }, + { + date: '2026-02-24', + description: 'Fix instance type and size badges.', + category: 'bugfix', + deployHint: null, + }, + { + date: '2026-02-23', + description: + 'Redesigned dashboard: live gateway process status, added ability to restart OpenClaw gateway.', + category: 'feature', + deployHint: null, + }, + { + date: '2026-02-23', + description: 'Deploy OpenClaw 2026.2.22. Added device pairing support to the dashboard.', + category: 'feature', + deployHint: 'redeploy_required', + }, + { + date: '2026-02-23', + description: + 'OpenClaw now binds only to the loopback interface and is managed by a Kilo controller.', + category: 'feature', + deployHint: 'redeploy_required', + }, + { + date: '2026-02-20', + description: + 'Added OpenClaw Doctor: run diagnostics and auto-fix from the dashboard. Renamed "Restart Gateway" to "Redeploy" to reflect actual behavior.', + category: 'feature', + deployHint: null, + }, + { + date: '2026-02-19', + description: 'Added Discord and Slack channel configuration', + category: 'feature', + deployHint: 'redeploy_suggested', + }, + { + date: '2026-02-18', + description: 'Fixed an issue where pending pair requests were not displayed', + category: 'bugfix', + deployHint: null, + }, + { + date: '2026-02-18', + description: 'Initial support for Telegram Channel pairing', + category: 'feature', + deployHint: 'redeploy_suggested', + }, + { + date: '2026-02-17', + description: 'Fixed errors on stopping an instance', + category: 'bugfix', + deployHint: null, + }, +]; diff --git a/kiloclaw/src/durable-objects/kiloclaw-instance.ts b/kiloclaw/src/durable-objects/kiloclaw-instance.ts index 32f185fc..6617c9f0 100644 --- a/kiloclaw/src/durable-objects/kiloclaw-instance.ts +++ b/kiloclaw/src/durable-objects/kiloclaw-instance.ts @@ -356,6 +356,7 @@ export class KiloClawInstance extends DurableObject { private flyMachineId: string | null = null; private flyVolumeId: string | null = null; private flyRegion: string | null = null; + private volumeSizeGb: number | null = null; private machineSize: MachineSize | null = null; private healthCheckFailCount = 0; private pendingDestroyMachineId: string | null = null; @@ -367,6 +368,7 @@ export class KiloClawInstance extends DurableObject { // In-memory only (not persisted to SQLite) — throttles live Fly checks in getStatus() private lastLiveCheckAt: number | null = null; + private backfillingVolumeSize = false; // ---- State loading ---- @@ -396,6 +398,7 @@ export class KiloClawInstance extends DurableObject { this.flyMachineId = s.flyMachineId; this.flyVolumeId = s.flyVolumeId; this.flyRegion = s.flyRegion; + this.volumeSizeGb = s.volumeSizeGb; this.machineSize = s.machineSize; this.healthCheckFailCount = s.healthCheckFailCount; this.pendingDestroyMachineId = s.pendingDestroyMachineId; @@ -467,6 +470,7 @@ export class KiloClawInstance extends DurableObject { ); this.flyVolumeId = volume.id; this.flyRegion = volume.region; + this.volumeSizeGb = volume.size_gb; console.log('[DO] Created Fly Volume:', volume.id, 'region:', volume.region); } @@ -515,6 +519,7 @@ export class KiloClawInstance extends DurableObject { flyMachineId: this.flyMachineId, flyVolumeId: this.flyVolumeId, flyRegion: this.flyRegion, + volumeSizeGb: this.flyVolumeId ? this.volumeSizeGb : null, healthCheckFailCount: 0, pendingDestroyMachineId: null, pendingDestroyVolumeId: null, @@ -1011,7 +1016,10 @@ export class KiloClawInstance extends DurableObject { console.warn('[DO] Volume not found during region check, clearing'); this.flyVolumeId = null; this.flyRegion = null; - await this.ctx.storage.put(storageUpdate({ flyVolumeId: null, flyRegion: null })); + this.volumeSizeGb = null; + await this.ctx.storage.put( + storageUpdate({ flyVolumeId: null, flyRegion: null, volumeSizeGb: null }) + ); await this.ensureVolume(flyConfig, 'start'); } // Other errors: proceed with cached region, createMachine will fail @@ -1215,6 +1223,7 @@ export class KiloClawInstance extends DurableObject { flyMachineId: string | null; flyVolumeId: string | null; flyRegion: string | null; + volumeSizeGb: number | null; openclawVersion: string | null; imageVariant: string | null; trackedImageTag: string | null; @@ -1234,6 +1243,17 @@ export class KiloClawInstance extends DurableObject { this.ctx.waitUntil(this.syncStatusFromLiveCheck()); } + // Fire-and-forget backfill: if we have a volume but no cached size, + // fetch it from the Fly API so the next poll returns the real value. + if (this.volumeSizeGb === null && this.flyVolumeId && !this.backfillingVolumeSize) { + this.backfillingVolumeSize = true; + this.ctx.waitUntil( + this.backfillVolumeSizeGb().finally(() => { + this.backfillingVolumeSize = false; + }) + ); + } + return { userId: this.userId, sandboxId: this.sandboxId, @@ -1248,6 +1268,7 @@ export class KiloClawInstance extends DurableObject { flyMachineId: this.flyMachineId, flyVolumeId: this.flyVolumeId, flyRegion: this.flyRegion, + volumeSizeGb: this.volumeSizeGb, openclawVersion: this.openclawVersion, imageVariant: this.imageVariant, trackedImageTag: this.trackedImageTag, @@ -1495,9 +1516,13 @@ export class KiloClawInstance extends DurableObject { return; } - // Verify volume still exists on Fly + // Verify volume still exists on Fly (and backfill size if missing) try { - await fly.getVolume(flyConfig, this.flyVolumeId); + const volume = await fly.getVolume(flyConfig, this.flyVolumeId); + if (this.volumeSizeGb === null) { + this.volumeSizeGb = volume.size_gb; + await this.ctx.storage.put(storageUpdate({ volumeSizeGb: volume.size_gb })); + } } catch (err) { if (fly.isFlyNotFound(err)) { reconcileLog(reason, 'replace_lost_volume', { @@ -1505,7 +1530,8 @@ export class KiloClawInstance extends DurableObject { old_volume_id: this.flyVolumeId, }); this.flyVolumeId = null; - await this.ctx.storage.put(storageUpdate({ flyVolumeId: null })); + this.volumeSizeGb = null; + await this.ctx.storage.put(storageUpdate({ flyVolumeId: null, volumeSizeGb: null })); await this.ensureVolume(flyConfig, reason); } // Other errors: leave as-is, retry next alarm @@ -1711,6 +1737,25 @@ export class KiloClawInstance extends DurableObject { } } + /** + * Fire-and-forget backfill for volumeSizeGb. Called from getStatus() and + * reconcileVolume() when the field is null but a volume exists. + */ + private async backfillVolumeSizeGb(): Promise { + if (!this.flyVolumeId) return; + + try { + const flyConfig = this.getFlyConfig(); + const volume = await fly.getVolume(flyConfig, this.flyVolumeId); + this.volumeSizeGb = volume.size_gb; + await this.ctx.storage.put(storageUpdate({ volumeSizeGb: volume.size_gb })); + console.log('[DO] Backfilled volumeSizeGb:', volume.size_gb); + } catch (err) { + // Non-fatal — will retry on next getStatus() or reconciliation cycle + console.warn('[DO] Failed to backfill volumeSizeGb:', err); + } + } + /** * Check that a running machine has the correct volume mount. * If the mount is wrong/missing, repair via stop → update → start. @@ -1839,7 +1884,10 @@ export class KiloClawInstance extends DurableObject { // Success or 404: clear this.pendingDestroyVolumeId = null; this.flyVolumeId = null; - await this.ctx.storage.put(storageUpdate({ pendingDestroyVolumeId: null, flyVolumeId: null })); + this.volumeSizeGb = null; + await this.ctx.storage.put( + storageUpdate({ pendingDestroyVolumeId: null, flyVolumeId: null, volumeSizeGb: null }) + ); } /** @@ -1877,6 +1925,7 @@ export class KiloClawInstance extends DurableObject { this.flyMachineId = null; this.flyVolumeId = null; this.flyRegion = null; + this.volumeSizeGb = null; this.machineSize = null; this.healthCheckFailCount = 0; this.pendingDestroyMachineId = null; @@ -2032,7 +2081,14 @@ export class KiloClawInstance extends DurableObject { this.flyVolumeId = volume.id; this.flyRegion = volume.region; - await this.ctx.storage.put(storageUpdate({ flyVolumeId: volume.id, flyRegion: volume.region })); + this.volumeSizeGb = volume.size_gb; + await this.ctx.storage.put( + storageUpdate({ + flyVolumeId: volume.id, + flyRegion: volume.region, + volumeSizeGb: volume.size_gb, + }) + ); reconcileLog(reason, 'create_volume', { volume_id: volume.id, @@ -2098,6 +2154,7 @@ export class KiloClawInstance extends DurableObject { ); this.flyVolumeId = forkedVolume.id; this.flyRegion = forkedVolume.region; + this.volumeSizeGb = forkedVolume.size_gb; reconcileLog(reason, 'fork_stranded_volume', { old_volume_id: oldVolumeId, old_region: oldRegion, @@ -2108,7 +2165,10 @@ export class KiloClawInstance extends DurableObject { // Fresh provision (never started) — no user data to preserve this.flyVolumeId = null; this.flyRegion = null; - await this.ctx.storage.put(storageUpdate({ flyVolumeId: null, flyRegion: null })); + this.volumeSizeGb = null; + await this.ctx.storage.put( + storageUpdate({ flyVolumeId: null, flyRegion: null, volumeSizeGb: null }) + ); const freshVolume = await fly.createVolumeWithFallback( flyConfig, @@ -2121,6 +2181,7 @@ export class KiloClawInstance extends DurableObject { ); this.flyVolumeId = freshVolume.id; this.flyRegion = freshVolume.region; + this.volumeSizeGb = freshVolume.size_gb; reconcileLog(reason, 'create_replacement_volume', { old_volume_id: oldVolumeId, old_region: oldRegion, @@ -2131,7 +2192,11 @@ export class KiloClawInstance extends DurableObject { // Persist new volume state await this.ctx.storage.put( - storageUpdate({ flyVolumeId: this.flyVolumeId, flyRegion: this.flyRegion }) + storageUpdate({ + flyVolumeId: this.flyVolumeId, + flyRegion: this.flyRegion, + volumeSizeGb: this.volumeSizeGb, + }) ); // Delete old volume (best-effort cleanup) @@ -2258,6 +2323,7 @@ export class KiloClawInstance extends DurableObject { flyMachineId: null, flyVolumeId: null, flyRegion: null, + volumeSizeGb: null, machineSize: null, healthCheckFailCount: 0, pendingDestroyMachineId: null, @@ -2281,6 +2347,7 @@ export class KiloClawInstance extends DurableObject { this.flyMachineId = null; this.flyVolumeId = null; this.flyRegion = null; + this.volumeSizeGb = null; this.machineSize = null; this.healthCheckFailCount = 0; this.pendingDestroyMachineId = null; diff --git a/kiloclaw/src/schemas/instance-config.ts b/kiloclaw/src/schemas/instance-config.ts index bc9b376d..ef995522 100644 --- a/kiloclaw/src/schemas/instance-config.ts +++ b/kiloclaw/src/schemas/instance-config.ts @@ -118,6 +118,8 @@ export const PersistedStateSchema = z.object({ flyMachineId: z.string().nullable().default(null), flyVolumeId: z.string().nullable().default(null), flyRegion: z.string().nullable().default(null), + // Actual provisioned volume size in GB (backfilled from Fly API for existing instances) + volumeSizeGb: z.number().nullable().default(null), machineSize: MachineSizeSchema.nullable().default(null), // Health check tracking healthCheckFailCount: z.number().default(0), diff --git a/src/app/(app)/claw/components/InstanceControls.tsx b/src/app/(app)/claw/components/InstanceControls.tsx index 7ff85a40..0ba6a102 100644 --- a/src/app/(app)/claw/components/InstanceControls.tsx +++ b/src/app/(app)/claw/components/InstanceControls.tsx @@ -60,7 +60,7 @@ export function InstanceControls({ - 20 GB SSD + {status.volumeSizeGb ?? 10} GB SSD diff --git a/src/app/(app)/claw/components/changelog-data.ts b/src/app/(app)/claw/components/changelog-data.ts index 0be19b81..c2d617ca 100644 --- a/src/app/(app)/claw/components/changelog-data.ts +++ b/src/app/(app)/claw/components/changelog-data.ts @@ -12,10 +12,16 @@ export type ChangelogEntry = { export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ { date: '2026-02-24', +<<<<<<< HEAD + description: 'Fix instance type and size badges.', + category: 'bugfix', + deployHint: null, +======= description: 'Improve OpenClaw restart handling when restarts are triggered via the OpenClaw Control UI.', category: 'bugfix', deployHint: 'redeploy_suggested', +>>>>>>> main }, { date: '2026-02-23', diff --git a/src/app/(app)/claw/components/withStatusQueryBoundary.test.ts b/src/app/(app)/claw/components/withStatusQueryBoundary.test.ts index 0bac5266..e4b48466 100644 --- a/src/app/(app)/claw/components/withStatusQueryBoundary.test.ts +++ b/src/app/(app)/claw/components/withStatusQueryBoundary.test.ts @@ -18,6 +18,7 @@ const baseStatus: KiloClawDashboardStatus = { flyMachineId: null, flyVolumeId: null, flyRegion: null, + volumeSizeGb: null, gatewayToken: 'token', workerUrl: 'https://claw.kilo.ai', }; diff --git a/src/lib/kiloclaw/types.ts b/src/lib/kiloclaw/types.ts index f3a4e038..32b8c464 100644 --- a/src/lib/kiloclaw/types.ts +++ b/src/lib/kiloclaw/types.ts @@ -109,6 +109,7 @@ export type PlatformStatusResponse = { flyMachineId: string | null; flyVolumeId: string | null; flyRegion: string | null; + volumeSizeGb: number | null; }; /** A Fly volume snapshot. */