Admin view in the web UI
This commit is contained in:
+28
-3
@@ -726,11 +726,18 @@ components:
|
||||
|
||||
Status:
|
||||
type: object
|
||||
required: [started_at, db_bytes, last_event_seq, projects, active_tasks, sse_clients, live_agents, running_turns, queued_turns, rate_limits]
|
||||
required: [started_at, db_bytes, last_event_seq, projects, active_tasks, sse_clients, live_agents, running_turns, queued_turns, caps, rate_limits]
|
||||
properties:
|
||||
live_agents: { type: integer, description: Agent processes currently running. }
|
||||
running_turns: { type: integer }
|
||||
queued_turns: { type: integer }
|
||||
running_turns:
|
||||
type: array
|
||||
description: Turns running now, oldest session first.
|
||||
items: { $ref: "#/components/schemas/TurnRef" }
|
||||
queued_turns:
|
||||
type: array
|
||||
description: Turns waiting for room under the caps, in the order they will start.
|
||||
items: { $ref: "#/components/schemas/TurnRef" }
|
||||
caps: { $ref: "#/components/schemas/Caps" }
|
||||
rate_limits:
|
||||
type: object
|
||||
description: The latest rate-limit status each vendor reported, keyed by vendor.
|
||||
@@ -741,3 +748,21 @@ components:
|
||||
projects: { type: integer }
|
||||
active_tasks: { type: integer }
|
||||
sse_clients: { type: integer }
|
||||
|
||||
Caps:
|
||||
type: object
|
||||
required: [global, per_vendor]
|
||||
properties:
|
||||
global: { type: integer, description: Most turns running at once across all vendors. }
|
||||
per_vendor:
|
||||
type: object
|
||||
description: Most turns running at once per vendor, keyed by vendor.
|
||||
additionalProperties: { type: integer }
|
||||
|
||||
TurnRef:
|
||||
type: object
|
||||
required: [session_id, task_id, vendor]
|
||||
properties:
|
||||
session_id: { type: string }
|
||||
task_id: { type: string }
|
||||
vendor: { type: string }
|
||||
|
||||
+7
-1
@@ -74,7 +74,13 @@ A turn's final message is its summary. When a turn ends normally (not an error,
|
||||
|
||||
Running turns are capped globally and per vendor (`DefaultLimits` in `internal/agents/manager.go`). A turn that would exceed a cap leaves its session `queued`, with the message held in the `turn_queue` table; whenever a turn ends, a process exits, or a task is merged or discarded, the oldest queued turns whose vendor has room start. Interrupting a `queued` session cancels its queued turn. Idle sessions with a live process don't count against the caps. Whether a turn resumes the Claude conversation is decided by whether the session has ever sent one.
|
||||
|
||||
When Claude Code reports a rate-limit status of `rejected` and the turn then fails, the inbox gets a `rate_limited` item carrying the vendor's status (including when the limit resets) instead of a generic error. `GET /api/admin/status` reports running and queued turns and the latest rate-limit status each vendor sent.
|
||||
When Claude Code reports a rate-limit status of `rejected` and the turn then fails, the inbox gets a `rate_limited` item carrying the vendor's status (including when the limit resets) instead of a generic error.
|
||||
|
||||
## Admin view
|
||||
|
||||
`GET /api/admin/status` reports `running_turns` (sessions now `running`) and `queued_turns` (rows of `turn_queue`, in the order they will start), each as session, task and vendor; `caps`, the global and per-vendor limits from `DefaultLimits`; `rate_limits`, the latest `rate_limit_info` each vendor sent since byre started, unchanged; and server counts (start time, agent processes, projects, active tasks, SSE clients, database size including WAL, last event seq). A turn's queue row is written before its session's `queued` state event, so a refetch triggered by that event sees it.
|
||||
|
||||
The Admin view (a pane tab, also opened in the right pane by the navbar's **Admin** button) shows all of it: running turns against each cap, queued turns in order, both linking to their tasks, each vendor's rate-limit status with its reset time and per-window utilization, and the server counts. It refetches the status when a `session.created`, `session.state`, `task.*` or `project.created` event arrives, when an `agent.vendor` event carries a `rate_limit_event`, and when the SSE stream reconnects; other events only advance the last-event seq it shows. There is no timer, so the agent-process count can lag until one of those events, and the "in 2h 10m" beside a reset time is as of the last render.
|
||||
|
||||
Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `turn.ended`, `session.state`, `session.model`, and `session.context`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. The one exception is partial output: each top-level `text_delta` is broadcast to SSE clients as an `agent.delta` event (`payload.text`, `seq` 0) that is never stored, and the transcript grows a live message from them until the finished `agent.message` replaces it. A `turn.ended` event carries `inbox_item_id` when its final message was filed as a `report` item. `session.context` records the context size whenever it changes: `used` is the last assistant message's input, cache-read, cache-creation and output tokens, and `window` is the model's context window from the turn's `result` (`modelUsage[model].contextWindow`), so it is 0 until the session's first turn after a server start ends. The task header shows the latest one as a percentage of the window, coloured green, amber from 70%, red from 90%.
|
||||
|
||||
|
||||
+1
-1
@@ -474,7 +474,7 @@ Record transcripts from the spike as the seed of the contract test suite.
|
||||
### Milestones
|
||||
|
||||
- **M0 — Skeleton.** launchd service, tailscale serve auth, SQLite schema, OpenAPI spec, projects, tasks, worktree create/delete, event log, SSE stream with resume, embedded SPA shell, basic server observability.
|
||||
- **Status: done**, and running as a service on the Mac Studio. Observability is exposed through `GET /api/admin/status` only; the SPA has no admin view yet.
|
||||
- **Status: done**, and running as a service on the Mac Studio. Observability covers queue state and vendor rate-limit status, through `GET /api/admin/status` and the SPA's Admin view; process liveness and stuck-process detection, adapter parse errors, and per-task snapshot counts and disk usage are not done.
|
||||
- **M1 — One agent end to end (Claude).** Sessions, streaming, messaging, the interface coverage for Claude, the `ask_human` MCP tool, turn summaries, inbox with all views and response types, web push notifications, restart-and-resume, concurrency caps.
|
||||
- **Done:** sessions, streaming, messaging, interrupt; `ask_human` with validation; each turn's final message filed to the inbox as its summary; inbox with global, project, and task views and every response type, answered as one batch and delivered to the agent as one message; restart-and-resume; global and per-vendor caps with queueing; rate limits as their own inbox state; the web UI (two panes, task switcher, transcript, inbox) with Markdown rendering; the session instructions from "Session configuration", given as an appended system prompt and a per-turn reminder; per-turn commits by byre; bouncing questions asked in plain prose.
|
||||
- **Not done:** web push notifications. Compaction is done, as a handoff into a fresh session; model switching is done, per message; image input is done, pasted or dropped into the composer and stored as blobs.
|
||||
|
||||
+27
-12
@@ -282,27 +282,38 @@ func (m *Manager) LiveCount() int {
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
Running int
|
||||
Queued int
|
||||
Running []api.TurnRef
|
||||
Queued []api.TurnRef
|
||||
Caps api.Caps
|
||||
RateLimits map[string]map[string]any
|
||||
}
|
||||
|
||||
func (m *Manager) Usage(ctx context.Context) (Usage, error) {
|
||||
u := Usage{RateLimits: map[string]map[string]any{}}
|
||||
u := Usage{
|
||||
Running: []api.TurnRef{},
|
||||
Queued: []api.TurnRef{},
|
||||
Caps: api.Caps{Global: m.limits.Global, PerVendor: map[string]int{}},
|
||||
RateLimits: map[string]map[string]any{},
|
||||
}
|
||||
for vendor, n := range m.limits.PerVendor {
|
||||
u.Caps.PerVendor[string(vendor)] = n
|
||||
}
|
||||
|
||||
counts, err := m.store.CountRunning(ctx)
|
||||
running, err := m.store.SessionsInState(ctx, api.AgentSessionStateRunning)
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
for _, n := range counts {
|
||||
u.Running += n
|
||||
for _, sess := range running {
|
||||
u.Running = append(u.Running, api.TurnRef{SessionId: sess.Id, TaskId: sess.TaskId, Vendor: string(sess.Vendor)})
|
||||
}
|
||||
|
||||
queued, err := m.store.QueuedTurns(ctx)
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
u.Queued = len(queued)
|
||||
for _, q := range queued {
|
||||
u.Queued = append(u.Queued, api.TurnRef{SessionId: q.SessionID, TaskId: q.TaskID, Vendor: string(q.Vendor)})
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -325,11 +336,6 @@ func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task
|
||||
return m.start(ctx, sess, task, actor, text, images, api.AgentSessionStateIdle)
|
||||
}
|
||||
|
||||
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateQueued, actor)
|
||||
if err != nil {
|
||||
return api.AgentSession{}, err
|
||||
}
|
||||
|
||||
err = m.store.EnqueueTurn(ctx, store.QueuedTurn{
|
||||
SessionID: sess.Id,
|
||||
Vendor: sess.Vendor,
|
||||
@@ -342,6 +348,15 @@ func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task
|
||||
return api.AgentSession{}, err
|
||||
}
|
||||
|
||||
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateQueued, actor)
|
||||
if err != nil {
|
||||
derr := m.store.DeleteQueuedTurn(ctx, sess.Id)
|
||||
if derr != nil {
|
||||
slog.Error("remove queued turn", "session", sess.Id, "err", derr)
|
||||
}
|
||||
return api.AgentSession{}, err
|
||||
}
|
||||
|
||||
return m.store.GetSession(ctx, sess.Id)
|
||||
}
|
||||
|
||||
|
||||
@@ -753,7 +753,7 @@ func TestConcurrencyQueue(t *testing.T) {
|
||||
}
|
||||
|
||||
usage, err := m.Usage(e.ctx)
|
||||
if err != nil || usage.Running != 1 || usage.Queued != 1 {
|
||||
if err != nil || len(usage.Running) != 1 || len(usage.Queued) != 1 || usage.Queued[0].TaskId != second.TaskId {
|
||||
t.Fatalf("usage %+v %v", usage, err)
|
||||
}
|
||||
|
||||
|
||||
+98
-75
@@ -312,6 +312,15 @@ type Blob struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
// Caps defines model for Caps.
|
||||
type Caps struct {
|
||||
// Global Most turns running at once across all vendors.
|
||||
Global int `json:"global"`
|
||||
|
||||
// PerVendor Most turns running at once per vendor, keyed by vendor.
|
||||
PerVendor map[string]int `json:"per_vendor"`
|
||||
}
|
||||
|
||||
// Error defines model for Error.
|
||||
type Error struct {
|
||||
Message string `json:"message"`
|
||||
@@ -458,17 +467,22 @@ type SessionCreate struct {
|
||||
// Status defines model for Status.
|
||||
type Status struct {
|
||||
ActiveTasks int `json:"active_tasks"`
|
||||
Caps Caps `json:"caps"`
|
||||
DbBytes int64 `json:"db_bytes"`
|
||||
LastEventSeq int64 `json:"last_event_seq"`
|
||||
|
||||
// LiveAgents Agent processes currently running.
|
||||
LiveAgents int `json:"live_agents"`
|
||||
Projects int `json:"projects"`
|
||||
QueuedTurns int `json:"queued_turns"`
|
||||
|
||||
// QueuedTurns Turns waiting for room under the caps, in the order they will start.
|
||||
QueuedTurns []TurnRef `json:"queued_turns"`
|
||||
|
||||
// RateLimits The latest rate-limit status each vendor reported, keyed by vendor.
|
||||
RateLimits map[string]map[string]interface{} `json:"rate_limits"`
|
||||
RunningTurns int `json:"running_turns"`
|
||||
|
||||
// RunningTurns Turns running now, oldest session first.
|
||||
RunningTurns []TurnRef `json:"running_turns"`
|
||||
SseClients int `json:"sse_clients"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
}
|
||||
@@ -510,6 +524,13 @@ type TaskCreate struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// TurnRef defines model for TurnRef.
|
||||
type TurnRef struct {
|
||||
SessionId string `json:"session_id"`
|
||||
TaskId string `json:"task_id"`
|
||||
Vendor string `json:"vendor"`
|
||||
}
|
||||
|
||||
// Unmerged defines model for Unmerged.
|
||||
type Unmerged struct {
|
||||
// Commits Commits on the task branch that aren't on the default branch.
|
||||
@@ -1662,79 +1683,81 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
// const string: with thousands of chunks the chained `+` fold is several
|
||||
// times slower for the Go compiler than parsing a slice literal.
|
||||
var swaggerSpec = []string{
|
||||
"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",
|
||||
"//8=",
|
||||
"7Fzdb9u4lv9XCO0CaQHFydw7d4H1PHXaziAX09lu02Ie7g4MWjq2OJFIlaTieoP874tzSOrDomwnE6e9",
|
||||
"uPsURx/k4eHvfB/qLslUVSsJ0ppkfpdoMLWSBuift1orjT8yJS1Iiz95XZci41YoefGHURKvmayAiuOv",
|
||||
"f9ewSubJv110o164u+bCjXZ/f58mOZhMixoHSeZumlmCN/yzONSrNUh7DcYIN0mtVQ3aCkdapoFbyBec",
|
||||
"iFopXeGvJOcWzq2oIEkTu60hmSfGaiHXyX3avrPc4juj2yKPXq5UDiXeGRL9AT43YCzkjB74gUFV2y2r",
|
||||
"gEvDbAHsFmSu9JlhOax4U9pZjCStSsCxQTZVMv9HIqq6hAqkBZ38HnneWG5hTMznBhqkhCbnzDZaMmHY",
|
||||
"hgsr5JqtlGacrTQAM6WyrJE5aCIyUzJrtAaZbVnGa4NEtrTkJbLRjZ2kiW6kRDJSgkwJFvIxjWny5RwH",
|
||||
"OL/lWvIKN+sfg628xhVcuaFH1/87zDW686GdfHTrdUfNfZpYbm4WE1vptqTP76zkTQ5RVruHF8bNFB8S",
|
||||
"dxA+N0JD7jiWdAS00wUIxYYMO+qRMMBo2gd5R6Fa/gGZRQpfWcuzovKCuSMf3GMjwgaQ+aIUEno3hbSw",
|
||||
"Bk1iUPF1BGJ0ec5ybjn79OGXKJpvhMz73F0JWlMuVqskDIz/8rXmlWcVskDy2hTKJmnS6DK6GRXoirst",
|
||||
"GJLlB5uzd+4JZlSjM4iSV3NbjEdAIudMQ63ONZTciltg+GRcXlG9jWlYreZsLSzTcCtwa5nmch0nIix2",
|
||||
"CqPGcm33bM8+fCP3DoKUNikGpx9LtYwAyan+hXv8blqrnloTx2RtQNzDhOc1r814tetSLXlE279TxpJa",
|
||||
"NcyrQcYtUzIDxjOtjGG8LL3KN719721cDXrRKSCe5wLH5uX7AQHj944mpAbtKUjZDWwhZ8utv9CjKHBg",
|
||||
"h51+4QMyY2xrXYIh3yowxquN/dsWHoyOfRtVZTyzbsoRTjiagv0a+nH4DIosokK2peL9ex35tVb4c1Kw",
|
||||
"4fOAACHtf3wfRcq0iO8wE4f0xKaeTR2JByXgJ1HC6wIVVYTnHp4TmFwKyXVfdpdKlcClw2sJe15VZb6I",
|
||||
"K2IN6DDkc3JM8BG2hJXSQP+7m3vVetRZakzfIPE8J/eiUrlYCfpJ5Dr/xs3vp1hkxJqIi7O7CzR/O1va",
|
||||
"412fGS3TprbiDVrJ0UbU3GbFBBSjy44T54aJTX0ll+rLlYUqAoJjxGtZquwGf0fB8Bjhm5ho17kg9xuR",
|
||||
"gwuulUb3IdAKpKXSRHMLi1JUIuqrDuV55FHT6HNGNuaLTVm4lLJNsV3QuiFPmaJXTMo0ZKqqQOaQL9zF",
|
||||
"lPHWRTMz5sicMxyOvUBUr4TkJfM6kalV8N1twS2jkZhEnpXl9uWMDVY3Z/RnxvprnLcGoLvqZmqjEbxx",
|
||||
"7m44xL5M/Uj/I5M0ptWE0sJu+6z3a0/SZO2dV7gVsEnSZLUVcT7v140ajCpvH4iT9qUJL2IQygoLlTkU",
|
||||
"oZIsfPCvkSZ2Y3Kt+dY5b0r9L+SLRlpRHk9pG7UdnJwCmkNuni008Pz4iKTH/H54MhLv/sitWWn3vyfq",
|
||||
"fSMT4peepPdZP6lyHJvzsdb5k9v2muigmEHIK/f6d7s7ucOlo8k1MVPZifjRFPcitwjKHqM0cyjFLein",
|
||||
"UbW4hinwqem40knHQ4UDtWEc5j7iOHrrP+ILGAYZ0EeKRlipf8lPmg429aAjFcPfM8DkuF0amjV3fc6U",
|
||||
"JHODliGYtTMTTNlE3Drc3OG47u6cbQqQNCoSxzTwugbuIqKHoWGHbFuAdo4hGU8D0jKr6AKpsSjJjwTQ",
|
||||
"Lk5aiNB4BwHw0c8ajKXfitQtIgmcpESIqYQxUYPZswaDsQBH4tJsUNa7Mei3aWrQBvIJX+edczOmAErp",
|
||||
"GTPm/Y+lWjKRG7bSqmJNjXofr6XjXWDcMDcM46WSayNyaDdt5sXN7BH2DtwTWdd3eJnymbYQxjlLXOas",
|
||||
"5BY0otrM2G+IQWFZLlYr0J5upMKbujPjUrZp/yIzG2GzAkw/6AiOmTC01tnxgH3Ht2wJPidMQhH4ooGt",
|
||||
"xS3IyGA7wKORY3B77+z5syTFffJ6sdRcTgQiEyoIg6kJv6xSdupWrRbHBTYkkTRH/7URwe10D8sOeRZP",
|
||||
"ycqetR29gBHtMTp8nvtryGytVVU/VmpjAUBV2yPiVfdclBdtOD/KD4lbWKBjO5FzyHyub58hoHwgAn65",
|
||||
"WG6tG/mIXE3JjV3ArfOkj03woKu2IO5H9o7qG8j9DIwBw1x9xpbbkPCbSC86xE5wwBVxFpQ4HE/5kfKJ",
|
||||
"/WqRVoibrkzEa5My4Uy70v7ylm1EWTLKWA9wso/PONcHWMXQ08Ws5ogUaQeMncUUQObA2HGsy4BnhY+F",
|
||||
"fTiOIfzhbGlb/drPwpCSlWqTMlXmSEQwMCuhzZOwyRhYZKUI6BlvNm3Ig8zAbl6xG6AnECOw90CXDqVw",
|
||||
"SOQQ8Lus3AGnl9YhGmLq4CM3N2NlsOQGFr5KM85VTRuxrFSmZdlwa38LPi2ujW24YRXoNeRMaZYLk3Gd",
|
||||
"Q368h/uMlWskc5GpqhKRZblFOKfafG64KZh7lCm3XG9JmePaD31nBj1ZIFZIZQtEvFWOK/EM7YHM+DhP",
|
||||
"S2BK/BK8n+sYfWTJGdHhDMarMFZ36V0Ytbv0phsfRU7YMm7gN0rfWA3wEC9lmH+hoVss7o6YdgjupZQf",
|
||||
"4rrgmlxa38TFI+KtIj/O8V4LgVWHeEcpudljWET3eyXKB6RvenWIiLZD1kTUrYY2gA0MJE9mxeAW9NaB",
|
||||
"EoMk1PxrJ5/7t4pY4+cLS5jk74Q/NoWbXc+eHosO7tX+aOQDWfjjuh4OVZP6mcDdLoYYtZ+kl89I1bgK",
|
||||
"Zny4ca/djaBj+gCjpDfXIM8mVFDc8TlQb2qxOKQDQWfclC5O7MJEWhMLcBgOmCaNdGuzkDNXIjJMyKxs",
|
||||
"yAjMZVOWqJGlQWZMUtUbZUzbe24LwzbCFiw+2wD2Ma7s7GzYjcCNAX3DIlWfsOiW+9zacLtzYeqSbxeT",
|
||||
"cdFa2AVUXJSTdydfLdVayMPodY+lQ1J6Q/dpmFrYpzqPivWjqd8tsh9HzD0BaKV6GiVZbjWQMGrXD5dc",
|
||||
"zi5nly7VB5LXIpknf51dzv7qin0F0X3Ba3HB80rIi87GroGcAVwfdfFd5ck8+RnsdTA2gwT8Xy4vn6wB",
|
||||
"0M8Q6QC8Bn0LmhXAS+ssTWM4OhP39DCtY1mqpbNoykTcmWurNMawPtNCUUxjAKWFh0TOjGFwsFT5lgnX",
|
||||
"pKf5xj3/A0OthBuBt3KwkKHMtUqBXGCirGqMZUtg73/9OWV/f//255T9fPUTOoO/wfL9jL2yrFLGsr+x",
|
||||
"d+JHV1Qb8vpTG44nDh5g7I8q3+7wmci6WIvVkMGtw+jLyTGz5l79o4b1Y9+t5aNf3cCyfui7AzmxuoH7",
|
||||
"EQy/ezIYEucjIERsGERRzhBrMxSu7x38Y8O19IXG1jT5/ru/Hv30DrAv7vDPVX6/T0RbzOwV0H8x4Iw3",
|
||||
"ETl5ZpzIps6OonB7FpGQ+839/gHbhXpV8wosaIww7hK0SqG/wun/xO1hsovmtLem3RX8HmBAcXVfQ8dU",
|
||||
"5Dnl79yjM/aWZ4X758wwkaPmwoUa+PyDa9T0Wo4aq9A9/vv1f/06c/8axlcWNPuFG3tOV86v3rAXyiV8",
|
||||
"3L3PDegta5f9krLXGtC+opC4RDnG9oGiiLa7thp45aZM4jwsgOfkW3kuDkhK0hggpnJr96kfk0jvhqT1",
|
||||
"PHCo3w9KmoUv1u3buaFlDuF7EKxuX9yrA1sn5FJ9mYQC1YaowmZSVoh1AcayUKd3eSb2ou1PWXMLKXMd",
|
||||
"GilbbcVLKnzIkJ9yeSn2kyhxV5jkWquNS/RJ8BCwinHmw9gzw6idHF33M8Nw2Ni+/yKMJUInNn1ngwYx",
|
||||
"8jQLJ7a3i1Ye/GroXzjOdvSbNKZG9NHAwjeLxIhqO6SOANl+Q3d8gwT1eI0C7DEo33GbUfRMQzv1aQsQ",
|
||||
"mrWEztgbF5gZRAY6oClrpF+ve28M54vBOuNO3CsqbBpXntasQv3iyBCSLppmWQkKU2csPKwpqTsoaOCj",
|
||||
"bVeVzHAQUo55CeTDSV//Rm0ZulraQrgr0jUyVFlj6PbdKwHg017c492UQZfMUS7S10YO2t6GQqgOBA92",
|
||||
"oR5gk/Hp/3yEw+XgeIckHnC4uuWfMCzq8TjOU+qlaN2YTgpP48M4tjzOh3FB8BQ338Ep2UiZiQkOZrws",
|
||||
"UcQbA3TQDBnRRIh0CQBP59OLdC/HcAKBfixzgsh65jxMYjup6hcgowhAj+B9VzA6vfIKfRJHqK5XZRn8",
|
||||
"GxMA4i3UcBEu8xtGPg1Mhs0Hzxwdt0yLg0XCJjDqCbByced/HdDDfX6fSEIOrHu45qfWuS0XHqd2Y+y8",
|
||||
"aPshJoXxo6/Vnl4SqVZ7pAfRhRm0ghQh18Up3xT/0wk31kmuaSscZ6ZfRGvrVW1ysVvyuOARUz8f3bnJ",
|
||||
"U+ieXnXrmRWPw8i01kFGntih7BSUr4WZizv/64CCCufUT6igBufhp9KX7vaJpKRlxeO0VIynRBX3vYyn",
|
||||
"ImBKTF+ZGyejFBd23aEujHSH5gsuc7VasRdrxcuU5ZDRAV+TUkcR4BWeM5C5SSkQ7qLIlFGx7aVvSKVS",
|
||||
"I/Wruof7vafCsPZIPakITogPt30x1PDKF0ypDEGdsNSp0wXpLbncZQJdLihUPyJx7GvH/kn4/uVZ4Ruo",
|
||||
"JzZtuGG+EYkpykU2kM+eK0aMglVIC1o39deB61uZO7j6jkBi09hEXAUiv5FNbZlGO9qeU/m6O+lFwnyV",
|
||||
"jbx2QuuE3GHdya8ww0plUACh5Ih6arzh1yBz39t/IqdgeHLgKL/geTFGTCw6hfGNJp7Iob24wz8H/InW",
|
||||
"wzuRM7HP2eo5Wk/tQbiVP859GDLvIuta3KJFkrddG5iQfU+8db9ftJ0uqW8Soyw2/XI9CNJq7ooouF7I",
|
||||
"qD16thZWrKXS8BLNLGc4WEoGnGvIO1tcdV11e9rppnz/FI0ejWWEzMD1XbqTI6QbwjLODFMbyYTM4Qs6",
|
||||
"ElJZZlWTFfHEtcdWaBAcbdqOhxT6Aa0KC/SJSGksOj5+Yd1SU2YaXB0yhhJKIQ0fypQFcGrGi9ZicKXJ",
|
||||
"AZ16UpkIfImIxnvQ5+jRkeP3raW2n1n4cv+tgqjkUU5R0vcVqLcOQaIkkDvMlmA3AJJxarBzUtDGwusB",
|
||||
"OGdT4H3jvid0RFXRN/Edv+Cp+qJrCv3T4/gN+dPjtB/R+Fqy0n6wYiqJw21W/KvLCHWUH+dhPmbeKffy",
|
||||
"A1Tqdph96lqkZR4MEOpx1VjS3UKunVXxz2vIlHZG0HV2MA0VF5Fow/fNf01/pT0a8DjP5WmcuUPtOz3m",
|
||||
"Qr/9hjMDn9OdXoz3aDCXW1ZzY+hwBR1porNEn9mSZzdkX/H9WbTvArm1t+PmCbpjJjQTHdgZDFXxL6Jq",
|
||||
"qmT+3eXlJX2Vwf/7qJabJ0hGu89MHZGN/lSj3+POj/lNexGctL9dXr789p1k8sueXwP5ALdNWrmEmjus",
|
||||
"7U9PnQsZTYy137tJ2XKrw8mkCW0mpFXkWgwPLk271Ctu7PlK6Q3XPpfSPZsVkN2gQrSKCet7pHRPl8aU",
|
||||
"KGXiuL7p6POHw2bsV38yShhWNwbdcfYBVo3BEKFAV6j7Tmc4NtiluSKeOx3SievZ5w223fmIgzm6bzv4",
|
||||
"vggofH7paNPOHs5eOlRg7B4EO8TvhJIUnarylr4FKLcsU3JVisy6dnUNa5CEI7lm/ifkLjc9Y9e94+Ce",
|
||||
"nH4ynIJQE9JVIR218R9YKOj8n4SnAPaV/H9YPwWsQ7Zzb+H3Ojz0HOZ2yPvjasADyQhL2vGSvh3jeyjP",
|
||||
"K4cLmcpHobQasoHC9r7AMKzlxHP+LjPbT/g/fRJ4+CmKZy4OHyPAvZLZP4esNr3jk9Gg4TdyjSJQKVSZ",
|
||||
"+9OLCI1Jc5ErMPLMRjMp7eHNU7adhTmmWs/8/XC2cfZPmwO4v/+/AAAA//8=",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+16
-16
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+15
-15
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -391,6 +391,7 @@ func (s *Server) GetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
LiveAgents: s.agents.LiveCount(),
|
||||
RunningTurns: usage.Running,
|
||||
QueuedTurns: usage.Queued,
|
||||
Caps: usage.Caps,
|
||||
RateLimits: usage.RateLimits,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
type QueuedTurn struct {
|
||||
SessionID string
|
||||
TaskID string
|
||||
Vendor api.AgentSessionVendor
|
||||
Actor string
|
||||
Text string
|
||||
@@ -59,7 +60,7 @@ func (s *Store) EnqueueTurn(ctx context.Context, q QueuedTurn) error {
|
||||
|
||||
func (s *Store) QueuedTurns(ctx context.Context) ([]QueuedTurn, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT q.session_id, s.vendor, q.actor, q.text, q.images, q.created_at
|
||||
SELECT q.session_id, s.task_id, s.vendor, q.actor, q.text, q.images, q.created_at
|
||||
FROM turn_queue q JOIN agent_sessions s ON s.id = q.session_id
|
||||
ORDER BY q.created_at
|
||||
`)
|
||||
@@ -74,7 +75,7 @@ func (s *Store) QueuedTurns(ctx context.Context) ([]QueuedTurn, error) {
|
||||
vendor := ""
|
||||
images := ""
|
||||
createdAt := ""
|
||||
err = rows.Scan(&q.SessionID, &vendor, &q.Actor, &q.Text, &images, &createdAt)
|
||||
err = rows.Scan(&q.SessionID, &q.TaskID, &vendor, &q.Actor, &q.Text, &images, &createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Vendored
+144
@@ -0,0 +1,144 @@
|
||||
import { get } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { findTask, on, onEach, state } from "./state.js";
|
||||
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created"]);
|
||||
export function mountAdmin(root) {
|
||||
let status;
|
||||
let error = "";
|
||||
let inFlight = false;
|
||||
let again = false;
|
||||
let disposed = false;
|
||||
const render = () => {
|
||||
if (!status) {
|
||||
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
|
||||
return;
|
||||
}
|
||||
replace(root, h("div", { class: "admin-view" }, error && h("p", { class: "error-text" }, error), turnsSection(status), rateLimitsSection(status.rate_limits), serverSection(status)));
|
||||
};
|
||||
const refresh = async () => {
|
||||
if (inFlight) {
|
||||
again = true;
|
||||
return;
|
||||
}
|
||||
inFlight = true;
|
||||
try {
|
||||
const next = await get("/api/admin/status");
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
status = next;
|
||||
error = "";
|
||||
}
|
||||
catch (err) {
|
||||
error = `Couldn't load status: ${errorText(err)}`;
|
||||
}
|
||||
inFlight = false;
|
||||
render();
|
||||
if (again && !disposed) {
|
||||
again = false;
|
||||
void refresh();
|
||||
}
|
||||
};
|
||||
const onEvent = (detail) => {
|
||||
const ev = detail;
|
||||
if (refreshKinds.has(ev.kind) || (ev.kind === "agent.vendor" && ev.payload["type"] === "rate_limit_event")) {
|
||||
void refresh();
|
||||
return;
|
||||
}
|
||||
if (status && ev.seq > status.last_event_seq) {
|
||||
status.last_event_seq = ev.seq;
|
||||
render();
|
||||
}
|
||||
};
|
||||
const offs = [
|
||||
on("event", onEvent),
|
||||
on("connection", () => {
|
||||
if (state.connected) {
|
||||
void refresh();
|
||||
}
|
||||
}),
|
||||
onEach(["projects", "tasks"], render),
|
||||
];
|
||||
render();
|
||||
void refresh();
|
||||
return () => {
|
||||
disposed = true;
|
||||
for (const off of offs) {
|
||||
off();
|
||||
}
|
||||
};
|
||||
}
|
||||
function turnsSection(s) {
|
||||
const vendors = [...new Set([...Object.keys(s.caps.per_vendor), ...s.running_turns.map((t) => t.vendor), ...s.queued_turns.map((t) => t.vendor)])].sort();
|
||||
const runningFor = (vendor) => s.running_turns.filter((t) => t.vendor === vendor).length;
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Turns"), h("div", { class: "caps" }, capTile("All vendors", s.running_turns.length, s.caps.global), ...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])), h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length))), h("h3", {}, "Running"), turnList(s.running_turns, "running", "No turns running."), h("h3", {}, "Queued"), turnList(s.queued_turns, "queued", "Nothing queued."));
|
||||
}
|
||||
function capTile(label, running, cap) {
|
||||
const full = cap !== undefined && running >= cap;
|
||||
return h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, label), h("div", { class: full ? "cap-value warn" : "cap-value" }, running, h("span", { class: "cap-max" }, cap === undefined ? " / no cap" : ` / ${cap}`)));
|
||||
}
|
||||
function turnList(turns, kind, empty) {
|
||||
return h("ul", { class: "task-list" }, ...turns.map((t, i) => {
|
||||
const task = findTask(t.task_id);
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
return h("li", {}, h("a", { href: `#/task/${t.task_id}` }, kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }), h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id), h("span", { class: "task-state" }, t.vendor)));
|
||||
}), turns.length === 0 && h("li", { class: "empty-row" }, empty));
|
||||
}
|
||||
function rateLimitsSection(limits) {
|
||||
const vendors = Object.keys(limits).sort();
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Rate limits"), ...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})), vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."));
|
||||
}
|
||||
function rateLimitCard(vendor, info) {
|
||||
const { status, rateLimitType, resetsAt, unifiedWindows, ...rest } = info;
|
||||
const statusText = String(status ?? "unknown");
|
||||
const tone = statusText === "allowed" ? "ok" : statusText === "rejected" ? "danger" : "warn";
|
||||
const windows = (unifiedWindows ?? {});
|
||||
return h("div", { class: "card rate-limit" }, h("header", {}, h("span", { class: "vendor" }, vendor), h("span", { class: `rl-status ${tone}` }, statusText), rateLimitType !== undefined && h("span", {}, String(rateLimitType)), typeof resetsAt === "number" && h("span", { class: "resets" }, "resets ", resetText(resetsAt))), Object.keys(windows).length > 0 &&
|
||||
h("table", { class: "rl-windows" }, h("tr", {}, h("th", {}, "Window"), h("th", {}, "Used"), h("th", {}, "Resets")), ...Object.entries(windows).map(([name, w]) => h("tr", {}, h("td", {}, name), h("td", { class: utilizationClass(w.utilization) }, w.utilization === undefined ? "—" : `${Math.round(w.utilization * 100)}%`), h("td", {}, w.resetsAt === undefined ? "—" : resetText(w.resetsAt))))), Object.keys(rest).length > 0 &&
|
||||
h("dl", { class: "kv" }, ...Object.entries(rest).flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, typeof v === "object" ? JSON.stringify(v) : String(v))])));
|
||||
}
|
||||
function utilizationClass(u) {
|
||||
if (u === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (u >= 0.9) {
|
||||
return "danger";
|
||||
}
|
||||
if (u >= 0.7) {
|
||||
return "warn";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function resetText(unixSeconds) {
|
||||
const at = new Date(unixSeconds * 1000);
|
||||
const time = at.toLocaleString(undefined, { weekday: "short", hour: "numeric", minute: "2-digit" });
|
||||
const minutes = Math.round((at.getTime() - Date.now()) / 60000);
|
||||
if (minutes <= 0) {
|
||||
return `${time} (passed)`;
|
||||
}
|
||||
const rel = minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
|
||||
return `${time} (in ${rel})`;
|
||||
}
|
||||
function serverSection(s) {
|
||||
const rows = [
|
||||
["Started", `${new Date(s.started_at).toLocaleString()} (${ago(s.started_at)})`],
|
||||
["Agent processes", String(s.live_agents)],
|
||||
["Projects", String(s.projects)],
|
||||
["Active tasks", String(s.active_tasks)],
|
||||
["Live clients", String(s.sse_clients)],
|
||||
["Database", bytes(s.db_bytes)],
|
||||
["Last event", `#${s.last_event_seq}`],
|
||||
];
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
|
||||
}
|
||||
function bytes(n) {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let i = 0;
|
||||
let v = n;
|
||||
while (v >= 1024 && i < units.length - 1) {
|
||||
v /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
}
|
||||
//# sourceMappingURL=admin.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+1
-1
@@ -43,7 +43,7 @@ async function main() {
|
||||
const left = new Pane("left", "transcript");
|
||||
const right = new Pane("right", "inbox");
|
||||
replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el));
|
||||
mountNavbar(navbar, () => right.show("inbox"));
|
||||
mountNavbar(navbar, (view) => right.show(view));
|
||||
identityBanner(banner);
|
||||
const status = await get("/api/admin/status");
|
||||
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEzG,SAAS,cAAc,CAAC,IAAiB;IACvC,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;QACpB,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;YACzC,OAAO,CAAC,IAAI,CAAC,CAAC;YACd,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,CAAC,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7F,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACtF,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,qCAAqC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QAC9I,IAAI,CAAC,QAAQ,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE;YAC1B,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,CAAC;gBACH,KAAK,CAAC,EAAE,GAAG,MAAM,GAAG,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC5F,MAAM,EAAE,CAAC;YACX,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACtB,CAAC,CAAC;IACF,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACjB,MAAM,EAAE,CAAC;AACX,CAAC;AAED,SAAS,KAAK;IACZ,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,MAAM,KAAK,KAAK,CAAC,cAAc,EAAE,CAAC;QACpC,KAAK,UAAU,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3D,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAExF,WAAW,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAChD,cAAc,CAAC,MAAM,CAAC,CAAC;IAEvB,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC9C,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IAE3D,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,WAAW,EAAE,CAAC,SAAS,EAAE,EAAE;QAC1D,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC;QAC5B,MAAM,CAAC,YAAY,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,gBAAgB,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACtC,KAAK,EAAE,CAAC;AACV,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,yBAAyB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAClG,CAAC,CAAC,CAAC"}
|
||||
Vendored
+5
-3
@@ -1,10 +1,12 @@
|
||||
import { h, replace } from "./dom.js";
|
||||
import { activeTasks, findTask, onEach, state } from "./state.js";
|
||||
export function mountNavbar(root, showInbox) {
|
||||
export function mountNavbar(root, show) {
|
||||
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
|
||||
crumb.onclick = openSwitcher;
|
||||
const inbox = h("button", { class: "inbox-count" });
|
||||
inbox.onclick = showInbox;
|
||||
inbox.onclick = () => show("inbox");
|
||||
const admin = h("button", { class: "nav-link" }, "Admin");
|
||||
admin.onclick = () => show("admin");
|
||||
const conn = h("span", { class: "connection down", title: "connecting" });
|
||||
const me = h("span", { class: "me" });
|
||||
const render = () => {
|
||||
@@ -19,7 +21,7 @@ export function mountNavbar(root, showInbox) {
|
||||
replace(me, state.me?.display_name ?? "");
|
||||
};
|
||||
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
|
||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, conn, me);
|
||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, conn, me);
|
||||
render();
|
||||
addEventListener("keydown", (e) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-1
@@ -1,3 +1,4 @@
|
||||
import { mountAdmin } from "./admin.js";
|
||||
import { mountChanges } from "./changes.js";
|
||||
import { h, replace } from "./dom.js";
|
||||
import { mountInbox } from "./inbox.js";
|
||||
@@ -8,8 +9,9 @@ const views = {
|
||||
changes: { label: "Changes", mount: (root) => mountChanges(root) },
|
||||
tasks: { label: "Tasks", mount: mountTasks },
|
||||
inbox: { label: "Inbox", mount: mountInbox },
|
||||
admin: { label: "Admin", mount: mountAdmin },
|
||||
};
|
||||
const order = ["transcript", "changes", "tasks", "inbox"];
|
||||
const order = ["transcript", "changes", "tasks", "inbox", "admin"];
|
||||
export class Pane {
|
||||
key;
|
||||
el;
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+1
@@ -148,5 +148,6 @@ export function handleEvent(ev) {
|
||||
void loadInbox();
|
||||
break;
|
||||
}
|
||||
notify("event", ev);
|
||||
}
|
||||
//# sourceMappingURL=state.js.map
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,242 @@
|
||||
import { get, type ByreEvent, type Status, type TurnRef } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { findTask, on, onEach, state } from "./state.js";
|
||||
|
||||
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created"]);
|
||||
|
||||
export function mountAdmin(root: HTMLElement): () => void {
|
||||
let status: Status | undefined;
|
||||
let error = "";
|
||||
let inFlight = false;
|
||||
let again = false;
|
||||
let disposed = false;
|
||||
|
||||
const render = () => {
|
||||
if (!status) {
|
||||
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
|
||||
return;
|
||||
}
|
||||
replace(
|
||||
root,
|
||||
h(
|
||||
"div",
|
||||
{ class: "admin-view" },
|
||||
error && h("p", { class: "error-text" }, error),
|
||||
turnsSection(status),
|
||||
rateLimitsSection(status.rate_limits),
|
||||
serverSection(status),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
if (inFlight) {
|
||||
again = true;
|
||||
return;
|
||||
}
|
||||
inFlight = true;
|
||||
try {
|
||||
const next = await get("/api/admin/status");
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
status = next;
|
||||
error = "";
|
||||
} catch (err) {
|
||||
error = `Couldn't load status: ${errorText(err)}`;
|
||||
}
|
||||
inFlight = false;
|
||||
render();
|
||||
if (again && !disposed) {
|
||||
again = false;
|
||||
void refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const onEvent = (detail?: unknown) => {
|
||||
const ev = detail as ByreEvent;
|
||||
if (refreshKinds.has(ev.kind) || (ev.kind === "agent.vendor" && ev.payload["type"] === "rate_limit_event")) {
|
||||
void refresh();
|
||||
return;
|
||||
}
|
||||
if (status && ev.seq > status.last_event_seq) {
|
||||
status.last_event_seq = ev.seq;
|
||||
render();
|
||||
}
|
||||
};
|
||||
|
||||
const offs = [
|
||||
on("event", onEvent),
|
||||
on("connection", () => {
|
||||
if (state.connected) {
|
||||
void refresh();
|
||||
}
|
||||
}),
|
||||
onEach(["projects", "tasks"], render),
|
||||
];
|
||||
|
||||
render();
|
||||
void refresh();
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
for (const off of offs) {
|
||||
off();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function turnsSection(s: Status): HTMLElement {
|
||||
const vendors = [...new Set([...Object.keys(s.caps.per_vendor), ...s.running_turns.map((t) => t.vendor), ...s.queued_turns.map((t) => t.vendor)])].sort();
|
||||
const runningFor = (vendor: string) => s.running_turns.filter((t) => t.vendor === vendor).length;
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ class: "admin-section" },
|
||||
h("h2", {}, "Turns"),
|
||||
h(
|
||||
"div",
|
||||
{ class: "caps" },
|
||||
capTile("All vendors", s.running_turns.length, s.caps.global),
|
||||
...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])),
|
||||
h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length)),
|
||||
),
|
||||
h("h3", {}, "Running"),
|
||||
turnList(s.running_turns, "running", "No turns running."),
|
||||
h("h3", {}, "Queued"),
|
||||
turnList(s.queued_turns, "queued", "Nothing queued."),
|
||||
);
|
||||
}
|
||||
|
||||
function capTile(label: string, running: number, cap: number | undefined): HTMLElement {
|
||||
const full = cap !== undefined && running >= cap;
|
||||
return h(
|
||||
"div",
|
||||
{ class: "cap-tile" },
|
||||
h("div", { class: "cap-label" }, label),
|
||||
h("div", { class: full ? "cap-value warn" : "cap-value" }, running, h("span", { class: "cap-max" }, cap === undefined ? " / no cap" : ` / ${cap}`)),
|
||||
);
|
||||
}
|
||||
|
||||
function turnList(turns: TurnRef[], kind: "running" | "queued", empty: string): HTMLElement {
|
||||
return h(
|
||||
"ul",
|
||||
{ class: "task-list" },
|
||||
...turns.map((t, i) => {
|
||||
const task = findTask(t.task_id);
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
return h(
|
||||
"li",
|
||||
{},
|
||||
h(
|
||||
"a",
|
||||
{ href: `#/task/${t.task_id}` },
|
||||
kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }),
|
||||
h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id),
|
||||
h("span", { class: "task-state" }, t.vendor),
|
||||
),
|
||||
);
|
||||
}),
|
||||
turns.length === 0 && h("li", { class: "empty-row" }, empty),
|
||||
);
|
||||
}
|
||||
|
||||
function rateLimitsSection(limits: Status["rate_limits"]): HTMLElement {
|
||||
const vendors = Object.keys(limits).sort();
|
||||
return h(
|
||||
"section",
|
||||
{ class: "admin-section" },
|
||||
h("h2", {}, "Rate limits"),
|
||||
...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})),
|
||||
vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."),
|
||||
);
|
||||
}
|
||||
|
||||
function rateLimitCard(vendor: string, info: Record<string, unknown>): HTMLElement {
|
||||
const { status, rateLimitType, resetsAt, unifiedWindows, ...rest } = info;
|
||||
const statusText = String(status ?? "unknown");
|
||||
const tone = statusText === "allowed" ? "ok" : statusText === "rejected" ? "danger" : "warn";
|
||||
const windows = (unifiedWindows ?? {}) as Record<string, { utilization?: number; resetsAt?: number }>;
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ class: "card rate-limit" },
|
||||
h(
|
||||
"header",
|
||||
{},
|
||||
h("span", { class: "vendor" }, vendor),
|
||||
h("span", { class: `rl-status ${tone}` }, statusText),
|
||||
rateLimitType !== undefined && h("span", {}, String(rateLimitType)),
|
||||
typeof resetsAt === "number" && h("span", { class: "resets" }, "resets ", resetText(resetsAt)),
|
||||
),
|
||||
Object.keys(windows).length > 0 &&
|
||||
h(
|
||||
"table",
|
||||
{ class: "rl-windows" },
|
||||
h("tr", {}, h("th", {}, "Window"), h("th", {}, "Used"), h("th", {}, "Resets")),
|
||||
...Object.entries(windows).map(([name, w]) =>
|
||||
h(
|
||||
"tr",
|
||||
{},
|
||||
h("td", {}, name),
|
||||
h("td", { class: utilizationClass(w.utilization) }, w.utilization === undefined ? "—" : `${Math.round(w.utilization * 100)}%`),
|
||||
h("td", {}, w.resetsAt === undefined ? "—" : resetText(w.resetsAt)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Object.keys(rest).length > 0 &&
|
||||
h(
|
||||
"dl",
|
||||
{ class: "kv" },
|
||||
...Object.entries(rest).flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, typeof v === "object" ? JSON.stringify(v) : String(v))]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function utilizationClass(u: number | undefined): string {
|
||||
if (u === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (u >= 0.9) {
|
||||
return "danger";
|
||||
}
|
||||
if (u >= 0.7) {
|
||||
return "warn";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function resetText(unixSeconds: number): string {
|
||||
const at = new Date(unixSeconds * 1000);
|
||||
const time = at.toLocaleString(undefined, { weekday: "short", hour: "numeric", minute: "2-digit" });
|
||||
const minutes = Math.round((at.getTime() - Date.now()) / 60000);
|
||||
if (minutes <= 0) {
|
||||
return `${time} (passed)`;
|
||||
}
|
||||
const rel = minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
|
||||
return `${time} (in ${rel})`;
|
||||
}
|
||||
|
||||
function serverSection(s: Status): HTMLElement {
|
||||
const rows: [string, string][] = [
|
||||
["Started", `${new Date(s.started_at).toLocaleString()} (${ago(s.started_at)})`],
|
||||
["Agent processes", String(s.live_agents)],
|
||||
["Projects", String(s.projects)],
|
||||
["Active tasks", String(s.active_tasks)],
|
||||
["Live clients", String(s.sse_clients)],
|
||||
["Database", bytes(s.db_bytes)],
|
||||
["Last event", `#${s.last_event_seq}`],
|
||||
];
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
|
||||
}
|
||||
|
||||
function bytes(n: number): string {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let i = 0;
|
||||
let v = n;
|
||||
while (v >= 1024 && i < units.length - 1) {
|
||||
v /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
}
|
||||
+18
-2
@@ -655,8 +655,11 @@ export interface components {
|
||||
Status: {
|
||||
/** @description Agent processes currently running. */
|
||||
live_agents: number;
|
||||
running_turns: number;
|
||||
queued_turns: number;
|
||||
/** @description Turns running now, oldest session first. */
|
||||
running_turns: components["schemas"]["TurnRef"][];
|
||||
/** @description Turns waiting for room under the caps, in the order they will start. */
|
||||
queued_turns: components["schemas"]["TurnRef"][];
|
||||
caps: components["schemas"]["Caps"];
|
||||
/** @description The latest rate-limit status each vendor reported, keyed by vendor. */
|
||||
rate_limits: {
|
||||
[key: string]: Record<string, never>;
|
||||
@@ -671,6 +674,19 @@ export interface components {
|
||||
active_tasks: number;
|
||||
sse_clients: number;
|
||||
};
|
||||
Caps: {
|
||||
/** @description Most turns running at once across all vendors. */
|
||||
global: number;
|
||||
/** @description Most turns running at once per vendor, keyed by vendor. */
|
||||
per_vendor: {
|
||||
[key: string]: number;
|
||||
};
|
||||
};
|
||||
TurnRef: {
|
||||
session_id: string;
|
||||
task_id: string;
|
||||
vendor: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Error. */
|
||||
|
||||
@@ -12,6 +12,7 @@ export type InboxResponseCreate = Schemas["InboxResponseCreate"];
|
||||
export type Attachment = Schemas["Attachment"];
|
||||
export type ByreEvent = Schemas["Event"];
|
||||
export type Status = Schemas["Status"];
|
||||
export type TurnRef = Schemas["TurnRef"];
|
||||
|
||||
type Method = "get" | "post" | "put";
|
||||
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ async function main(): Promise<void> {
|
||||
const right = new Pane("right", "inbox");
|
||||
replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el));
|
||||
|
||||
mountNavbar(navbar, () => right.show("inbox"));
|
||||
mountNavbar(navbar, (view) => right.show(view));
|
||||
identityBanner(banner);
|
||||
|
||||
const status = await get("/api/admin/status");
|
||||
|
||||
+7
-3
@@ -1,12 +1,16 @@
|
||||
import { h, replace } from "./dom.js";
|
||||
import type { View } from "./panes.js";
|
||||
import { activeTasks, findTask, onEach, state } from "./state.js";
|
||||
|
||||
export function mountNavbar(root: HTMLElement, showInbox: () => void): void {
|
||||
export function mountNavbar(root: HTMLElement, show: (view: View) => void): void {
|
||||
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
|
||||
crumb.onclick = openSwitcher;
|
||||
|
||||
const inbox = h("button", { class: "inbox-count" });
|
||||
inbox.onclick = showInbox;
|
||||
inbox.onclick = () => show("inbox");
|
||||
|
||||
const admin = h("button", { class: "nav-link" }, "Admin");
|
||||
admin.onclick = () => show("admin");
|
||||
|
||||
const conn = h("span", { class: "connection down", title: "connecting" });
|
||||
const me = h("span", { class: "me" });
|
||||
@@ -26,7 +30,7 @@ export function mountNavbar(root: HTMLElement, showInbox: () => void): void {
|
||||
};
|
||||
|
||||
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
|
||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, conn, me);
|
||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, conn, me);
|
||||
render();
|
||||
|
||||
addEventListener("keydown", (e) => {
|
||||
|
||||
+4
-2
@@ -1,19 +1,21 @@
|
||||
import { mountAdmin } from "./admin.js";
|
||||
import { mountChanges } from "./changes.js";
|
||||
import { h, replace } from "./dom.js";
|
||||
import { mountInbox } from "./inbox.js";
|
||||
import { mountTranscript } from "./task.js";
|
||||
import { mountTasks } from "./tasks.js";
|
||||
|
||||
export type View = "transcript" | "changes" | "tasks" | "inbox";
|
||||
export type View = "transcript" | "changes" | "tasks" | "inbox" | "admin";
|
||||
|
||||
const views: Record<View, { label: string; mount: (root: HTMLElement) => () => void }> = {
|
||||
transcript: { label: "Transcript", mount: mountTranscript },
|
||||
changes: { label: "Changes", mount: (root) => mountChanges(root) },
|
||||
tasks: { label: "Tasks", mount: mountTasks },
|
||||
inbox: { label: "Inbox", mount: mountInbox },
|
||||
admin: { label: "Admin", mount: mountAdmin },
|
||||
};
|
||||
|
||||
const order: View[] = ["transcript", "changes", "tasks", "inbox"];
|
||||
const order: View[] = ["transcript", "changes", "tasks", "inbox", "admin"];
|
||||
|
||||
export class Pane {
|
||||
readonly el: HTMLElement;
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
import { get, type AgentSession, type ByreEvent, type InboxItem, type Project, type Task, type User } from "./api.js";
|
||||
|
||||
export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection";
|
||||
export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection" | "event";
|
||||
|
||||
export const state = {
|
||||
me: undefined as User | undefined,
|
||||
@@ -170,4 +170,6 @@ export function handleEvent(ev: ByreEvent): void {
|
||||
void loadInbox();
|
||||
break;
|
||||
}
|
||||
|
||||
notify("event", ev);
|
||||
}
|
||||
|
||||
+130
@@ -1247,3 +1247,133 @@ pre.diff .meta {
|
||||
padding: 24px;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.admin-view {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 8px 16px 16px;
|
||||
}
|
||||
|
||||
.admin-section h2 {
|
||||
font-size: 13px;
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.admin-section h3 {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
|
||||
.caps {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cap-tile {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
padding: 6px 12px;
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.cap-label {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.cap-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cap-value.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.cap-max {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.queue-pos {
|
||||
width: 3ch;
|
||||
color: var(--muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rate-limit + .rate-limit {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.rate-limit .vendor {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rate-limit .resets {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.rl-status {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rl-status.ok,
|
||||
.rl-windows .ok {
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.rl-status.warn,
|
||||
.rl-windows .warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.rl-status.danger,
|
||||
.rl-windows .danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.rl-windows {
|
||||
border-collapse: collapse;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rl-windows th,
|
||||
.rl-windows td {
|
||||
text-align: left;
|
||||
padding: 2px 12px 2px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.rl-windows th {
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.kv {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 2px 16px;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
.kv dt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.kv dd {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user