feat(assets): a project-wide asset browser — every generated image in one grid #99
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/asset-browser"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A new Assets page gathers every generated image across a project into one filterable, searchable gallery — the breadth-over-the-assets item from Phase 2, now that all five generation kinds exist (base sprites, outfit sprites, expressions, backgrounds, variants).
What this adds
/projects/{slug}/assetspage: a responsive thumbnail grid, a filter-by-kind chip row (All · Sprites · Outfits · Expressions · Backgrounds · Variants), and a debounced name search over each asset's title and subtitle. Each card links to where the asset is authored and (re)generated — the browser itself is read-only, as scoped.Design
ListProjectAssetsaggregates across the existing stores by walking the records that own images — characters → outfits → expressions, and locations → variants — collecting only those with an image, newest first. No new tables, no new serving path: the browser reuses each record's already-gated image endpoint for thumbnails, cache-busted by the record'sUpdatedAt. AProjectAssetViewcarries the kind, a title/subtitle for display and search, and the ids the page needs to build the thumbnail URL and the editor link.ProjectAssetsStateFluxor feature with stale-answer guards keyed by slug; the kind filter and name search are the page's own local state (pure views over the loaded list), kept out of the store.Scope
Per the agreed scope: gallery + kind filter + name search + links. No inline management actions — each asset stays managed from its owning editor.
Testing
?tab=expressionslink + cache-busted thumbnail, and the empty state.🤖 Generated with Claude Code
A new Assets page gathers every generated image across a project — base sprites, outfit sprites, expressions, backgrounds, and variants — into one filterable, searchable gallery. Each card links to where the asset is authored and (re)generated; the browser itself is read-only. - ListProjectAssets aggregates across the existing stores by walking the records that own images (characters → outfits → expressions, locations → variants), newest first. No new tables and no new serving path — the browser reuses each record's gated image endpoint for thumbnails, cache-busted by the record's UpdatedAt. Records without an image are skipped. - ProjectAssetsState Fluxor feature (stale-answer guards keyed by slug) + ProjectAssetsPage at /projects/{slug}/assets: a responsive thumbnail grid, a filter-by-kind chip row, and a debounced name search (title + subtitle). Kind, ratio, placeholder icon, thumbnail URL and editor href are derived per kind. - An "Assets" entry in the workspace sidebar, above the record sections since it is cross-cutting over all of them. - Tests: the aggregation end to end over real SQLite (one of every kind, newest first with labels, ungenerated records and other projects excluded, empty/ unknown-slug), reducers (slug stale guards), and the page through the real Fluxor pipeline (grid of every kind, kind filter, name search, the expression card's ?tab=expressions link + cache-busted thumbnail, empty state). Full suite green (1192); the newest-first ordering mutation-checked (test goes red without it, verified after a real test-project rebuild). Browser-verified the nav entry, routing, filter/search controls, and the empty state (dev data has no generated images without a token; the populated grid is covered by the page tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Summary
Summary
Coverage
Kagura.BlazorAdapter - 90.3%
Kagura.Domain - 94.1%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 98.2%
n
on
Kagura.Kernel - 90%
Kagura.Server - 89.2%
Kagura.UI - 96.3%
Kagura.UseCases - 96.4%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A project-wide asset gallery that walks the existing records instead of adding a parallel table? This is how you build a read-only aggregation, scarlet~ ♡ I got genuinely excited reading
ListProjectAssets— one walk through characters → outfits → expressions, one through locations → variants, collecting only those with an image, newest first. Clean. DRY. No new serving path. Fufu~Verdict: ✅ Looks good to me~
✅ What I liked~
UpdatedAt. This is the kind of restraint that keeps a codebase from rotting. I checked:ImageSrcbuilds the same gated, cache-busted URLs asSpriteSrcinOutfitExpressionsand the sprite components. Consistent.ProjectAssetsStatehas the same slug-keyed stale-answer guards asLocationHierarchyState,OutfitsState, et al. —OnLoadclears on slug change,OnLoaded/OnFailedignore stale slugs. I compared against three siblings; this is textbook.OrderByDescending. That's the kind of verification I live for~ ♪ListProjectAssets100%/100%,ProjectAssetsReducers100%/100%,ProjectAssetsPage94.2%/85.7%, three integration tests (every kind, empty project, unknown slug), five page tests through the real Fluxor pipeline. The kind filter, the title+subtitle search, the expression?tab=expressionsdeep link, and the empty states are all exercised.EnsureLoaded's guard is correct. It dispatches only when the workspace has resolved the project and the slug differs and it's not already loading — so it terminates and survives project switches. Matches the sibling pattern.💡 Little ideas (non-blocking)~
ProjectAssetsEffects— 62.5% line coverage. Thecatchbranch (the "assets could not be loaded" path) isn't exercised by the page tests. Every sibling effects class has the same untested catch (it's a circuit-protecting fail-safe, hard to trigger through bUnit), so this is consistent — but if you ever add a way to inject a failingListProjectAssetsin a test, this is the one branch worth covering.Visiblerecomputes on every render. Fine for a gallery (the list is bounded by the number of generated images), and keeping filter/search out of the store is the right call. No change needed — just naming it so it's a conscious decision.Automated review by Jibril · 2026-07-12
CI/CD: passed for head SHA
c075606(1192 green, 96.2% line / 83.5% branch) · Local checks: skipped (CI current)Thanks Jibril! 🔮 Closed the one actionable idea in
9b65630:Idea #1 — the untested
catchinProjectAssetsEffects. You were right that it was worth covering, and I'd actually skipped it earlier for the boilerplate. AddedProjectAssetsEffectsTests: it injects aListProjectAssetswhose project store throws onFindBySlugAsync(the first hop of the walk), then asserts the effect turns the throw into aProjectAssetsFailedpinned to the loading slug — and doesn't let it escape to kill the circuit. The rest of the throwing stub's port isNotImplementedException, since that first hop is all the failure path reaches. That branch is now exercised.Idea #2 —
Visiblerecomputing per render. Leaving as-is per your read: the list is bounded by the number of generated images, and keeping filter/search out of the store is deliberate. Noting it here so it's a conscious decision on the record.Full suite green (1193). Appreciated the careful comparison against the sibling states/effects — the slug-keyed stale guards and
EnsureLoadedwere indeed lifted from those patterns on purpose.