Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
aad0111
update infra oapi spec
ben-fornefeld Dec 23, 2025
3489b2b
wip: first version of logs viewer
ben-fornefeld Dec 29, 2025
11e744d
Merge branch 'main' into implement-sandbox-details-logs
ben-fornefeld Feb 9, 2026
4b2a586
improve: sandboxes details title
ben-fornefeld Feb 10, 2026
656aab1
fix: initial logs ordering
ben-fornefeld Feb 10, 2026
edc5f95
refactor: streamline header components and improve logs handling
ben-fornefeld Feb 10, 2026
9560582
add: storage icon
ben-fornefeld Feb 11, 2026
600e519
improve: timestamps
ben-fornefeld Feb 11, 2026
dbebfec
improve: log level column
ben-fornefeld Feb 11, 2026
bb06a7d
add: dashboard layout copyable header title value
ben-fornefeld Feb 11, 2026
6ad9e7e
refactor: header layout
ben-fornefeld Feb 11, 2026
3ea7053
improve: logs readability
ben-fornefeld Feb 12, 2026
d53c53e
improve: loading / error state handling + config + state management
ben-fornefeld Feb 13, 2026
97057b5
fix: address cursor comments
ben-fornefeld Feb 13, 2026
c1aee93
fix: log state management
ben-fornefeld Feb 13, 2026
967f78a
cleanup: sandbox logs
ben-fornefeld Feb 13, 2026
1ab6870
chore: sync sandbox and build logs styling
ben-fornefeld Feb 13, 2026
5a71da5
Merge branch 'main' into implement-sandbox-details-logs
ben-fornefeld Feb 13, 2026
aecda0f
fix: refactor init cursor logic for forward fetching logs
ben-fornefeld Feb 13, 2026
719f8cb
improve: sandbox stopped log forward fetch draining
ben-fornefeld Feb 13, 2026
c02c4a3
refactor: build logs to match sandbox logs behavior
ben-fornefeld Feb 14, 2026
0442b21
chore: improve dashboard tabs memo
ben-fornefeld Feb 14, 2026
4ad367b
refactor: use reverse in backwards logs router and cut unecessary sor…
ben-fornefeld Feb 18, 2026
f062c48
chore: let log source be chosen automatically in infra
ben-fornefeld Feb 19, 2026
ce58dd9
improve: fix mobile responsiveness for sandbox details
ben-fornefeld Feb 19, 2026
470f89e
chore: require persistent log source for log order regression on buil…
ben-fornefeld Feb 19, 2026
f479c7c
chore: rename backward logs queries
ben-fornefeld Feb 20, 2026
feb4f71
chore: fix auth test depending on env
ben-fornefeld Feb 20, 2026
f111546
chore: abstract timestamp cursor utils
ben-fornefeld Feb 20, 2026
82400a2
fix: logs timestamp arrow showing
ben-fornefeld Feb 20, 2026
addd311
Merge branch 'implement-sandbox-details-logs' into feature/single-san…
ben-fornefeld Feb 23, 2026
7608d3b
add: sandbox metrics api layer
ben-fornefeld Feb 24, 2026
910cc3d
add: sandbox metrics chart + base hooks
ben-fornefeld Feb 24, 2026
752287d
wip: monitoring page + components
ben-fornefeld Feb 24, 2026
7c7cbda
mv: dir
ben-fornefeld Feb 25, 2026
e2efcdd
improve: time picker abstractions
ben-fornefeld Feb 25, 2026
dbf7a39
Merge branch 'main' into feature/single-sandbox-resource-metrics
ben-fornefeld Feb 25, 2026
e2d9c6d
chore: add missing type export + remove accidential build log source …
ben-fornefeld Feb 25, 2026
04f5502
Merge remote-tracking branch 'origin/main' into feature/single-sandbo…
ben-fornefeld Mar 2, 2026
a09a43a
chore: fix types
ben-fornefeld Mar 2, 2026
83259c0
Merge remote-tracking branch 'origin/main' into feature/single-sandbo…
ben-fornefeld Mar 2, 2026
1334994
refactor: state management
ben-fornefeld Mar 2, 2026
9d57012
chore: tests
ben-fornefeld Mar 2, 2026
b70311d
refactor: update monitoring chart model and tests
ben-fornefeld Mar 3, 2026
0ae0d00
Merge remote-tracking branch 'origin/main' into feature/single-sandbo…
ben-fornefeld Mar 3, 2026
130522d
feat: enhance monitoring charts with marker value formatters
ben-fornefeld Mar 4, 2026
26bf121
feat: add chart tooltips
ben-fornefeld Mar 4, 2026
e24ef99
chore: formatting, ui nits
ben-fornefeld Mar 4, 2026
00e4ffb
chore: only show loading layout when sandboxInfo is missing
ben-fornefeld Mar 4, 2026
03e010d
chore: cleanup
ben-fornefeld Mar 4, 2026
2a3db01
chore: ui, svg renderer charts
ben-fornefeld Mar 4, 2026
e966c15
chore: format
ben-fornefeld Mar 4, 2026
60c0ea2
address comments
ben-fornefeld Mar 5, 2026
cbe449c
fix: clamp timeframe bounds comment
ben-fornefeld Mar 5, 2026
be852df
chore: padding right + fix: percent max formatter
ben-fornefeld Mar 5, 2026
cf22f60
chore: use sandbox id schema for details router
ben-fornefeld Mar 5, 2026
46a89a3
chore: fix lifecycle bounds
ben-fornefeld Mar 5, 2026
c56e639
chore: responsiveness
ben-fornefeld Mar 5, 2026
45cf87c
fix: server side timestamp validation
ben-fornefeld Mar 5, 2026
c57edfc
address comments
ben-fornefeld Mar 5, 2026
3a7bcc4
chore: format
ben-fornefeld Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 82 additions & 4 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/__test__/integration/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,14 @@ describe('Auth Actions - Integration Tests', () => {
ok: true,
json: () => Promise.resolve({ version: 'v2.60.7', name: 'GoTrue' }),
})
global.fetch = fetchMock as unknown as typeof fetch
verifyTurnstileToken.mockResolvedValue(true)
})

