Release screenshot 03-hiring-home-overview.png predates the Average Days to Offer tile - #76
Conversation
The Hiring Overview shot and its caption predated the fourth tile #55 adds to `ats_employer_hiring`. Re-taken as `admin@quillstone.example` on a fresh memory-driver boot at 818 rows, same recipe the set documents: headless Chromium, 1440 x 900 viewport capture, cli 17.3.0. The tile renders 30.0 (the dataset's `format: '0.0'`). Cross-checked against the analytics query the widget compiles to, with the filter under `selection.runtimeFilter` -- the only key the service reads; `body.filter`, `selection.filter`, `filters` and `where` are each accepted with HTTP 200 and silently dropped, and each answers 30.333... over every Quillstone application instead of the nine hired ones. Compiled SQL for the correct call: `SELECT AVG(days_to_offer) ... WHERE stage = $1 AND (employer_org IN ($2))`. The other four caption numbers were re-measured the same way rather than carried over, and none moved: 3 open jobs, 18 awaiting action, 6 interviews this week, pipeline 10 / 8 / 5 / 2 / 1 (+1 rejected). Two consequences recorded in the caption rather than hidden. The new tile opens a second tile row, so Pipeline by Stage now starts below the 900 px fold -- the same shape shot `01` has always had, so the pipeline values are now cited as dataset-query measurements the way `02` cites the funnel's. And on the demo seed the 30.0 is the hired applications' age, not elapsed decision time (#65); the dashboard source says so and the caption now does too. Whole-set pass: all seven re-shot against this boot and compared to the committed files. 01, 02, 04 and 07 are identical; 03 is this change. Neither ripple reaches the set -- `last_activity_at` (#65) is rendered only by the seeker `mine` timeline, which no shot frames, and Find Jobs is `viewName: 'published'`, an authored ListView with an explicit column list, so #33's `highlightFields` cannot move it (verified live: Job Title, Employer, City, Work Mode, Salary min/max, Employment Type, 22 records, unchanged). The only `filters` slices are the two platform review queues and My Profile, none of them photographed. Docs only: no `src/` file is touched, and validate / lint / typecheck are byte-identical to their output on the untouched base. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
Review — I opened the image. The number in the caption is the number on screen.For a screenshot card that is the whole acceptance, so I did not take it on report: I extracted both PNGs from git and looked at them. The new shot (1440 × 900, same as the old one) renders four KPI tiles — Open Jobs 3, Applications Awaiting Action 18, Interviews This Week 6, and Average Days to Offer 30.0 in the second row — above a partially visible Pipeline by Stage chart showing its title, description, y-axis and the tops of two bars. The caption's five numbers are exactly those. The The crop, which is the judgement call in this PRThe fourth tile opens a second row and pushes the bars past the fold. You kept the 1440 × 900 viewport capture and moved the pipeline numbers into the caption as dataset-query measurements, citing I agree, and the caption is honest in the way that matters — it says the bars are cropped and says where the numbers come from, rather than quietly asserting values the reader cannot see. Becoming the set's only full-page capture to avoid a crop the set already contains twice would have been the worse trade. You also carried the #65 caveat into the caption — that on the demo seed 30.0 is the hired applications' age, not elapsed decision time, because a seed cannot set The trap, exercised rather than avoided
The two ripples
All seven shots re-taken against a live boot and compared to the committed files, with only One observation, filed not fixedThe dashboard now has an awkward 3 + 1 tile layout — one tile alone on the second row with three empty slots — and that is precisely what pushes the chart below the fold. Four #75 is a good catch and correctly not fixed — shots Merging. Generated by Claude Code |
Fixes #66.
docs/screenshots/03-hiring-home-overview.pngand its caption predated the fourth tile #55 adds toats_employer_hiring. #55 has since merged (PR #68), so the shot was stale in fact, not prospectively.Docs only — no file under
src/is touched.Old and new, side by side
e759ba5)Both files live at the same path; the left link pins the base commit so the replaced image is still reachable.
The visible difference. A fourth KPI card, Average Days to Offer — 30.0 · Avg Days to Offer, in green
(
colorVariant: 'success'), on a new second tile row atlayout: { x: 0, y: 2 }. The dashboard's ownsubtitle gains a clause: "…interviews this week, average days to offer and the pipeline by stage."
Because the tile opens that second row, Pipeline by Stage moves from
y: 2toy: 4and its bar chartnow begins below the 900 px fold — in the old shot the whole chart fitted; in the new one the header and the
top of the plot area show. The three original tiles, the sidebar and the chrome are otherwise unchanged.
That crop is the set's existing shape, not a regression introduced here:
01-platform-home-overview.pnghasalways had two tile rows with its "Applications per Week" chart cut off at exactly the same place. Matching
01is why the shot stays a 1440 × 900 viewport capture instead of becoming a full-page one — all sevenfiles are 1440 × 900, and a taller
03would be the only shot in the set that is not.How the shot was taken
The recipe
docs/screenshots/README.mddocuments, unchanged:@objectstack/cli17.3.0, same as the set.ats_employer,ats_interview,ats_offer) return 0 rows to platform admins holdingviewAllRecords; the memory driver returns all of them #39 empties four objects for platform personas on sqlite).INFO [Seeder] Seed loading complete {"inserted":818,"updated":0,"skipped":0,"errored":0}.deviceScaleFactor: 1, viewport capture. Output is 1440 × 900.admin@quillstone.example, then/_console/apps/ats/dashboard/ats_employer_hiring.The set was shot at 809 rows; this one is at 818. The README's preamble already explained that gap (#53 added
one
acceptedats_offerper hired application) and now records that03is the one shot re-taken on thefar side of it.
The caption numbers, each re-measured
Every number was measured on this boot through
POST /api/v1/analytics/dataset/query— the same datasets thewidgets bind to — as
admin@quillstone.example, so the row-level scope in the compiled statement is the onethe dashboard actually applies. None was carried over from the old caption.
The new tile: 30.0
The filter key the analytics service reads is
selection.runtimeFilter, and only that one:The widget filter (
stage = $1) and the row-level read scope (employer_org IN ($2)) are both visible in thestatement, which is what makes this the right number rather than a plausible one.
The four wrong spellings were run against the same boot to confirm they do not fail loudly. Each returns
HTTP 200 and compiles a statement with no stage predicate at all:
selection.runtimeFilterstage = $1 AND (employer_org IN ($2))body.filter(employer_org IN ($1))selection.filter(employer_org IN ($1))selection.filters(employer_org IN ($1))selection.where(employer_org IN ($1))30.3 would have looked entirely reasonable in a caption. It is the average over all 27 of Quillstone's
applications that carry a
days_to_offer, not over the hired ones the tile is scoped to.The number in the caption is the number in the image. The tile renders
30.0, becauseats_application_metrics.avg_days_to_offerdeclaresformat: '0.0'; the query returns30. The caption says30.0, read off the rendered page (document.body.innerTextof the captured page containsAverage Days to Offer … 30.0 … Avg Days to Offer), and the query agrees with it.The four carried-over numbers — all still correct
SELECT COUNT(*) FROM "ats_job" WHERE status = $1 AND (employer_org IN ($2))SELECT COUNT(*) FROM "ats_application" WHERE stage IN ($1, $2) AND (employer_org IN ($3))SELECT COUNT(*) FROM "ats_interview" WHERE ((status IS NULL OR status != $1) AND scheduled_at >= $2 AND scheduled_at < $3) AND (application IN ($4 … $30))SELECT stage, COUNT(*) FROM "ats_application" WHERE (employer_org IN ($1)) GROUP BY stageNothing moved, which is the expected result: #55 added a measure, it changed no stage counts. The first three
are also legible in the image itself and match.
Whole-set pass
All seven shots were re-taken against this boot with the same recipe and compared to the committed files.
Method per shot, not a bare "checked":
01-platform-home-overview.pngadmin@platform.example. Renders 9 / 22 / 30 / 70, review queue 2 / 6, same weekly bars — identical to the committed image.02-platform-hiring-funnel.pngadmin@platform.example: 15 rejected · 0 withdrawn · 185 in pipeline, unchanged. The funnel's hover-only values re-measured through the dataset query (GROUP BY stage, noemployer_orgscope for platform staff): 88 / 46 / 28 / 14 / 9, and 88+46+28+14+9 = 185 confirms the third tile.03-hiring-home-overview.png04-hiring-pipeline-kanban.pngadmin@quillstone.example. 27 records; Applied 10 · Screening 8 · Interview 5 · Offer. Card face and everyapplied_atdate match the committed image exactly (9/5/2026, 8/30/2026, 8/29/2026, 8/25/2026, 8/23/2026 …) — an independent check that #65 moved no date this shot renders. Only difference is a cursor hover highlight on one card.05-hiring-interview-calendar.pngadmin@quillstone.example: 10 records, September 2026, R1 rounds on the 8th–12th and R2 on the 13th–17th, all inside the next two weeks — identical grid. Two chrome differences, neither caused by this card's merges: see the note below.06-seeker-home-find-jobs.pngcandidate01@mail.example. 22 records; columns Job Title · Employer · City · Work Mode · Salary (min) · Salary (max) · Employment Type — unchanged. Same chrome note.07-hiring-pipeline-kanban-zh-CN.pnglocale: 'zh-CN': 已投递 10 · 筛选中 8 · 面试中 5 · OFFER, 27 条记录, same translated nav and column labels — identical to the committed image.Does any other shot frame the Hiring Overview dashboard? No. Reading
src/apps/*.nav.ts, exactly one entrymounts
ats_employer_hiring(nav_hiring_overview), and03is the only shot on it. The other two dashboardshots are
ats_platform_overviewandats_hiring_funnel.Ripple 1 — #65,
ats_application.last_activity_atDoes not reach the set.
last_activity_atis rendered by exactly one surface: the seeker'sminetimeline view (
src/views/application.view.ts,columns: ['display_name', 'job', 'stage', 'applied_at', 'last_activity_at']). A repo-wide grep finds no other view, kanban card list, calendar field or datasetdimension naming it. No shot frames My Applications —
06is Find Jobs, and the seeker persona appears inthat one shot only.
Checked rather than assumed on the two shots that could plausibly have carried a date: the kanban's card
fields are
['display_name', 'job', 'rating', 'applied_at']and the calendar anchors onats_interview.scheduled_at. Both re-shot and both byte-comparable to the committed images,applied_atvalues included. #74's diff touches only
src/hooks/stamp.hook.ts, and only thelast_activity_atassignment inside it —
applied_atanddays_to_offerare untouched.Ripple 2 — #33,
highlightFieldsonats_employer/ats_job/ats_candidateDoes not reach the set.
highlightFieldsdrives surfaces with no authored column list. The derivedsurfaces in this app are the three
filtersslices —nav_platform_employers_pending,nav_platform_jobs_pending(bothsrc/apps/platform.nav.ts) andnav_seeker_my_profile(
src/apps/seeker.nav.ts) — plus record headers and lookup previews. None of the seven shots is any ofthem.
01's sidebar lists "Employers Pending" and "Jobs Pending" as nav labels, but photographs neitherqueue's contents.
Find Jobs specifically, which was the one to check hardest. It is an authored view, not a
filtersslice:
nav_seeker_find_jobsis{ type: 'object', objectName: 'ats_job', viewName: 'published' }, andJobViews.listViews.publishedcarries an explicitcolumns: seekerColumnslist. Soats_job's newhighlightFields(title, employer, status, city, employment_type, work_mode) cannot reach it — and doesnot: the live re-shot renders Job Title · Employer · City · Work Mode · Salary (min) · Salary (max) ·
Employment Type, exactly
seekerColumnsand exactly the committed image. Had the slice path been in play,statuswould have appeared and both salary columns would have gone.One thing the pass turned up — filed, not fixed here
05and06show a breadcrumb leaf one word shorter than a boot today renders:… / Calendarvs… / Interview Calendar, and… / Publishedvs… / Published Jobs. Cause established withgit log -S:both
_viewslabel keys landed insrc/translations/en.tsin commit0ad34d7(#62) — the same commit thatadded the screenshots — so those two shots came from a boot whose bundle did not yet carry them and the
breadcrumb fell back to the title-cased view name. It predates every merge this card was checked against:
none of #68, #69, #71 or #74 touches a view, a nav or either label. Reproduced under both direct-URL and
sidebar-click navigation, so it is not a navigation-path artifact. Nothing either caption asserts is affected.
Filed as #75 rather than ridden along here.
The committed
05,06and07also carry aConnectedrealtime pill that a fresh capture does notreliably reproduce (it depends on whether the socket has connected when the shutter fires) — a reason to batch
any re-shoot of those, recorded in #75.
Gates
Run in the worktree, exit code captured before any pipe (
cmd > file 2>&1; EXIT=$?):pnpm lintincludespnpm check:i18n-source; its verdict line reads✓ every bundle key restates the label its metadata declaresat 512/512 keys.pnpm validateends on thesame three pre-existing approver-staffing warnings as on
mainand exits 0.All three were also run on the untouched base (
e759ba5) before the first edit, and all three werealready green there.
pnpm validate's output is byte-identical before and after — which is the expectedresult for a docs-only change and the check the card asked for: had one moved, that would have been the
finding.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PbJ5Cy9KDAzeQHo8bsMadG
Generated by Claude Code