You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
dashboard 的日期区间上界打在 datetime 列上丢失当天数据 —— 默认配置即命中 objectstack#3777 — open, bug/priority:p1, and 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, by an amount that grows through the day.
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.
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.
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.
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.
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:
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.
Activity
yinlianghui commentedon Jul 29, 2026
源码确认(objectstack@fc156fa4a):CONFIRMED,平台 bug。写路径存 ISO TEXT、过滤路径按声明类型无条件转 epoch-ms;平台自身 created_at/updated_at 同样中招;分桶路径已有 typeof() 兼容而过滤路径没有。9.10.0 埋雷(#2034),17.0 服务端占位符解析(#3582)引爆。已上报平台:objectstack-ai/objectstack#3912
yinlianghui commentedon Jul 29, 2026
📎 分支
upgrade/objectstack-17已推送至上游(仅分支,无 PR,勿合并——阻塞于 17.0 正式版与 objectstack#3912/#3913/#3914)。正文引用的文档现在可直接访问:测试报告 | 测试计划yinlianghui commentedon Jul 30, 2026
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_metricsdataset the widgets use, varying only thecreated_datefilter:$gteonly (2026-05-01)$lteonly (2026-07-30)$gte+$lte(the dashboard's own window)$gte+$lte, full ISO with end-of-daySo the rule on a datetime column is directional:
$ltematches no rows,$gtematches every row (the lower bound is not applied at all — a floor that should exclude older rows doesn't).SELECT typeof(created_date)istextfor all 38 rows, and binding those same bounds as strings in raw SQL returns 38.Worth noting the
$gtehalf separately: it fails silently in the other direction, so a widget with only a lower bound looks fine while actually being unfiltered.service_dashboard'sdaily_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:
datetime列上丢失当天数据 —— 默认配置即命中 objectstack#3777 — open,bug/priority:p1, and a separate datetime bug: a bareYYYY-MM-DD$lteupper bound on a datetime column drops every record created after 00:00 that day, by an amount that grows through the day.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
dateRangeon a datetime field.#546 works around it for
service_dashboardon 16.1.0 by removing thedateRangeblock (the range is ANDed into every widget query, so the$ltehalf 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 adatetimefield, 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
docs(acceptance): record the read-only acceptance results for rc.2
docs(acceptance): add the parallel task-package split for the write p…
docs(acceptance): record the object CRUD write phase and the six defe…
docs(acceptance): finalize the rc.2 report as a partial run with a GA…
docs(acceptance): add the handoff page and commit the run's text evid…
docs(acceptance): record the full action sweep and all ten blocker ve…
yinlianghui commentedon Aug 5, 2026
[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)对照:$gte 2026-07-25$lte 2026-07-25T23:59:59.999Z$gte 07-20 + $lte 07-31$lte "2026-07-25"created_date < $1(次日半开)$gte "{30_days_ago}"token即 objectstack#3912(epoch-ms vs ISO TEXT)与 #3777(bare-date 上界)在 rc.2 均已确认落地;
temporalColumn/coerce 两侧代码可在 service-analytics dist 中对到。两条附注:
service.dashboard.ts依然刻意不带dateRange(fix(dashboards): drop the Service dashboard date range — datetime filters match nothing (#460) #546 的守卫仍在)——恢复它是本单/相关 Service dashboard renders empty by default — dateRange defaults to last_30_days but seed cases are older #460 的收尾。dataset/queryAPI 不解析last_30_days这类预设串(service-analytics 源码注释明确"Relative phrases are NOT resolved here",由 console 端解析后传具体日期数组);bare 预设串会退化为单点窗口静默 0 行(实测)。浏览器组复测 dashboard 时请从 UI 端确认预设 → 日期数组的解析链路。环境:hotcrm@0899b4f + @objectstack 17.0.0-rc.2
建议:平台前提已过时;恢复 dashboard
dateRange(连同放宽 #546 的 CI 守卫)后本单可关。Generated by Claude Code
huangyiirene commentedon Aug 11, 2026
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 adatetimecolumn and adatecontrol column, four rows at 1 / 5 / 20 / 100 days ago: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 inSqlDriver.temporalFilterValuethat 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_metricswidget path with adateRange: last_30_daysbinding 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
hotlong commentedon Aug 14, 2026
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 againstSqliteWasmDriver(:memory:), four rows at 1 / 5 / 20 / 100 days old:The
created_date >= ? AND created_date <= ?shape now selects correctly — the write/read inconsistency inSqlDriver.temporalFilterValuethat 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 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.tsstill deliberately ships without adateRangeblock, and #546's CI guard still fails any dashboard that windows adatetimefield.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/querydoes not resolve preset strings likelast_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
dateRangerestored, 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.Nothing on this card was changed: still open, labels untouched.
Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 14, 2026
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, hotcrmd4ddee0,SqlDriver(better-sqlite3), fresh seeded DB.created_dateon 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
$gtehalf 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/queryoncase_metrics$gteonly$lteonly$gte+$lte(the dashboard's own window)$lte "2026-07-15"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
filterreplayed with the datetime window ANDed in, which is what the runtime does with a dashboarddateRange(perservice.dashboard.ts's own note: "The runtime ANDs the dashboard range into every widget query"). Widgets carryingfilterBindings: { dateRange: false }are excluded, as the runtime excludes them.Zero blank widgets. On rc.0 every one of these read 0 while unwindowed read non-zero — that renders in this table as
windowed = 0on 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/querydoes not resolve preset strings and flagged that the console-sidepreset → date-arraychain was unconfirmed. The shipped console's own mapping table settles it (read out of@objectstack/console/dist/assets/framework-*.js):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:
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_metricsdataset 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:
#546workaround 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."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 with400 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