Skip to content

dashboards(tiles): each tile can measure its OWN slice, not the view's total (#304) - #331

Merged
ikrasovytskyi merged 1 commit into
mainfrom
web/304-per-tile-filter
Aug 12, 2026
Merged

dashboards(tiles): each tile can measure its OWN slice, not the view's total (#304)#331
ikrasovytskyi merged 1 commit into
mainfrom
web/304-per-tile-filter

Conversation

@ikrasovytskyi

Copy link
Copy Markdown
Contributor

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-text
label describing something it wasn't measuring.

"it's like bullshitty functionality now. like I can't select what to show. Except
for how to aggregate..."

What changed

A tile carries its own filter, ANDed with the view's filter and the viewer's
personal override. It uses:

  • the same filter AST as views / /records/query / rollups — not a second
    condition language;
  • the same FiltersSection builder the view toolbar uses — one filter UI in the
    product, not two.

Why a tile is now its own component

It owns its query. That's deliberate:

  • the tile's scope goes to the server, so operator semantics stay the server's —
    no client-side filter evaluator that can drift from it;
  • results stay grant-scoped, so 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. That's #305's rule holding: partially-dead is not junk.

Deliberately not in this PR

  • Cross-database tiles. A per-tile database_id needs viewer-scoped access
    checks — a mistake there turns a dashboard into a data-leak surface. That deserves
    its own reviewable change, not a footnote in this one.
  • Widget (chart) filters. Their fetch isn't per-widget yet, so I did not add
    a filter field to the widget schema — a field that would be accepted and then
    silently ignored is worse than no field.

Verification

  • 2 new unit tests: a tile filter survives the read path (zod would strip an unknown
    key) and a condition on a deleted field is pruned while the tile itself
    survives.
  • API suite 1824 passed / 187 files (only backup-restore.test.ts fails — needs
    Docker, documented in CLAUDE.md). Web 491 passed.
  • pnpm lint, pnpm typecheck, pnpm build green; pnpm sdk:generate run and the
    drift committed.
  • Not browser-verified (no jsdom in apps/web). Worth confirming after deploy: two
    tiles, same database, different filters → different numbers.

🤖 Generated with Claude Code

…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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@ikrasovytskyi
ikrasovytskyi added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit c7b204d Aug 12, 2026
4 checks passed
@github-actions
github-actions Bot deleted the web/304-per-tile-filter branch August 12, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant