Skip to content

quarantine-tape: tape individual images (stacked on #319)#320

Open
spencerc99 wants to merge 3 commits into
feat/quarantine-tapefrom
feat/quarantine-tape-images
Open

quarantine-tape: tape individual images (stacked on #319)#320
spencerc99 wants to merge 3 commits into
feat/quarantine-tapefrom
feat/quarantine-tape-images

Conversation

@spencerc99

Copy link
Copy Markdown
Owner

Stacked on #319 (page tape) — review/merge that first. This PR adds the ability to tape a single image, not just a whole page.

Marking a whole page is too blunt when the slop is one AI-generated image among real content. While a roll is armed, hovering an image outlines it; dragging across it lays an X of tape over that element.

What's here

Element store (extends the worker):

  • Migration: quarantine_element_marks, keyed by the image's src (query preserved), indexed by src.
  • Three routes: bulk GET /quarantine/element-verdict (the on-page renderer asks about every tapeable image at once, capped at 100 — the pattern Phase-2 remote-surface rendering will reuse), POST /quarantine/element-mark, POST /quarantine/element-rip. 7 tests.
  • Lifts normalizeArtifactUrl into lib/artifactUrl so page + element routes share one normalizer — the query-preserving behavior is migration-critical and must not diverge.

Client (extends the same feature):

  • ElementMark type + three functions on the storage seam (still the only backend touchpoint).
  • Hover-to-target, drag-across-image to tape, image ripping folded into the existing slash gesture.
  • Element tape is content-bound: it re-measures the element's bounds each render, so it tracks the image through scroll/reflow (page tape stays viewport-fixed). Verdict keyed by src → one mark paints every copy of that image.

Why this is a good property

Because the verdict is keyed by src, this is already the portable shape needed to mark the same image elsewhere (search-result thumbnails) in a later phase — no migration, just an added bulk-lookup surface, which this PR's element-verdict route already models.

Notes for review

  • The image gesture and the page-tape edge-commit gesture share the armed state but split on mousedown: a drag starting on a hovered image tapes the image; anywhere else the click-edge commit runs untouched.
  • Host-page hover outline is a small <style> injected into document.head (the overlay's shadow-root CSS can't reach the page's own <img>s), removed on destroy.
  • Full worker suite green (67 tests). Extension type-checks clean.

@pkg-pr-new

pkg-pr-new Bot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

bun add https://pkg.pr.new/@playhtml/common@320
bun add https://pkg.pr.new/playhtml@320
bun add https://pkg.pr.new/@playhtml/react@320

commit: fce625d

@spencerc99

Copy link
Copy Markdown
Owner Author

Code review

  • P1 supabase/migrations/20260723002402_extension_quarantine_element_marks.sql:26 exposes unrestricted direct writes to the image-verdict table. The migration grants ALL to anon and authenticated without enabling RLS, so a client with the project's public Supabase credentials can bypass every worker validation/rate-limit path and arbitrarily mutate or delete image marks. Enable RLS and leave mutations available only to the service-role worker.

  • P1 extension/src/features/social/quarantine-tape/QuarantineTapeManager.ts:303 cannot discover the image underneath the armed overlay. Arming sets the full-viewport shadow SVG to pointer-events: auto at line 152, so document.elementFromPoint() is retargeted to the high-z-index closed-shadow host/overlay rather than the host page's <img>. hoverTarget therefore stays null and the image-drag branch never starts. Hit-test with elementsFromPoint() while skipping the injected host, or temporarily make the overlay non-hit-testable while resolving the underlying image.

  • P1 extension/worker/src/routes/quarantineElement.ts:183 lets the request's src determine another mark's teardown threshold. The mark is loaded/updated by markId alone, but line 204 counts the caller-controlled src. Supplying a real mark ID with an unrelated valid source that has fewer than three marks permanently snapshots rips_required = 1. Bind both queries to markId + key, or count using the loaded mark.src.

  • P2 extension/src/features/social/quarantine-tape/quarantine-api.ts:81 sends every image source before the worker applies its 100-source cap. On image-heavy pages, long CDN URLs can push this GET past browser/proxy URL limits, causing the entire verdict fetch to fail and render no existing tape. Apply the same cap client-side or request bounded batches.

  • P2 extension/src/features/social/quarantine-tape/QuarantineTapeManager.ts:138 adds user-facing extension behavior without the required extension/PENDING.md bullet. Add a release-note entry covering individual-image taping before merge.

Extends the store so tape can mark a single image, not just a whole
page. A mark is keyed by the image's src (query preserved — its identity
often lives there), mirroring the page-strip shape: one row per
tape-over, setness derived from the count per src, rips idempotent per
player with rips_required snapshotted on the first rip.

- Migration: quarantine_element_marks, indexed by src.
- Three routes: GET /quarantine/element-verdict (bulk by src — the
  on-page renderer asks about every tapeable image at once, capped at
  100), POST /quarantine/element-mark, POST /quarantine/element-rip.
- Lifts normalizeArtifactUrl into lib/artifactUrl so both the page and
  element routes share one normalizer — the query-preserving behavior is
  migration-critical and must not diverge.
The client half of image taping. While a roll is armed, hovering an
image outlines it; dragging across it lays an X of tape over that
element. The verdict is keyed by the image's src, so one mark paints
every copy on the page, and it travels with the content as you scroll —
element tape re-measures the element's bounds each render, unlike
viewport-fixed page tape.

- types.ts: ElementMark; isFullyTorn now covers strips and marks.
- quarantine-api.ts: getElementVerdicts (bulk by src), postElementMark,
  postElementRip — added to the same storage seam, so the manager still
  never touches the backend directly.
- tape-render.ts: segmentCrossesRect helper (ported from the prototype).
- QuarantineTapeManager: element-mark state + gElements layer, hover
  targeting via a host-page style injected into document.head, the
  drag-across-image tape gesture, content-bound X rendering re-measured
  on scroll/resize, and image ripping folded into the existing slash.

The image gesture and the page-tape edge-commit gesture share the armed
state but split on mousedown: a drag that starts on a hovered image tapes
the image; anywhere else the click-edge commit flow runs untouched.

Page tape only was the prior cut; this adds the image path on top.
Same gap the prototype had: dragging across an image rendered nothing
until release, so there was no sign you were mid-gesture or that the drag
would land. The X of tape now previews on the target as you drag,
matching the wall-to-wall pull.

Opacity ramps with drag distance and the ghost stays dashed until the
drag passes the commit threshold, so "not far enough yet" reads in the
gesture. Also stops re-targeting mid-drag — the image being taped stays
the target even if the cursor leaves its bounds.
@spencerc99
spencerc99 force-pushed the feat/quarantine-tape-images branch from a9c2dfa to fce625d Compare July 23, 2026 20:09
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