Skip to content

[17.0-rc][疑似平台] datetime 字段的时间窗过滤返回空集(客服仪表盘全空) #520

Closed

Description

@yinlianghui

现象

升级 @objectstack/* 到 17.0.0-rc.0 后,客服仪表盘所有 widget 显示 0 / 暂无数据,而数据库中有 38 条工单(30 条未关闭,全部在近 30 天窗口内)。

复现

  1. 升级到 17.0.0-rc.0,SQLite(better-sqlite3)驱动,全新种子数据
  2. 打开客服仪表盘(dateRange: last_30_days,字段 created_date,类型 datetime)
  3. 所有基于 case_metrics 的 widget 返回空

widget 实际 SQL:WHERE is_closed = ? AND created_date >= ? AND created_date <= ?。列内容为 ISO 文本(如 2026-07-26T00:00:00.000Z)。同一查询手工验证:ISO 字符串参数返回 29 行,epoch 毫秒参数返回 0 行 —— 17.0 驱动把 datetime 比较参数转成了 epoch 毫秒,与磁盘上的 ISO 文本比较永假。

date 类型字段(如 close_date)不受影响(销售/高管仪表盘正常)。数据库是 17.0 自己新建的,属于写入端(ISO)/读取端(epoch)不一致(对应 spec 中 SqlDriver.temporalFilterValue 的行为)。

影响

所有 datetime 字段上的 dateRange / {30_days_ago} 类时间窗过滤。升级阻断。

详见分支 upgrade/objectstack-17 的 docs/upgrade-17/test-report.md §3 P1。

Activity

  1. yinlianghui commented on Jul 29, 2026

    @yinlianghui
    ContributorAuthor

    源码确认(objectstack@fc156fa4a):CONFIRMED,平台 bug。写路径存 ISO TEXT、过滤路径按声明类型无条件转 epoch-ms;平台自身 created_at/updated_at 同样中招;分桶路径已有 typeof() 兼容而过滤路径没有。9.10.0 埋雷(#2034),17.0 服务端占位符解析(#3582)引爆。已上报平台:objectstack-ai/objectstack#3912

  2. yinlianghui commented on Jul 29, 2026

    @yinlianghui
    ContributorAuthor

    📎 分支 upgrade/objectstack-17 已推送至上游(仅分支,无 PR,勿合并——阻塞于 17.0 正式版与 objectstack#3912/#3913/#3914)。正文引用的文档现在可直接访问:测试报告 | 测试计划

  3. yinlianghui commented on Jul 30, 2026

    @yinlianghui
    ContributorAuthor

    Two things confirmed while fixing #460, which turns out to be this same defect seen from the 16.1.0 side.

    It is the platform, and it is not 17.0-specific. Reproduced on the pinned 16.1.0 with a fresh seed, against the 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

    So the rule on a datetime column is directional: $lte matches no rows, $gte matches every row (the lower bound is not applied at all — a floor that should exclude older rows doesn't). SELECT typeof(created_date) is text for all 38 rows, and binding those same bounds as strings in raw SQL returns 38.

    Worth noting the $gte half separately: it fails silently in the other direction, so a widget with only a lower bound looks fine while actually being unfiltered. service_dashboard's daily_case_volume ($gte: '{30_days_ago}') is currently plotting every case, not the last 30 days — indistinguishable today only because the seed spans exactly 30 days.

    Upstream tracking:

    That second one matters for the upgrade plan here: taking the 17.0 fix for #3912 does not by itself make it safe to window a datetime field. #3777 has to land too, otherwise a restored date picker silently drops records created earlier the same day. Both are preconditions for re-enabling dateRange on a datetime field.

    #546 works around it for service_dashboard on 16.1.0 by removing the dateRange block (the range is ANDed into every widget query, so the $lte half zeroes the whole dashboard at any preset — widening it, which is what #460 originally proposed, changes nothing). It also adds a CI guard that fails if any dashboard windows a datetime field, so this can't be reintroduced silently while the upstream fixes are outstanding. It does not resolve this issue — the platform-wide impact and the upgrade path stay open here.


    Generated by Claude Code

  4. yinlianghui commented on Aug 5, 2026

    @yinlianghui
    ContributorAuthor

    [17.0-rc2验收复核] 结论:已不复现(平台侧 datetime 时间窗过滤已修;dashboard 恢复 dateRange 属 CRM 收尾)

    证据:独立 dev server(端口 4091,全新 rc.2 种子库,crm_case.created_date 落盘仍为 ISO TEXT,typeof() 实查 = text)。先用 better-sqlite3 直查建立 ground truth,再走 POST /api/v1/analytics/dataset/query(case_metrics)对照:

    过滤 SQLite 直查 API 实测 判定
    $gte 2026-07-25 19 19 ✓(rc.0 此方向为"全量 38")
    $lte 2026-07-25T23:59:59.999Z 20 20 ✓(rc.0 此方向为 0)
    $gte 07-20 + $lte 07-31 15 15 ✓(rc.0 全空)
    bare-date $lte "2026-07-25" — 20,SQL 编译为 created_date < $1(次日半开) ✓ #3777 类同日丢失也已处理
    $gte "{30_days_ago}" token 38(min=07-06) 38 ✓

    即 objectstack#3912(epoch-ms vs ISO TEXT)与 #3777(bare-date 上界)在 rc.2 均已确认落地;temporalColumn/coerce 两侧代码可在 service-analytics dist 中对到。

    两条附注:

    环境:hotcrm@0899b4f + @objectstack 17.0.0-rc.2

    建议:平台前提已过时;恢复 dashboard dateRange(连同放宽 #546 的 CI 守卫)后本单可关。


    Generated by Claude Code

  5. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    Re-measured on @objectstack/* 17.0.0-rc.6 during the rc.5 → rc.6 upgrade (#1059, PR #1066). Not reproduced — the datetime window returns rows.

    In-process probe against SqliteWasmDriver (:memory:), an object with a datetime column and a date control column, four rows at 1 / 5 / 20 / 100 days ago:

    stored created_date sample: "2026-08-10T05:14:50.192Z"     ← ISO text, as before
    DATETIME last_30_days window rows: 3   (expected 3: a,b,c)
      matched: a,b,c
    DATE     window rows: 3   (control)
    

    The created_date >= ? AND created_date <= ? shape this card describes now selects correctly. The write side still stores ISO text, so the read side is no longer converting the comparison parameter to epoch milliseconds — the write/read inconsistency in SqlDriver.temporalFilterValue that made the comparison permanently false.

    Scope note on what this does not establish: this is a driver-level probe, not the customer-service dashboard. It measures the mechanism this card diagnosed (a datetime range filter on ISO-text storage), not the full case_metrics widget path with a dateRange: last_30_days binding and {30_days_ago} template resolution. Someone should open that dashboard against seeded data before closing — but the driver-level defect that made every such widget empty is no longer present.


    Generated by Claude Code

  6. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Not closed — the recorded evidence does not reach this card's defect. Needs a GA dashboard probe.

    This card was on the direct-close list for the GA close-out bookkeeping sweep (#1151, group 3), which authorised closing it on its recorded rc.2/rc.6 retest readings. Read in full, those readings do not support a close, so it stays open. Recording why, so the next pass does not have to re-derive it.

    What the readings do establish

    The driver-level defect is gone. On @objectstack/* 17.0.0-rc.6, an in-process probe against SqliteWasmDriver (:memory:), four rows at 1 / 5 / 20 / 100 days old:

    stored created_date sample: "2026-08-10T05:14:50.192Z"     ← still ISO text
    DATETIME last_30_days window rows: 3   (expected 3)
    DATE     window rows: 3   (control)
    

    The created_date >= ? AND created_date <= ? shape now selects correctly — the write/read inconsistency in SqlDriver.temporalFilterValue that made the comparison permanently false is no longer present. The earlier rc.2 reading agrees, and additionally confirmed the bare-date upper bound (objectstack#3777) compiles to a next-day half-open range.

    Source: rc.6 driver probe · rc.2 API reading

    Why that is not enough to close this card

    1. The most recent reading disclaims exactly the scope this card is about. Its own scope note:

    this is a driver-level probe, not the customer-service dashboard. It measures the mechanism this card diagnosed … not the full case_metrics widget path with a dateRange: last_30_days binding and {30_days_ago} template resolution. Someone should open that dashboard against seeded data before closing.

    This card's title and headline symptom are 客服仪表盘全空 — the dashboard rendering 0 for every widget. A driver probe is the mechanism underneath that symptom, not the symptom.

    2. Neither reading actually recommends an unconditional close. The rc.2 one is explicit that the close is gated on repo-side work: 「恢复 dashboard dateRange(连同放宽 #546 的 CI 守卫)后本单可关」. That work has not been done — service.dashboard.ts still deliberately ships without a dateRange block, and #546's CI guard still fails any dashboard that windows a datetime field.

    3. The symptom is currently unobservable by construction. Because the workaround and its guard are both still in place, the dashboard cannot be windowed on a datetime field at all — so "the dashboard is no longer empty" has not been and cannot be measured until that is unwound. Closing now would record a green on a path nothing has traversed since rc.0.

    4. There is a named, unresolved hazard on the widget path specifically. The rc.2 reading found that dataset/query does not resolve preset strings like last_30_days (service-analytics resolves relative phrases console-side, not in the API), and a bare preset degrades to a single-point window returning 0 rows silently — the same visual failure this card reports. It asked for the preset → date-array chain to be confirmed from the UI. That is unconfirmed.

    What would close it

    A GA probe that opens the customer-service dashboard against seeded data with dateRange restored, confirming the widgets populate — including the preset resolution chain in (4). That requires unwinding the #546 workaround and its CI guard, which is a code change and therefore outside #1151's bookkeeping mandate.

    ⚠️ Routing note for the PM: #520 is not in any of the four GA close-out sub-issues' probe sets — #1154 (browser/console) covers #664, #682, #680, #1122, #986. This card needs a browser probe and currently has no card that will run one.

    Nothing on this card was changed: still open, labels untouched.


    Generated by Claude Code


    Generated by Claude Code

  7. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    GA reading (17.0.0) — NOT REPRODUCED at the dashboard layer. The four gaps #1151 named are answered; closing.

    From #1153 (GA close-out B2), session session_01XAK3brMLjd4ykF4QAhFnuo. #1151 correctly refused to close this on a driver-level probe that disclaimed this card's dashboard scope in its own words. This is the dashboard-layer measurement it asked for.

    Environment: objectstack dev -p 4002 --seed-admin --fresh, @objectstack/* 17.0.0 GA, hotcrm d4ddee0, SqlDriver(better-sqlite3), fresh seeded DB. created_date on disk is still ISO TEXT ("2026-08-12T00:00:00.000Z", typeof = text) — the storage side of the original mismatch is unchanged, so any fix has to be on the read side, as diagnosed.

    Every API answer below is compared against a SQLite ground truth computed from the same database in the same run. That is what makes this probe able to fail: "0 rows" is only a defect if the truth is non-zero, and a filter that is silently ignored shows up as API = unfiltered-total while truth is lower — which is exactly the $gte half of this card's 16.1.0 reading. A bare "the number is not zero" check would have caught neither direction.

    1. The widget/dataset layer — POST /api/v1/analytics/dataset/query on case_metrics

    filter API ground truth rc.0 behaviour
    none 51 51 ✓ 38 (ok)
    $gte only 37 37 ✓ all 38 — floor not applied
    $lte only 1 1 ✓ 0 — matched nothing
    $gte + $lte (the dashboard's own window) 37 37 ✓ 0 — the headline symptom
    bare-date $lte "2026-07-15" 1 1 ✓ objectstack#3777

    All four directions correct. Both preconditions this repo's code comment named — objectstack#3912 (epoch-ms vs ISO TEXT) and objectstack#3777 (bare-date upper bound) — are satisfied on GA.

    2. The SERVICE DASHBOARD itself — the layer this card is titled on

    No product file was edited. The widgets were read from the compiled artifact and each one's own filter replayed with the datetime window ANDed in, which is what the runtime does with a dashboard dateRange (per service.dashboard.ts's own note: "The runtime ANDs the dashboard range into every widget query"). Widgets carrying filterBindings: { dateRange: false } are excluded, as the runtime excludes them.

      widget                     unwindowed   windowed   truth(windowed)   verdict
      open_cases                         43         30                30   ✓
      critical_cases                      8          7                 7   ✓
      sla_violations                      3          3                 3   ✓
      cases_by_status                    43         30                30   ✓
      cases_by_priority                  43         30                30   ✓
      cases_by_origin                    51         38                38   ✓
      open_cases_by_priority             43         30                30   ✓
    
      widgets that go BLANK once the datetime window is applied: 0 of 7
      widgets whose windowed count disagrees with ground truth : 0 of 7
    

    Zero blank widgets. On rc.0 every one of these read 0 while unwindowed read non-zero — that renders in this table as windowed = 0 on all seven rows, so the failing shape was reachable and did not occur.

    3. The preset-resolution chain — #1151's gap (4), answered without a browser

    The rc.2 reading found that dataset/query does not resolve preset strings and flagged that the console-side preset → date-array chain was unconfirmed. The shipped console's own mapping table settles it (read out of @objectstack/console/dist/assets/framework-*.js):

    last_7_days  -> { from: '{7_days_ago}',  to: '{today}' }
    last_30_days -> { from: '{30_days_ago}', to: '{today}' }
    last_90_days -> { from: '{90_days_ago}', to: '{today}' }
    

    A preset does not travel as a preset — it lowers to date-macro placeholders, which are exactly what the server-side filter-token resolver consumes. Replaying all seven widgets through that spelling, for all three presets:

    --- preset "last_7_days"  -> { $gte: "{7_days_ago}",  $lte: "{today}" } --- 7/7 match ground truth
    --- preset "last_30_days" -> { $gte: "{30_days_ago}", $lte: "{today}" } --- 7/7 match ground truth
    --- preset "last_90_days" -> { $gte: "{90_days_ago}", $lte: "{today}" } --- 7/7 match ground truth
    

    21 of 21 widget/preset combinations match ground truth exactly. The chain resolves, and the macro floor lands on the start of the day ({30_days_ago} = 38 rows against a start-of-day truth of 38), which is the calendar-correct reading.

    Disposition

    Closed as completed. Version: @objectstack/* 17.0.0 GA. Probe: case_metrics dataset queries and a full replay of the service dashboard's windowed widgets, each against a SQLite ground truth, plus the console preset chain. Result: the datetime time-window filter selects correctly in every direction and the dashboard populates — the 客服仪表盘全空 symptom does not reproduce.

    Two follow-ups, both filed rather than folded in here:

    • Restoring the dashboard's date picker is repo-side work, not evidence this defect survives. The #546 workaround and its CI guard are still in place by design; their documented restore precondition ("BOTH must land first") is now satisfied. Filed separately so it is tracked as the task it is — Service dashboard renders empty by default — dateRange defaults to last_30_days but seed cases are older #460 is closed and cannot carry it.
    • A separate silent-zero hazard with the same visual symptom, found while probing this: a date comparand the resolver cannot parse ("last_30_days", "not-a-date-at-all") is passed through and compares false, so the query answers HTTP 200 with 0 rows and no diagnostic — while an unknown {placeholder} is correctly rejected with 400 FILTER_TOKEN_UNKNOWN. Different cause, same blank screen. Mirrored upstream; it is not what the console sends, so it does not affect this close.

    Generated by Claude Code


    Generated by Claude Code

  8. added
    priority:p1High: required for production / M2
    and removed on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:p1High: required for production / M2upstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstream

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions