Skip to content

showcase authoring gap: the ops dashboard's global filter is field:'status' with *task* statuses, and the project widgets carry no filterBindings — so it also lands on showcase_project.status and zeroes those tiles #7568

Description

@huangyiirene

Symptom

This is a showcase authoring gap, not a platform defect. The run classified it as such deliberately. Do not go hunting for a bug in the global-filter engine — the engine does exactly what the metadata tells it to. The fix is in the example's dashboard authoring.

On the showcase ops dashboard, picking any value in the "Task Status" global filter zeroes the project-bound tiles (Active Projects, At-Risk (Red), Total Budget, Projects by Health) as well as filtering the task widgets it was meant for.

examples/app-showcase/src/ui/dashboards/ops-dashboard.dashboard.ts declares one dashboard-scoped global filter:

globalFilters: [
  { field: 'status', label: 'Task Status', type: 'select',
    options: [ backlog, todo, in_progress, in_review, done ],
    scope: 'dashboard' },
]

Those five options are the task status vocabulary. But the filter is declared by bare field name at scope: 'dashboard', and the widgets bound to the project dataset (kpi_active_projects, kpi_at_risk, kpi_total_budget, col_health) declare no filterBindings, so the same status filter is also applied to showcase_project.status — whose value domain is a different set entirely (active / health-style values). No project row matches in_review, so those tiles read 0.

Expected authoring: either give the project-bound widgets filterBindings that map (or opt out of) the dashboard filter, or scope the filter to the task widgets — the same per-widget field-mapping mechanism proven exact elsewhere in this run (region on invoice widgets vs sales_region on account widgets, correct on the wire and in the echoed SQL).

Still present on origin/main as of 2026-08-11.

Root cause

Authoring, in the example app: a dashboard-scoped global filter declared by bare field name (status) fans out to every widget that has not opted out via filterBindings, and the showcase's project widgets never opted out. The platform behaviour is the documented one — a widget without filterBindings inherits the dashboard filter on its own object's like-named field.

File: examples/app-showcase/src/ui/dashboards/ops-dashboard.dashboard.ts (the globalFilters block and the four projectDs widgets).

Reproduction

  1. Boot showcase and open the ops dashboard.
  2. Note the baseline values of Active Projects / At-Risk (Red) / Total Budget / Projects by Health.
  3. Set the "Task Status" global filter to any option (e.g. in_review).
  4. Observe the task widgets filter correctly while all four project-bound tiles drop to 0 — the filter reached showcase_project.status, whose domain does not contain task statuses.

Source

