Skip to content

Add piece detail view with local favorites#10

Merged
kossoy merged 2 commits into
mainfrom
feat/folio-2-7-piece-detail-image-first--provenance--fa
Jun 25, 2026
Merged

Add piece detail view with local favorites#10
kossoy merged 2 commits into
mainfrom
feat/folio-2-7-piece-detail-image-first--provenance--fa

Conversation

@kossoy

@kossoy kossoy commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Refs #7

What changed

  • Added a /pieces/:photoId image-first detail route backed by the existing photo detail API.
  • Added provenance fields to photo detail responses: source, provider, category, plus favorite state.
  • Persisted favorites locally on downloaded_photos.favorite through the existing favorite endpoints.
  • Linked gallery cards to piece detail pages and added API/database tests for provenance and favorite persistence.
  • Fixed review feedback: direct navigation to /pieces/:photoId now falls back to the embedded dashboard, and gallery favorite facets/filters now prefer the canonical favorite column that the toggle writes.

Verification

$ git fetch origin
$ git rebase origin/main
Current branch feat/folio-2-7-piece-detail-image-first--provenance--fa is up to date.

$ go test ./internal/api ./internal/database
# github.com/mattn/go-sqlite3
sqlite3-binding.c: In function 'sqlite3ShadowTableName':
sqlite3-binding.c:123133:9: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
ok  	ok-folio/internal/api	2.308s
ok  	ok-folio/internal/database	0.092s

$ ./scripts/product-verifier.sh
npm warn Unknown builtin config "globalignorefile". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
npm warn deprecated glob@10.5.0: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me

added 313 packages, and audited 314 packages in 4s

76 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
npm warn allow-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn allow-scripts   esbuild@0.28.1 (postinstall: node install.js)
npm warn allow-scripts
npm warn allow-scripts Run `npm approve-scripts --allow-scripts-pending` to review, or `npm approve-scripts <pkg>` to allow.
npm warn Unknown builtin config "globalignorefile". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.

> dashboard@0.0.0 build
> tsc -b && vite build

vite v7.3.6 building client environment for production...
transforming...
✓ 906 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                   0.68 kB │ gzip:  0.35 kB
dist/assets/index-mnmuewfG.css   21.50 kB │ gzip:  4.68 kB
dist/assets/query-Dk8W7wRr.js    34.83 kB │ gzip: 10.31 kB
dist/assets/index-Krc35nC_.js    73.31 kB │ gzip: 14.09 kB
dist/assets/react-DD2qiXMp.js   229.91 kB │ gzip: 73.54 kB
dist/assets/charts-CZ01G27q.js  335.22 kB │ gzip: 99.81 kB
✓ built in 3.63s
# github.com/mattn/go-sqlite3
sqlite3-binding.c: In function 'sqlite3ShadowTableName':
sqlite3-binding.c:123133:9: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
?   	ok-folio/cmd/extractor	[no test files]
?   	ok-folio/dashboard/node_modules/flatted/golang/pkg/flatted	[no test files]
ok  	ok-folio/internal/api	(cached)
ok  	ok-folio/internal/config	(cached)
?   	ok-folio/internal/dashboard	[no test files]
ok  	ok-folio/internal/database	(cached)
?   	ok-folio/internal/exif	[no test files]
ok  	ok-folio/internal/gallery	(cached)
?   	ok-folio/internal/photoprism	[no test files]
?   	ok-folio/internal/provider	[no test files]
ok  	ok-folio/internal/provider/telegram	(cached)
ok  	ok-folio/internal/provider/webgallery	(cached)
?   	ok-folio/internal/scheduler	[no test files]
?   	ok-folio/internal/scraper	[no test files]
ok  	ok-folio/internal/testguard	(cached)
ok  	ok-folio/pkg/retry	(cached)

Maestro-Backend: codex (0-end)

Greptile Summary

Adds an image-first /pieces/:photoId detail route, migrates favorites from PhotoPrism to local downloaded_photos.favorite, and surfaces provenance fields (source, provider, category) on photo detail responses. The gallery modal is replaced with navigation to the new route.

  • DownloadedPhoto.Favorite is added as a first-class GORM column with AutoMigrate; SetPhotoFavorite and the refactored favorite handlers write and read it directly, removing the PhotoPrism dependency entirely.
  • galleryFavoriteColumn priority ordering is fixed to prefer the canonical \"favorite\" column over legacy aliases, and the test workaround that manually ALTER TABLEd the schema is removed.
  • galleryCategoryIDFromSourcePage added to gallery.go is byte-for-byte identical to the pre-existing categoryIDFromSourcePage in database.go — the only code-quality concern in this PR.

Confidence Score: 4/5

Safe to merge; all tests pass, the PhotoPrism dependency is cleanly removed, and the new route and DB migration are correctly wired together.

The change is well-scoped, well-tested (full add/get/remove favorite cycle, provenance field assertions, dashboard-route coverage), and passes the product verifier. The one concern is the duplicated category-ID parser introduced in gallery.go — it's harmless today but can silently diverge from the database-layer copy in future edits.

internal/api/gallery.go — galleryCategoryIDFromSourcePage duplicates categoryIDFromSourcePage in database.go.

Important Files Changed

Filename Overview
internal/api/gallery.go Adds galleryCategoryIDFromSourcePage — identical logic to pre-existing categoryIDFromSourcePage in database.go; duplication risk.
internal/api/favorites.go Replaces PhotoPrism-backed favorite handlers with a clean local-DB implementation; logic is correct and well-extracted into photoFromRoute.
internal/database/database.go Adds Favorite column with AutoMigrate, SetPhotoFavorite helper, and fixes galleryFavoriteColumn priority ordering to prefer canonical "favorite" column.
dashboard/src/pages/PieceDetail.tsx New piece detail page; valid ID guard, loading/error states, and invalidation of both photo and gallery-catalog query keys are correct.
internal/api/api_test.go New tests cover provenance field extraction, favorite persistence (add/get/remove cycle), and dashboard route inclusion for /pieces/.
internal/database/database_test.go Removes legacy ALTER TABLE workaround now that Favorite is a first-class column; adds TestGalleryFavoriteColumnPrefersCanonicalFavorite covering priority-ordering fix.
dashboard/src/pages/Gallery.tsx Removes modal in favor of Link routing to PieceDetail; unused state and imports cleaned up correctly.
dashboard/src/types.ts Adds Favorite to Photo and source/provider/category/favorite to PhotoDetailResponse, matching backend JSON shape.
internal/api/images.go Adds source, provider, category, and favorite fields to photo detail response using existing helper functions.
internal/api/dashboard.go Registers /pieces/ prefix as a dashboard fallback route so direct navigation to /pieces/:id serves the SPA index.
dashboard/src/App.tsx Imports and mounts PieceDetail at /pieces/:photoId; straightforward single-line change.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
internal/api/gallery.go:280-304
**Duplicate of `categoryIDFromSourcePage` in `database.go`**

`galleryCategoryIDFromSourcePage` added here is byte-for-byte identical to the unexported `categoryIDFromSourcePage` at `internal/database/database.go:597`. Both functions parse the same path/query pattern and return the same values. The two copies can silently drift — for example, a future tweak to query-parameter keys (`"cat"`, `"category_id"`) would need to be applied in both packages, and a miss would cause the facet filter and the detail-page category display to disagree.

Reviews (1): Last reviewed commit: "Fix piece route fallback and favorite co..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5025b0dbf0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread dashboard/src/App.tsx
<Route path="/search" element={<Search />} />
<Route path="/artists" element={<ArtistList />} />
<Route path="/artists/:artistName" element={<ArtistDetail />} />
<Route path="/pieces/:photoId" element={<PieceDetail />} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serve the new piece route on direct navigation

This client-only route is not added to internal/api/dashboard.go:isDashboardRoute, which is the server-side allowlist used to fall back to index.html for BrowserRouter paths. In the embedded dashboard, clicking from the gallery works after the SPA is loaded, but reloading/bookmarking or opening /pieces/:photoId in a new tab gets a 404 because the server doesn't recognize the path.

Useful? React with 👍 / 👎.

Comment on lines +347 to +349
return db.DB.Model(&DownloadedPhoto{}).
Where("id = ?", id).
Update("favorite", favorite).Error

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read and write the same favorite column

When an adopted database already has one of the supported legacy favorite columns such as favorites or is_favorite, AutoMigrate adds the new favorite column but galleryFavoriteColumn() can still select the existing legacy column for catalog filters/facets while this setter always writes only favorite. In that environment toggling a piece succeeds and detail reads photo.Favorite, but the gallery favorite facet/filter continues to use the old column, so the toggle does not show up there; prefer the new column deterministically or migrate/sync the legacy values before writing only favorite.

Useful? React with 👍 / 👎.

Comment thread internal/api/gallery.go
Comment on lines +280 to +304
func galleryCategoryIDFromSourcePage(sourcePage string) string {
if sourcePage == "" {
return "unknown"
}
parsed, err := url.Parse(sourcePage)
if err != nil {
return "unknown"
}

parts := strings.Split(strings.Trim(parsed.EscapedPath(), "/"), "/")
for i := 0; i < len(parts)-1; i++ {
if strings.EqualFold(parts[i], "category") && parts[i+1] != "" {
return parts[i+1]
}
}

query := parsed.Query()
for _, key := range []string{"category", "category_id", "cat"} {
if value := strings.TrimSpace(query.Get(key)); value != "" {
return value
}
}

return "unknown"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate of categoryIDFromSourcePage in database.go

galleryCategoryIDFromSourcePage added here is byte-for-byte identical to the unexported categoryIDFromSourcePage at internal/database/database.go:597. Both functions parse the same path/query pattern and return the same values. The two copies can silently drift — for example, a future tweak to query-parameter keys ("cat", "category_id") would need to be applied in both packages, and a miss would cause the facet filter and the detail-page category display to disagree.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/api/gallery.go
Line: 280-304

Comment:
**Duplicate of `categoryIDFromSourcePage` in `database.go`**

`galleryCategoryIDFromSourcePage` added here is byte-for-byte identical to the unexported `categoryIDFromSourcePage` at `internal/database/database.go:597`. Both functions parse the same path/query pattern and return the same values. The two copies can silently drift — for example, a future tweak to query-parameter keys (`"cat"`, `"category_id"`) would need to be applied in both packages, and a miss would cause the facet filter and the detail-page category display to disagree.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@kossoy kossoy merged commit 1a48a78 into main Jun 25, 2026
2 checks passed
@kossoy kossoy deleted the feat/folio-2-7-piece-detail-image-first--provenance--fa branch June 25, 2026 19:23
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