quarantine-tape: tape individual images (stacked on #319) - #320
quarantine-tape: tape individual images (stacked on #319)#320spencerc99 wants to merge 15 commits into
Conversation
Throwaway prototype for the quarantine-tape social experiment, used to design and tune the gesture and the tape art before porting into the extension. No sync or extension plumbing — local state only. Interaction: equip a roll, click a glowing viewport edge to anchor, pull taut tape to a second edge, commit wall-to-wall. Strips accumulate per page (3+ reads as cordoned). Ripping is a deliberate drag-across slash — provisional tape falls in one, set tape takes three — and torn tape frays and stays rather than vanishing. Tape renders as a caution-tape ribbon: repeated category label plus a warning triangle on a clean center band, hazard slashes confined to the top and bottom edges, plastic sheen, drop shadow, and ends that overshoot the walls so a strip always runs off-screen. Each strip is drawn in local space then rotated, so it looks identical at any angle. Two colorways: AI SLOP (amber/black) and SEO SPAM (white/red).
Marking a whole page is too blunt when the slop is one AI-generated image among real content. Hovering an image while armed outlines it as a target; dragging across it lays an X of tape over that element. The verdict is keyed by the image's src URL rather than by position, so one mark paints every copy of that image on the page — and it is already the portable shape needed to mark the same artifact wherever else it appears later. Element tape re-measures its bounds each render, so unlike wall-to-wall page tape it travels with the content as you scroll. Reinforcing and ripping work as they do for page tape: taping an already-taped image adds a layer, slashing across one tears it. Images are natively draggable, so a drag on one fires dragstart and never fires mouseup — which swallowed the gesture entirely. Suppress the native drag while the gun is armed or a slash is underway. Adds real images to the test page to exercise this.
The backend for laying caution tape on a URL. A verdict is the set of strips for a normalized URL; setness is derived from the count. Persists so a lone tester's mark is there for the next visitor — the thing that makes it feel collective with a small userbase. - Supabase migration: quarantine_strips, indexed by url, rips in a jsonb column. RLS auto-enables; only the worker's service key writes. - Three public routes (called from content scripts on arbitrary pages): GET /quarantine/verdict, POST /quarantine/strip, POST /quarantine/rip. Rate-limited per IP, validated, url normalized server-side. - Ripping is idempotent per player; rips_required is snapshotted on the first rip so teardown can't oscillate as the strip count changes. The url normalizer KEEPS the query string (dropping only the hash), unlike the extension's page normalizer — an image src's identity often lives in its query, and collapsing distinct urls onto one key is irreversible.
Ports the tuned prototype into a real social experiment: equip a tape roll from the satchel, string caution tape wall-to-wall to mark a page as AI slop or SEO spam, reinforce by adding strips, rip by dragging across one. - tape-render.ts: the caution-tape ribbon + geometry, ported verbatim from the website prototype (hazard edges, center label + warning triangle, sheen, torn ends, fray). Stateless — takes an SVG defs to draw into. - QuarantineTapeManager: owns the Shadow-DOM overlay and gestures. Fetches the verdict once on load (the worker is the store; there's no live channel), keeps the local strip array as the session source of truth, and writes optimistically through the api seam — a committed strip renders instantly, then swaps in the server record or rolls back on failure; a rip applies locally then reconciles. - quarantine-api.ts: the storage seam — the only module that touches the backend. Swap these three functions to move the store later without touching the manager, rendering, or gestures. - index.ts: registers two rolls (slop, spam) in the inventory; arming a roll drives the overlay for that tape type. Esc disarms through the satchel so its state stays in sync. Registered in the social registry, gated by QUARANTINE_TAPE (off for users; runs under the internal-dev override, e.g. the social-playground). Page tape only in this cut; image/element taping is a follow-up (needs a contract extension). The image-opened-as-a-page path works for free since an image URL is just a page URL.
commit: |
Code review
|
Dragging across an image gave no feedback at all — the mousemove handler only drew a trail for the rip gesture, so an image drag rendered nothing until release. You couldn't tell you were mid-gesture or that the drag would land. Now the X of tape previews on the target as you drag, matching the wall-to-wall pull: you see the tape before you commit it. 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 itself. Also fixes the hint, which was stuck on the hover message mid-drag and said "as slop" regardless of which roll was equipped.
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.
a9c2dfa to
fce625d
Compare
Code review
|
Deploying we-were-online-website with
|
| Latest commit: |
319c7e1
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://7f15bde6.we-were-online-website.pages.dev |
| Branch Preview URL: | https://feat-quarantine-tape-images.we-were-online-website.pages.dev |
Deploying playhtml with
|
| Latest commit: |
319c7e1
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6dc339e4.playhtml.pages.dev |
| Branch Preview URL: | https://feat-quarantine-tape-images.playhtml.pages.dev |
…-320-quarantine-images-conflicts # Conflicts: # extension/src/features/social/quarantine-tape/QuarantineTapeManager.ts
…images-conflicts # Conflicts: # extension/shared/featureCatalog.ts # extension/src/__tests__/quarantine-tape-interactions.test.ts # extension/src/features/social/quarantine-tape/QuarantineTapeManager.ts # extension/src/features/social/quarantine-tape/quarantine-api.ts # extension/src/features/social/quarantine-tape/tape-render.ts # extension/src/features/social/quarantine-tape/types.ts # extension/worker/src/__tests__/accessControl.test.ts # extension/worker/src/index.ts # extension/worker/src/routes/quarantine.ts
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):
quarantine_element_marks, keyed by the image's src (query preserved), indexed by src.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.normalizeArtifactUrlintolib/artifactUrlso page + element routes share one normalizer — the query-preserving behavior is migration-critical and must not diverge.Client (extends the same feature):
ElementMarktype + three functions on the storage seam (still the only backend touchpoint).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-verdictroute already models.Notes for review
<style>injected intodocument.head(the overlay's shadow-root CSS can't reach the page's own<img>s), removed on destroy.