afterEach(() => {
// Restore original console.error after each test
console.error = originalConsoleError
global.fetch = originalFetch
})

describe('Sign In Flow', () => {
Expand Down
125 changes: 125 additions & 0 deletions src/__test__/unit/sandbox-monitoring-chart-model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, expect, it } from 'vitest'
import { buildMonitoringChartModel } from '@/features/dashboard/sandbox/monitoring/utils/chart-model'
import type { SandboxMetric } from '@/server/api/models/sandboxes.models'

const baseMetric = {
timestamp: '1970-01-01T00:00:00.000Z',
cpuCount: 2,
memTotal: 1_000,
diskTotal: 2_000,
} satisfies Omit<
SandboxMetric,
'timestampUnix' | 'cpuUsedPct' | 'memUsed' | 'diskUsed'
>

describe('buildMonitoringChartModel', () => {
it('builds deterministic time-series data sorted by timestamp', () => {
const metrics: SandboxMetric[] = [
{
...baseMetric,
timestampUnix: 10,
cpuUsedPct: 30,
memUsed: 300,
diskUsed: 600,
},
{
...baseMetric,
timestampUnix: 0,
cpuUsedPct: 10,
memUsed: 100,
diskUsed: 200,
},
{
...baseMetric,
timestampUnix: 5,
cpuUsedPct: 20,
memUsed: 200,
diskUsed: 400,
},
]

const result = buildMonitoringChartModel({
metrics,
startMs: 0,
endMs: 10_000,
hoveredTimestampMs: 6_000,
})

expect(result.latestMetric?.timestampUnix).toBe(10)
expect(result.resourceSeries).toHaveLength(2)
expect(result.diskSeries).toHaveLength(1)
expect(result.resourceSeries[0]?.data).toEqual([
[0, 10, null],
[5_000, 20, null],
[10_000, 30, null],
])
expect(result.resourceSeries[1]?.data).toEqual([
[0, 10, 0],
[5_000, 20, 0],
[10_000, 30, 0],
])
expect(result.diskSeries[0]?.data).toEqual([
[0, 10, 0],
[5_000, 20, 0],
[10_000, 30, 0],
])
expect(result.resourceHoveredContext).toEqual({
cpuPercent: 20,
ramPercent: 20,
timestampMs: 5_000,
})
expect(result.diskHoveredContext).toEqual({
diskPercent: 20,
timestampMs: 5_000,
})
})

it('returns null hovered contexts when no data is available', () => {
const result = buildMonitoringChartModel({
metrics: [],
startMs: 0,
endMs: 10_000,
hoveredTimestampMs: 1_000,
})

expect(result.resourceHoveredContext).toBeNull()
expect(result.diskHoveredContext).toBeNull()
})

it('filters out metrics outside range and invalid timestamps', () => {
const metrics: SandboxMetric[] = [
{
...baseMetric,
timestampUnix: 1,
cpuUsedPct: 5,
memUsed: 50,
diskUsed: 100,
},
{
...baseMetric,
timestampUnix: Number.NaN,
cpuUsedPct: 55,
memUsed: 550,
diskUsed: 1_100,
},
{
...baseMetric,
timestampUnix: 20,
cpuUsedPct: 80,
memUsed: 800,
diskUsed: 1_600,
},
]

const result = buildMonitoringChartModel({
metrics,
startMs: 0,
endMs: 10_000,
hoveredTimestampMs: null,
})

expect(result.latestMetric?.timestampUnix).toBe(1)
expect(result.resourceSeries[0]?.data).toEqual([[1_000, 5, null]])
expect(result.diskSeries[0]?.data).toEqual([[1_000, 5, 0]])
})
})
177 changes: 177 additions & 0 deletions src/__test__/unit/sandbox-monitoring-timeframe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { describe, expect, it } from 'vitest'
import {
SANDBOX_MONITORING_DEFAULT_RANGE_MS,
SANDBOX_MONITORING_MAX_RANGE_MS,
SANDBOX_MONITORING_MIN_RANGE_MS,
} from '@/features/dashboard/sandbox/monitoring/utils/constants'
import {
getSandboxLifecycleBounds,
normalizeMonitoringTimeframe,
parseMonitoringQueryState,
} from '@/features/dashboard/sandbox/monitoring/utils/timeframe'
import type { SandboxDetailsDTO } from '@/server/api/models/sandboxes.models'

describe('sandbox-monitoring-timeframe', () => {
describe('normalizeMonitoringTimeframe', () => {
it('should fallback to default range when inputs are invalid', () => {
const now = 1_700_000_000_000

const result = normalizeMonitoringTimeframe({
start: Number.NaN,
end: Number.POSITIVE_INFINITY,
now,
})

expect(result.end).toBe(now)
expect(result.start).toBe(now - SANDBOX_MONITORING_DEFAULT_RANGE_MS)
})

it('should enforce minimum range', () => {
const now = 1_700_000_000_000

const result = normalizeMonitoringTimeframe({
start: now - 1_000,
end: now,
now,
})

expect(result.end - result.start).toBe(SANDBOX_MONITORING_MIN_RANGE_MS)
})

it('should cap maximum range', () => {
const now = 1_700_000_000_000

const result = normalizeMonitoringTimeframe({
start: now - 365 * 24 * 60 * 60 * 1_000,
end: now,
now,
})

expect(result.end - result.start).toBe(SANDBOX_MONITORING_MAX_RANGE_MS)
})

it('should clamp future timestamps to now', () => {
const now = 1_700_000_000_000

const result = normalizeMonitoringTimeframe({
start: now + 5_000,
end: now + 10_000,
now,
})

expect(result.end).toBe(now)
expect(result.start).toBe(now - SANDBOX_MONITORING_MIN_RANGE_MS)
})
})

describe('parseMonitoringQueryState', () => {
it('should parse canonical query params', () => {
const result = parseMonitoringQueryState({
start: '1000',
end: '2000',
live: '1',
})

expect(result).toEqual({
start: 1000,
end: 2000,
live: true,
})
})

it('should reject non-canonical live values and invalid timestamps', () => {
const result = parseMonitoringQueryState({
start: '123abc',
end: String(Number.MAX_SAFE_INTEGER),
live: 'true',
})

expect(result).toEqual({
start: null,
end: null,
live: null,
})
})
})

describe('getSandboxLifecycleBounds', () => {
it('should clamp lifecycle anchor end to now for paused sandbox', () => {
const now = 1_700_000_000_000
const sandboxInfo: SandboxDetailsDTO = {
templateID: 'template-id',
sandboxID: 'sandbox-id',
startedAt: new Date(now - 60_000).toISOString(),
endAt: new Date(now + 60_000).toISOString(),
envdVersion: '1.0.0',
cpuCount: 2,
memoryMB: 512,
diskSizeMB: 1_024,
state: 'paused',
}

const bounds = getSandboxLifecycleBounds(sandboxInfo, now)

expect(bounds?.startMs).toBe(now - 60_000)
expect(bounds?.anchorEndMs).toBe(now)
expect(bounds?.isRunning).toBe(false)
})

it('should fall back to now for running sandbox without endAt', () => {
const now = 1_700_000_000_000
const sandboxInfo = {
startedAt: new Date(now - 60_000).toISOString(),
endAt: null,
state: 'running' as const,
}

const bounds = getSandboxLifecycleBounds(sandboxInfo, now)

expect(bounds?.startMs).toBe(now - 60_000)
expect(bounds?.anchorEndMs).toBe(now)
expect(bounds?.isRunning).toBe(true)
})

it('should use stoppedAt when endAt is null for killed sandbox', () => {
const now = 1_700_000_000_000
const stoppedAt = now - 30_000
const sandboxInfo: SandboxDetailsDTO = {
templateID: 'template-id',
sandboxID: 'sandbox-id',
startedAt: new Date(now - 60_000).toISOString(),
endAt: null,
stoppedAt: new Date(stoppedAt).toISOString(),
cpuCount: 2,
memoryMB: 512,
diskSizeMB: 1_024,
state: 'killed',
}

const bounds = getSandboxLifecycleBounds(sandboxInfo, now)

expect(bounds?.startMs).toBe(now - 60_000)
expect(bounds?.anchorEndMs).toBe(stoppedAt)
expect(bounds?.isRunning).toBe(false)
})

it('should fall back to now for killed sandbox without end timestamp', () => {
const now = 1_700_000_000_000
const sandboxInfo: SandboxDetailsDTO = {
templateID: 'template-id',
sandboxID: 'sandbox-id',
startedAt: new Date(now - 60_000).toISOString(),
endAt: null,
stoppedAt: null,
cpuCount: 2,
memoryMB: 512,
diskSizeMB: 1_024,
state: 'killed',
}

const bounds = getSandboxLifecycleBounds(sandboxInfo, now)

expect(bounds?.startMs).toBe(now - 60_000)
expect(bounds?.anchorEndMs).toBe(now)
expect(bounds?.isRunning).toBe(false)
})
})
})
Loading
Loading