Commit Graph

11485 Commits

Author SHA1 Message Date
Vitor Pamplona ce6741ce92 Merge pull request #2450 from vitorpamplona/claude/add-pdf-preview-DIUJE
Add PDF viewing support with preview cards and full-page viewer
2026-04-19 13:25:47 -04:00
Claude 496cc9876f fix(pdf): revert RGB_565 — PdfRenderer requires ARGB_8888
PdfRenderer.Page.render() only accepts ARGB_8888 bitmaps; RGB_565 is
silently rejected (the renderer produces no output), which is why the
preview card and viewer both stopped showing anything. Go back to
ARGB_8888 and update the memory estimates in the comments. The other
perf wins from 49e913b (FilterQuality.Medium, 4096 hi-res cap, 1.5x
threshold, remembered ImageBitmap wrapper) are unaffected and stay.
2026-04-19 17:19:36 +00:00
Claude 49e913bec4 perf(pdf): reduce pan/zoom jitter in viewer dialog
The main cost during a live pan/zoom is the GPU re-sampling the page
bitmap every frame under the zoomable modifier's transform. Three
changes bring that cost down substantially:

- Bitmap.Config.RGB_565 instead of ARGB_8888. PDFs are opaque, so the
  alpha channel is unused. Halves texture memory and GPU upload
  bandwidth; visually identical.
- HI_RES_MAX_DIM_PX lowered from 6144 to 4096. 4096 stays within the
  common GPU texture-size limit, keeping rendering hardware-accelerated
  on mid-range devices. Peak bitmap is now ~24 MB (A4, RGB_565) instead
  of ~107 MB (ARGB_8888, 6144).
- FilterQuality.Medium (bilinear) instead of High (bicubic/Mitchell).
  High is recomputed per frame during pan/zoom and is the main source
  of jitter on multi-megapixel sources. At 3072-4096 px base resolution
  bilinear is effectively indistinguishable.
- HI_RES_ZOOM_THRESHOLD raised from 1.2 to 1.5 so we don't trigger a
  costly re-render for marginal zoom levels where the base is fine.
- Cache the ImageBitmap wrapper via remember to avoid per-recomposition
  allocations.
