Skip to content

System Overview board mislabels two tiles: "Total Users" is really users created in the last 7 days, and "Active Sessions" counts every session #7531

Closed

Description

@huangyiirene

Symptom

Two tiles on the built-in System Overview board say something other than what they count.

1. "Total Users" is not a total. The board's default dateRange filter is "Last 7 days", and it applies to sys_user.created_at. So the tile labelled Total Users actually reports users created in the last 7 days. On a fresh datastore this is indistinguishable from the true total — every user was created recently — which is exactly why it reads as correct in a demo and will read as a catastrophic user-loss event on any instance older than a week.

2. "Active Sessions" is not active-only. The tile is backed by sys_session_metrics, which carries no active-only filter, so the label overstates what the number represents: it is a session count, not a count of currently-active sessions.

Both are label/query disagreements on a shipped platform board, not showcase authoring — the board is packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts, so every deployment sees them.

The tiles are otherwise genuinely live: each equals its own captured query and an independent direct aggregate, and Active Sessions moved 25 → 26 in lockstep with the direct count during the run. Nothing is stale or fabricated — the numbers are real, they are just not the numbers the labels promise.

Once-observed weakness recorded in the same run (not reproduced)

On the first capture, the "Events by User" bar chart rendered its y-axis with zero bars and no empty state. Its query had returned a single {user_id: null, …} row, and the renderer drops the null-keyed category — leaving an axis, no marks, and no "no data" affordance.

Observed once and not reproduced, so it is recorded as a weakness per RUNNER rule 2 rather than promoted to a failure. It is worth carrying anyway: it is exactly the blank-widget shape the system-overview-live-counts clause exists to guard, and it is reachable on any real instance whose only audit actor is the system itself — a common state for a freshly provisioned deployment.

Root cause

Not root-caused to a line in this run; the observations are behavioural and the fix is an authoring decision on the board metadata:

  • The dateRange global filter's binding to sys_user.created_at is what makes the Total Users tile a windowed count. Either the tile should opt out of the global filter (the way a true total must), or its label should say what it counts.
  • The sys_session_metrics widget needs an active-only predicate, or a label that matches the unfiltered count.
  • The null-category drop is a renderer behaviour (objectui) rather than metadata, and would need its own reproduction before filing on that side — captured here only so the observation is not lost.

Reproduction

  1. Boot the showcase and open the built-in System Overview board.
  2. Note the default dateRange filter reads "Last 7 days".
  3. Compare the "Total Users" tile against a direct unfiltered count over sys_user. On a datastore with users older than 7 days these diverge; on a fresh one they coincide, which is the trap.
  4. Widen the dateRange filter and watch "Total Users" change — a total that moves with a date filter is not a total.
  5. Compare "Active Sessions" against a direct count over sys_session_metrics with and without an active-only predicate; the tile matches the unfiltered count.

Re-check the board source:

rg -n "Total Users|Active Sessions|dateRange|sys_session_metrics" packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts packages/platform-objects/src/apps/dashboards/system.datasets.ts

Source

Extracted from the QA run #7515 (framework a86db17). Checklist item system-overview-live-counts (PARTIAL) plus the matching entry under "Smaller observations".

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 11, 2026
  2. removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 11, 2026
  3. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Findings triage — resolved the filing-time pm:queue+finding dual state to pm:queue (finding removed).

    • Grade rationale: two concrete label/query disagreements on a shipped platform board with a step-wise repro; the fix is board-metadata authoring (opt the Total Users tile out of the global filter or relabel; add an active predicate to the sessions widget or relabel). Small in-scope judgment, dispatchable. The once-observed null-category renderer weakness stays recorded here only (objectui-side, explicitly needs its own reproduction first — not filed there this round).
    • Premise verified on origin/main @ 744b8f5: packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts:34 ("Total Users"), :57-58 (sys_session_metrics / "Active Sessions"), and the globalFilters block (field: 'created_at', defaultValue: 'last_7_days', scope: 'dashboard') all hold; system.datasets.ts:31 defines sys_session_metrics with no active-only predicate in sight.
    • Routing check: fix lands in packages/platform-objects ⇒ filer's domain:metadata matches the domain table. Kept.
    • Release read: shipped-face cosmetic/semantics mislabel, loud only to someone comparing counts — judged not RC-blocking (no data wrong at rest, no contract lie in an API); flag for re-judgment if the board is featured in release material.

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


    Generated by Claude Code

  4. self-assigned this
    on Aug 11, 2026
  5. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Claimed — domain:metadata seat.

    • Session: session_018XWfHvFJPt37LcT7SoF2dJ
    • Branch: claude/issue-7531-system-overview-tile-labels
    • Worktree: ../objectstack-7531 (dedicated)
    • File surface: packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts, .../system.datasets.ts, and the label strings under packages/platform-objects/src/apps/translations/. Thanks to the triage seat for verifying all three sites still hold on origin/main @ 744b8f5 — the dev starts from a confirmed premise rather than re-deriving it.

    ⚠️ Serialization constraint, named: #7544 (the dead JWKS nav entry) is deliberately held to a later round because it edits packages/platform-objects/src/apps/setup-nav.contributions.ts in this same package. ⛔ This dev does not touch setup-nav.contributions.ts; if the card cannot be fixed without it, the dev stops and reports rather than reaching in.

    Both tiles are dispatched as rulings hung on a premise the dev must verify BEFORE implementing, with an explicit ban on silently switching options. The card offers "opt out of the filter or relabel" for each, and which one is right is a fact about the board rather than a matter of taste:

    • "Total Users" — ruled: opt the tile out of the dateRange global filter. Premise: the System Overview board is an inventory/health board whose other tiles are not windowed counts, so the filter reaching sys_user.created_at is bare-field fan-out rather than intent. If the board turns out to be deliberately a 7-day activity board, the honest fix is the label and the dev reports that fork instead of implementing the opt-out.
    • "Active Sessions" — ruled: add the active-only predicate. Premise: sys_session_metrics carries a field that can express "active" (expiry, ended-at, status). If no such field exists the count cannot be made active-only, and the honest fix is to relabel — reported, not invented over a field that is not there.

    The pin must assert label/query agreement, not a snapshot. A test freezing today's numbers passes again the day the label drifts back. The property is that each tile's declared label and the query it issues answer the same question — the "total" must not move when dateRange changes, and the sessions count must match an independently-computed active-only count — with the opposite direction included so it cannot hold vacuously.

    Explicitly out of scope: the once-observed "Events by User" empty-axis weakness. It is an objectui renderer behaviour (null-keyed category dropped, leaving an axis with no marks and no empty state), observed once and not reproduced. It is recorded on this card so it is not lost; it needs its own reproduction before being filed on that side, and this dev does not chase it.


    Generated by Claude Code

  6. claude commented on Aug 11, 2026

    @claude
    Contributor
    {
      "issue": 7531,
      "status": "done",
      "branch": "claude/issue-7531-system-overview-tile-labels",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7614",
      "premise_still_valid": true,
      "summary": "Both dispatched premises were verified against origin/main BEFORE implementing, and both hold, so both rulings are implemented as ruled and neither tile was relabelled. Tile 1: the board is a MIXED board, not a 7-day activity board -- its header splits row 1 (Platform KPIs) from the audit rows, every row-1 tile is labelled as a stock rather than a flow, and the row-3 comment introduces globalFilters specifically as the way to scope 'these widgets', meaning the sys_audit_log charts. So the date bar reaching sys_user.created_at is #2501 fan-out, and widget_total_users now opts out with filterBindings: { created_at: false }. Tile 2: sys_session does carry fields that express 'active' exactly (ADR-0069 D4 revoked_at, plus the required expires_at), so widget_active_sessions now filters { revoked_at: null, expires_at: { $gt: '{now}' } } -- {now} is a declared date macro that resolveFilterTokens substitutes on the widget's runtimeFilter in service-analytics' dataset-executor, verified end to end before writing it, not the unsubstituted NOW()-INTERVAL shape the row-3 comment warns about. That tile opts out of the date bar too: that is what the ruling MEANS rather than scope creep, since 'currently active' is a statement about now, so an old-but-live session must still count; left windowed the tile would report 'sessions CREATED in the last 7 days that are active', neither the old number nor the labelled one. No label or translation string changed (check:i18n green, all 8 platform-objects bundles in sync). The date bar still reaches all six sys_audit_log widgets and the test asserts it. setup-nav.contributions.ts untouched per the #7544 constraint; the 'Events by User' empty-axis weakness untouched per the explicit out-of-scope instruction.",
      "tests": "The pin (packages/platform-objects/src/apps/dashboards/system-overview-tile-semantics.test.ts, 8 cases) asserts label/query AGREEMENT, not a snapshot: a total invariant across windows [7, 30, 365, null], and a sessions count equal to an active-only count derived from the fixture itself rather than from the widget's filter (deriving it from the filter would be a tautology). Each claim carries its opposite direction in the same block because each can pass vacuously: (a) 'total does not move' also holds if the harness never applies a window -> an audit tile on the same board MUST move across the same two windows (e_recent at 7d vs e_old+e_recent at 365d); (b) it also holds if no fixture row sits outside the window -> the same tile computed WITHOUT its opt-out must report strictly fewer (1 of 3); (c) 'active-only' also holds if the predicate matched everything -> the expired and revoked rows must be dropped (2 kept of 4, named row by row); (d) it also holds if the window never bit -> an old-but-live session is kept and would be dropped if windowed. Fixtures deliberately straddle the 7-day window, since an all-recent fixture makes every assertion pass for free -- the exact trap the card describes. Only the dashboard-filter->widget composition is modelled locally (that step lives in objectui; no code in this repo performs it) and it states the precedence the filterBindings contract declares, the rule @objectstack/lint's effectiveFilterField mirrors; everything downstream is real machinery -- resolveFilterTokens and matchesFilterCondition, added as devDependencies -- so a {now} that stopped resolving or a $gt that stopped meaning $gt fails here. REVERSE VERIFICATION, direction predicted before running as RED, and red is what happened: with the board file reverted and the test in place, 4 of 8 go red naming the defect rather than a shape -- 'window=7: expected [ u_this_week ] to deeply equal [ Array(3) ]', 'window=7: expected [ Array(3) ] to deeply equal [ s_active, s_old_active ]', 'expected [...] to have a length of 2 but got 4'. The 4 that stayed green are the ones that model the pre-fix board on purpose (the ignoreOptOut legs and the audit control), which is the expected direction for them. COMMANDS, all foreground under flock /tmp/os-heavy-verify.lock: `pnpm --filter '@objectstack/platform-objects^...' build` (run FIRST, per the stale-dist rule) -> 55 tasks successful; `pnpm --filter @objectstack/platform-objects test` -> Test Files 13 passed (13), Tests 304 passed (304); `pnpm --filter @objectstack/platform-objects typecheck` -> clean, no output; `pnpm exec eslint packages/platform-objects/src/apps/dashboards --max-warnings=0` -> exit 0; `node scripts/check-nul-bytes.mjs` -> OK (scanned 7047 text files, no raw ASCII control bytes) plus a manual grep -naP self-scan of the three touched files -> clean; `pnpm check:i18n` -> OK (9 packages, all bundles in sync), which required building the CLI first since the gate refuses to run without it; `pnpm check:empty-changeset` and `pnpm check:adr-0087-registration` -> both exit 0 (the changeset is patch and non-breaking, so no ADR-0087 marker is owed); and validateWidgetBindings run directly over the real board + real datasets + the 45 platform sys_* objects -> 0 errors from this change, only two pre-existing chart-config-missing warnings on the two audit charts. NOT RUN, honestly: `pnpm check:i18n-coverage` refuses to judge in this worktree -- @objectstack/connector-mcp has no build output, so 1 of 12 configs failed to lint and the gate declines a partial round rather than guess. Environmental, unrelated to this diff, needs a full pnpm build; CI does one. CI gate status at report time is in_progress -- the draft PR was opened moments ago and CI convergence is the PM's read per #6644 L2.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #7613: the same created_at fan-out still windows the other two Row 1 inventory tiles, widget_organizations ('Total organizations on the platform') and widget_packages_installed. Identical defect, identical one-line filterBindings opt-out, deliberately NOT folded in because this card names two tiles and doubling its surface is not the implementing seat's call. Searched the 187 open issues first: no twin; #7568 is the same CLASS on the showcase ops dashboard (different surface, different file) and is cross-referenced rather than duplicated. Filed unassigned and unlabeled for PM triage, since it is a concrete defect a user hits rather than an observation.",
        "not filed, deliberately, and offered only as a datum for whoever reproduces the 'Events by User' empty-axis weakness recorded on #7531: validateWidgetBindings already reports chart-config-missing on BOTH widget_events_by_type and widget_events_by_user on main ('the renderer cannot determine which measure to plot, so the series renders empty'). It fell out of a gate run here rather than a hunt, it is pre-existing and unchanged by this PR, and it is not a diagnosis -- but a reproduction attempt may want to rule it in or out before blaming the null-category drop. Not chased and not filed on objectui, per the explicit instruction that it needs its own reproduction first."
      ]
    }

    Generated by Claude Code

  7. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

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

    Both dispatched premises were verified before implementing, and both held — so both rulings shipped as ruled and neither tile was relabelled. That order was the whole point of dispatching them as rulings-with-premises rather than handing over the card's "opt out or relabel" fork, and it was followed.

    • "Total Users" — premise confirmed from the artifact, not assumed. The board is a mixed board: its header splits row 1 (Platform KPIs) from the audit rows, every row-1 tile is labelled as a stock rather than a flow, and the row-3 comment introduces globalFilters specifically to scope the sys_audit_log charts. So the date bar reaching sys_user.created_at is Dashboard-level filters (date / region) driving multiple charts #2501 fan-out, not intent. Reading the author's intent out of the board's own structure and comments is a stronger answer than the one I asked for.
    • "Active Sessions" — the field exists, and the macro was verified end to end before being written. sys_session expresses "active" exactly (ADR-0069 D4 revoked_at, plus the required expires_at), so the predicate is real rather than invented over a field that is not there. Notably the dev checked that {now} is a declared date macro actually substituted by resolveFilterTokens in service-analytics' dataset-executor — rather than emitting the unsubstituted NOW() - INTERVAL shape the row-3 comment warns about. Verifying that the macro resolves before depending on it is exactly the step that usually gets skipped.

    On the sessions tile also opting out of the date bar: that is inside the ruling, not scope creep, and the argument for it is correct. "Currently active" is a statement about now, so an old-but-live session must still count. Left windowed, the tile would have reported "sessions created in the last 7 days that are active" — neither the old number nor the labelled one, i.e. the predicate would have been added and the label would still have been lying. Adding the predicate without the opt-out would have been a half-fix that passes a naive review.

    The pin is the right shape and its anti-vacuity work is the part worth naming. Eight cases asserting label/query agreement: a total invariant across windows [7, 30, 365, null], and a sessions count equal to an active-only count derived from the fixture rather than from the widget's own filter — deriving it from the filter would have been a tautology. Each claim carries its opposite direction in the same block, because each can pass for the wrong reason: "total does not move" also holds if the harness never applies a window (so an audit tile on the same board must move across the same two windows), and also holds if no fixture row sits outside the window (so the same tile computed without its opt-out must report strictly fewer). The fixtures deliberately straddle the 7-day boundary — an all-recent fixture makes every assertion pass for free, which is precisely the trap the card describes about fresh datastores.

    Reverse verification behaved as predicted: with the board reverted, 4 of 8 go red naming the defect rather than a shape (window=7: expected [ u_this_week ] to deeply equal [ Array(3) ]), and the 4 that stayed green are the legs that model the pre-fix board on purpose. Predicting which stay green, and why, is what makes the other four meaningful.

    Verified independently of the report:

    • Path surface read from git against a freshly-fetched main: five files — the changeset, the board, the new test, and package.json + pnpm-lock.yaml for the devDependencies the test needs. No docs/adr/**, no content/docs/releases/.
    • setup-nav.contributions.ts untouched, so the Setup › Advanced › Signing Keys (JWKS) is a dead nav entry — sys_jwks is API-disabled and a requiredPermissions gate cannot prune it #7544 serialization constraint held.
    • Gate jobs' own conclusions: ESLint success, TypeScript Type Check success — and Validate Package Dependencies success, which is the gate that matters for the devDeps addition.
    • closed_by_pull_requests confirms this PR is configured to close the card, and Fixes is correct: nothing is left pending — the two extra tiles went to their own issue, and the renderer weakness stays recorded here.

    Two disclosures I'm accepting as honest rather than as gaps. pnpm check:i18n-coverage declined to run in that worktree (@objectstack/connector-mcp has no build output, so the gate refuses a partial round rather than guessing) — environmental, unrelated to this diff, and CI covers it. And the report offers a datum for whoever reproduces the "Events by User" weakness — validateWidgetBindings already reports chart-config-missing on both event charts on main — while explicitly not chasing it, not diagnosing it and not filing on objectui, per the out-of-scope instruction. Recording a lead without acting on it is the correct handling of an instruction not to chase something.

    Out-of-scope finding filed as #7613 rather than folded in: the same created_at fan-out still windows two more row-1 inventory tiles, widget_organizations and widget_packages_installed — identical defect, identical one-line opt-out. Doubling this card's surface was correctly judged not to be the implementing seat's call, and the dedup search against the 187 open issues was done first. It is unassigned and unlabeled, awaiting triage routing.


    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

pm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions