dashboards(tiles): each tile can measure its OWN slice, not the view's total (#304) - #331
Merged
Merged
Conversation
…s total (#304) Every tile on a dashboard necessarily showed the same number, because the only scope was the view's single filter. The founder's screenshot: three tiles, all reading 303, one of them labelled "Count of Epics Opened" — the label was free text and lied. "it's like bullshitty functionality now. like I can't select what to show. Except for how to aggregate." A tile now carries its own `filter`, ANDed with the view's (and the viewer's personal override). Same filter AST as views / /records/query / rollups — not a second condition language — and the same `FiltersSection` builder the view toolbar uses, so there's one filter UI in the product rather than two. Implementation note: a tile is now its own component that owns its query, rather than every tile sharing one page-level fetch. That means the tile's scope goes to the SERVER, so operator semantics are the server's (no client-side filter evaluator to drift), and results stay grant-scoped — a tile can never read past the viewer's access. Two tiles with identical scope share one request; react-query dedupes on the query key, so N tiles is not N round trips. `cleanViewConfig` prunes a tile filter's dead conditions exactly as it prunes the view's, but keeps the TILE — #305's rule: unconfigured/partially-dead is not junk. Scope: same-database only. Cross-database tiles are the other half of #304 and are deliberately separate — a per-tile database needs viewer-scoped access checks, and that belongs in its own reviewable change. Widget (chart) filters likewise wait for their fetch, so no field is added that would be silently ignored. Verified: API suite 1824 passed / 187 files (only backup-restore.test.ts fails — needs Docker, documented); web 491 passed; lint, typecheck, build green; SDK regenerated for the schema change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
storyos-docs | ed22954 | Commit Preview URL Branch Preview URL |
Aug 12 2026, 10:41 PM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First half of #304 — the half that makes a dashboard mean anything.
The problem
Every tile showed the same number, because the only scope was the view's single
filter. Three tiles reading
303, one labelled "Count of Epics Opened" — a free-textlabel describing something it wasn't measuring.
What changed
A tile carries its own
filter, ANDed with the view's filter and the viewer'spersonal override. It uses:
/records/query/ rollups — not a secondcondition language;
FiltersSectionbuilder the view toolbar uses — one filter UI in theproduct, not two.
Why a tile is now its own component
It owns its query. That's deliberate:
no client-side filter evaluator that can drift from it;
query key), so N tiles is not N round trips.
cleanViewConfigprunes a tile filter's dead conditions exactly as it prunes theview's — but keeps the tile. That's #305's rule holding: partially-dead is not junk.
Deliberately not in this PR
database_idneeds viewer-scoped accesschecks — a mistake there turns a dashboard into a data-leak surface. That deserves
its own reviewable change, not a footnote in this one.
a
filterfield to the widget schema — a field that would be accepted and thensilently ignored is worse than no field.
Verification
key) and a condition on a deleted field is pruned while the tile itself
survives.
backup-restore.test.tsfails — needsDocker, documented in CLAUDE.md). Web 491 passed.
pnpm lint,pnpm typecheck,pnpm buildgreen;pnpm sdk:generaterun and thedrift committed.
apps/web). Worth confirming after deploy: twotiles, same database, different filters → different numbers.
🤖 Generated with Claude Code