Skip to content

ats_employer_hiring has a 3 + 1 tile layout since the fourth KPI landed, which pushes Pipeline by Stage below the fold - #81

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-78-kpi-row
Sep 8, 2026
Merged

ats_employer_hiring has a 3 + 1 tile layout since the fourth KPI landed, which pushes Pipeline by Stage below the fold#81
os-sam merged 2 commits into
mainfrom
claude/issue-78-kpi-row

Conversation

@os-sam

@os-sam os-sam commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #78

ats_employer_hiring put four KPI tiles at w: 4, so three filled row 0 and the fourth opened row 2 alone, displacing Pipeline by Stage to y: 4 and past the 900 px fold. This puts all four KPIs on one row at w: 3 (x 0 / 3 / 6 / 9) and brings the bar chart back to y: 2, and shortens the two titles that the narrower tile makes tight — the dashboard header already says "applications awaiting action" and "average days to offer", so the tiles need not repeat it.

widget before after title before title after
open_jobs x0 y0 w4 h2 x0 y0 w3 h2 Open Jobs (unchanged)
awaiting_action x4 y0 w4 h2 x3 y0 w3 h2 Applications Awaiting Action Awaiting Action
interviews_this_week x8 y0 w4 h2 x6 y0 w3 h2 Interviews This Week (unchanged)
avg_days_to_offer x0 y2 w4 h2 x9 y0 w3 h2 Average Days to Offer Days to Offer
pipeline_by_stage x0 y4 w12 h5 x0 y2 w12 h5 Pipeline by Stage (unchanged)

src/translations/en.ts restates the two changed titles (pnpm check:i18n-source compares the bundle to the metadata labels and fails on drift). The zh-CN titles are left alone: they are 62–96 px wide in the same 222 px box, and 平均到 Offer 天数 is DESIGN.md §04's own wording — measured below.

Before / after, both at 1440 × 900

Before — main (c3c6526): three tiles, a lone fourth on row 2, the chart cropped at the fold.

before

After — this branch (37d3402), the committed re-take of docs/screenshots/03-hiring-home-overview.png: one KPI row, no wrapped title, every bar and both axes inside the fold.

after

Boot for the shot, the recipe docs/screenshots/README.md records, on port 4661:

OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai npx objectstack dev --fresh --database-driver memory -p 4661 --log-level info
INFO [Seeder] Seed loading complete {"inserted":818,"updated":0,"skipped":0,"errored":0}

Headless Chromium (Playwright 1.56.1), viewport 1440 × 900, deviceScaleFactor: 1, signed in through the Console as admin@quillstone.example.

No title wraps — measured, not eyeballed

Per title element: Range.getClientRects() (one rect per line box), the element's height against its computed line-height, and the rendered text width against the box it sits in.

tile title line boxes height / line-height text width title box
Open Jobs 1 24 / 24 px 77.2 px 222 px
Awaiting Action 1 24 / 24 px 117.9 px 222 px
Interviews This Week 1 24 / 24 px 161.5 px 222 px
Days to Offer 1 24 / 24 px 101.3 px 222 px
Pipeline by Stage 1 24 / 24 px 131.9 px 1086 px

All four KPI tiles report top: 210, left: 305 / 593 / 881 / 1169 — one row, 288 px pitch. The chart's SVG spans top: 522 … bottom: 872 and document.scrollHeight is 900, so nothing is below the fold and the page does not scroll. text-overflow is ellipsis on these titles, and scrollWidth === clientWidth on all five, so nothing is truncated either.

One nuance worth recording against the card's estimate: the tile's title box is 222 px, not ~280 px, and the retired "Applications Awaiting Action" measures 215.5 px in the same font — it would have fitted, with 6.5 px to spare (97 % fill), rather than wrapping. The shortened labels are still the right call at that margin (a different font stack, a longer locale or a slightly narrower sidebar eats 6 px), but the honest finding is "it was tight", not "it wrapped".

zh-CN pass at the same viewport (navigator.language = zh-CN): 在招岗位 62.4 px · 待处理投递 78.0 px · 本周面试 62.4 px · 平均到 Offer 天数 95.7 px, one line box each, same single row, chart equally inside the fold. That is why this PR does not shorten the Chinese titles.

The five numbers, re-measured

Re-measured on this boot, not copied. The analytics path is POST /api/v1/analytics/dataset/query with { datasetName, selection: { measures, dimensions, runtimeFilter } }, where runtimeFilter carries each widget's own filter read out of dist/objectstack.json. Independent column: GET /api/v1/data/OBJECT?$filter=...&$count=true, asserting hasMore === false and records.length === total on every read.

widget dataset query independent list count card
open_jobs [{"job_count":3}] total=3 records=3 hasMore=false 3 ✅
awaiting_action [{"application_count":18}] total=18 records=18 hasMore=false 18 ✅
interviews_this_week [{"interview_count":5}] total=5 records=5 hasMore=false 6 ⚠️ see below
avg_days_to_offer [{"avg_days_to_offer":30}] 1 hired row, days_to_offer=[30], avg 30.0000 30.0 ✅
pipeline_by_stage Applied 10 · Screening 8 · Interview 5 · Offer 2 · Hired 1 · Rejected 1 per-stage counts 10 / 8 / 5 / 2 / 1 / 1, withdrawn 0 10/8/5/2/1 (+1 rejected) ✅

The compiled statements, from the analytics NativeSQLStrategy (the memory driver logs the SQL as it declines to run it, then the query falls back to the driver's own aggregate):

SELECT COUNT(*) AS "job_count" FROM "ats_job" WHERE status = ? AND ("ats_job"."employer_org" IN (?))
SELECT COUNT(*) AS "application_count" FROM "ats_application" WHERE stage IN (?, ?) AND ("ats_application"."employer_org" IN (?))
SELECT COUNT(*) AS "interview_count" FROM "ats_interview" WHERE ((status IS NULL OR status != ?) AND scheduled_at >= ? AND scheduled_at < ?) AND ("ats_interview"."application" IN (?, ...))
SELECT AVG(days_to_offer) AS "avg_days_to_offer" FROM "ats_application" WHERE stage = ? AND ("ats_application"."employer_org" IN (?))
SELECT stage AS "stage", COUNT(*) AS "application_count" FROM "ats_application" WHERE ("ats_application"."employer_org" IN (?)) GROUP BY stage

The card's warning about the filter key reproduces exactly, and the compiled SQL is what shows it — same request, same 200, one missing WHERE clause:

selection.runtimeFilter {stage:'hired'} -> 200 [{"avg_days_to_offer":30}]
    SELECT AVG(days_to_offer) ... WHERE stage = ? AND ("ats_application"."employer_org" IN (?))
selection.filter        {stage:'hired'} -> 200 [{"avg_days_to_offer":30.333333333333332}]
selection.where         {stage:'hired'} -> 200 [{"avg_days_to_offer":30.333333333333332}]
no filter at all                        -> 200 [{"avg_days_to_offer":30.333333333333332}]
    SELECT AVG(days_to_offer) ... WHERE ("ats_application"."employer_org" IN (?))

⚠️ Interviews This Week reads 5, not 6 — and this PR did not move it

Both independent paths agree on 5, this PR touches no dataset, no widget filter and no seed row, and the widget's filter is byte-identical to main's. The cause is the calendar:

Quillstone's 10 rounds, this boot (2026-09-08, Tuesday):
  2026-09-09 Wed  in window      2026-09-14 Mon  OUT  (one day past the window)
  2026-09-10 Thu  in window      2026-09-15 Tue  OUT
  2026-09-11 Fri  in window      2026-09-16 Wed  OUT
  2026-09-12 Sat  in window      2026-09-17 Thu  OUT
  2026-09-13 Sun  in window      2026-09-18 Fri  OUT
count in Mon 09-07 .. Mon 09-14, not cancelled = 5
same rows shifted -1 day (i.e. a Monday boot)  = 6

The seed schedules rounds at boot + 1 … + 10 days; the tile counts a fixed Monday-to-Sunday week. A Monday boot — which is what the previous shot was taken on — puts six inside the window, every later weekday one fewer. The other four numbers are boot-day stable.

The 03 caption now says this instead of pinning a 6, and src/dashboards/employer-hiring.dashboard.ts says it beside the "this week" note. The wider problem (the #8 evidence table pins 3 · 18 · 6 too) is filed separately as #80, which this PR deliberately leaves alone: it lands in src/data/ or in documents this card does not own.

Gates

$ pnpm validate
  ✓ Validation passed (533ms)
  Data: 12 Objects  147 Fields
  UI: 1 Apps  10 Views  3 Dashboards  4 Actions
  Logic: 6 Flows · Security: 5 Positions  5 Permissions · Runtime: 3 plugins
  exit 0

$ pnpm lint            # objectstack lint --i18n-strict && pnpm check:i18n-source
  ✓ Build complete (541ms)
  ✓ 7 assertions — the comparator still reports drift, orphans and gaps
  bundle keys (src/translations/en.ts)    512
  source labels collected                 512   (487 os i18n extract + 25 view-nested)
  compared, key present on both sides     512
  ✓ every bundle key restates the label its metadata declares
  exit 0

$ pnpm typecheck       # tsc --noEmit
  (no output)
  exit 0

Every exit code was captured before any pipe (cmd > log 2>&1; EXIT=$?). A control-character scan over the three edited text files (grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]') returns no matches.

Scope

Touched: src/dashboards/employer-hiring.dashboard.ts (layout, two titles, header comment), src/translations/en.ts (the same two titles), docs/screenshots/03-hiring-home-overview.png + docs/screenshots/README.md (re-take and caption). No object, field, dataset, measure, hook, flow, security or seed row is touched, and neither of the other two dashboards is.


Generated by Claude Code

The fourth KPI (#55) landed at w: 4, so three tiles filled row 0 and it
opened row 2 alone, displacing Pipeline by Stage to y: 4 — past the
900 px fold at the screenshot set's 1440 x 900 viewport.

Four w: 3 tiles now share row 0 (x 0/3/6/9) and the bar chart returns to
y: 2. Two titles lose the words the dashboard header already supplies:
"Applications Awaiting Action" -> "Awaiting Action" and "Average Days to
Offer" -> "Days to Offer", so nothing wraps in a ~280 px tile. en.ts
restates both, as check:i18n-source requires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
…tion

The Hiring Overview shot now shows what the dashboard shows: four KPI
tiles on one row and the whole Pipeline by Stage bar chart inside the
900 px fold. Same recipe as the set (CLI 17.3.0, memory driver, --fresh,
1440 x 900, after "[Seeder] Seed loading complete {inserted:818}"),
-p 4661.

The caption loses "the bars are cropped here" and gains what the re-take
measured: interviews this week reads 5, not the 6 the previous shot
recorded. Neither number is wrong and neither moved with this change --
the seed schedules Quillstone's ten rounds at boot + 1 ... + 10 days
while the tile counts a fixed Monday-to-Sunday week, so the count is 6
only when the boot is a Monday. The README now says so; the dashboard's
own header comment says so too, beside the layout it documents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG

os-sam commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Review — 布局照做了,而且把我的理由证伪了

我看了新截图,四块瓦片在同一行,四个标题都没有换行(最长的 Interviews This Week 也是单行),Pipeline by Stage 连同两条轴完整地在折叠线以上。卡片要的效果拿到了。

我给的理由是错的,你测出来了

派发里我写:担心 280 px 放不下 Applications Awaiting Action,并且断言"标签太长才是问题"。你实测:标题框是 222 px,不是 280;而那个长标题在这套字体下是 215.5 px —— 它本来就放得下,还余 6.5 px。

所以缩短标签是对的(97% 填充率太满,没有余量),但我给的根据不成立。我当时是按主流看板的经验推的,没有量。你去量了,并且报告的是量出来的结果而不是我想听的结果 —— 这正是我在派发里要求的那种行为,谢谢。

zh-CN 标题不动也是对的:62–96 px 在同一个 222 px 框里,而且最长那个是 DESIGN §04 自己的措辞。

「本周面试 6 → 5」不是这个 diff 造成的,而且是一个真问题

你的因果说明我自己独立算了一遍,完全对上:

2026-09-07(周一)启动 → 本周窗口 09-07~09-13 → 落在窗口内 = 6
2026-09-08(周二)启动 → 本周窗口 09-07~09-13 → 落在窗口内 = 5

种子把 Quillstone 的十轮面试排在 boot + 1 … + 10 天,而瓦片数的是固定的周一至周日。所以原来那个 6 之所以是 6,只因为上一次拍摄那天恰好是周一。仓库里把它当成固定值记了两处,#80 记下了这件事 —— 这个发现比布局本身更值钱,因为它是一个"文档里写着、但读者复现不出来"的数字,而这正是这个仓库反复在清理的那一类。

你的 open question:选 A

保留周二那张(5)并在说明里写清楚它随启动日变化。理由和你给的一致:截图的职责是展示布局在折叠线以上装得下什么,它做到了;为了钉住 6 而要求在特定星期几启动,等于让仓库继续记录一个工作日读者复现不出来的数字。C 更不行 —— 用一个数据问题挡住一个布局修复。

顺带说明:这个数字也出现在我为维护者做的产品介绍材料里,我会一并改掉。这就是把它写进说明的价值。

合并。


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review September 8, 2026 10:02
@os-sam
os-sam merged commit 084c8b8 into main Sep 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ats_employer_hiring has a 3 + 1 tile layout since the fourth KPI landed, which pushes Pipeline by Stage below the fold

2 participants