2026-04-19 16:59:23 +00:00
Claude 640848a2f6 fix(pdf): always scale render to target, never keep 72-DPI native size
PdfRenderer.Page.getWidth()/getHeight() return the page size in
PostScript points (1/72"), so for a standard A4 that's 595 x 842
"pixels" passed to cappedRenderSize. The old early-return kept native
dimensions whenever longest <= targetDim (which was always), so the
base bitmap was 595 x 842 regardless of what we asked for — effectively
72 DPI. Zoom-aware re-renders hit the same early return and also
stayed tiny, which is why double-tap produced no visible improvement.

Remove the early return: since PDFs are vector, always rescale the
point dimensions so the longest side equals targetDim. Base renders
now produce ~3072 px bitmaps and the hi-res path actually goes to
6144 px when zoomed.
2026-04-19 16:45:17 +00:00
Claude 2e4a985b36 feat(images): load full-resolution source in the image dialog
SubcomposeAsyncImage was being called with just the URL (or local
File), so Coil auto-sized the decode target to the composable's
layout bounds. In the feed that's desirable — a small thumbnail
decode — but in the zoomable dialog the composable is fillMaxWidth
(~screen width), and pinch-zooming then GPU-upscales the already
downsampled bitmap, producing the same blurriness the PDF viewer
had before.

Add a fullResolution: Boolean = false flag to UrlImageView and
LocalImageView. When true, swap the model for an ImageRequest with
size(Size.ORIGINAL) so Coil decodes at the image's native
dimensions. Feed call sites keep the default (fast, sampled). Both
dialog call sites in ZoomableContentDialog now pass
fullResolution = true, so pinch-zoom shows native pixels.

The existing blurhash/aspect-ratio placeholder path already covers
the brief high-res load window in the dialog.
2026-04-19 16:32:34 +00:00
Claude 0c88b83c3f feat(pdf): wire double-tap to toggle zoom in viewer
The zoomable library's onDoubleTap callback is not wired by default,
so double-tapping the PDF page did nothing — no scale change, no
hi-res re-render. Pass an onDoubleTap handler that calls
zoomState.toggleScale(2.5x, tapPosition). The scale animation runs
through the same Animatable snapshotFlow already observes, so the
debounced hi-res render kicks in automatically once the animation
settles.
2026-04-19 16:14:43 +00:00
Vitor Pamplona 5aa63c96a6 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-19 12:11:12 -04:00
Vitor Pamplona a9fba25acd ignores the state when running marmot tests 2026-04-19 12:09:53 -04:00
Vitor Pamplona e5a87a83ae Adds a quick exit to the address parser 2026-04-19 12:09:35 -04:00
Vitor Pamplona be24dc34e4 Merge pull request #2448 from vitorpamplona/claude/unpin-deleted-posts-gTD4S
Add deleted items banner to bookmarks, pins, and bookmark groups
2026-04-19 11:55:43 -04:00
Claude e244271bd0 feat(pdf): zoom-aware hi-res re-render for crisp pinch-zoom
Base bitmap stays at 3072 px for the initial render. When the user
zooms past 1.2x and scale settles for 200 ms, asynchronously re-render
the current page at (VIEWER_MAX_DIM_PX * scale) capped at 6144 px
(~100 MB peak for an A4 page). Swap the hi-res bitmap in while zoomed;
revert to base when scale drops back under threshold or the page
leaves composition. Only the currently-focused page gets the hi-res
treatment.

Also apply FilterQuality.High to the Image composable in both the
viewer and the preview card so any residual GPU upscaling uses
bicubic-ish sampling instead of bilinear.

Factored the render path into a shared renderPageCatching() helper.
2026-04-19 15:46:25 +00:00
Claude 013c211e6a fix(pdf): top-align viewer controls and bump render resolution
- Wrap pager + controls Row in a Box(fillMaxSize) and align the Row to
  TopCenter so the back/share buttons sit at the top of the screen,
  matching ZoomableContentDialog's layout. They were previously
  vertically centered.
- Raise VIEWER_MAX_DIM_PX from 2048 to 3072 so pinch-zoomed pages stay
  legible. A4-sized pages now render at ~26 MB each (ARGB_8888).
- Replace the unbounded mutableStateMapOf page cache with a tiny
  LinkedHashMap-based LRU (PAGE_CACHE_SIZE = 3) to keep total bitmap
  memory around 80 MB regardless of PDF length. The cache only feeds
  produceState's initial value, so it doesn't need to be a snapshot
  state.
2026-04-19 15:21:01 +00:00
Claude c2101c16dc fix(pdf): avoid closing renderer via delegated read in DisposableEffect
DisposableEffect(handleState) captured handleState as a property
delegate, so onDispose read the *current* delegated value at dispose
time. When the async load transitioned handleState from null to the
new handle, the previous DisposableEffect(null) was forgotten and its
onDispose fired — reading the freshly-created handle via the delegate
and closing it immediately. That left the dialog stuck on the loading
spinner (page renders bailed out due to the closed flag) and double-
closed the renderer on dismiss.

Capture the handle as a local val before DisposableEffect so the
lambda closes the specific handle that was current at effect creation.
2026-04-19 15:02:46 +00:00
Vitor Pamplona 368928e59f Merge pull request #2445 from nrobi144/feat/desktop-app-drawer
feat(desktop): App Drawer with workspaces, customizable nav bar
2026-04-19 10:57:23 -04:00
Vitor Pamplona 5240f29634 Merge pull request #2447 from vitorpamplona/claude/content-warning-media-overlay-zjdoP
Refactor content warning UI with backdrop support and image preloading
2026-04-19 10:56:56 -04:00
Claude 876513ad58 fix(pdf): capture page count eagerly to survive PagerState teardown
PagerState's saveable reads the pageCount lambda during composition
teardown, which runs *after* DisposableEffect.onDispose has already
closed the PdfRenderer. That triggered
IllegalStateException("Document already closed") the first time the
dialog was dismissed.

Store pageCount as a stored val sampled at handle construction instead
of re-querying the renderer. Also add a @Volatile closed flag so the
PdfPageView render coroutine bails out if it wakes up after close();
existing catch clause still swallows any narrow race.
2026-04-19 14:55:32 +00:00
Claude dcaa6607ea perf(feeds): remember isSensitive and content warning reasons per note
These derivations iterate every tag and parse every iMeta, so running
them on each recomposition churns through feed scrolls. Cache them in
remember(note) alongside the other per-note derivations.
2026-04-19 14:47:20 +00:00
nrobi144 cb306b5219 fix(desktop): icon picker shows selected state with background highlight
Replace tint-only selection with Surface background + primaryContainer
color so the selected icon is clearly visible in the workspace editor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 05:55:09 +03:00
nrobi144 486ff2e8cf fix(desktop): UI polish — single-line layout toggle, checkmark margin
- Shorten "Single Pane" to "Single" in SegmentedButton to prevent wrapping
- Add 8dp left margin before checkmark in workspace card

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 05:52:02 +03:00
nrobi144 200c5227fc refactor(desktop): pin/unpin syncs to active workspace
PinnedNavBarState now takes WorkspaceManager reference. Pin/unpin
actions update the active workspace's singlePaneScreens list,
making sidebar customization and workspace editing the same action.

- PinnedNavBarState.syncToWorkspace() updates active workspace on pin/unpin
- PinnedNavBarState.loadFromWorkspace() loads from active workspace
- Remove separate DesktopPreferences.pinnedNavItems persistence
- Workspace is the single source of truth for nav bar screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 05:40:28 +03:00
nrobi144 0455d1f8c6 feat(desktop): single-pane workspaces store nav bar screen list
Change Workspace.singlePaneScreen (single string) to
singlePaneScreens (list of typeKey strings). First screen is
the default. On workspace switch, loads the screen list into
PinnedNavBarState and navigates to the first screen.

- Add SinglePaneScreensEditor to workspace editor dialog
- Add PinnedNavBarState.loadFromList() for workspace-driven nav
- Backward compat: load old "singlePaneScreen" format as single-item list
- Cmd+Shift+S captures current deck columns as singlePaneScreens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 05:21:12 +03:00
Claude 9fbe8eba78 refactor(pdf): share Coil disk cache, gate on showImages, cap bitmap size
- PdfFetcher now reuses Amethyst.instance.diskCache (Coil) via
  openSnapshot/openEditor instead of a custom cacheDir folder. PDFs share
  the same LRU budget as images and benefit from automatic eviction.
- PdfPreviewCard now respects accountViewModel.settings.showImages():
  when disabled, shows a lightweight "Tap to load PDF" placeholder and
  only downloads+renders after the user opts in.
- Both the card thumbnail (1600px) and viewer page (2048px) bitmaps are
  capped to a maximum longest-side dimension, preventing OOM on very
  large or unusually tall PDF pages.
- PdfViewerDialog holds the cache snapshot for the dialog's lifetime so
  the underlying file can't be evicted mid-view, and closes it in
  DisposableEffect alongside the renderer and ParcelFileDescriptor.
2026-04-18 23:59:50 +00:00
Claude 60a56440d1 refactor(feeds): unify content warning behind ContentWarningGate
Collapse the per-media, per-blurhash and grid-of-blurhash variants into a
single ContentWarningGate that takes a backdrop slot and a sizing
modifier. Callers choose the backdrop (single blurhash, grid of
blurhashes, or none).

Remove the isSensitive field from MediaUrl{Image,Video}; it was a
rendering concern leaked into a media model. Sensitivity is now only
expressed to the gate. Picture/Video feeds and their display variants
compute isSensitive + reasons themselves and wrap with the gate.

ZoomableContentView keeps a default internal gate (fires when
content.contentWarning != null) so the 14 callers that don't need
grid-level handling still get the blurhash overlay automatically.

Also rename collectPictureReasons to collectContentWarningReasons.
2026-04-18 23:58:36 +00:00
Claude ecdbc80fc1 feat: preview PDF links inline with first-page thumbnail and full pager
Adds MediaUrlPdf / PdfSegment to the rich-text pipeline so PDF URLs
(detected by .pdf extension, NIP-92 imeta m tag, or application/pdf
Content-Type) render a card showing the first page, filename, and page
count. Tapping opens a full-screen HorizontalPager over every page
rendered on demand with PdfRenderer. Long-press surfaces the existing
share menu. Uses only the built-in Android PdfRenderer; no new
dependencies. Desktop continues to fall back to a clickable link.
2026-04-18 23:06:49 +00:00
Claude d34b5702a5 feat(feeds): single grid-level content warning with distinct reasons
Multi-image picture posts now show one warning covering the whole grid
instead of one per image (which required tapping each individually). The
warning backdrop is a grid of the images' blurhashes, and the overlay
lists any distinct reasons collected from both the event's content-warning
tag and per-imeta content-warning fragments as chips. Preloads every
image in the grid while the warning is visible.
2026-04-18 22:59:39 +00:00
Claude 85fcf50df9 feat: warn and offer cleanup for deleted items in pin and bookmark lists
Adds a dismissible top-of-screen banner to the Pinned Notes, Bookmarks,
Old Bookmarks, and Bookmark Set (detail) screens that appears when one
or more listed items have been deleted by their author (kind-5 deletion,
pubkey-matched via DeletionIndex). Tapping "Remove from list" rewrites
the underlying list event (NIP-51 kind 10001/10003/30001/30003) with
the deleted entries stripped out and broadcasts it once.

The scan is scoped to each screen's loaded list (typically <20 items)
and runs only while composed, so it avoids the performance hit of
hooking Account.deletedEventBundles, which fires for every kind-5
deletion in the firehose. Public and private (NIP-44 encrypted)
bookmarks are both cleaned in a single resign() call per list.
2026-04-18 22:58:54 +00:00
Claude 00ab199e32 fix(feeds): clip + preload content warning on cropped grid cells
- Pass contentScale through to ContentWarningOverBlurhash so grid cells
  (ContentScale.Crop) use fillMaxSize instead of forcing the image's
  native aspect ratio, keeping the blurhash and overlay within the cell.
- Clip the overlay to its bounds to stop the icon/button from drawing
  over neighboring cells or other components.
- Preload the image through Coil while the warning is visible so the
  image is cached by the time the user taps "Show anyway".
2026-04-18 22:36:04 +00:00
Vitor Pamplona aca37b80db Merge pull request #2444 from vitorpamplona/claude/optimize-okhttp-images-N3KJA
Optimize media loading with improved HTTP connection pooling
2026-04-18 18:13:54 -04:00
Claude 6dc723ae2c feat: auto-unpin deleted posts from NIP-51 pin list
When a kind-5 deletion event arrives for a note that is currently pinned,
rewrite the user's PinListEvent to drop the deleted entries and broadcast
the new list. Mirrors the existing deletedNotes() pattern used by
peopleLists, followLists, and labeledBookmarkLists in Account.

Previously the Pinned Notes screen silently hid deleted posts via the
FeedContentState deletion filter, leaving orphan entries in the pin list
that the user had no way to clean up.
2026-04-18 21:07:16 +00:00
Claude 79cb480ea4 perf(media-http): log phase timings and dispatcher queuing
Adds MediaCallEventListener on the images/videos OkHttpClient. For each
call it records DNS, TCP, TLS, TTFB, total time, connection reuse, and
the dispatcher queue depth at call start.

Release builds log only when a call is slow (>= 1.5s), was queued by
the dispatcher (saturation of maxRequests / maxRequestsPerHost), or
failed. Debug builds log every call.

Purpose: give us signal on whether the new 16/host + 128/total limits
are still the ceiling, and whether remaining latency is DNS, TLS, or
server-side.

https://claude.ai/code/session_01PUbqGyUc6oq6V1MdmLi8sw
2026-04-18 20:46:53 +00:00
Claude 9cca64f6ac perf: tune image/video OkHttp dispatcher and connection pool
OkHttp's default dispatcher caps inflight requests per host at 5 and
total inflight at 64. Amethyst feeds typically pull most media from a
single host (e.g. a primary Blossom/imgproxy server), so that per-host
cap serialized feed loading — the browser-feels-faster effect.

- Give the images/videos factory a dedicated Dispatcher (16 per host,
  128 total on device; 5/64 on emulator).
- Give it a larger ConnectionPool (32 idle, 5 min keep-alive) so HTTP/2
  connections to the common media host stay warm across scrolls and
  avoid repeated TLS handshakes.
- Extract isEmulator() to a shared helper used by both the relay and
  image/video factories.

The relay factory is untouched (already tuned for websockets).

https://claude.ai/code/session_01PUbqGyUc6oq6V1MdmLi8sw
2026-04-18 19:34:12 +00:00
Claude 1166654149 refactor(feeds): push content warning into ZoomableContentView per media
Move the blurhash-aware sensitivity warning from feed card wrappers into
ZoomableContentView so every media item shows its own warning sized to
its own aspect ratio over its own blurhash. Grid posts now warn per
image instead of once over the whole grid.

- Add isSensitive to MediaUrl{Image,Video} (defaults to contentWarning != null)
- Populate isSensitive and contentWarning when building media in the
  picture, video, and file-header feeds plus their display variants
- Drop the outer SensitivityWarning wrappers now handled inside
  ZoomableContentView via SensitivityWarningOverBlurhash
2026-04-18 18:51:17 +00:00
Claude 75c96115cb feat(feeds): overlay content warning on blurhash at media size
In the picture and video feeds, render the sensitivity warning over the
media's blurhash placeholder using the media's aspect ratio, instead of
a fixed-size dialog that hides the context entirely. Falls back to the
existing dialog when no blurhash is available.
2026-04-18 15:53:41 +00:00
Vitor Pamplona 8c71d490a7 Merge pull request #2441 from vitorpamplona/claude/marmot-protocol-tests-YZPul
Add Marmot interop test harness for Amethyst ↔ whitenoise-rs
2026-04-18 10:47:47 -04:00
Vitor Pamplona 34d46b08c3 Merge pull request #2442 from vitorpamplona/claude/hide-payment-targets-button-tDYK9
Simplify payment button logic and handle empty targets earlier
2026-04-18 10:28:59 -04:00
Claude 82f5405f01 fix(marmot-interop): robust pubkey extraction from wn output
wn prints either JSON (with --json) or a yaml-ish "key: value" pretty
form depending on the command and build. On the user's macOS run,
create-identity printed yaml ("pubkey: npub1..."), the subsequent
wn --json whoami didn't return the expected JSON shape, and
ensure_identity bailed with "could not determine npub for B".

Add a shared extract_pubkey helper in lib.sh that tries, in order:
  1. JSON object .pubkey / .npub / .public_key
  2. JSON array .[0].pubkey / .[0].npub / .[0].public_key
  3. yaml-ish "pubkey: ..." via sed
  4. yaml-ish "npub: ..." via sed

ensure_identity and prompt_for_a_npub now use it, and also fall back
to the non-JSON whoami output if --json returns nothing. On total
failure, the raw output is echoed to the log for diagnosis.

Also made npub_to_hex best-effort with the same double-try approach.
When hex lookup fails, it returns the npub unchanged — downstream
expect_contains assertions work against either form since wn's own
group listings may use either encoding.

https://claude.ai/code/session_01PacmbE1KKWMSRmDw3bnCNo
2026-04-18 14:27:48 +00:00
Claude 10b229ab9d fix(marmot-interop): don't pass --socket to wnd; derive path from --data-dir
wnd only accepts --data-dir and --logs-dir. The socket path is
{data_dir}/release/wnd.sock (release build) or {data_dir}/dev/wnd.sock
(debug build) per src/cli/config.rs in whitenoise-rs. Updated the
script to point B_SOCKET / C_SOCKET at the correct release path and
dropped --socket from the wnd invocation.

Reported by user running the harness on macOS:
  error: unexpected argument '--socket' found
  Usage: wnd --data-dir <PATH> --logs-dir <PATH>

https://claude.ai/code/session_01PacmbE1KKWMSRmDw3bnCNo
2026-04-18 14:27:48 +00:00
Claude 7107cecedc test: add Marmot interop harness driving whitenoise-rs CLI
Interactive bash script under tools/marmot-interop/ that validates
Amethyst's Marmot/MLS implementation against the whitenoise-rs wn/wnd
CLI. Builds wn/wnd from source on first run, launches two daemons for
Identities B and C, configures public (or --local-relays) Nostr relays
shared with Amethyst, then walks a human operator through 13 scenarios
covering MIP-00 KeyPackages, MIP-01 metadata, MIP-02 Welcome,
MIP-03 group messages, admin changes, reactions, concurrent-commit race,
leave, offline catch-up, and KeyPackage rotation. Push-notification
(MIP-05) test opt-in via --transponder.

No Amethyst code is modified — black-box testing only. State (daemon
sockets, logs, whitenoise-rs checkout, results TSVs) is kept under
tools/marmot-interop/state/ and gitignored.

https://claude.ai/code/session_01PacmbE1KKWMSRmDw3bnCNo
2026-04-18 14:27:47 +00:00
Claude 85ece39ece fix: hide payment targets button on profile when user has none
Previously the wallet icon always rendered on the profile header and
opened a dialog showing "No payment targets" when empty. Skip rendering
the button entirely when the user has no payment targets.

https://claude.ai/code/session_01X51KAzYWnqxkr5WdCm4a7J
2026-04-18 14:19:51 +00:00
Vitor Pamplona 9ac39069bb Merge pull request #2438 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-04-18 09:52:32 -04:00
Crowdin Bot dbade2b2fc New Crowdin translations by GitHub Action 2026-04-18 13:50:00 +00:00
Vitor Pamplona 939c597bdf Merge pull request #2440 from vitorpamplona/claude/optimize-video-playback-yBvXm
Optimize video player mutex with single-winner cache
2026-04-18 09:48:41 -04:00
Vitor Pamplona de0469a177 Merge pull request #2437 from vitorpamplona/claude/blur-hash-video-loading-9sfYL
Skip audio animation when blurhash is present
2026-04-18 09:46:48 -04:00
Vitor Pamplona 20bb25f4d4 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-04-18 09:30:13 -04:00
Vitor Pamplona 6e77807c69 Merge pull request #2439 from vitorpamplona/claude/add-nav-filters-sections-aVBW7
Rename kind3GlobalPeople to kind3GlobalPeopleRoutes
2026-04-18 09:30:01 -04:00
davotoula 51ad84591f sonar fixes 2026-04-18 12:12:17 +02:00
Claude 3fec1e1de6 perf(video): cut active-video election from O(N^2) to O(1) per scroll
VideoPlayerActiveMutex previously iterated over every tracked video on
every onGloballyPositioned callback, and every visible video gets that
callback every scroll frame — so an N-video viewport did O(N^2) work
per frame just to pick the closest one.

Replace the iterate-and-vote scheme with a single-winner cache: each
position update only compares against the current winner (O(1)) and
re-elects (O(N)) only when the winner becomes invisible.

Other allocation/native-call savings on the hot path:
- Reuse the bounds Rect on VisibilityData instead of allocating a new
  android.graphics.Rect per callback per video.
- Cache view.getGlobalVisibleRect for ~half a frame so all videos in
  one layout pass share one native call instead of N.
- Skip the whole update when distance hasn't changed.
- Swap HashSet for ArrayList (faster iteration, no hash work).

Also make ControlWhenPlayerIsActive's pause idempotent so we don't
churn the player when it's already paused.
2026-04-18 03:49:35 +00:00
Claude 905c8eb8a7 fix(video): show blurhash instead of fake waveform while loading
VideoView already renders the blurhash underneath VideoViewInner, but the
player still showed FakeWaveformAnimation over it during loading. Two issues
combined: Tracks.isAudio() returned true for an empty track list (the state
before the player has resolved any tracks), and AudioPlayingAnimation had no
way to know the media was a video. If a blurhash is available we know this is
a video, so propagate that hint down and skip the audio waveform entirely.
Also guard isAudio() so an empty track list is no longer treated as audio.
2026-04-18 03:06:19 +00:00
Claude 507c8fad1b feat: add hashtag and geohash top nav filters to Pictures, Shorts, Articles, Polls and Products
Switches the top nav filter spinner on these screens from kind3GlobalPeople
to kind3GlobalPeopleRoutes so followed hashtags and geohashes show up as
filter options, matching the Home and Video (Stories) screens.

https://claude.ai/code/session_01KrifP1JxfuDkycqzUA4onH
2026-04-18 00:27:56 +00:00
Vitor Pamplona 898dca26af Merge pull request #2436 from vitorpamplona/claude/review-marmot-mips-compliance-wRDux
Implement Marmot MIP compliance fixes for admin gates and media handling
2026-04-17 19:58:01 -04:00