Skip to content

Promote coherent media platform (#319, #312, #321) - #344

Merged
lopugit merged 16 commits into
mainfrom
codex/promote-media-platform
Aug 21, 2026
Merged

Promote coherent media platform (#319, #312, #321)#344
lopugit merged 16 commits into
mainfrom
codex/promote-media-platform

Conversation

@lopugit

@lopugit lopugit commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Included scope

This is the deliberately scoped production promotion for the coherent media dependency closure:

The live domain is now verified as gitBranch: develop, customEnvironmentId: null, and aliases the exact READY develop deployment 58ef890c61030ccf1b75baad6bee14189399b78c.

The runtime files in this promotion byte-match current develop. The branch intentionally excludes unrelated develop work, including Components, Connections, Fable Five, passkeys/FedCM, Commander, iOS feature work, and unrelated CI/deployment changes.

Graphify's generated graph/manifest/report/cache refreshes are isolated in generated-only commits so the hand-authored runtime commits remain reviewable.

Verification on this exact branch

  • npm --prefix remix run test:unit — passed
  • npm --prefix remix run build — passed, including Vercel static-shell/CSP output verification
  • attachment suites — 124 passed
  • schema suites — 61 passed
  • moderation suites — 52 passed
  • migration suites — 19 passed
  • storage suites — 7 passed
  • Things suites — 6 passed
  • git diff --check — clean
  • scope audit — every promoted runtime file matches origin/develop; no unrelated runtime file is present
  • signed-in dev.thingtime.com runtime reports 🌱 develop after the binding repair

Post-merge safety gate

After production is READY:

  1. dry-run the detected-type backfill and review counts;
  2. run bounded passes using nextCursor to convergence;
  3. confirm a final idempotent zero-work pass;
  4. exercise upload/render/reorder/lightbox/media-page/annotation/comment/reaction/delete/refund paths with disposable fixtures;
  5. verify inline video, opaque-file labels, object versions, and storage accounting.

Media layout transport reconciliation

Source fix #345 is merged into develop as 1e9bab325941d75abb2e8472557598c54aef81dd, and this promotion carries the same focused transport change. On the exact READY develop deployment, a fresh signed-in S3 run created two unique raster attachments with Grid=4, one wide span and one tall span. Reload preserved the rendered geometry; reopening Edit reported Grid selected, four columns, and the same wide/tall spans. This closes the pre-promotion live gate for the create transport regression.

lopugit and others added 14 commits August 21, 2026 16:05
… + label sniffed types

Widen the magic-byte inline allowlist from MP4/WebM to every video
container mainstream browsers can natively play: QuickTime (.mov —
including screen recordings misnamed .mp4), M4V, Ogg, 3GPP, 3GPP2, and
Matroska. The decision still comes only from server-side byte sniffing,
never the filename extension, and SVG/HTML/active formats remain opaque
octet-stream downloads.

When a sniffed container stays a generic download (AVI, WMV, MPEG-TS…),
preserve the sniffed MIME as a new optional crystal field
detectedContentType so the download row can say 'AVI video' instead of
application/octet-stream. The canonical crystal union accepts the field
only alongside an octet-stream contentType, keeping it display metadata
that can never widen inline serving; client normalization mirrors that.

Since codec support inside an allowed container varies per browser
(HEVC QuickTime on Firefox, MKV on Safari), a failed <video> element now
degrades to its named download row instead of an inert black player.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Attachment crystals gain optional title (<=200 chars, single line) and
description (<=2000 chars, newlines allowed) - sanitized, canonical
(closed shape with the two optional keys), projected in public
metadata, and absent-never-empty. POST /api/v1/attachments/annotate
sets/clears them on an owned READY attachment (ready-only so finalize
cannot clobber early annotations) with exact storage-delta accounting
like markReady. Client PublicAttachment + normalize + useApi carry the
fields; mediaPageUrl() names the /media/:id deeplink.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/v1/things?id=<attachmentId> now returns the attachment's
post-shaped projection (reactions, comments, views - the resolvers are
target-generic) plus parent/root resolving to the post it is bound to,
powering the /media/:id Thing page. Attachment kind stays protected:
annotate is the only crystal mutation path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post image attachments now flow as a natural-aspect CSS-columns masonry
(responsive 1/2/3 columns, title chip overlay). Clicking opens the new
MediaLightbox: arrow-key/edge navigation, description caption, download,
and an Open deeplink to /media/:id - every attachment's own Thingtime
page. The media page renders the attachment as a full PostCard (its own
reactions, comments, and views - relational things targeting the
attachment id), owner title/description editing via annotate, and a
link back to the parent post. PostCard gains a mediaThing mode that
keeps interactions but drops edit/privacy/delete (attachment kind is
protected; lifecycle belongs to the parent post).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s) + lightbox Esc close

- AttachmentAnnotatePopover: shared pencil popover editing title/description,
  saved via /api/v1/attachments/annotate, optimistic apply + revert with a
  Lopu toast on failure; fits 375px viewports
- wired into ready composer tiles (visual + file rows) via a new
  updateAttachment patch in useAttachmentUploads, and into the edit-mode
  AttachmentReorderGallery (id order untouched, so no false order change)
- MediaLightbox: close on Escape from the window keydown listener too —
  autoFocus is off, so Chakra's closeOnEsc never hears the key when focus
  sits outside the modal (found in preview E2E)
- TESTING.md: media thing pages checklist (masonry, lightbox, /media/:id,
  annotate, delete cascade)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…erver validation

- crystal.mediaLayout on posts: mode masonry|rows|grid, rows pattern (1-25
  entries of 1-6, extras repeat the last row size), grid columns 1-6 +
  per-image spans (wide/tall/big; redundant normals dropped) — sanitized in
  sanitizePostCrystal with strict bounds, legacy crystals stay valid; PATCH
  sets/clears it through the same validator
- projected as PublicPost/PublicComment.mediaLayout (toPublicPosts project +
  buildComment + addComment echo) so cards, comments, shares, and media
  things all carry it
- PostAttachments renders the three modes: masonry unchanged; rows via
  pattern chunks (hero 16:9, pairs 4:3, 3+ square); grid via CSS grid with
  span-aware aspect ratios and dense flow — lightbox order stays attachment
  order in every mode
- 8 new schema tests (test:schemas 61/61); TESTING.md layout checklist

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ayout editor)

