Repository navigation
Restore the Service dashboard's dateRange and relax the #546 CI guard — both upstream preconditions have landed and the window is measurably correct on 17.0.0 GA #1157
Description
Activity
- addedupstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstreamBlocked on / caused by the ObjectStack platform — tracked upstream
on Aug 14, 2026 upstream:objectstackstripped — this is hotcrm work. Card stays open.Checked as part of the transfer sweep #1156 (part 2). This card carried
upstream:objectstack, but under the maintainer ruling that sweep runs on — quoted verbatim and untranslated —hotcrm 席位的原则是用平台的能力做元数据应用的开发,平台的需求应该转给平台
the label asks one question: where does the fix land? Here it lands entirely in this repo, so the label is wrong. Removed. ⛔ Not closed — this is live hotcrm work.
Verified rather than taken from the card
The card states both upstream preconditions have landed. Since this sweep has already falsified three inherited "already fixed" premises, I checked them at the source rather than trusting the sentence:
precondition state closed by objectstack#3912 — datetime filter comparands coerced to epoch-ms against ISO TEXT storage closed / completed 2026-07-29 merged PR objectstack#3953 (plus #3979) objectstack#3777 — bare-date $lteupper bound drops same-day recordsclosed / completed 2026-07-30 merged PR objectstack#4041 Both are the exact two named in PR #546's restore precondition comment, and both closed via merged PRs. The card's own GA measurements agree independently and are the stronger evidence: every filter shape compared against a SQLite ground truth computed in the same run (
$gteonly 37/37,$lteonly 1/1, both 37/37, bare-date$lte1/1), all seven dashboard widgets matching truth under the window with 0 of 7 going blank, and 21 of 21 preset-chain combinations matching.Why the remaining work is this repo's
Every item in the card's "What the work is" list touches hotcrm files only:
- uncomment the
dateRangeblock insrc/dashboards/service.dashboard.tsand rewrite the root-cause comment into a historical note; - relax — not delete — the
datetime-window guard intest/metadata-references.test.ts; - re-examine
daily_case_volume's{30_days_ago}floor and itsfilterBindings: { dateRange: false }opt-out, now that the floor is no longer inert; - browser-verify the picker;
- changeset.
Nothing on that list is blocked on the platform, and no platform card is waiting on it. The adjacent hazard the card names — objectstack#8690, an unparseable date comparand answering HTTP 200 with zero rows — is a hazard to know about while doing this work, not a precondition for it: the card records that the console path does the macro mapping correctly, so it only bites if this work hand-writes a bare preset name into a filter.
Label state after this edit
This card now carries no labels. I stripped the incorrect one and deliberately did not regrade
prio:*— grading is the PM's triage call, not mine, and inventing a priority here would be guessing. Flagging it so the card gets a grade rather than sitting ungraded by accident.Authority: #1156.
Generated by Claude Code
Generated by Claude Code
- uncomment the
- addedmetadataDeclarative metadata — schema, security posture, UI surfacesDeclarative metadata — schema, security posture, UI surfacespm:queueReady for the PM dispatch loopReady for the PM dispatch loopand removedupstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstreamBlocked on / caused by the ObjectStack platform — tracked upstream
on Aug 14, 2026 - addedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchand removedpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Aug 16, 2026 Claimed — PM dispatch, session
c94a8dab-47f1-52c9-936a-011792389e89. Branch:claude/issue-1157-service-dashboard-daterange. No earlier claim comment on this card.
Generated by Claude Code
{ "issue": 1157, "status": "done", "branch": "claude/issue-1157-service-dashboard-daterange", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1170", "premise_still_valid": true, "summary": "All five items done. (1) `dateRange: { field: 'created_date', defaultRange: 'last_90_days', allowCustomRange: true }` restored in src/dashboards/service.dashboard.ts, 35-line root-cause comment rewritten as a short historical note. (2) Guard relaxed by CONVERSION, not version-pinning: the datetime ban is removed and replaced by test/dashboard-date-range-window.test.ts, which executes the window against a real SQLite database and compares every widget to a ground truth computed in the same run. Note the guard was in test/action-references.test.ts, not metadata-references.test.ts (#814 split that file by family). (3) daily_case_volume KEEPS filterBindings.dateRange:false, decided on purpose. (4) Browser-verified: picker renders, all 13 presets, KPIs move with the preset, no widget blank. (5) Changeset added. One in-scope addition, named in the PR body: a static guard in analytics-integrity.test.ts rejecting a bare date-range preset name as a filter comparand — the objectstack#8690 trap the card flags; measured in-process, `$gte: 'last_30_days'` returns 0 rows and does NOT throw.", "tests": "All at final head 7154ff18. Full suite `npx vitest run --maxWorkers=2`: 105 files, 2568 passed, 1 skipped. Every CI step of ci.yml run locally green: check-stackblitz-lock (in sync), check-source-hygiene, pnpm validate, pnpm lint, pnpm lint:i18n-gate (0 i18n/missing-*), pnpm typecheck, pnpm build (dist 1990.8 KB), pnpm test:coverage (96.3% stmts, thresholds pass). NOT run: E2E (playwright suite — the installed browser revision 1194 does not match this Playwright build's expected 1234; drove Chromium directly at /opt/pw-browsers/chromium-1194 instead of running `playwright install`), CodeQL, Link Check, Labeler. REVERSE VERIFICATION, three ablations, each applied to a committed tree then reverted: (a) drop the $gte floor (objectstack#3912's floor-ignored half) -> 3 tests red, `expected [13,13,13,13] to equal [4,7,10,13]`; (b) ceiling matches nothing -> 6 tests red incl. every no-widget-blank case; (c) bare preset comparand -> 5 tests red. BROWSER (objectstack dev -p 4011, fresh seeded DB, dev admin, 14 Recharts marks confirmed hydrated before reading): Last 90 days -> Open 30 / Critical 7 / Avg 45.0h / SLA 3 / KB 8 / Closed 8; Last 7 days -> 12 / 2 / 24.0 / 2 / 2 / 2; Last 30 days -> same as 90 (demo seed spans ~30 days). Wire evidence: widgets receive {\"$and\":[{\"is_closed\":false},{\"created_date\":{\"$gte\":\"2026-05-18\",\"$lte\":\"2026-08-16\"}}]} — the console lowers the preset to a BARE-DATE upper bound on a datetime column, exactly objectstack#3777's shape, answering correctly; daily_case_volume sends its own unbound {\"created_date\":{\"$gte\":\"2026-07-17\"}} with no $lte, so the opt-out works in the renderer and not only in metadata. 0 non-200 analytics calls, 0 empty result sets in any preset state.", "open_questions": [], "out_of_scope_findings": [] }Item 2 — which guard shape, and why
Of the card's two options I took "assert the window actually selects rows" and rejected the version pin. A pin asserts a belief about version numbers and re-measures nothing: on 17.0.0 it becomes a no-op, and it is blind to the same symptom arriving by another mechanism — objectstack#8690 produces an identical all-zero dashboard on any version.
The executable guard is sharper than the card's phrasing in one way worth recording: "selects rows" would not have caught half of the original bug. objectstack#3912's
$gtehalf matched every row, so a broken floor reads as a healthy widget with a bigger number. Every count widget is therefore compared for equality against a truth computed in the same run, plus an explicit assertion that the result set narrows as the preset narrows (ablation (a) above is exactly that case, and it goes red).The ban is replaced, not deleted: a dashboard windowing a
datetimefield is still gated — on evidence now. A new datetime-windowed dashboard whose object has no fixture in that file fails with "add a fixture here".Item 3 —
filterBindings: { dateRange: false }stays, deliberatelyMeasured on today's
main, the{30_days_ago}floor is no longer inert (#546 recorded it as inert): 13 fixture rows,$gte {30_days_ago}returns exactly the 7 inside the window. So the widget's title "New cases created over the last 30 days" is true again — which is precisely why it must not follow the picker. The dashboard range is ANDed into every bound widget, so a reader selecting "last 7 days" would get a 7-day chart still labelled "last 30 days". Self-described windows opt out; that is the rule the Executive dashboard's YTD tile follows andtest/action-references.test.tsenforces.The alternative — bind it and retitle — was considered and costs the widget's own filter plus retitling in all four locale bundles, to make the dashboard's one time-series chart stop being a stable 30-day trend. A fixed 30-day volume series beside a 90-day case load is the intended reading, and the browser run shows both behaviours side by side.
Contradicting the card
Nothing contradicts the card's measurements; they reproduced independently at
6bf85045(the card measuredd4ddee0). Two corrections of detail:- The guard's location. The card and PR fix(dashboards): drop the Service dashboard date range — datetime filters match nothing (#460) #546's own comment name
test/metadata-references.test.ts; the guard actually lives intest/action-references.test.ts—test/metadata-references.test.ts距 100KB hygiene 上限只剩 817 字节 —— 下一个往里加测试的 PR 会被 gate 拦住 #814 split the former by family and that file records the move. - objectstack#8690 does not throw in-process either. The card describes it as HTTP 200 with zero rows. Measured through
ctx.apidirectly,{ created_date: { $gte: 'last_30_days' } }also returns 0 with no error on bothdriver-sqlite-wasmanddriver-memory— a bare preset name is not brace-wrapped, so the token resolver never classifies it and never rejects it. That is why the new static guard is a grep over metadata rather than a runtime assertion.
Generated by Claude Code
- The guard's location. The card and PR fix(dashboards): drop the Service dashboard date range — datetime filters match nothing (#460) #546's own comment name
- removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Aug 16, 2026 - addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 9, 2026
Filed unassigned from #1153 (GA close-out B2) while closing #520. Search-checked first: #460 is closed (via PR #546) and cannot carry this; #520 is the platform card and is now closed as a non-reproducer; no open issue tracks the restore.
What this is
src/dashboards/service.dashboard.tsdeliberately ships without adateRange, andtest/metadata-references.test.tscarries a guard that fails any dashboard windowing adatetimefield. Both were the right call at the time. PR #546 wrote the restore precondition into the file itself:The two named preconditions are objectstack#3912 (datetime filter comparands coerced to epoch-ms against ISO TEXT storage) and objectstack#3777 (bare-date
$lteupper bound dropping same-day records).The precondition is now satisfied — measured, not assumed
On
@objectstack/*17.0.0 GA (hotcrmd4ddee0), against a fresh seeded database, every answer compared to a SQLite ground truth computed in the same run:case_metrics$gteonly$lteonly$gte+$lte$lteAnd the dashboard's own widgets, replayed from the compiled artifact with the window ANDed in exactly as the runtime does:
The preset chain resolves too. The shipped console lowers
last_30_daysto{ from: '{30_days_ago}', to: '{today}' }, and replaying all seven widgets through that spelling forlast_7_days/last_30_days/last_90_daysgives 21 of 21 combinations matching ground truth.Full reading: #520 (comment dated 2026-08-14).
What the work is
dateRangeblock insrc/dashboards/service.dashboard.tsand rewrite the long root-cause comment above it into a short historical note (the mechanism is fixed; the block should not read as if the bug were live).test/metadata-references.test.tsthat fails anydatetimedashboard window. Do not simply delete it — it caught a real class of defect. Better shapes: pin it to the platform version range it was true for, or convert it into a test that asserts the window actually selects rows (which is now a thing that can be asserted).daily_case_volumewidget's$gte: '{30_days_ago}'floor, which PR fix(dashboards): drop the Service dashboard date range — datetime filters match nothing (#460) #546 recorded as inert at the time ("that chart currently plots every case, not the last 30 days"). It is no longer inert — measured,{30_days_ago}now resolves to a start-of-day floor and selects correctly — so the widget's title becomes true again and itsfilterBindings: { dateRange: false }opt-out should be re-examined on purpose rather than by inheritance.One adjacent hazard to know about while doing this
Filed upstream as objectstack#8690: a date comparand the platform cannot parse (for example the bare preset name
"last_30_days", rather than the{30_days_ago}macro the console sends) is passed through, compares false, and returns HTTP 200 with zero rows and no diagnostic. The console path is safe — it does the mapping — but if any part of this work hand-writes a preset name into a filter, the symptom is an all-zero dashboard that looks exactly like the bug this restore is undoing.Generated by Claude Code