Skip to content

fix(dashboards): drop the Service dashboard date range — datetime filters match nothing (#460) - #546

Merged
yinlianghui merged 4 commits into
mainfrom
claude/service-dashboard-empty-render-3a2hwr
Jul 30, 2026
Merged

fix(dashboards): drop the Service dashboard date range — datetime filters match nothing (#460)#546
yinlianghui merged 4 commits into
mainfrom
claude/service-dashboard-empty-render-3a2hwr

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Description

Note: the first push on this branch widened the default preset to last_90_days. Running the app disproved that diagnosis — see below. The current commits remove the date range instead. The earlier description is superseded.

service_dashboard opened with every KPI at 0 and every chart reporting no rows, with 38 cases in the system. The preset was never the cause, and no preset fixes it.

crm_case.created_date is a Field.datetime(). On the SQLite path, driver-sql 16.1.0 coerces datetime filter comparands to epoch-millisecond INTEGERs while writes store ISO TEXT. SQLite orders every INTEGER before every TEXT, so on a datetime column:

created_date >= <int>   is TRUE  for every row   (the window has no floor)
created_date <= <int>   is FALSE for every row   (the window matches nothing)

The runtime ANDs the dashboard range into every widget query, so the $lte half zeroed the whole dashboard.

Measured, not inferred

Against the running 16.1.0 console (pnpm demo:reset && pnpm dev, logged in, same case_metrics dataset the widgets use, varying only the created_date filter):

filter rows
none 38
$gte only (2026-05-01) 38
$lte only (2026-07-30) 0
$gte + $lte (the dashboard's own window) 0
$gte + $lte, full ISO with end-of-day 0

Confirmed at the storage layer: SELECT typeof(created_date) is text for all 38 rows, and binding the same bounds as strings in raw SQL returns 38 — so the window is right and the binding is not.

Why the other three dashboards are fine: they window close_date, a Field.date(), which stays TEXT YYYY-MM-DD on both sides of the comparison. That is why Service was the outlier — not the preset choice.

Upstream status

This is a platform defect, already reported:

The second one matters for the restore condition: upgrading the platform alone does not make it safe to window a datetime field. Both fixes must land first, which is what the code comment and the CI guard now encode.

On this side, #520 already tracks the same root cause from the 17.0-rc upgrade angle — #460 is the 16.1.0 view of one dashboard, #520 is the platform-wide impact and the upgrade path. This PR resolves the former and leaves the latter open; measurements are cross-posted there.

What this PR does

Removes the dateRange block rather than widening it. Verified in the browser: 30 open / 7 critical / 45.0h avg resolution / 3 SLA breaches, every chart and the table populated.

The cost is visible and intentional: this dashboard has no date picker for now. The block is left commented out at the exact spot to restore, with both upstream issue numbers as the precondition. A picker that zeroes every widget is strictly worse than no picker.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • CI/CD update

Related Issues

Fixes #460
Refs #520 (same root cause, platform-wide impact and 17.0 upgrade path — stays open)
Refs objectstack-ai/objectstack#3912 (fixed upstream, 17.0 train) · objectstack-ai/objectstack#3777 (open — blocks restoring the range)

Changes Made

  • src/dashboards/service.dashboard.ts — remove the dateRange block; document the root cause, the measurements, both upstream issues, and the restore precondition in place.
  • src/dashboards/service.dashboard.ts — document that the daily_case_volume widget's $gte: '{30_days_ago}' floor is inert for the same reason (a $gte on a datetime is true for every row), so that chart currently plots every case, not the last 30 days. Indistinguishable today because the seed spans exactly 30 days; it starts working when the driver is fixed. Flagged rather than papered over — the title's claim isn't enforced yet.
  • test/metadata-references.test.ts — replaces the window-arithmetic guard from the first push, which could not detect this class of defect at all. The new guard fails if any dashboard windows a datetime field, and a second test checks the range field actually exists on the objects its widgets aggregate.
  • .changeset/service-dashboard-default-date-range.md — patch changeset.

Note on the issue's other suggestion (seed recent cases): already landed independently in #481 — the case seed dates every record relative to boot, daysAgo(1..30). It didn't help, because the $lte bound matches nothing regardless of how recent the rows are.

Not touched, deliberately: the four hardcoded trend blocks in this file are fabricated figures, but they belong to the review pass in #500 (which turns out to cover only the executive dashboard's four of sixteen — noted there).

Testing

  • Unit tests pass (pnpm test — 8 files, 91 tests)
  • Linting passes (pnpm lint — 2 warnings, both pre-existing and unrelated: crm_quote_line_item naming/relationship suggestions)
  • Build succeeds (pnpm build, plus pnpm validate and pnpm typecheck clean)
  • Manual testing completed — pnpm demo:reset && pnpm dev, logged in as the seeded dev admin, opened the Customer Service dashboard in Chromium. Zeros before, real numbers after; captured the analytics request/response pairs and the executed SQL for the table above.
  • New tests added (if applicable)

The new guard was verified in both directions: it fails with the exact message service_dashboard: dateRange windows crm_case.created_date, a datetime field — the $lte bound matches no rows, so every widget renders empty when the removed block is restored, and passes without it.

Screenshots

Customer Service dashboard after the fix — KPI tiles at 30 / 7 / 45.0h / 3, donut, pie and bar charts all populated. (Before: every tile 0, every chart "No rows".)

Checklist

  • I have added a changeset (pnpm changeset) — required on every PR, or the skip-changeset label is applied
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation — no doc mentions this dashboard's date range; docs-drift passes
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

Two things a reviewer may want to weigh in on:

  1. No picker vs. a broken picker. I chose removal. The alternatives were keeping dateRange with filterBindings: { dateRange: false } on every widget (a visible control that does nothing — the ADR-0078 shape this repo keeps removing), or adding a date-typed mirror of created_date to crm_case (a schema change to work around a driver bug, needing hook and seed upkeep).
  2. Scope. This stops HotCRM from tripping the defect; it does not fix it. Every other datetime filter in the app has the same problem — $gte silently matches everything, $lte silently matches nothing — so more surfaces are likely affected. That sweep belongs to [17.0-rc][疑似平台] datetime 字段的时间窗过滤返回空集(客服仪表盘全空) #520; #3912's own report notes the platform's created_at / updated_at audit columns are equally affected, and those are everywhere.

The Customer Service dashboard opened on all zeros — every KPI 0, every
chart reporting no rows — with 38 cases in the system.

`dateRange.defaultRange` was `last_30_days` while the demo cases run from
1 to 30 days old, so the default window sat exactly on the edge of the
data it aggregates. The runtime ANDs the dashboard range into every
widget query, so the oldest cases dropped out and a business user's first
paint looked like a broken screen.

Default to a rolling `last_90_days`, which always contains the full case
history whatever day the demo is opened on. `this_quarter` — what the
CRM, Sales and Executive dashboards use — is deliberately not the fix:
those window `close_date` over a forward-looking pipeline, where a
calendar quarter is the intended framing, whereas a support desk reads a
trailing window, and a calendar quarter is only a few days long on
1 July, which would reintroduce the same emptiness.

Add a regression guard that resolves the default preset against every
reference day of a leap year and requires the window to clear the oldest
seeded case by a margin, so a flush-to-the-edge default fails in CI
instead of in a demo. Verified it fails on `last_30_days` and passes on
`last_90_days`.

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

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Jul 30, 2026 2:51am

Request Review

…ters match nothing (#460)

Running the app disproved the diagnosis in the previous commit. Widening
the preset does not fix #460; nothing about the window does.

`crm_case.created_date` is a `Field.datetime()`. On the SQLite path
`driver-sql` 16.1.0 coerces datetime filter values to epoch-millisecond
INTEGERs (`coerceFilterValue`), on the documented assumption that
datetime columns hold INTEGER ms. They hold ISO TEXT — including the
platform's own `created_at`/`updated_at` audit columns. SQLite orders
every INTEGER before every TEXT, so on a datetime column:

    created_date >= <int>   is TRUE for every row
    created_date <= <int>   is FALSE for every row

The runtime ANDs the dashboard range into every widget query, so the
`$lte` half zeroed the whole dashboard at any preset. Measured against
the running 16.1.0 console: `$gte` alone returns all 38 cases, `$lte`
alone returns 0, both bounds return 0, in every date format tried.

Remove the `dateRange` block instead of widening it. The dashboard now
renders 30 open / 7 critical / 45.0h / 3 SLA breaches with every chart
populated, confirmed in the browser. The cost is visible: no date picker
on this dashboard until the driver is fixed. The block is left
commented-out at the spot to restore.

Also document that `daily_case_volume`'s `$gte: '{30_days_ago}'` floor is
inert for the same reason, so that chart currently plots every case.

Replace the previous window-arithmetic test, which could not detect this
class of defect, with a guard that fails if any dashboard windows a
`datetime` field, plus one checking the range field exists on the objects
its widgets aggregate. Verified failing when the removed block is
restored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZJSA2ZpuwDtz2Nc8bgXjy
@yinlianghui yinlianghui changed the title fix(dashboards): widen the Service dashboard default date range (#460) fix(dashboards): drop the Service dashboard date range — datetime filters match nothing (#460) Jul 30, 2026
@yinlianghui
yinlianghui marked this pull request as ready for review July 30, 2026 02:06
@yinlianghui
yinlianghui requested a review from os-zhuang July 30, 2026 02:06
claude added 2 commits July 30, 2026 02:31
…fect (#460)

A duplicate check on the platform repo found this already reported:
objectstack-ai/objectstack#3912, closed 2026-07-29, same mechanism and
same measurements, filed while upgrading this app 16.1.0 → 17.0.0-rc.0.
No new upstream issue needed.

More importantly it turned up #3777 (open, p1), which is a SEPARATE
datetime bug: a bare `YYYY-MM-DD` `$lte` upper bound on a datetime column
drops every record created after 00:00 that day, silently, by an amount
that grows through the day. So a platform upgrade alone does not make it
safe to restore this dashboard's date range — both fixes must land first.

Record both in the dashboard comment and the changeset so the restore
condition is precise rather than "once it's fixed upstream".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZJSA2ZpuwDtz2Nc8bgXjy
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.

Service dashboard renders empty by default — dateRange defaults to last_30_days but seed cases are older

2 participants