- MediaLayoutControls: Auto 🧱 / Rows 🥞 / Grid 🔳 segmented picker with a
  live pattern preview ('1-2-3' style input, bounded parse) and a grid
  columns stepper; SpanCycleButton cycles a tile normal → wide → tall → big
- PostComposer owns the layout state (seeded from editPost.mediaLayout),
  shows the picker from 2+ visual attachments, prunes spans to the outgoing
  visual set, and sends crystal.mediaLayout through create, edit (full-crystal
  replace), and the frozen create expectation (sameJson stays exact)
- createPost/addComment map body.mediaLayout into the sanitized crystal
- AttachmentComposer + AttachmentReorderGallery grow a tileExtras render prop
  (badge bottom-left; grip/pencil corners unchanged), active in grid mode

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tor)

- MediaLayoutCanvas: live CSS-grid preview of the actual layout with a
  columns slider (1-6, relayouts live) and a ⤡ corner handle per tile —
  pointer-event drag (mouse + touch, touchAction none, pointer capture)
  snaps tiles between normal/wide/tall/big at ~45% of a cell, with a live
  outline preview; arrow-key fallback on the focused handle (right wider,
  left narrower, down taller, up shorter)
- shown in the composer + edit mode whenever Grid is active with 2+ visual
  attachments; writes the same layoutSpans/columns state as the tier-2
  badges, so canvas drags, badges, and the saved crystal never drift
- span geometry helpers (spanColumns/spanRows/spanAspect) exported from
  PostAttachments so render and editor share one source of truth

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
handlePointerDown calls preventDefault to keep the drag from selecting or
scrolling, which also suppresses the native focus — so clicking a tile's
resize handle and then pressing an arrow key did nothing. Take focus
explicitly (preventScroll so the canvas does not jump) and the documented
keyboard fallback works after a click, not only after tabbing to the handle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tals

Ready attachments finalized before PR #319's magic-byte detection keep
crystal contentType application/octet-stream with no detectedContentType,
so browser-playable uploads still render as file cards. Add an admin-only
POST /api/v1/attachments/backfill-detected-types that re-runs detection
from the object's first 8 KiB and publishes exactly what completion would
have: browser-playable containers flip to their inline contentType and
mediaKind, other canonical sniffed types gain detectedContentType, and
undetectable bytes stay untouched so a later, wider detector can claim
them. Names, sizes, object keys, and object versions never change; the
crystal's JSON-payload delta settles through the same transactional
storage ledger markReady uses.

Each pass is bounded (at most 200 rows, 5 workers, 25s wall clock),
idempotent, cursor-paginated, and supports dryRun. New store methods
listReadyUndetected and upgradeReadyCrystal (markReady-modeled, name and
size immutable, object-version-fenced); rate key
attachments.detectionBackfill; route + import map + apiDocs registration;
service and route tests mirroring the completion-test mocks; TESTING.md
checklist line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview Aug 21, 2026 7:04am

Request Review

@lopugit lopugit added promotion Automated develop → main promotion PR no-ai-rebase Opt this PR's head branch out of AI history rewriting; the merge resolver owns its conflicts no-ai-merge Never auto-resolve this PR's conflicts; the AI merge resolver skips it entirely labels Aug 21, 2026
@lopugit
lopugit merged commit ea6e8b9 into main Aug 21, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-ai-merge Never auto-resolve this PR's conflicts; the AI merge resolver skips it entirely no-ai-rebase Opt this PR's head branch out of AI history rewriting; the merge resolver owns its conflicts promotion Automated develop → main promotion PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant