Skip to content

Commit e5ee467

Browse files
authored
fix(overview): make selecting queue + slot fly-in visible again (#448)
* fix(overview): make selecting queue + slot fly-in visible again Root causes: 2.5s /api/live poll missed the 500ms selecting dwell, and live queue jobs (request id) never matched slot jobs (live-${account}), so queue→slot fly never fired. - Keep account_id on dwell SelectingRequests; expose on LiveJob - Bump display dwell to 1.6s (Acquire still immediate) - Poll /api/live at 400ms while InflightLiveCard is expanded - Fly wait-rail departures to bind account (or single newly busy slot) * fix(overview): per-slot model + elapsed, GSAP slide into cell Queue→slot fly uses GSAP MotionPath arc into data-cell-id (滑过去槽里). /api/live occupancy from BoundRequests maps each in-flight request to a concurrency cell with RequestedModel and Bind started_at — SlotBar shows model + seconds per cell, not one account timer. * fix(overview): per-cell Done/Failed fly-out + worker rail FLIP Finish one concurrency cell at a time (request_id completions), keep sibling cells occupied, and GSAP FLIP the worker rail on busy↔available reorder. Changelog + mockInflight tests; sync embedded webui. * fix(overview): harden per-cell exit flies + worker yield FLIP Drive Done/Failed exits from each concurrency cell (stagger same-tick multi-cell finishes) and give the worker rail a drag-up / 避让上顶 FLIP when busy rises and idle drops. Changelog ZH+EN; synced static. * fix(overview): land-gate slot chrome + wait-rail vacancy yield Keep concurrency-cell model/elapsed hidden until the queue→slot fly lands (pendingInbound during dwell; React land-gate survives clock re-renders). When a selecting pill leaves, remaining wait pills FLIP up interrupt-safely so another mid-FLIP selecting can still fly out. * fix(frontend): sync embedded static for inflight-queue-visible CI frontend job fails when npm run sync drifts internal/webui/static. Also polish unreleased notes into a concise numbered bilingual list. --------- Co-authored-by: caigee-cmd <210803857+caigee-cmd@users.noreply.github.com>
1 parent 7f7db1f commit e5ee467

20 files changed

Lines changed: 1505 additions & 552 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
### English
2+
3+
- 1. Overview InflightLiveCard selecting (选号) wait rail is visible again: expanded `/api/live` polls at 400ms, 1.6s display dwell (Acquire still immediate), and queue→slot fly-in uses GSAP MotionPath into the bound concurrency cell; cell model/elapsed stays hidden until the fly lands (or a short grace if skipped).
4+
- 2. `/api/live` adds `occupancy` from BoundRequests: each in-flight request maps to its own concurrency cell (model + Bind elapsed). Finishing cells fly to Done/Failed from their own rect (`recent_completions.request_id`), staggered when several finish together — not one shared account animation.
5+
- 3. Worker rail and wait rail use GSAP FLIP for yield/reorder: busy rises with a drag-up feel; remaining wait pills slide up into vacated gaps without fighting the next queue→slot fly.
6+
7+
### 中文
8+
9+
- 1. Overview「实时进行中」选号/排队重新可见:展开后 `/api/live` 400ms 轮询、选号展示约 1.6s(Acquire 不拖延);排队→槽位用 GSAP 弧线滑入 Bind 并发格,模型/计秒等飞入落地后再显示(飞不起来则短 grace)。
10+
- 2. `/api/live` 增加 `occupancy`(BoundRequests):每个在途请求占一个并发格(各格模型与 Bind 秒数);结束时各格从自身飞向完成/失败(`recent_completions.request_id`),同帧多格会错开——不是整账号共用一个飞出动画。
11+
- 3. Worker 轨与等待轨用 GSAP FLIP 避让上顶:busy 上浮、空闲下沉;选号 pill 飞走后剩余排队上滑补位,不和下一次飞入打架。
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
### English
22

3-
- Overview Account pool collapses cooling accounts into a summary chip (`N cooling`); details expand on click with a quiet secondary wash. Ready workers stay primary in the list.
3+
- 4. Overview Account pool collapses cooling accounts into a summary chip (`N cooling`); details expand on click with a quiet secondary wash. Ready workers stay primary in the list.
44

55
### 中文
66

7-
- Overview「账号池」将冷却账号收成摘要 chip(「N 冷却」),点击后以安静次级底展开明细;列表优先展示就绪 worker。
7+
- 4. Overview「账号池」将冷却账号收成摘要 chip(「N 冷却」),点击后以安静次级底展开明细;列表优先展示就绪 worker。

‎frontend/scripts/mockInflight.test.ts‎

Lines changed: 208 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getMockInflightSnapshot,
66
liveHasSignal,
77
MOCK_INFLIGHT,
8+
tickMockInflight,
89
} from '../src/lib/mockInflight.ts'
910

1011
function run(name: string, fn: () => void) {
@@ -307,8 +308,6 @@ run('applyLiveProjection maps max_inflight / in_flight to slot cells', () => {
307308
assert.equal(idle?.busy, false)
308309
})
309310

310-
console.log('all mockInflight tests passed')
311-
312311
run('applyLiveProjection maps queue onto queued MockJobStatus', () => {
313312
const started = new Date(Date.now() - 5_000).toISOString()
314313
const snap = applyLiveProjection(getMockInflightSnapshot(), {
@@ -341,6 +340,35 @@ run('applyLiveProjection maps queue onto queued MockJobStatus', () => {
341340
assert.equal(queued[1].id, 'req-2')
342341
})
343342

343+
run('applyLiveProjection keeps queue accountId for slot handoff', () => {
344+
const snap = applyLiveProjection(getMockInflightSnapshot(), {
345+
in_flight: 1,
346+
rpm: 1,
347+
workers: [
348+
{
349+
id: 'acc-x',
350+
name: 'wb-x',
351+
provider: 'workbuddy',
352+
busy: true,
353+
progress: 0.5,
354+
rate_limited: false,
355+
},
356+
],
357+
rate_limits: [],
358+
sticky: { bindings: 0, hits: 0, misses: 0, capacity: 64, ttl_seconds: 600 },
359+
queue: [
360+
{ id: 'req-dwell', name: 'gpt · dwell', status: 'started', account_id: 'acc-x' },
361+
],
362+
})
363+
const queued = snap.jobs.filter((j) => j.status === 'queued')
364+
assert.equal(queued.length, 1)
365+
assert.equal(queued[0].id, 'req-dwell')
366+
assert.equal(queued[0].accountId, 'acc-x')
367+
const running = snap.jobs.filter((j) => j.status === 'running')
368+
assert.equal(running.length, 1)
369+
assert.equal(running[0].name, 'gpt · dwell')
370+
})
371+
344372
run('liveHasSignal true when only queue present', () => {
345373
assert.equal(
346374
liveHasSignal({
@@ -354,3 +382,181 @@ run('liveHasSignal true when only queue present', () => {
354382
true,
355383
)
356384
})
385+
386+
run('applyLiveProjection occupancy maps per-cell model and started_at', () => {
387+
const t0 = new Date(Date.now() - 8_000).toISOString()
388+
const t1 = new Date(Date.now() - 3_000).toISOString()
389+
const snap = applyLiveProjection(getMockInflightSnapshot(), {
390+
in_flight: 2,
391+
rpm: 1,
392+
workers: [
393+
{
394+
id: 'acc-m',
395+
name: 'wb-m',
396+
provider: 'workbuddy',
397+
busy: true,
398+
progress: 0.5,
399+
rate_limited: false,
400+
max_inflight: 4,
401+
in_flight: 2,
402+
},
403+
],
404+
rate_limits: [],
405+
sticky: { bindings: 0, hits: 0, misses: 0, capacity: 64, ttl_seconds: 600 },
406+
occupancy: [
407+
{ id: 'req-a', name: 'claude-sonnet', status: 'started', started_at: t0, account_id: 'acc-m' },
408+
{ id: 'req-b', name: 'gpt-4.1', status: 'started', started_at: t1, account_id: 'acc-m' },
409+
],
410+
})
411+
const w = snap.workers.find((x) => x.id === 'acc-m')
412+
assert.ok(w)
413+
assert.equal(w.cells?.length, 2)
414+
assert.equal(w.cells?.[0].id, 'req-a')
415+
assert.equal(w.cells?.[0].name, 'claude-sonnet')
416+
assert.equal(w.cells?.[1].id, 'req-b')
417+
assert.equal(w.cells?.[1].name, 'gpt-4.1')
418+
assert.ok((w.cells?.[0].elapsedMs ?? 0) >= 7_000)
419+
assert.ok((w.cells?.[1].elapsedMs ?? 0) >= 2_000)
420+
assert.ok((w.cells?.[0].elapsedMs ?? 0) > (w.cells?.[1].elapsedMs ?? 0))
421+
const running = snap.jobs.filter((j) => j.status === 'running')
422+
assert.equal(running.length, 2)
423+
assert.equal(running[0].id, 'req-a')
424+
assert.equal(running[1].name, 'gpt-4.1')
425+
})
426+
427+
run('applyLiveProjection occupancy dwell stays on queue not double-running', () => {
428+
const snap = applyLiveProjection(getMockInflightSnapshot(), {
429+
in_flight: 1,
430+
rpm: 1,
431+
workers: [
432+
{
433+
id: 'acc-d',
434+
name: 'wb-d',
435+
provider: 'workbuddy',
436+
busy: true,
437+
progress: 0.4,
438+
rate_limited: false,
439+
max_inflight: 2,
440+
in_flight: 1,
441+
},
442+
],
443+
rate_limits: [],
444+
sticky: { bindings: 0, hits: 0, misses: 0, capacity: 64, ttl_seconds: 600 },
445+
queue: [{ id: 'req-dwell', name: 'sonnet', status: 'started', account_id: 'acc-d' }],
446+
occupancy: [{ id: 'req-dwell', name: 'sonnet', status: 'started', account_id: 'acc-d' }],
447+
})
448+
const queued = snap.jobs.filter((j) => j.status === 'queued')
449+
const running = snap.jobs.filter((j) => j.status === 'running')
450+
assert.equal(queued.length, 1)
451+
assert.equal(queued[0].id, 'req-dwell')
452+
assert.equal(running.length, 0)
453+
const w = snap.workers.find((x) => x.id === 'acc-d')
454+
assert.equal(w?.cells?.length, 1)
455+
assert.equal(w?.cells?.[0].id, 'req-dwell')
456+
assert.equal(w?.cells?.[0].name, 'sonnet')
457+
// Dwell-bound cell is a fly target but chrome stays gated until land.
458+
assert.equal(w?.cells?.[0].pendingInbound, true)
459+
})
460+
461+
run('applyLiveProjection occupancy without queue is not pendingInbound', () => {
462+
const snap = applyLiveProjection(getMockInflightSnapshot(), {
463+
in_flight: 1,
464+
rpm: 1,
465+
workers: [
466+
{
467+
id: 'acc-p',
468+
name: 'wb-p',
469+
provider: 'workbuddy',
470+
busy: true,
471+
progress: 0.5,
472+
rate_limited: false,
473+
max_inflight: 2,
474+
in_flight: 1,
475+
},
476+
],
477+
rate_limits: [],
478+
sticky: { bindings: 0, hits: 0, misses: 0, capacity: 64, ttl_seconds: 600 },
479+
occupancy: [{ id: 'req-run', name: 'opus', status: 'started', account_id: 'acc-p' }],
480+
})
481+
const w = snap.workers.find((x) => x.id === 'acc-p')
482+
assert.equal(w?.cells?.[0].id, 'req-run')
483+
assert.equal(w?.cells?.[0].pendingInbound, false)
484+
assert.equal(snap.jobs.filter((j) => j.status === 'running').length, 1)
485+
})
486+
487+
run('applyLiveProjection request_id completion matches that cell', () => {
488+
const busy = applyLiveProjection(cloneMockInflight(MOCK_INFLIGHT), {
489+
in_flight: 2,
490+
rpm: 1,
491+
workers: [
492+
{
493+
id: 'acc-x',
494+
name: 'wb-x',
495+
provider: 'workbuddy',
496+
busy: true,
497+
progress: 0.5,
498+
rate_limited: false,
499+
max_inflight: 4,
500+
in_flight: 2,
501+
},
502+
],
503+
rate_limits: [],
504+
sticky: { bindings: 0, hits: 0, misses: 0, capacity: 64, ttl_seconds: 600 },
505+
occupancy: [
506+
{ id: 'req-keep', name: 'keep', status: 'started', account_id: 'acc-x' },
507+
{ id: 'req-gone', name: 'gone', status: 'started', account_id: 'acc-x' },
508+
],
509+
})
510+
assert.equal(busy.jobs.filter((j) => j.status === 'running').length, 2)
511+
const after = applyLiveProjection(busy, {
512+
in_flight: 1,
513+
rpm: 1,
514+
workers: [
515+
{
516+
id: 'acc-x',
517+
name: 'wb-x',
518+
provider: 'workbuddy',
519+
busy: true,
520+
progress: 0.5,
521+
rate_limited: false,
522+
max_inflight: 4,
523+
in_flight: 1,
524+
},
525+
],
526+
rate_limits: [],
527+
sticky: { bindings: 0, hits: 0, misses: 0, capacity: 64, ttl_seconds: 600 },
528+
occupancy: [{ id: 'req-keep', name: 'keep', status: 'started', account_id: 'acc-x' }],
529+
recent_completions: [{ request_id: 'req-gone', account_id: 'acc-x', status: 'error' }],
530+
})
531+
assert.equal(after.jobs.filter((j) => j.status === 'running').length, 1)
532+
assert.equal(after.jobs.find((j) => j.status === 'running')?.id, 'req-keep')
533+
assert.ok(after.completions?.some((c) => c.id === 'req-gone' && c.ok === false))
534+
assert.equal(after.workers.find((w) => w.id === 'acc-x')?.cells?.length, 1)
535+
})
536+
537+
run('tickMockInflight finishes one cell without clearing siblings', () => {
538+
const base = cloneMockInflight(MOCK_INFLIGHT)
539+
const w = base.workers.find((x) => x.id === 'acc-wb-a1')
540+
assert.ok(w && (w.cells?.length ?? 0) >= 2)
541+
const siblingIds = (w!.cells ?? []).map((c) => c.id).filter((id) => id !== 'j1')
542+
assert.ok(siblingIds.length >= 1)
543+
// Force first cell to completion threshold; leave siblings mid-flight.
544+
for (const j of base.jobs) {
545+
if (j.id === 'j1') j.progress = 0.999
546+
else if (j.accountId === 'acc-wb-a1') j.progress = 0.4
547+
}
548+
const next = tickMockInflight(base, 300)
549+
const w2 = next.workers.find((x) => x.id === 'acc-wb-a1')
550+
assert.ok(w2)
551+
// Finished cell is gone (may refill from queue, so length can stay the same).
552+
assert.ok(!(w2!.cells ?? []).some((c) => c.id === 'j1'))
553+
// At least one prior sibling still occupies a cell — account not wiped.
554+
assert.ok(siblingIds.some((id) => (w2!.cells ?? []).some((c) => c.id === id)))
555+
assert.ok((w2!.inFlight ?? 0) >= 1)
556+
// One completion (or retry) for j1 only — not a bulk clear of all cells.
557+
const terminal = next.completions?.filter((c) => c.id === 'j1') ?? []
558+
const retried = next.jobs.some((j) => j.id === 'j1' && j.status === 'queued')
559+
assert.ok(terminal.length === 1 || retried)
560+
})
561+
562+
console.log('all mockInflight tests passed')

‎frontend/src/api/live.ts‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,26 @@ export type LiveWorker = {
2121

2222
/** Terminal request finish drained this /api/live poll (Done vs Failed fly-out). */
2323
export type LiveCompletion = {
24+
/** Open request id — per-cell Done/Failed fly-out. */
25+
request_id?: string
2426
account_id: string
2527
/** ok | error | incomplete | canceled */
2628
status: string
2729
}
2830

2931
/**
30-
* Selecting (选号) request for the InflightLiveCard waiting rail.
31-
* status reuses request_logs RequestStatus* (typically started) — not a new machine.
32+
* Selecting (选号) or bound occupancy request for InflightLiveCard.
33+
* status reuses request_logs RequestStatus* — not a new machine.
3234
*/
3335
export type LiveJob = {
3436
id: string
3537
name: string
36-
/** RequestStatus* from recorder (started while selecting). */
38+
/** RequestStatus* from recorder (started while selecting / bound). */
3739
status: string
38-
/** RFC3339 Prepare/Start time. */
40+
/** RFC3339: queue = Prepare/Start; occupancy = Bind (slot entry). */
3941
started_at?: string
42+
/** Queue: dwell fly target. Occupancy: always set (cell owner). */
43+
account_id?: string
4044
}
4145

4246
/** Active cooldown window from AccountView.DownUntil / model_cooldowns. */
@@ -75,8 +79,10 @@ export type LiveSnapshot = {
7579
sticky: LiveSticky
7680
/** Terminal finishes since last poll — InflightLiveCard routes fly-outs. */
7781
recent_completions?: LiveCompletion[]
78-
/** Prepare→Acquire 选号 (RequestStatusStarted, no pool bind). */
82+
/** Prepare→Acquire 选号 (RequestStatusStarted / dwell). */
7983
queue?: LiveJob[]
84+
/** Bound open requests — one per concurrency cell (model + per-cell started_at). */
85+
occupancy?: LiveJob[]
8086
}
8187

8288
export function fetchLive() {

0 commit comments

Comments
 (0)