oh-my-opencode-dashboard 0.0.5 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/dist/assets/index-B1tQFDjw.js +40 -0
- package/dist/assets/index-BFRahC0d.css +1 -0
- package/dist/index.html +3 -3
- package/package.json +2 -2
- package/src/App.tsx +486 -214
- package/src/app-payload.test.ts +108 -1
- package/src/server/api.test.ts +28 -7
- package/src/server/api.ts +1 -1
- package/src/server/dashboard.test.ts +41 -0
- package/src/server/dashboard.ts +79 -0
- package/src/server/dev.ts +1 -1
- package/src/server/start.ts +1 -1
- package/src/styles.css +58 -0
- package/src/time-series-ui.test.tsx +111 -0
- package/src/timeseries-stacked.test.ts +36 -24
- package/src/timeseries-stacked.ts +21 -5
- package/dist/assets/index--GqzhA4-.css +0 -1
- package/dist/assets/index-CiC6k4Yg.js +0 -40
package/src/app-payload.test.ts
CHANGED
|
@@ -155,4 +155,111 @@ describe('toDashboardPayload', () => {
|
|
|
155
155
|
// #then: should handle non-array steps gracefully
|
|
156
156
|
expect(payload.planProgress.steps).toEqual([])
|
|
157
157
|
})
|
|
158
|
-
|
|
158
|
+
|
|
159
|
+
it('should parse mainSession.sessionId from camel or snake keys', () => {
|
|
160
|
+
// #given: server JSON with main session id in camel and snake case
|
|
161
|
+
const camelJson = {
|
|
162
|
+
mainSession: {
|
|
163
|
+
agent: "sisyphus",
|
|
164
|
+
currentTool: "dashboard_start",
|
|
165
|
+
currentModel: "anthropic/claude-opus-4-5",
|
|
166
|
+
lastUpdatedLabel: "just now",
|
|
167
|
+
session: "test-session",
|
|
168
|
+
sessionId: "ses_main",
|
|
169
|
+
statusPill: "busy",
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const snakeJson = {
|
|
174
|
+
main_session: {
|
|
175
|
+
agent: "sisyphus",
|
|
176
|
+
current_tool: "dashboard_start",
|
|
177
|
+
current_model: "anthropic/claude-opus-4-5",
|
|
178
|
+
last_updated: "just now",
|
|
179
|
+
session: "test-session",
|
|
180
|
+
session_id: "ses_snake",
|
|
181
|
+
status: "busy",
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// #when: converting to dashboard payload
|
|
186
|
+
const camelPayload = toDashboardPayload(camelJson)
|
|
187
|
+
const snakePayload = toDashboardPayload(snakeJson)
|
|
188
|
+
|
|
189
|
+
// #then: sessionId should be preserved
|
|
190
|
+
expect(camelPayload.mainSession.sessionId).toBe("ses_main")
|
|
191
|
+
expect(snakePayload.mainSession.sessionId).toBe("ses_snake")
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should preserve mainSessionTasks from server JSON', () => {
|
|
195
|
+
// #given: server JSON with mainSessionTasks
|
|
196
|
+
const serverJson = {
|
|
197
|
+
mainSession: {
|
|
198
|
+
agent: "sisyphus",
|
|
199
|
+
currentTool: "dashboard_start",
|
|
200
|
+
currentModel: "anthropic/claude-opus-4-5",
|
|
201
|
+
lastUpdatedLabel: "just now",
|
|
202
|
+
session: "test-session",
|
|
203
|
+
sessionId: "ses_main",
|
|
204
|
+
statusPill: "busy",
|
|
205
|
+
},
|
|
206
|
+
planProgress: {
|
|
207
|
+
name: "test-plan",
|
|
208
|
+
completed: 0,
|
|
209
|
+
total: 0,
|
|
210
|
+
path: "/tmp/test-plan.md",
|
|
211
|
+
statusPill: "not started",
|
|
212
|
+
steps: [],
|
|
213
|
+
},
|
|
214
|
+
mainSessionTasks: [
|
|
215
|
+
{
|
|
216
|
+
id: "main-session",
|
|
217
|
+
description: "Main session",
|
|
218
|
+
subline: "ses_main",
|
|
219
|
+
agent: "sisyphus",
|
|
220
|
+
lastModel: "anthropic/claude-opus-4-5",
|
|
221
|
+
sessionId: "ses_main",
|
|
222
|
+
status: "running",
|
|
223
|
+
toolCalls: 3,
|
|
224
|
+
lastTool: "delegate_task",
|
|
225
|
+
timeline: "2026-01-01T00:00:00Z: 2m",
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
backgroundTasks: [],
|
|
229
|
+
timeSeries: {
|
|
230
|
+
windowMs: 300000,
|
|
231
|
+
buckets: 150,
|
|
232
|
+
bucketMs: 2000,
|
|
233
|
+
anchorMs: 1640995200000,
|
|
234
|
+
serverNowMs: 1640995500000,
|
|
235
|
+
series: [
|
|
236
|
+
{
|
|
237
|
+
id: "overall-main",
|
|
238
|
+
label: "Overall",
|
|
239
|
+
tone: "muted",
|
|
240
|
+
values: new Array(150).fill(0),
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// #when
|
|
247
|
+
const payload = toDashboardPayload(serverJson)
|
|
248
|
+
|
|
249
|
+
// #then
|
|
250
|
+
expect(payload.mainSessionTasks).toEqual([
|
|
251
|
+
{
|
|
252
|
+
id: "main-session",
|
|
253
|
+
description: "Main session",
|
|
254
|
+
subline: "ses_main",
|
|
255
|
+
agent: "sisyphus",
|
|
256
|
+
lastModel: "anthropic/claude-opus-4-5",
|
|
257
|
+
sessionId: "ses_main",
|
|
258
|
+
status: "running",
|
|
259
|
+
toolCalls: 3,
|
|
260
|
+
lastTool: "delegate_task",
|
|
261
|
+
timeline: "2026-01-01T00:00:00Z: 2m",
|
|
262
|
+
},
|
|
263
|
+
])
|
|
264
|
+
})
|
|
265
|
+
})
|
package/src/server/api.test.ts
CHANGED
|
@@ -15,6 +15,10 @@ function mkStorageRoot(): string {
|
|
|
15
15
|
return root
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function mkProjectRoot(): string {
|
|
19
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "omo-dashboard-project-"))
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
function writeMessageMeta(opts: {
|
|
19
23
|
storageRoot: string
|
|
20
24
|
sessionId: string
|
|
@@ -75,9 +79,18 @@ function hasSensitiveKeys(value: unknown): boolean {
|
|
|
75
79
|
|
|
76
80
|
const createStore = (): DashboardStore => ({
|
|
77
81
|
getSnapshot: (): DashboardPayload => ({
|
|
78
|
-
mainSession: {
|
|
82
|
+
mainSession: {
|
|
83
|
+
agent: "x",
|
|
84
|
+
currentModel: null,
|
|
85
|
+
currentTool: "-",
|
|
86
|
+
lastUpdatedLabel: "never",
|
|
87
|
+
session: "s",
|
|
88
|
+
sessionId: null,
|
|
89
|
+
statusPill: "idle",
|
|
90
|
+
},
|
|
79
91
|
planProgress: { name: "p", completed: 0, total: 0, path: "", statusPill: "not started", steps: [] as PlanStep[] },
|
|
80
92
|
backgroundTasks: [],
|
|
93
|
+
mainSessionTasks: [],
|
|
81
94
|
timeSeries: {
|
|
82
95
|
windowMs: 0,
|
|
83
96
|
bucketMs: 0,
|
|
@@ -93,8 +106,9 @@ const createStore = (): DashboardStore => ({
|
|
|
93
106
|
describe('API Routes', () => {
|
|
94
107
|
it('should return health check', async () => {
|
|
95
108
|
const storageRoot = mkStorageRoot()
|
|
109
|
+
const projectRoot = mkProjectRoot()
|
|
96
110
|
const store = createStore()
|
|
97
|
-
const api = createApi({ store, storageRoot })
|
|
111
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
98
112
|
|
|
99
113
|
const res = await api.request("/health")
|
|
100
114
|
expect(res.status).toBe(200)
|
|
@@ -103,8 +117,9 @@ describe('API Routes', () => {
|
|
|
103
117
|
|
|
104
118
|
it('should return dashboard data without sensitive keys', async () => {
|
|
105
119
|
const storageRoot = mkStorageRoot()
|
|
120
|
+
const projectRoot = mkProjectRoot()
|
|
106
121
|
const store = createStore()
|
|
107
|
-
const api = createApi({ store, storageRoot })
|
|
122
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
108
123
|
|
|
109
124
|
const res = await api.request("/dashboard")
|
|
110
125
|
expect(res.status).toBe(200)
|
|
@@ -122,8 +137,9 @@ describe('API Routes', () => {
|
|
|
122
137
|
|
|
123
138
|
it('should reject invalid session IDs', async () => {
|
|
124
139
|
const storageRoot = mkStorageRoot()
|
|
140
|
+
const projectRoot = mkProjectRoot()
|
|
125
141
|
const store = createStore()
|
|
126
|
-
const api = createApi({ store, storageRoot })
|
|
142
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
127
143
|
|
|
128
144
|
const res = await api.request("/tool-calls/not_valid!")
|
|
129
145
|
expect(res.status).toBe(400)
|
|
@@ -132,8 +148,9 @@ describe('API Routes', () => {
|
|
|
132
148
|
|
|
133
149
|
it('should return 404 for missing sessions', async () => {
|
|
134
150
|
const storageRoot = mkStorageRoot()
|
|
151
|
+
const projectRoot = mkProjectRoot()
|
|
135
152
|
const store = createStore()
|
|
136
|
-
const api = createApi({ store, storageRoot })
|
|
153
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
137
154
|
|
|
138
155
|
const res = await api.request("/tool-calls/ses_missing")
|
|
139
156
|
expect(res.status).toBe(404)
|
|
@@ -142,9 +159,10 @@ describe('API Routes', () => {
|
|
|
142
159
|
|
|
143
160
|
it('should return empty tool calls for existing sessions', async () => {
|
|
144
161
|
const storageRoot = mkStorageRoot()
|
|
162
|
+
const projectRoot = mkProjectRoot()
|
|
145
163
|
writeMessageMeta({ storageRoot, sessionId: "ses_empty", messageId: "msg_1", created: 1000 })
|
|
146
164
|
const store = createStore()
|
|
147
|
-
const api = createApi({ store, storageRoot })
|
|
165
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
148
166
|
|
|
149
167
|
const res = await api.request("/tool-calls/ses_empty")
|
|
150
168
|
expect(res.status).toBe(200)
|
|
@@ -160,6 +178,7 @@ describe('API Routes', () => {
|
|
|
160
178
|
|
|
161
179
|
it('should redact tool call payload fields', async () => {
|
|
162
180
|
const storageRoot = mkStorageRoot()
|
|
181
|
+
const projectRoot = mkProjectRoot()
|
|
163
182
|
writeMessageMeta({ storageRoot, sessionId: "ses_redact", messageId: "msg_1", created: 1000 })
|
|
164
183
|
writeToolPart({
|
|
165
184
|
storageRoot,
|
|
@@ -175,7 +194,7 @@ describe('API Routes', () => {
|
|
|
175
194
|
},
|
|
176
195
|
})
|
|
177
196
|
const store = createStore()
|
|
178
|
-
const api = createApi({ store, storageRoot })
|
|
197
|
+
const api = createApi({ store, storageRoot, projectRoot })
|
|
179
198
|
|
|
180
199
|
const res = await api.request("/tool-calls/ses_redact")
|
|
181
200
|
expect(res.status).toBe(200)
|
|
@@ -185,4 +204,6 @@ describe('API Routes', () => {
|
|
|
185
204
|
expect(data.toolCalls.length).toBe(1)
|
|
186
205
|
expect(hasSensitiveKeys(data)).toBe(false)
|
|
187
206
|
})
|
|
207
|
+
|
|
208
|
+
// /sessions was intentionally removed along with the manual session picker.
|
|
188
209
|
})
|
package/src/server/api.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { deriveToolCalls, MAX_TOOL_CALL_MESSAGES, MAX_TOOL_CALLS } from "../inge
|
|
|
6
6
|
|
|
7
7
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
|
|
8
8
|
|
|
9
|
-
export function createApi(opts: { store: DashboardStore; storageRoot: string }): Hono {
|
|
9
|
+
export function createApi(opts: { store: DashboardStore; storageRoot: string; projectRoot: string }): Hono {
|
|
10
10
|
const api = new Hono()
|
|
11
11
|
|
|
12
12
|
api.get("/health", (c) => {
|
|
@@ -76,9 +76,49 @@ describe("buildDashboardPayload", () => {
|
|
|
76
76
|
expect(payload.mainSession.currentTool).toBe("delegate_task")
|
|
77
77
|
expect(payload.mainSession.agent).toBe("sisyphus")
|
|
78
78
|
expect(payload.mainSession.currentModel).toBeNull()
|
|
79
|
+
expect(payload.mainSession.sessionId).toBe(sessionId)
|
|
79
80
|
|
|
80
81
|
expect(payload.raw).not.toHaveProperty("prompt")
|
|
81
82
|
expect(payload.raw).not.toHaveProperty("input")
|
|
83
|
+
|
|
84
|
+
expect(payload).toHaveProperty("mainSessionTasks")
|
|
85
|
+
expect((payload as any).mainSessionTasks).toEqual([
|
|
86
|
+
{
|
|
87
|
+
id: "main-session",
|
|
88
|
+
description: "Main session",
|
|
89
|
+
subline: sessionId,
|
|
90
|
+
agent: "sisyphus",
|
|
91
|
+
lastModel: null,
|
|
92
|
+
status: "running",
|
|
93
|
+
toolCalls: 1,
|
|
94
|
+
lastTool: "delegate_task",
|
|
95
|
+
timeline: "1970-01-01T00:00:01Z: 1s",
|
|
96
|
+
sessionId,
|
|
97
|
+
},
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
expect(payload.raw).toHaveProperty("mainSessionTasks.0.lastTool", "delegate_task")
|
|
101
|
+
} finally {
|
|
102
|
+
fs.rmSync(storageRoot, { recursive: true, force: true })
|
|
103
|
+
fs.rmSync(projectRoot, { recursive: true, force: true })
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("includes mainSessionTasks in raw payload when no sessions exist", () => {
|
|
108
|
+
const storageRoot = mkStorageRoot()
|
|
109
|
+
const storage = getStorageRoots(storageRoot)
|
|
110
|
+
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omo-project-"))
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const payload = buildDashboardPayload({
|
|
114
|
+
projectRoot,
|
|
115
|
+
storage,
|
|
116
|
+
nowMs: 2000,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
expect(payload).toHaveProperty("mainSessionTasks")
|
|
120
|
+
expect((payload as any).mainSessionTasks).toEqual([])
|
|
121
|
+
expect(payload.raw).toHaveProperty("mainSessionTasks")
|
|
82
122
|
} finally {
|
|
83
123
|
fs.rmSync(storageRoot, { recursive: true, force: true })
|
|
84
124
|
fs.rmSync(projectRoot, { recursive: true, force: true })
|
|
@@ -149,6 +189,7 @@ describe("buildDashboardPayload", () => {
|
|
|
149
189
|
expect(payload).toHaveProperty("timeSeries")
|
|
150
190
|
expect(payload.raw).toHaveProperty("timeSeries")
|
|
151
191
|
expect(payload.mainSession.currentModel).toBeNull()
|
|
192
|
+
expect(payload.mainSession.sessionId).toBeNull()
|
|
152
193
|
|
|
153
194
|
const sensitiveKeys = ["prompt", "input", "output", "error", "state"]
|
|
154
195
|
|
package/src/server/dashboard.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { readBoulderState, readPlanProgress, readPlanSteps, type PlanStep } from
|
|
|
4
4
|
import { deriveBackgroundTasks } from "../ingest/background-tasks"
|
|
5
5
|
import { deriveTimeSeriesActivity, type TimeSeriesPayload } from "../ingest/timeseries"
|
|
6
6
|
import { getMainSessionView, getStorageRoots, pickActiveSessionId, readMainSessionMetas, type MainSessionView, type OpenCodeStorageRoots, type SessionMetadata } from "../ingest/session"
|
|
7
|
+
import { deriveToolCalls } from "../ingest/tool-calls"
|
|
7
8
|
|
|
8
9
|
export type DashboardPayload = {
|
|
9
10
|
mainSession: {
|
|
@@ -12,6 +13,7 @@ export type DashboardPayload = {
|
|
|
12
13
|
currentTool: string
|
|
13
14
|
lastUpdatedLabel: string
|
|
14
15
|
session: string
|
|
16
|
+
sessionId: string | null
|
|
15
17
|
statusPill: string
|
|
16
18
|
}
|
|
17
19
|
planProgress: {
|
|
@@ -33,6 +35,18 @@ export type DashboardPayload = {
|
|
|
33
35
|
timeline: string
|
|
34
36
|
sessionId: string | null
|
|
35
37
|
}>
|
|
38
|
+
mainSessionTasks: Array<{
|
|
39
|
+
id: string
|
|
40
|
+
description: string
|
|
41
|
+
subline?: string
|
|
42
|
+
agent: string
|
|
43
|
+
lastModel: string | null
|
|
44
|
+
status: string
|
|
45
|
+
toolCalls: number
|
|
46
|
+
lastTool: string
|
|
47
|
+
timeline: string
|
|
48
|
+
sessionId: string | null
|
|
49
|
+
}>
|
|
36
50
|
timeSeries: TimeSeriesPayload
|
|
37
51
|
raw: unknown
|
|
38
52
|
}
|
|
@@ -92,6 +106,33 @@ function mainStatusPill(status: string): string {
|
|
|
92
106
|
return "unknown"
|
|
93
107
|
}
|
|
94
108
|
|
|
109
|
+
function formatIsoNoMs(ts: number): string {
|
|
110
|
+
const iso = new Date(ts).toISOString()
|
|
111
|
+
return iso.replace(/\.\d{3}Z$/, "Z")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatElapsed(ms: number): string {
|
|
115
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000))
|
|
116
|
+
const seconds = totalSeconds % 60
|
|
117
|
+
const totalMinutes = Math.floor(totalSeconds / 60)
|
|
118
|
+
const minutes = totalMinutes % 60
|
|
119
|
+
const totalHours = Math.floor(totalMinutes / 60)
|
|
120
|
+
const hours = totalHours % 24
|
|
121
|
+
const days = Math.floor(totalHours / 24)
|
|
122
|
+
|
|
123
|
+
if (days > 0) return hours > 0 ? `${days}d${hours}h` : `${days}d`
|
|
124
|
+
if (totalHours > 0) return minutes > 0 ? `${totalHours}h${minutes}m` : `${totalHours}h`
|
|
125
|
+
if (totalMinutes > 0) return seconds > 0 ? `${totalMinutes}m${seconds}s` : `${totalMinutes}m`
|
|
126
|
+
return `${seconds}s`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatTimeline(startAt: number | null, endAtMs: number): string {
|
|
130
|
+
if (typeof startAt !== "number") return ""
|
|
131
|
+
const start = formatIsoNoMs(startAt)
|
|
132
|
+
const elapsed = formatElapsed(endAtMs - startAt)
|
|
133
|
+
return `${start}: ${elapsed}`
|
|
134
|
+
}
|
|
135
|
+
|
|
95
136
|
export function buildDashboardPayload(opts: {
|
|
96
137
|
projectRoot: string
|
|
97
138
|
storage: OpenCodeStorageRoots
|
|
@@ -136,6 +177,41 @@ export function buildDashboardPayload(opts: {
|
|
|
136
177
|
const mainCurrentModel = "currentModel" in main
|
|
137
178
|
? (main as MainSessionView).currentModel
|
|
138
179
|
: null
|
|
180
|
+
|
|
181
|
+
const mainSessionTasks = (() => {
|
|
182
|
+
if (!sessionId) return []
|
|
183
|
+
|
|
184
|
+
const mainStatus = main.status
|
|
185
|
+
const status = mainStatus === "running_tool" || mainStatus === "thinking" || mainStatus === "busy"
|
|
186
|
+
? "running"
|
|
187
|
+
: mainStatus === "idle"
|
|
188
|
+
? "idle"
|
|
189
|
+
: "unknown"
|
|
190
|
+
|
|
191
|
+
const { toolCalls } = deriveToolCalls({
|
|
192
|
+
storage: opts.storage,
|
|
193
|
+
sessionId,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const startAt = sessionMeta?.time?.created ?? null
|
|
197
|
+
const endAtMs = status === "running" ? nowMs : (main.lastUpdated ?? nowMs)
|
|
198
|
+
|
|
199
|
+
return [
|
|
200
|
+
{
|
|
201
|
+
id: "main-session",
|
|
202
|
+
description: "Main session",
|
|
203
|
+
subline: sessionId,
|
|
204
|
+
agent: main.agent,
|
|
205
|
+
lastModel: mainCurrentModel,
|
|
206
|
+
status,
|
|
207
|
+
toolCalls: toolCalls.length,
|
|
208
|
+
lastTool: toolCalls[0]?.tool ?? "-",
|
|
209
|
+
timeline: formatTimeline(startAt, endAtMs),
|
|
210
|
+
sessionId,
|
|
211
|
+
},
|
|
212
|
+
]
|
|
213
|
+
})()
|
|
214
|
+
|
|
139
215
|
const payload: DashboardPayload = {
|
|
140
216
|
mainSession: {
|
|
141
217
|
agent: main.agent,
|
|
@@ -143,6 +219,7 @@ export function buildDashboardPayload(opts: {
|
|
|
143
219
|
currentTool: main.currentTool ?? "-",
|
|
144
220
|
lastUpdatedLabel: formatIso(main.lastUpdated),
|
|
145
221
|
session: main.sessionLabel,
|
|
222
|
+
sessionId: sessionId ?? null,
|
|
146
223
|
statusPill: mainStatusPill(main.status),
|
|
147
224
|
},
|
|
148
225
|
planProgress: {
|
|
@@ -164,6 +241,7 @@ export function buildDashboardPayload(opts: {
|
|
|
164
241
|
timeline: typeof t.timeline === "string" ? t.timeline : "",
|
|
165
242
|
sessionId: t.sessionId ?? null,
|
|
166
243
|
})),
|
|
244
|
+
mainSessionTasks,
|
|
167
245
|
timeSeries,
|
|
168
246
|
raw: null,
|
|
169
247
|
}
|
|
@@ -172,6 +250,7 @@ export function buildDashboardPayload(opts: {
|
|
|
172
250
|
mainSession: payload.mainSession,
|
|
173
251
|
planProgress: payload.planProgress,
|
|
174
252
|
backgroundTasks: payload.backgroundTasks,
|
|
253
|
+
mainSessionTasks: payload.mainSessionTasks,
|
|
175
254
|
timeSeries: payload.timeSeries,
|
|
176
255
|
}
|
|
177
256
|
return payload
|
package/src/server/dev.ts
CHANGED
package/src/server/start.ts
CHANGED
|
@@ -30,7 +30,7 @@ const store = createDashboardStore({
|
|
|
30
30
|
pollIntervalMs: 2000,
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
app.route('/api', createApi({ store, storageRoot }))
|
|
33
|
+
app.route('/api', createApi({ store, storageRoot, projectRoot: project }))
|
|
34
34
|
|
|
35
35
|
const distRoot = join(import.meta.dir, '../../dist')
|
|
36
36
|
|
package/src/styles.css
CHANGED
|
@@ -161,6 +161,64 @@ body {
|
|
|
161
161
|
transform: translateY(0px);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
.fieldRow {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
flex-wrap: wrap;
|
|
168
|
+
gap: 10px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.field {
|
|
172
|
+
border: 1px solid rgba(31, 36, 38, 0.14);
|
|
173
|
+
background: rgba(255, 255, 255, 0.64);
|
|
174
|
+
padding: 9px 12px;
|
|
175
|
+
border-radius: 999px;
|
|
176
|
+
color: var(--ink);
|
|
177
|
+
font-size: 12px;
|
|
178
|
+
line-height: 1;
|
|
179
|
+
box-shadow: 0 6px 16px rgba(29, 32, 33, 0.06);
|
|
180
|
+
transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
|
|
181
|
+
min-width: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.field:hover {
|
|
185
|
+
transform: translateY(-1px);
|
|
186
|
+
background: rgba(255, 255, 255, 0.78);
|
|
187
|
+
border-color: rgba(31, 36, 38, 0.18);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.field:active {
|
|
191
|
+
transform: translateY(0px);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.field:focus-visible {
|
|
195
|
+
outline: 2px solid rgba(15, 90, 81, 0.30);
|
|
196
|
+
outline-offset: 2px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.field:disabled {
|
|
200
|
+
opacity: 0.55;
|
|
201
|
+
cursor: not-allowed;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
select.field {
|
|
205
|
+
appearance: none;
|
|
206
|
+
padding-right: 34px;
|
|
207
|
+
background-image:
|
|
208
|
+
linear-gradient(45deg, transparent 50%, rgba(31, 36, 38, 0.50) 50%),
|
|
209
|
+
linear-gradient(135deg, rgba(31, 36, 38, 0.50) 50%, transparent 50%);
|
|
210
|
+
background-position:
|
|
211
|
+
calc(100% - 18px) 50%,
|
|
212
|
+
calc(100% - 13px) 50%;
|
|
213
|
+
background-size: 5px 5px;
|
|
214
|
+
background-repeat: no-repeat;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
input.field {
|
|
218
|
+
appearance: none;
|
|
219
|
+
flex: 1 1 220px;
|
|
220
|
+
}
|
|
221
|
+
|
|
164
222
|
.pill {
|
|
165
223
|
display: inline-flex;
|
|
166
224
|
align-items: center;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
TimeSeriesActivitySection,
|
|
7
|
+
computeMainAgentsScaleMax,
|
|
8
|
+
computeOtherMainAgentsCount,
|
|
9
|
+
} from "./App";
|
|
10
|
+
|
|
11
|
+
type TimeSeriesProps = React.ComponentProps<typeof TimeSeriesActivitySection>;
|
|
12
|
+
|
|
13
|
+
function mkTimeSeries(override?: Partial<TimeSeriesProps["timeSeries"]>): TimeSeriesProps["timeSeries"] {
|
|
14
|
+
const buckets = 3;
|
|
15
|
+
const mkSeries = (id: TimeSeriesProps["timeSeries"]["series"][number]["id"], values: number[]) => ({
|
|
16
|
+
id,
|
|
17
|
+
label: id,
|
|
18
|
+
tone: "muted" as const,
|
|
19
|
+
values,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
windowMs: 300_000,
|
|
24
|
+
buckets,
|
|
25
|
+
bucketMs: 2_000,
|
|
26
|
+
anchorMs: 0,
|
|
27
|
+
serverNowMs: 0,
|
|
28
|
+
series: [
|
|
29
|
+
mkSeries("overall-main", [0, 0, 0]),
|
|
30
|
+
mkSeries("agent:sisyphus", [0, 0, 0]),
|
|
31
|
+
mkSeries("agent:prometheus", [0, 0, 0]),
|
|
32
|
+
mkSeries("agent:atlas", [0, 0, 0]),
|
|
33
|
+
mkSeries("background-total", [0, 0, 0]),
|
|
34
|
+
],
|
|
35
|
+
...override,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("TimeSeriesActivitySection (SSR)", () => {
|
|
40
|
+
it("should not render top axis and should keep bottom axis", () => {
|
|
41
|
+
// #given
|
|
42
|
+
const timeSeries = mkTimeSeries();
|
|
43
|
+
|
|
44
|
+
// #when
|
|
45
|
+
const html = renderToStaticMarkup(<TimeSeriesActivitySection timeSeries={timeSeries} />);
|
|
46
|
+
|
|
47
|
+
// #then
|
|
48
|
+
expect(html).not.toContain("timeSeriesAxisTop");
|
|
49
|
+
expect(html).toContain("timeSeriesAxisBottom");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should render sand bars for other main agents when derived otherMain is non-zero", () => {
|
|
53
|
+
// #given
|
|
54
|
+
const timeSeries = mkTimeSeries({
|
|
55
|
+
series: [
|
|
56
|
+
{ id: "overall-main", label: "Overall", tone: "muted", values: [10, 0, 0] },
|
|
57
|
+
{ id: "agent:sisyphus", label: "Sisyphus", tone: "teal", values: [0, 0, 0] },
|
|
58
|
+
{ id: "agent:prometheus", label: "Prometheus", tone: "red", values: [0, 0, 0] },
|
|
59
|
+
{ id: "agent:atlas", label: "Atlas", tone: "green", values: [0, 0, 0] },
|
|
60
|
+
{ id: "background-total", label: "Background", tone: "muted", values: [0, 0, 0] },
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// #when
|
|
65
|
+
const html = renderToStaticMarkup(<TimeSeriesActivitySection timeSeries={timeSeries} />);
|
|
66
|
+
|
|
67
|
+
// #then
|
|
68
|
+
expect(html).toContain("timeSeriesBar--sand");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("time-series helpers", () => {
|
|
73
|
+
it("computeOtherMainAgentsCount should clamp to >= 0 and ignore invalid numbers", () => {
|
|
74
|
+
// #given
|
|
75
|
+
const value = computeOtherMainAgentsCount({
|
|
76
|
+
overall: 10,
|
|
77
|
+
background: 3,
|
|
78
|
+
sisyphus: 2,
|
|
79
|
+
prometheus: 1,
|
|
80
|
+
atlas: 0,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// #then
|
|
84
|
+
expect(value).toBe(4);
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
computeOtherMainAgentsCount({
|
|
88
|
+
overall: NaN,
|
|
89
|
+
background: Infinity,
|
|
90
|
+
sisyphus: -1,
|
|
91
|
+
prometheus: 0,
|
|
92
|
+
atlas: 0,
|
|
93
|
+
})
|
|
94
|
+
).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("computeMainAgentsScaleMax should include otherMain in the max", () => {
|
|
98
|
+
// #given
|
|
99
|
+
const scaleMax = computeMainAgentsScaleMax({
|
|
100
|
+
buckets: 1,
|
|
101
|
+
overallValues: [10],
|
|
102
|
+
backgroundValues: [0],
|
|
103
|
+
sisyphusValues: [0],
|
|
104
|
+
prometheusValues: [0],
|
|
105
|
+
atlasValues: [0],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// #then
|
|
109
|
+
expect(scaleMax).toBe(10);
|
|
110
|
+
});
|
|
111
|
+
});
|