Extracted from the QA run #7515 (framework a86db17).

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Findings triage: resolved the co-applied pm:queue + finding dual state → pm:queue (kept domain:metadata as filed). Concrete authoring defect with a named file and fix shape (ops-dashboard.dashboard.ts: add filterBindings to the four projectDs widgets or scope the filter), user-visible on every showcase boot (zeroed tiles under any filter selection), reproduced and verified "still present on origin/main" by the filer today — queue-class, not a held observation.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 11, 2026
  3. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Claimed — domain:metadata seat.

    • Session: session_0127HHmCr5vd3NudQmW6QiNN
    • Branch: claude/issue-7568-ops-dashboard-filter-bindings
    • Worktree: ../objectstack-7568 (dedicated)
    • File surface: examples/app-showcase/src/ui/dashboards/ops-dashboard.dashboard.ts — the globalFilters block and the four projectDs widgets.

    The dispatch carries the card's own framing verbatim, because it is the part most likely to be discarded under momentum: this is a showcase AUTHORING gap, not a platform defect. The global-filter engine does exactly what the metadata tells it — a widget without filterBindings inherits the dashboard filter on its own object's like-named field. ⛔ The dev does not go hunting in the filter engine and does not "fix" this in packages/. If they become convinced the platform behaviour is itself wrong, they stop and report: that is a different card with a far larger blast radius, not something to fold in.

    There is a proven-correct precedent in the same QA run, and the dev is pointed at it rather than left to invent a shape — region on the invoice widgets versus sales_region on the account widgets, measured correct both on the wire and in the echoed SQL.

    Two admissible fixes, one to be chosen with the reason in the PR body: give the four project-bound widgets filterBindings that map or opt out, or scope the filter to the task widgets. ⛔ Not both.

    Verification has to be by driving it, not by reading it. This card was produced by booting the showcase and watching four tiles drop to 0 under a filter selection, so the dev captures that baseline first — the four tiles before the filter and under a selection — and shows the same capture after the fix, with the task widgets still filtering correctly. A test asserting only that a filterBindings key is now present proves the key is present, not that the tiles stopped zeroing; if no seam exists to pin the behaviour, the report says so plainly rather than implying coverage that is not there.

    If the fix turns out to need a packages/spec schema change, the dev stops and reports — this is meant to be example-app authoring.


    Generated by Claude Code

  4. claude commented on Aug 11, 2026

    @claude
    Contributor
    {
      "issue": 7568,
      "status": "done",
      "branch": "claude/issue-7568-ops-dashboard-filter-bindings",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7612",
      "premise_still_valid": true,
      "summary": "Showcase authoring fix only — nothing under packages/ was touched and no spec change was needed. The Delivery Operations global filter is now named `task_status` (it carries the showcase_task vocabulary), and every project-bound widget declares `filterBindings: { task_status: false }`. Chose option A (per-widget filterBindings) over option B (scope: 'widget' + targetWidgets) because filterBindings is the mechanism measured working in the same QA run (Revenue Pulse: region vs sales_region), is what the Studio widget inspector authors (objectui#2586), and is the only one drivable here — packages/console/dist is absent, so `scope: 'widget'` could only have been asserted, not measured; packages/lint also documents targetWidgets as the legacy allow-list filterBindings overrides. The two status vocabularies are disjoint (task: backlog/todo/in_progress/in_review/done; project: planned/active/on_hold/completed/cancelled), so there is no project field to re-target onto and the honest binding is an opt-out. TWO DEVIATIONS FROM THE CARD, both deliberate: (1) there are FIVE project-bound widgets, not four — `table_spend` has the identical defect and is fixed with the other four; it escaped the card because an emptied table reads as 'no data' rather than as a broken filter. (2) The filter was renamed from a bare `status` to `task_status` so an opt-out reads as 'this control is about tasks' instead of 'ignore project status'; nothing consumed `page.status`, checked repo-wide. dateRange stays inherited on every widget on purpose — projects do carry created_at.",
      "tests": "DRIVEN, not read. Booted the showcase backend (`pnpm dev -- --fresh`, sqlite, 130 seeded rows), read published metadata from GET /api/v1/meta/dashboard/showcase_ops_dashboard, resolved each dashboard filter per widget, and issued one POST /api/v1/analytics/dataset/query per widget — the same path the renderer takes. dateRange held unset in every capture so the status selection was the only variable. BEFORE, pristine -> status=in_review: kpi_active_projects 2->0, kpi_at_risk 1->0, kpi_total_budget 1090000->0, col_health 5->0, table_spend 5->0; task widgets 10->2 correctly. Echoed SQL showed the cause: `WHERE (status = $1 AND status = $2)` on showcase_project. AFTER, pristine -> task_status=in_review: 2->2, 1->1, 1090000->1090000, 5->5, 5->5 (project SQL carries no status predicate at all); task widgets still 10->2, and kpi_awaiting_review still composes both filters (`WHERE (status = $1 AND status = $2)` on showcase_task) so the QA checklist's Delivery-Operations composition clause still holds. SCOPE LIMIT, stated rather than implied: the per-widget resolution in the harness is transcribed from effectiveFilterField/dashboardFilterDefs in packages/lint/src/validate-widget-bindings.ts — this repo's own executable mirror of objectui resolveBoundField. Everything downstream (metadata -> wire -> SQL -> number) is the real server; the objectui browser bundle is not vendored in this repo (packages/console/dist absent) and was NOT exercised — that half stays a checklist item (dashboards.global-filters-rescope). PIN: new examples/app-showcase/test/dashboard-filter-vocabulary.test.ts, generic over every showcase dashboard, asserts (1) every value a global filter offers is a value its effective field can hold on each widget it reaches, (2) every filter still reaches at least one widget (so 'opt everybody out' is not a legal repair), (3) task_status governs the task widgets and no project widget. REVERSE VERIFICATION, direction predicted before running: revert ops-dashboard.dashboard.ts to main -> (1) and (3) red, (2) GREEN (the broken filter did reach widgets, it just could not answer them). Measured exactly that; (1) named all five widgets individually including table_spend. Restored -> all green. GATES: `pnpm --filter @objectstack/example-showcase verify` (os validate + tsc --noEmit + vitest run) green, 18 files / 171 tests passed — the validate step is what runs validate-widget-bindings, the rule family adjacent to this card; `eslint --no-inline-config` on both changed files clean; `node scripts/check-nul-bytes.mjs` OK (7046 files). CI at report time: in_progress — reporting at draft-PR time per the #6644 L2 contract, CI convergence is the PM's read. No changeset: @objectstack/example-showcase is private:true and absent from the `fixed` group in .changeset/config.json, so the PR releases nothing; `skip-changeset` applied by me as a union write and read back settled as ['size/m', 'skip-changeset'].",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    ACCEPT — PR #7612, marked ready and armed into the merge queue. 27/27 green.

    The option choice is the best thing in this run, and it was made on the right criterion. Both fixes were admissible; A (filterBindings) was chosen partly because B could not be driven, only asserted — packages/console/dist is absent, so scope: 'widget' had no measurable path here. Choosing the option you can verify over the one you can only claim is Prime Directive #10 turned on the work itself, and it is the kind of reasoning that usually goes unwritten. The supporting reasons hold too: filterBindings is what the same QA run measured working (region vs sales_region, on the wire and in the echoed SQL), it is what the Studio inspector authors, and with disjoint vocabularies there is no project field to re-target onto, so an opt-out is the honest binding rather than a shortcut.

    Two disclosed deviations from the card, both correct:

    1. There are five project-bound widgets, not four. table_spend carries the identical defect and is fixed with the others. The reason it escaped the card is worth keeping: an emptied table reads as "no data", while a zeroed KPI tile at least looks wrong. That is a general lesson about which surfaces make a defect visible, not a detail about this dashboard.
    2. Renaming the filter status → task_status, having checked repo-wide that nothing consumed page.status. This is scope-adjacent and I'm accepting it deliberately: with a bare status, an opt-out reads as "ignore project status"; with the name, the metadata says what the control governs and every widget it does not govern says so on its own line. In a showcase — whose job is to teach the shape — that distinction is the deliverable, not decoration.

    Verified independently of the report:

    • Path surface, read from git against a freshly-fetched main (my first read was against a stale ref and was contaminated with commits that had landed meanwhile — worth stating since it nearly produced a wrong review): exactly two files, examples/app-showcase/src/ui/dashboards/ops-dashboard.dashboard.ts and the new test. Nothing under packages/, which is the claim that mattered most on a card explicitly framed as authoring-not-platform.
    • Gate jobs' own conclusions: ESLint success, TypeScript Type Check success — not the aggregate.
    • Fixes #7568 as the first line; no docs/adr/**, no content/docs/releases/.
    • skip-changeset is the right call and correctly applied: @objectstack/example-showcase is private: true and absent from the fixed group, so the PR releases nothing — the label, not an empty changeset, which would otherwise sit in the release forever.

    The verification is the standard this card asked for and it was met. It was driven against a live backend rather than read: metadata from GET /meta/dashboard/..., one POST /analytics/dataset/query per widget, dateRange held unset in every capture so the status selection was the only variable — a real control, not just a before/after. The echoed SQL closes it: WHERE (status = $1 AND status = $2) on showcase_project before, no status predicate at all after, while kpi_awaiting_review still composes both filters on the task side, so the checklist's Delivery Operations composition clause still holds where it is coherent.

    The pin's second property is the one I'd single out: a filter must still reach at least one widget. Without it, the legal repair for property (1) is "opt everybody out", which passes the test and leaves an inert control on the header bar. Writing down the degenerate solution and forbidding it is what separates a pin from a snapshot.

    Scope limit accepted as stated, not glossed: the per-widget resolution in the harness is transcribed from packages/lint/src/validate-widget-bindings.ts — this repo's own executable mirror of objectui's resolveBoundField — and everything downstream of it is the real server. The browser bundle is not vendored here and was not exercised; the objectui half stays the checklist item dashboards.global-filters-rescope. That is the honest boundary, and saying so is worth more than a claim of end-to-end coverage would have been.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions