Updated the media browser with tessera#534
Merged
Merged
Conversation
The modal viewer for images now uses Tessera (OpenSeadragon under the
hood) instead of a plain `<img>`. The interaction model:
- **Initial render**: the existing `medium` variant. Already in
storage, ships fast, fills the viewport sharply. The user sees a
clean preview the moment the modal opens — no blurry lowest-
pyramid tile, no waiting for the original.
- **On real zoom (past 2x fit-to-view)**: the viewer swaps to a DZI
tile pyramid that's generated lazily, per-tile, on demand. OSD
requests only the tiles its viewport covers at the current level;
`FileController.serve_tile/2` checks storage, and on cache miss
calls `Tessera.generate_tile/4` to crop one tile from the original
and persist it via the Tessera storage adapter.
- **Storage adapter**: `PhoenixKit.Modules.Storage.TesseraAdapter`
implements `Tessera.Storage` by forwarding tile writes through
`Storage.Manager.store_file/2` under a `_tiles/` prefix, so tiles
land in every configured bucket alongside variants.
- **URL exposure**: `generate_urls_from_instances/3` now takes the
mime type and surfaces `urls["dzi"]` for image files. The manifest
URL is always set; the manifest itself materializes lazily on the
first request (it's just XML derived from the file's stored
width/height, then cached like any other tile).
- **Routes**: `/tiles/:dzi_filename` and
`/tiles/:files_segment/:level/:tile_filename` map to the new
`FileController.serve_manifest/2` + `serve_tile/2` actions; the
`/tiles/:files_segment/...` shape matches OSD's tile-URL
convention derived from a `<base>.dzi` manifest path.
`mix.exs` picks up `:tessera` as a `path:` dep until the package is
on hex.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `system_managed` flag + `parent_file_uuid` FK to
`phoenix_kit_files` so every Tessera-generated DZI tile (and manifest)
gets a real DB row, not a shadow blob:
* `system_managed BOOLEAN NOT NULL DEFAULT false` — flips MediaBrowser
listings to hide these rows and short-circuits VariantGenerator
(a 256×256 tile doesn't need its own thumbnail / medium / large)
* `parent_file_uuid` references `phoenix_kit_files(uuid)` with
`ON DELETE :delete_all` — deleting a source image cascades to its
tiles automatically (DB-level)
* partial indexes: per-parent for cleanup + listing, and a
`system_managed = false` index keyed on `inserted_at DESC` so the
user-facing "recent files" sort stays cheap as the tile catalog
grows
* drops `NOT NULL` on `user_uuid` (system rows don't have a user
owner); the changeset's `validate_system_managed_invariants`
enforces "user_uuid XOR parent_file_uuid" at the app level instead
`Storage.store_system_file/3` is the new entry point: writes through
`Manager.store_file/2` (so tiles replicate across every configured
bucket), then creates the File row + a single `"original"`
FileInstance — no variant fan-out. `TesseraAdapter.put/3` is rewired
to use it; `FileController.serve_tile/2` and `serve_manifest/2` pass
the source's UUID + content mime type via Tessera's `storage_opts`.
MediaBrowser listing/count queries (`list_files`, `list_files_in_scope`,
`orphaned_files_query`, `build_trashed_query`) get an
`exclude_system_managed` filter so tile chunks never appear as
user-facing media or as orphans.
Schema additions: `belongs_to :parent_file`, `"tile"` added to the
`file_type` allowlist. No data migration needed (default false +
nullable FK).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the previous `src` + `upgrade_src` pair with a per-image
`sources` list. `tessera_sources/1` walks `f.urls` + the new
`f.variant_widths` map and produces an ordered low → high quality
layer list:
* `medium` always — the initial render
* `large` only when its width actually exceeds medium's — small
images skip this layer to avoid a useless middle hop
* `dzi` always for images — the top of the pyramid with
infinite zoom headroom
Each non-DZI layer carries its intrinsic pixel width so Tessera's
client-side hook can compute thresholds dynamically (a layer is
"good enough" until zoom scales it past 2× its native resolution).
`generate_widths_from_instances/1` mirrors the URL builder, pulling
the existing `FileInstance.width` column (already populated by
`ImageProcessor.extract_dimensions/1` during variant generation —
no new field, no migration).
Three file-map call sites updated to thread `variant_widths`
alongside `urls`. Template's image branch swaps to `sources={tessera_sources(f)}`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `storage_tile_generation_enabled` (default `"false"`) to the
storage settings UI. With it off — the new default — the MediaBrowser
still does its multi-layer zoom (medium → large as the user zooms in),
but never surfaces a DZI manifest URL and never asks the lazy tile
endpoint for anything. ImageMagick effort is paid only when an
operator explicitly opts in via /admin/settings/media.
* `generate_urls_from_instances/3` skips the `"dzi"` key when the
setting is off — Tessera's `sources` list naturally degrades to the
raster-variant layers.
* `FileController.serve_manifest/2` and `serve_tile/2` short-circuit
to 404 ("Tile generation disabled") so a stale client URL or a
direct hit can't trigger generation when the toggle is off.
* Settings LiveView gets the new checkbox + a `toggle_form_tile_generation`
event; `apply_storage_settings` persists it alongside redundancy /
auto-variants / max-upload-size and the apply-button's dirty check
learns the extra field.
Why off by default: tile generation hits ImageMagick on every uncached
tile request, and most images in most apps don't need deep zoom. Users
who do want it flip one switch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tessera 0.1.0 is live on Hex (https://hex.pm/packages/tessera). Swap the in-tree path dep for the standard hex constraint so the package resolves the same way everywhere — no sibling-checkout assumption, parent apps consuming phoenix_kit from hex get tessera transparently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon
pushed a commit
that referenced
this pull request
May 11, 2026
Findings: two CRITICAL (unauthenticated tile endpoints; synchronous in-band tile generation = DoS surface), several HIGH (dedup race, DB-level CHECK invariant, test coverage), plus MEDIUM/NITPICK items. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
timujinne
added a commit
to timujinne/phoenix_kit
that referenced
this pull request
May 11, 2026
ddon
pushed a commit
that referenced
this pull request
May 11, 2026
Addresses the CRITICAL + HIGH items from CLAUDE_REVIEW.md for the Tessera deep-zoom feature. Auth (CRITICAL): - `/tiles/:token/:dzi_filename` and `/tiles/:token/:files_segment/...` now require a signed `URLSigner` token in the URL path. The "dzi" variant name is distinct from the storage variants so a leaked file-serving token can't grant tile access. Token lives in the path (not query) so OpenSeadragon's base-URL derivation preserves it across manifest → tile fetch transitions. Unauthorized requests return 404 (not 401/403) so UUID enumeration can't distinguish "wrong token" from "no such file". Dedup (HIGH): - V113 adds partial unique `phoenix_kit_files_system_dedup_index` on `(parent_file_uuid, file_name) WHERE system_managed = true`. - `Storage.store_system_file/3` now does check-then-insert with unique-violation recovery: if a concurrent writer wins the race, we re-fetch their row instead of bubbling a Postgrex error. - Per-`file_uuid` `:global.set_lock` mutex with double-checked locking in `FileController.ensure_manifest_cached/3` and `ensure_tile_cached/8` serializes cold-path generators for the same image while keeping different images parallel. Lock timeout surfaces as 503 + Retry-After header. DB invariants (HIGH): - V113 adds `phoenix_kit_files_user_or_parent_check` CHECK constraint enforcing `user_uuid IS NOT NULL OR parent_file_uuid IS NOT NULL` at the DB level. The schema's `validate_system_managed_invariants` is the user-facing check; this constraint is the safety net for raw inserts / Repo.insert_all. Tempfile safety (HIGH): - All `Manager.retrieve_file` + `Tessera.generate_tile` tempfile lifecycles wrapped in `try/after` so exceptions don't leak files into `System.tmp_dir!()`. Test coverage (HIGH): - New `test/phoenix_kit/migrations/v113_test.exs` pins each V113 addition: column shape, nullability, FK cascade, three indexes, the new CHECK constraint (with a raw-SQL test that double-null inserts are rejected), and the `phoenix_kit_comment_media` table. Misc: - Dropped the misleading "previously V112 in dev branches" sentence from V113's moduledoc and the migrator changelog — public consumers will never hit that path. - Fixed credo issues: aliased `Manager` in storage.ex's new helper; extracted nested case blocks into private functions to satisfy the max-depth check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon
pushed a commit
that referenced
this pull request
May 11, 2026
Republishing 1.7.108 within the 1-hour Hex replace window so the Tessera deep-zoom feature (PR #534) and its review-fix follow-ups ship under the same version, rather than as a separate 1.7.109. mix.lock locks `tessera 0.1.0`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
timujinne
added a commit
to timujinne/phoenix_kit
that referenced
this pull request
May 12, 2026
8 tasks
mdon
added a commit
to mdon/phoenix_kit
that referenced
this pull request
May 12, 2026
Upstream's V113 (PR BeamLabEU#534, Tessera tile media) added the `phoenix_kit_files_user_or_parent_check` CHECK constraint requiring `user_uuid IS NOT NULL OR parent_file_uuid IS NOT NULL`, but the existing `create_file!/1` fixtures in three test files insert `phoenix_kit_files` rows with neither column set — fixture pattern predates V113. Result: 23 failures across: * `test/integration/storage/scope_test.exs` (17) * `test/integration/media_browser_scope_test.exs` (4) * `test/integration/phoenix_kit_web/components/media_browser_test.exs` (2) All three reproduce on clean `upstream/dev` (commit `93f242b3`) with zero changes from this PR — verified via a separate `git worktree`. Fix: each `create_file!/1` now stamps `user_uuid` from a new memoised `ensure_user!/0` helper. The helper registers an owner user once per test process (via `PhoenixKit.Users.Auth.register_user/1` + `Process.put/2`) and reuses the uuid on subsequent calls — keeps the existing call sites unchanged, avoids the unique-email collision that would fire if we registered fresh per file. The user identity itself is incidental to every assertion in these tests; it exists only to satisfy the V113 CHECK. Folded into this PR (rather than a follow-up) since the rebase brought the broken upstream tests into our `mix test` baseline, and leaving them red would mask future regressions in the next sweep. Same fix-shape would apply on `upstream/dev` directly — if/when alexdont or @timujinne ship a patch there, this commit should merge cleanly with theirs (the bodies of `create_file!/1` are otherwise unchanged). Verification: full `mix test` 1229 tests / 1 failure (down from 24 on the unfixed baseline). The lone remaining failure is the pre-existing `Permissions.module_label("db")` returning `"Db"` instead of `"DB"` — flagged in an earlier session, unrelated.
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.
No description provided.