Files
hackdex-website/supabase/seed.sql
Jared Schoeny b06cb7af93 Hackdex redesign, private drafts, AI disclosure, and more (#101)
* Restyle site chrome for v2: tokens, class-based theme toggle, Archivo, bottom tabs

Move the color system to the v2 tokens in globals.css (cool neutrals, rose only
for actions, green only for on-device ROM state) and alias the old variable
names so existing components keep working. Dark mode is now a class on <html>
set before first paint, with a sun/moon toggle in the header. Archivo replaces
Geist and the Arial body fallback; headings use its width axis via
font-display. Header drops the guest login and Beta pill, adds search and a
quiet Submit link; phones get a three-tab bottom bar instead of a hamburger.

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

* Rebuild HackCard for v2 and add a list row

The card keeps the embla screenshot carousel, drag guard, and dots from the
old card and reshuffles the text to the v2 layout: title, author with an
outline completion pill, two-line summary, first two tags, then base ROM (or
Ready), last update, and downloads. Screenshots sit in a well at native width
on desktop and stretch on phones when the grid asks for it; pixelated
rendering is now opt-in and only applied at whole-number scales. HackRow is
the Discover list view. formatRelativeDate feeds the new updated field.

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

* Rebuild Discover for v2: filter rail, phone filter sheet, list view, pager above the grid

Filtering, sorting, and URL state are unchanged in substance; the browser
component now renders the v2 layout around them. Desktop gets a sticky filter
rail with in-facet search, platform disclosures that collapse ROM revisions
into one row, and catalog-wide counts. Phones get one Filters button that
opens a 90% draft sheet with a live "Show N hacks" footer; close, scrim,
Escape, and Back discard. Tags now OR within a category and AND across
categories. Pages are 24 long with a small pager beside the result range and
a full one below; grid and list views are remembered per device.

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

* Rebuild Home for v2: promise hero, shelves from the discover cache, How it works last

The page now reads the cached discover catalog instead of running its own
queries, and slices it into Trending, New, and Recently updated shelves with
hover-revealed paging arrows. A ready shelf leads once a base ROM is on the
device, and the hero flips to "N hacks are one click away". The hero patch
diagram and guest login link are gone; How it works keeps its copy and moves
to the bottom for cold visitors, where the hero link jumps to it.

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

* Rebuild the hack page for v2: patch module in the rail, compact header bar, tabs, lightbox filmstrip

The sticky bottom bar is gone. The patch flow (HackActions and its state,
onboarding, ROM checks, error modal) now renders as a patch module at the top
of the rail: status line, version picker, one action button, facts, terms.
When it scrolls behind the site header the title, picker, and the same
button reappear in a compact bar portaled into the header, wrapping onto a
second row on phones. The body is About / Gallery / Versions tabs; About
keeps the screenshot stage, thumbnails, description, and latest changelog,
Versions lists selectable patches and links to the full history. The
lightbox keeps pixel-perfect scaling and scroll locking and gains a title bar
and filmstrip. Tags collapse to one row with a peek and expand in place.
Compatibility, box art, details, links, and more-from-author fill the rail.

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

* Start hacks as private server-side drafts with a checklist and Submit for review

Submit now opens with a start card (title, base ROM, summary, own or on
behalf) that creates a hack row with submitted_at null and sends the creator
to the edit page. The edit page shows a status strip (Draft / In review /
Listed) with a Preview-as-a-player link, a publishing checklist computed from
the real row (patch, screenshots, description, tags, completion status), and
Submit for review, which stamps submitted_at, opens the review thread, and
tells the admins. Patch uploads on a draft no longer ping reviewers, the
admin queue hides drafts, and the dashboard list shows Draft / In review /
Approved. The old wizard stays for archive entries. Migration backfills
submitted_at from created_at for everything that already exists.

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

* Restyle My ROMs and the creator dashboard to match v2

Light touch only: page widths, display headings, the ready count as a green
dot pill, ROM cards on the new tokens (green for on-device, amber for a
permission prompt, orange-red for errors), tokenized buttons and stat cards.
No layout changes.

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

* Keep base ROM revisions visible in labels and Discover filters

A hack is built against one dump, and revisions and regions hash differently,
so the player picking a ROM needs the revision in front of them. The label
helper now only drops the "Pokémon" prefix, and the Discover rail lists every
base ROM on its own row instead of collapsing revisions into one game.

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

* Nest ROM revisions under one game row in the Discover rail

Games with several dumps (FireRed Rev 0 / Rev 1, regional releases) get a
parent checkbox that selects or clears them all, an indeterminate dash when
only some are on, and a chevron that reveals each dump with its own count.
Single-dump games stay a plain row. Applied chips name the exact dump.

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

* Put the revision chevron after the game name and open the list from the row

The checkbox still selects every dump under a game; the rest of the row
(name, chevron, count) is a button that expands or collapses the revisions.

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

* Give the Discover rail room between the counts and its scrollbar

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

* Drop the updated date from hack cards

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

* Move the onboarding gate into the patch module and follow the action when scrolled

The gate was styled for the old sticky bar and hung off the module as a stray
tab. It is now a row under the primary action, a help icon beside the compact
action on desktop, and a floating pill above the phone tab bar once the module
scrolls away. The compact homes scroll back to the module before opening the
tour. A rose beacon marks the icon while the player still needs a ROM.

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

* Switch to the compact bar when the action button hides and settle scroll before the tour opens

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

* Scroll the module into place from every help entry and hold user scrolling on the way

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

* Place the phone coach card under the patch module instead of above it

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

* Edit drafts in place on the session page

The session page becomes the hack page in edit mode for unlisted hacks:
title, summary and tags in the header, a Write / Preview description in
About, a screenshot manager in Gallery, and an Edit details sheet in the
rail for language, completion, box art and links. Base ROM locks once a
patch exists. Fields autosave; the status strip shows save state and a
player preview via ?preview=1. The edit page redirects unlisted hacks
here, and listed hacks keep the existing form.

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

* Edit listed hacks in place too, behind ?edit=1

The middleware now carries the query string through the session rewrite
so ?edit=1 and ?preview=1 survive. Listed hacks open as players see them
and switch to the editor from Edit in the options menu or the dashboard;
the strip ends with Done editing. Only archive hacks still use the form.

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

* Hide the patch card and header actions while editing; checklist takes the patch slot

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

* Stage edits on listed hacks until Save; only drafts save live

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

* Open the details sheet from rail group headings, give the About editor its section, wrap the strip on phones

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

* Show the current version in the hack page header

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

* Fix the avatar-in-paragraph hydration error and re-apply the theme class after mount

The byline held a block avatar inside a <p>, so the browser restructured the
server HTML, hydration failed, and the client re-render reset <html> and
dropped the dark class the head script had set. The byline is a <div> now,
and ThemeToggle re-applies the stored or system theme on mount and follows
system changes while no choice is stored.

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

* Polish pass: handle styling, card-wide ready outline, version on cards, rail height, sort width, How to play, footer login

- Author handles render the at-sign in mono and muted so it reads as a handle.
- Ready hacks outline the whole card (grid and list) instead of the screenshot.
- Grid cards show the version next to the completion pill.
- Desktop Discover rail fits the viewport from wherever its top sits, so it
  never needs both panes scrolled to reach the bottom.
- Sort select fills the row on phones and has a stable id so the listbox
  button never hits a useId hydration mismatch.
- Results area keeps a viewport of height so narrowing filters doesn't yank
  the page.
- Version label centers against the picker pill in the patch module.
- Tab strip overhang no longer creates a 1px vertical scroller; screenshot
  thumbnails keep room for the rose ring.
- "Plays on" becomes a "How to play" rail group listing the FAQ's
  recommended emulators per platform.
- Footer gains Log in (Dashboard once signed in, swapped after mount) and
  extra phone bottom padding for the floating help pill.

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

* Open pending hacks in edit mode only for their creator; keep the options menu for admins editing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Let creators edit the creator name, permission source, and verification contact from the details sheet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Restyle Select, Share, Report, and the auth forms to v2; share one dialog hook between Sheet and Modal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cards: mark archives, show when a linked ROM needs permission again, fall back to the description

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Desktop compact bar takes the search box's place so the nav stays visible

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Phone menu: a three-bar toggle that folds into an X and slides out Submit, account, FAQ, and Contact links from the right

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* My ROMs: show the unsupported-browser note only after mount to fix a hydration mismatch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Light v2 restyle of the remaining v1 pages: tokens, radii, status colors, primary buttons, and display-face titles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Let the page scroll while selects and option menus are open (no scroll lock, no scrollbar jump)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Label account links as creator-only: Become a creator / Creator log in, with a note that players need no account

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Footer: Play / Create / Hackdex columns, legal links beside the copyright

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Desktop compact bar: fold nav, search, and Submit into a Menu dropdown

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Submit form: admin-only on-behalf option; creators confirm they made the hack, with a ToS note by the button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Edit header: tags render as published with an Edit tags pill that opens the picker in a modal

Byline moves under the title in edit mode to match the published order.
TagSelector gets v2 tokens and a phone layout with category chips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tag picker: replace drag-to-reorder with a review view (card slots, Put on card, checkbox removal)

The picker ends in a selected-count bar with Review. Review shows the two
card tags with Make 1st/2nd and the rest with Put on card (asks which to
replace) and undoable checkbox removal. Categories show pick counts and the
picker opens on the first category. Escape inside a menu no longer closes
the surrounding dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tag review: fixed height with only Also tagged scrolling; icon arrows on Back and Make 1st/2nd

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Submit for review: confirm modal with verification contact check, success screen, and While you wait card

The modal shows the contact for a last look (editable) or asks for one,
skippable, and submitForReview saves it in the same update. Success shows
what happens next. In review, a While you wait card replaces the checklist.
Also fixes submitForReview not revalidating the cached hack metadata, which
left the page on Draft after submitting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Admin view of unsubmitted drafts: explanatory notice with progress and contact; block approving drafts

Admins opening someone else's draft see an Unsubmitted draft notice with
dates, required checklist progress, and the verification contact instead
of the creator's private-draft copy. Approve and Create review thread are
hidden for drafts, approveHack refuses them, and the approve page
redirects. Admins in the editor can't submit on the creator's behalf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Start form: editable page address with live availability check; taken addresses block create instead of getting a suffix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AI disclosure: per-hack levels for six areas, label in the rail, creator form in a modal, required checklist item

Six nullable ai_level columns (content limited to none/some/most), an optional
note, and ai_disclosed_at. The label is the round 10 design: headline (Contains
AI / AI in code only / No direct AI usage), strip, and a collapsible breakdown.
Hacks without one show a muted not-filled-in state.

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

* AI disclosure: Discover filter (Any / No AI content / No direct AI usage) and a levels table in the FAQ

Hacks without a disclosure still show under the strict options, since most use
no AI and reports catch the rest. The URL keeps it as ?ai=no-content|none.

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

* AI disclosure: new versions confirm the label (Still accurate) or update it before uploading

Confirming re-stamps the label only once the upload lands. The first upload
skips the step since the draft checklist already requires the label.

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

* AI disclosure: Needs AI label nudge on the dashboard, linking to the editor with the form open

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

* AI disclosure: approve page warns when a hack has no AI label yet, without blocking

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

* AI label: mouse-only strip hover so taps don't leave a cell looking selected; FAQ levels explained in one short paragraph instead of a table

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

* AI label: link strip cells and breakdown rows only while the breakdown is open

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

* AI form: intro copy matches the FAQ (vibes-based levels, intentional additions, one example of what doesn't count)

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

* AI form: on phones the save hint gets its own line and Cancel / Save split the width

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

* Security: move hack page reads out of the server actions file

getHackMetadata and getHackDownloads lived in a "use server" file, so each was a
public endpoint. Anyone could post a slug and get a private draft or pending
hack, including the creator's email and verification contact, read with the
service client. They now live in a plain server module that only the pages
import, and those pages already gate what they render.

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

* Security: keep upload keys inside the hack the caller can edit

Signing trusted a client-supplied object key, so a creator could get an upload
URL for another hack's screenshot or patch. Saving covers took any key too, and
removing one later deleted that file from storage.

- Screenshot keys must sit under `${slug}/` (new keys only; existing rows stay
  valid), and storage cleanup only touches that folder.
- Patch keys are made on the server with a random suffix (new versions,
  reuploads, the archive form), and confirm steps check the key was made for
  this hack.

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

* Security: patch download route applies the patcher's permission checks

/api/patches/[id]/download signed any patch by id, so pending hacks, unpublished
versions, and archived versions were downloadable anonymously. It now goes
through getPatchDownloadUrl (published, not archived, the hack's download
permission, and a parent hack the caller can see) and 404s otherwise.

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

* Editing: keep the About and Gallery editors mounted across tab switches

Switching tabs unmounted the editors, which dropped staged screenshot changes
(and unregistered their save) and showed a stale description while the staged
one still published. Their panels now stay mounted and are hidden instead.

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

* Editing: upload staged screenshots before publishing anything on Save

Save changes wrote the text fields first and uploaded screenshots after, so a
failed upload left half the edit published. Committers now have a prepare step
(uploads) that runs before any write. Edits made while a save is running also
stay staged instead of being cleared.

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

* Uploads: stop when the storage PUT fails, and check the file exists before finalizing

A failed PUT resolved normally, so the version was still created and could be
made current without a file behind it. Clients now check the response, and
confirming a patch upload or reupload checks the object is in storage first.

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

* Uploads: refresh the hack page's cached data after finalizing a patch

confirmPatchUpload only refreshed Discover, so a draft's first upload could keep
showing no patch (or an old version) for up to four hours.

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

* Dialogs: Shift+Tab right after opening stays inside the dialog

Focus starts on the panel, and the trap only wrapped from its first control, so
Shift+Tab escaped to the page behind. The trap now wraps from anywhere outside
its controls and skips inert or hidden ones. The Discover filter sheet had a
copy of the same logic and now uses useDialog.

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

* Upload page: fit phones, and make the base ROM badge readable in light mode

File inputs kept their intrinsic width, pushing the page to 410px at 360px. They
now fill their container, and the base ROM upload's label sits above it. The
badge used white text on a pale background in light mode.

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

* Editing: enforce title, summary, and screenshot limits on the server

updateHack accepted any title or summary length, so a 110-character summary
autosaved and listed hacks could publish one. It now rejects titles over 64 and
summaries over 100, cover saves reject more than 10 screenshots, and drafts
don't autosave a summary while it's over the limit (the counter is already
red). The limits live in one shared module.

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

* Versions: unique (hack, version) index so concurrent uploads can't duplicate one

The app checked for an existing version before inserting, but two uploads
finishing together could both pass. Postgres now enforces it, and the
duplicate-key error reads the same as the existing check. (Distinct upload keys
came with the storage key change.)

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

* AI label: fixed breakdown id instead of useId to clear a hydration warning

useId produced different ids on the server and client for the rail label on a
creator's draft, so aria-controls and id didn't match after hydration. The
label takes a fixed id; the form's preview copy uses its own.

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

* AI form: open ?ai=1 after mount so the page doesn't 500 on the server

The dashboard's Needs AI label link rendered the modal open during SSR, and
Modal portals into document.body, which threw and returned a 500 before the
client recovered.

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

* A11y: 44px AI level buttons on phones; sheets don't slide with reduced motion

On phones each level control spans the width with equal segments and 44px
targets (they were 27px). The Discover filter sheet and the phone menu drawer
skip their slide transition when reduced motion is on.

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

* Types: replace any in hack, cover, and patch write payloads with generated table types

updateHack's payload, the cover upsert rows, and the patch insert were typed any,
so schema mismatches passed the typecheck. They now use TablesUpdate and
TablesInsert, row maps use inferred types, and the permission helpers take
SupabaseClient<Database>.

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

* Security: patch rows follow hack visibility and only the server creates them

Patch rows were readable by anyone, so drafts' and pending hacks' file names
leaked, and creators could insert a row pointing at another hack's file and then
get it signed through their own hack. Reads now use the same rules as covers and
tags, the client insert policy is gone (finalization inserts with the service
client after its checks), a key can only be registered once, and patch keys use
"__" after the slug so hack "foo" can't claim "foo-bar"'s uploads.

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

* Security: draft titles stay private in <title>; the archive wizard action needs archiver rights

generateMetadata treated any signed-in visitor as allowed to see restricted
titles, so a draft 404'd but its title showed in the tab. It now uses the same
edit-permission check as the page. prepareSubmission (the archive wizard) only
checked for a login, so anyone could call it to create an already-submitted hack
without the checklist; it now requires is_archiver like its page.

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

* Drafts migration: backfill submitted_at without bumping updated_at

The backfill fired set_hacks_updated_at, which would stamp every production hack
with migration day, reordering dashboards and OpenGraph modified times. The
trigger is disabled just for that update.

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

* Editing: saves recover from network errors and never drop the last edit

- run() catches thrown server actions, so a failed save shows an error instead of
  sticking on "Saving…" with Save disabled.
- Listed hacks stage edits right away, so Save always includes the last one, and
  a draft edit still waiting on its debounce is committed when the editor
  unmounts (clicking Preview mid-typing lost it).
- Gallery Save uploads and saves one snapshot, so a screenshot added while
  saving stays staged instead of being saved without its file.

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

* Caching: new drafts aren't stuck on a cached 404; notifications can't skip invalidation; drafts don't rebuild the catalog

- createDraft invalidates the slug's metadata, so an address visited before it
  existed no longer 404s for its creator after creation.
- Upload finalization and approval invalidate right after their writes, and
  their Discord/email notifications are best effort, so a failed webhook can't
  leave stale pages or report a saved upload as failed. Re-approving also
  refreshes metadata.
- updateHack and saveHackCovers only rebuild the Discover catalog for listed
  hacks; draft autosaves were rebuilding it on every typing pause.

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

* Drafts: need a playable patch to submit or approve; base ROM locks once any patch exists

- A hack's first patch always becomes current (a draft's stays private), the
  checklist item is "A playable patch uploaded" (current_patch set), and
  approval refuses non-archive hacks without one. Before, an unpublished upload
  satisfied the checklist and could be approved with nothing to play.
- The base ROM locks as soon as any version exists, and updateHack enforces it
  and checks the ROM id; it only locked once a patch was current, UI-only.
- createDraft enforces the shared title and summary limits and a known base ROM.

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

* AI disclosure: server-enforced upload confirmation, UTC dates, and archives can be labeled

- New versions pass the AI label stamp the uploader reviewed; confirmPatchUpload
  requires it to match the stored label and re-stamps it with the upload. The
  check was client-only and a failed re-stamp was ignored. confirmAiDisclosure
  is gone, and updateHack returns the stored stamp.
- The label's date and the draft strip's submitted date render in UTC, so the
  server and visitors in other time zones agree (it broke hydration).
- Archives get the AI label on their edit page through a shared AiLabelEditor
  (the in-place editor uses the same component), since the archive form had no
  way to fill it in.

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

* Discover: the filter sheet's count includes the search, and committing leaves no extra Back step

The results and the sheet's "Show N hacks" now share one predicate; the sheet
skipped the search query, so it could promise 14 and show 4. Committing replaces
the sheet's own history entry instead of pushing on top of it.

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

* Hack page: compact version picker selects; lightbox traps focus and closes on the backdrop; autoFocus survives dialogs

- The module and compact-bar pickers shared one open flag, so the hidden one's
  outside-click handler closed the menu before an option registered. Each copy
  now opens on its own.
- The lightbox uses useDialog (focus trap, Escape, focus back to the opener),
  and clicks outside the picture close it; the image's full-size wrapper used to
  swallow every click.
- useDialog leaves focus on a field that autofocused inside the dialog (the
  submit modal's contact box) and still restores the real opener.

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

* Polish: admin edit note matches how listed hacks save; named gallery handles; no shelf prefetch; seeded hacks submitted

- Admins editing a listed hack were told changes save as they type; listed
  hacks publish on Save.
- Gallery drag handles get an accessible name, staged previews' object URLs are
  released, and file extensions are cleaned so keys pass the server check.
- Home shelves no longer prefetch every visible hack page (HackCard's default
  is off, as on Discover).
- seed.sql marks seeded hacks as submitted, so local pending hacks aren't drafts.

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

* Lightbox: page scrolls again after closing

Moving the lightbox onto useDialog left two effects saving and restoring
<html>'s overflow. Cleanups run in declaration order, so useDialog restored it
and then the lightbox put back the "hidden" it had captured, leaving the page
unscrollable. useDialog now owns the <html> lock; the lightbox keeps only the
scrollbar padding and touch blocking, measured before the lock.

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

* Lightbox: the selected thumbnail's ring isn't clipped by the filmstrip

The strip scrolls horizontally, which also clips vertically, so the 2px ring
was cut off; it now has room with p-1. On phones the centering scroll also
overshot: thumbs' offsetLeft was measured from the lightbox, not the strip,
pushing the first thumb past the edge. The strip is now the offset parent.

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

* Discover: AI filter radios fill when picked in the phone sheet

The rail (hidden but mounted on phones) and the filter sheet rendered radios
with the same name, so they formed one group across the page. Re-rendering the
rail's checked option unchecked the sheet's pick, leaving no fill. Each filter
instance now gets its own group name.

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

* Discover: the phone Filters button stays screen-width with many filters applied

The button and the applied-filter chips share a column that's a flex item, and
flex items won't shrink below their content by default, so the unwrapped chip
row stretched the column (and the button) past the screen. min-w-0 lets the
column fit; the chip row already scrolls sideways.

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

* HackCard: Ready cards get a light green tint and faint green border instead of an outline

The full-strength border plus ring clashed with the card and split into two lines
with a gap on hover (a border and a box-shadow ring don't share a corner curve).
Ready cards and list rows now tint the surface 4% green (5% in dark) with a border
mixed 22% toward green, 40% on hover: one line, and calm when every card is Ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* HackCard: tag pills keep their contrast on the Ready tint

The pills used a fixed surface-2, which sat too close to the tinted Ready card
(especially in dark mode) and clashed with the green. They now shade whatever
they're on with 8% of the text color, which matches surface-2 on a plain card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Discover: the filter rail's height is plain CSS, fixing a runaway scroll at the page bottom

A scroll listener set the rail's max-height to the viewport minus its current
top. At the end of the results the sticky rail gets pushed up, its top went
negative, the rail grew, which pushed it further, so it raced to its bottom and
snapped back when scrolling up. It's now max-h calc(100dvh - 84px) with no
script; before the rail sticks, its bottom sits just below the fold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hack page: opening "How do I download?" no longer shifts the patch module

The in-module entry row was removed while the tour was open, so everything below
jumped up 42px, and back down on close. The row now stays in place, inactive and
without its beacon, while the tour is open; the floating pill and compact-bar
icon still go away since they don't affect layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Home: downloads milestone becomes an announcement card with room for a note

The v1 pill (ring, shadow, gradient text) becomes a rose-tinted card above the
hero headline: confetti disc, "1,000,000 downloads. Thank you!", and a short
note about the v2 redesign. The whole card replays the confetti; hover deepens
the tint and tilts the icon. No dismiss, no link. The number still comes from
NEXT_PUBLIC_DOWNLOADS_MILESTONE (no trailing +), the note from a constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fresh-look banner on player pages; fix the before-paint theme script that never ran

- A soft rose strip under the header on Discover, hack pages, FAQ, and My ROMs
  (not home, which has the milestone card, and not the editor): "Hackdex has a
  fresh new look. A thank you for 1,000,000 downloads..." The react-icons X is
  vertically centered at any wrap. Dismissal is stored per event, so the next
  milestone brings it back, and a before-paint script hides it for returning
  visitors without a flash.
- The theme init script was exported from a "use client" module, so the root
  layout got a client reference instead of the string and it never ran (dark
  mode only applied after hydration). Both inline scripts now live in
  src/utils/initScripts.ts.

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

* Editor: confirm before in-app links drop unsaved work; failed autosaves offer Retry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Details sheet: Cancel discards the session's edits instead of keeping them for the next save

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Version picker: keep the list on screen on phones, opening upward above the tab bar when needed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Discover filter sheet: focusing a hidden radio or checkbox no longer scrolls the sheet's frame

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Phone menu and filter sheet close when the window grows to desktop, releasing their scroll lock

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Start form: a network failure shows a retryable error instead of sticking on "Creating…"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Lightbox: the thumbnail strip swipes on touch again; the page behind still stays put

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hack page tabs and screenshot strips: arrow, Home, and End keys with a roving tabIndex; inset focus ring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Screenshot manager: reorder with Space and the arrow keys

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Version management: readable text and real hovers on status buttons, legible Re-upload badges, file inputs fit on phones

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Account and version menus: surface panels so the highlighted item stands out

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Touch: 40px hit areas for compact editor controls and the version chip; the Details sheet respects reduced motion

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Copy: the trending shelf no longer claims a seven-day window; signup asks people to invite a hack's creator instead of sharing it for them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Screenshot manager: an add builds on the latest list and saves run in order, so a delete or reorder during an upload sticks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Header search updates Discover in place when it's already open

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Editor strip: a later successful save no longer hides an earlier failure and its Retry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drafts: warn before reloading while an autosave is waiting or in flight

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Submit and AI dialogs recover from thrown actions; a failed Discord fallback no longer fails a submission that already landed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Home: linked hero names the ROMs in its paragraph instead of a pill; milestone card becomes a full-width strip above the hero

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* My ROMs shows the number of ready base ROMs instead of a dot

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Patch button keeps keyboard focus through patching

It was an inner component, so every status tick remounted it, and busy states used disabled, which blurs a focused button. It's now rendered by a function, and busy and just-patched states use aria-disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Archivers submit someone else's hack through drafts; the archive wizard is gone

The wizard never set is_archive, so it made ordinary hacks that skipped the draft checklist and AI label. Archivers (admins included) now get the on-behalf option on the start form. Archives skip the AI re-check on new versions and the dashboard's Needs AI label badge, since the label is optional there, and AI save errors on the patch page show as a toast instead of behind the modal. The start form also asks for a page address when the title has no ASCII letters or digits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Editor: gallery hydrates cleanly and clears Unsaved on save; archive form matches server limits and cleans cover names

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Discover: unchanged filter sheet leaves no extra history entry, copy-link shows on phones, counts format as en-US

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Toasts and Turnstile follow the site theme; phone menu clears the notice banner; focus opens collapsed tags; only the top dialog handles Escape and Tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Announcement lock records its PID so a killed run's lock clears

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Discover: AI filter by area, with presets that tick their areas and an Allow small usage step for code

The preset radios stay. Choose areas opens a checklist that mirrors the pick; a set matching no preset shows as Custom. Ticking Code yourself allows small use (a bug fix or a few) by default, while the presets stay strict. Custom picks go in the URL as areas joined by ".", and the catalog now carries each hack's six AI levels.

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

* Discover: the AI filter is a saved preference instead of a URL param

It's stored on the device and read before the results show, so it applies on every visit and never travels in a shared link. Changing it anywhere saves it, and Any clears it. The AI use group notes that its settings are saved across sessions.

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

* Discover: indent the AI area checklist under Choose areas

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

* Hack credits, submission, downloads, current patch and AI label are server-only in the database

The owner policies let a creator write any column straight through PostgREST: mark a draft submitted without its checklist, credit someone else, set downloads, or repoint current_patch. hacks_update_guard now covers inserts too and blocks those columns for everyone but admins, the service role and postgres. The app writes them with the service client after its own permission checks.

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

* A first patch upload returns to the hack page, not Manage Versions

The first upload always goes live, but the redirect only looked at the publish checkbox, so a draft's first upload landed away from its checklist.

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

* Milestone confetti draws under the phone menu

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

* Remove CSS left from the v1 hero, wizard and buttons

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

* Discover: the saved AI filter shows on the results count line instead of as a chip, and Clear leaves it

The count line reads "· No AI content" (or "Some AI usage hidden" for a custom pick) and opens the filter sheet at AI use on phones, or flashes it in the rail on desktop. Clear all, Clear N and Clear filters now reset only the search's filters; the phone Filters badge still counts the AI filter. The empty state offers Change AI filter when one is on.

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

* Discover: on phones the copy-link button sits beside Filters

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

* Hack page: Share and the options menu sit on the right on phones

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

* Hack page: the About tab's screenshot swipes again

The v2 stage was a static image, which dropped main's Embla carousel. It's a looping carousel again (swipe or drag), kept in sync with the thumbnails, arrow keys and the lightbox; a drag never opens the lightbox.

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

* Start form: title placeholder reads "My Pokémon Romhack"

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

* Start form: "Page URL" instead of "Page address", and no autofill on its fields

Phones read "address" as a street address and offered the visitor's home address for every field below it.

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

* Home shelves: cards are 266px so screenshots sit at exactly 1×

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Combine the branch's five migrations into one v2_redesign migration

None of them have run in production, and db push now applies them in one go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cards: phone screenshots stay at 1× with dimmed neighbors over a blurred backdrop; bars under the shot

On phones the grid stretched shots to the card width (1.13–1.42×), so the pixel
art was smoothed. Now each shot stays at 1×, the neighbors peek in at 40%, and a
blurred copy of the current shot fills the card and crossfades as you swipe.
The dots were invisible over white text boxes, so they're bars under the shot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Start form: creator name placeholder reads "Their name or handle"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cards: dragging the screenshots no longer selects text in Firefox

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fresh-look banner: the confetti icon centers vertically in the banner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fresh-look banner: only shows while NEXT_PUBLIC_DOWNLOADS_MILESTONE is 1000000

Its copy thanks players for 1,000,000 downloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* FAQ: disclose that a large portion of Hackdex's code was written with AI assistance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AI label: content areas offer None / Some / All instead of Most

Content areas now top out at All (hint: "Most or all of …"). Code keeps its
five-level scale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: pin the Supabase CLI to 2.111.0 so the generated-types check is stable

CLI 2.119.0 (what "latest" resolves to since Sep 30) writes generated types
unformatted, so the check failed for every PR touching supabase/. 2.111.0
reproduces the checked-in src/types/db.ts exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 23:36:36 -06:00

550 lines
23 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Development seed data for local Supabase.
-- Run via: supabase db reset (or supabase db query --local -f supabase/seed.sql)
--
-- Shared constants (keep in sync with scripts/seed-storage.mjs):
-- SEED_SHARED_BPS = seed-shared.bps
-- SEED_SHARED_XDELTA = seed-shared.xdelta
-- SEED_APPROVED_SLUG = seed-emerald-demo
-- SEED_PENDING_SLUG = seed-pending-demo
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Fixed user UUIDs
-- admin: 11111111-1111-1111-1111-111111111111
-- creator: 22222222-2222-2222-2222-222222222222
-- creator2: 33333333-3333-3333-3333-333333333333
DO $$
DECLARE
v_password text := crypt('Password1', gen_salt('bf'));
BEGIN
-- Admin
INSERT INTO auth.users (
id, instance_id, aud, role, email, encrypted_password,
email_confirmed_at,
confirmation_token, recovery_token, email_change_token_new, email_change,
email_change_token_current, reauthentication_token, phone_change_token,
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
) VALUES (
'11111111-1111-1111-1111-111111111111',
'00000000-0000-0000-0000-000000000000',
'authenticated', 'authenticated',
'admin@hackdex.local', v_password, now(),
'', '', '', '',
'', '', '',
'{"provider":"email","providers":["email"],"claims_admin":true}'::jsonb,
'{"full_name":"Admin User"}'::jsonb,
now(), now()
);
INSERT INTO auth.identities (
id, user_id, identity_data, provider, provider_id,
last_sign_in_at, created_at, updated_at
) VALUES (
'11111111-1111-1111-1111-111111111111',
'11111111-1111-1111-1111-111111111111',
'{"sub":"11111111-1111-1111-1111-111111111111","email":"admin@hackdex.local","email_verified":true}'::jsonb,
'email', '11111111-1111-1111-1111-111111111111',
now(), now(), now()
);
-- Creator (owns approved hack)
INSERT INTO auth.users (
id, instance_id, aud, role, email, encrypted_password,
email_confirmed_at,
confirmation_token, recovery_token, email_change_token_new, email_change,
email_change_token_current, reauthentication_token, phone_change_token,
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
) VALUES (
'22222222-2222-2222-2222-222222222222',
'00000000-0000-0000-0000-000000000000',
'authenticated', 'authenticated',
'creator@hackdex.local', v_password, now(),
'', '', '', '',
'', '', '',
'{"provider":"email","providers":["email"]}'::jsonb,
'{"full_name":"Creator User"}'::jsonb,
now(), now()
);
INSERT INTO auth.identities (
id, user_id, identity_data, provider, provider_id,
last_sign_in_at, created_at, updated_at
) VALUES (
'22222222-2222-2222-2222-222222222222',
'22222222-2222-2222-2222-222222222222',
'{"sub":"22222222-2222-2222-2222-222222222222","email":"creator@hackdex.local","email_verified":true}'::jsonb,
'email', '22222222-2222-2222-2222-222222222222',
now(), now(), now()
);
-- Creator 2 (owns pending hack)
INSERT INTO auth.users (
id, instance_id, aud, role, email, encrypted_password,
email_confirmed_at,
confirmation_token, recovery_token, email_change_token_new, email_change,
email_change_token_current, reauthentication_token, phone_change_token,
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
) VALUES (
'33333333-3333-3333-3333-333333333333',
'00000000-0000-0000-0000-000000000000',
'authenticated', 'authenticated',
'creator2@hackdex.local', v_password, now(),
'', '', '', '',
'', '', '',
'{"provider":"email","providers":["email"]}'::jsonb,
'{"full_name":"Creator Two"}'::jsonb,
now(), now()
);
INSERT INTO auth.identities (
id, user_id, identity_data, provider, provider_id,
last_sign_in_at, created_at, updated_at
) VALUES (
'33333333-3333-3333-3333-333333333333',
'33333333-3333-3333-3333-333333333333',
'{"sub":"33333333-3333-3333-3333-333333333333","email":"creator2@hackdex.local","email_verified":true}'::jsonb,
'email', '33333333-3333-3333-3333-333333333333',
now(), now(), now()
);
END $$;
-- Profiles (handle_new_user trigger creates rows; set usernames for dashboard access)
UPDATE public.profiles SET username = 'admin', full_name = 'Admin User', verified = true WHERE id = '11111111-1111-1111-1111-111111111111';
UPDATE public.profiles SET username = 'creator', full_name = 'Creator User', verified = true WHERE id = '22222222-2222-2222-2222-222222222222';
UPDATE public.profiles SET username = 'creator2', full_name = 'Creator Two', verified = false WHERE id = '33333333-3333-3333-3333-333333333333';
-- Tag catalog (103 tags from prod-like export; tags 93/94/96 use now() for New-tag demo on seed-emerald-demo)
INSERT INTO public.tags (id, name, category, created_at) VALUES
(1, 'Additional Story', 'Altered', null),
(2, 'Day/Night Cycle', 'Altered', null),
(3, 'Double Battles', 'Altered', null),
(4, 'Fairy Type', 'Altered', null),
(5, 'Gigantamax', 'Altered', null),
(6, 'Megas', 'Altered', null),
(7, 'Move Changes', 'Altered', null),
(8, 'PSS', 'Altered', null),
(9, 'Stat Changes', 'Altered', null),
(10, 'Tera', 'Altered', null),
(11, 'Type Changes', 'Altered', null),
(12, 'Type Chart Changes', 'Altered', null),
(13, 'Z-Moves', 'Altered', null),
(14, 'Difficulty Options', 'Difficulty', null),
(15, 'Easy', 'Difficulty', null),
(16, 'Hard', 'Difficulty', null),
(17, 'Kaizo Difficulty', 'Difficulty', null),
(18, 'Level Caps (Mandatory)', 'Difficulty', null),
(19, 'Level Caps (Optional)', 'Difficulty', null),
(20, 'Nuzlocke Mode', 'Difficulty', null),
(21, 'Rare Candies', 'Difficulty', null),
(22, 'Vanilla+ Difficulty', 'Difficulty', null),
(23, 'Vanilla++ Difficulty', 'Difficulty', null),
(24, 'Demo', 'Gameplay', null),
(25, 'Level Scaling', 'Difficulty', null),
(26, 'Multiplayer Compatibility', 'Gameplay', null),
(27, 'Open World', 'Gameplay', null),
(28, 'Post-Game Content', 'Gameplay', null),
(29, 'Quests', 'Gameplay', null),
(30, 'Roguelite', 'Category', null),
(31, 'Story', 'Category', null),
(32, 'Vanilla Multiplayer Compatibility', 'Gameplay', null),
(33, 'Custom Abilities', 'New', null),
(34, 'Custom Battle Gimmick', 'New', null),
(35, 'Custom Moves', 'New', null),
(36, 'Custom Types', 'New', null),
(37, 'New Items', 'New', null),
(38, 'New Mechanics', 'New', null),
(39, 'New Music', 'New', null),
(40, 'New Region', 'New', null),
(41, 'Betamon', 'Pokédex', null),
(42, 'Custom Dex', 'Pokédex', null),
(43, 'Custom Megas', 'Pokédex', null),
(44, 'Custom Regionals', 'Pokédex', null),
(45, 'Fakemon', 'Pokédex', null),
(46, 'Fusions', 'Pokédex', null),
(47, 'Gen 1 NatDex', 'Pokédex', null),
(48, 'Gen 2 NatDex', 'Pokédex', null),
(49, 'Gen 3 NatDex', 'Pokédex', null),
(50, 'Gen 4 NatDex', 'Pokédex', null),
(51, 'Gen 5 NatDex', 'Pokédex', null),
(52, 'Gen 6 NatDex', 'Pokédex', null),
(53, 'Gen 7 NatDex', 'Pokédex', null),
(54, 'Gen 8 NatDex', 'Pokédex', null),
(55, 'Gen 9 NatDex', 'Pokédex', null),
(56, 'New Forms', 'Pokédex', null),
(57, 'Some Fakemon', 'Pokédex', null),
(58, 'Vanilla Dex', 'Pokédex', null),
(59, 'Bug Fixes', 'Quality of Life', null),
(60, 'Built-in Randomizer', 'Quality of Life', null),
(61, 'Clarified Natures', 'Quality of Life', null),
(62, 'Decapitalization', 'Quality of Life', null),
(63, 'DexNav', 'Quality of Life', null),
(64, 'Easy EVs/IVs/DVs', 'Quality of Life', null),
(65, 'Easy EXP', 'Quality of Life', null),
(66, 'Following Pokémon', 'Quality of Life', null),
(67, 'HM Improvements', 'Quality of Life', null),
(68, 'Reusable TMs', 'Quality of Life', null),
(69, 'Revised Trade Evos', 'Quality of Life', null),
(70, 'Running Shoes', 'Quality of Life', null),
(71, 'Tons of QoL', 'Quality of Life', null),
(72, 'Visible IVs/EVs/DVs', 'Quality of Life', null),
(73, 'Wonder Trade', 'Quality of Life', null),
(74, '<1 hour', 'Scale', null),
(75, '1-2 hrs', 'Scale', null),
(76, '2-4 hrs', 'Scale', null),
(77, '4-8 hrs', 'Scale', null),
(78, '8+ hrs', 'Scale', null),
(79, 'New Graphics', 'Graphics', null),
(80, 'Vanilla Graphics', 'Graphics', null),
(81, 'Comedy', 'Tone', null),
(82, 'Exploration', 'Tone', null),
(83, 'Lighthearted', 'Tone', null),
(84, 'Mature', 'Tone', null),
(85, 'Non-Traditional', 'Category', null),
(86, 'Puzzle', 'Category', null),
(87, 'Traditional', 'Category', null),
(88, 'CFRU', null, null),
(89, 'hg-engine', null, null),
(90, 'pokeemerald-expansion', null, null),
(91, 'Vanilla', 'Gameplay', '2026-01-01 07:00:00+00'),
(92, 'Translation', 'Altered', '2026-01-01 07:00:00+00'),
(93, 'Demake', 'Category', now()),
(94, 'Minigame', 'Category', now()),
(95, 'Escape Room', 'Category', '2026-01-01 07:00:00+00'),
(96, 'Technical Concept', 'Category', now()),
(97, '<100 Dex Size', 'Pokédex', '2026-01-01 07:00:00+00'),
(98, '100-300 Dex Size', 'Pokédex', '2026-01-01 07:00:00+00'),
(99, '300-500 Dex Size', 'Pokédex', '2026-01-01 07:00:00+00'),
(100, '500-700 Dex Size', 'Pokédex', '2026-01-01 07:00:00+00'),
(101, '700+ Dex Size', 'Pokédex', null),
(102, 'Built-in Cheats', 'Quality of Life', null),
(103, 'Profanity', 'Tone', null);
SELECT setval(pg_get_serial_sequence('public.tags', 'id'), (SELECT MAX(id) FROM public.tags));
-- Hacks (18 total; tags_updated_at past on emerald + pending-demo for New-tag demo)
-- downloads defaults to 0; patch_downloads inserts increment via trigger
INSERT INTO public.hacks (
slug, title, summary, description, base_rom, patch_url, version,
created_by, language, approved, approved_at, approved_by,
completion_status, patches_download_permission,
is_archive, published, tags_updated_at,
original_author, permission_from
) VALUES
(
'seed-emerald-demo',
'Seed Emerald Demo',
'Approved dev hack for discover, download, and in-browser patching.',
'Seeded development data using the shared example BPS for Pokémon Emerald. Run npm run seed:storage after db reset.',
'poke_emerald', '', '1.0',
'22222222-2222-2222-2222-222222222222',
'English', true, now(), '11111111-1111-1111-1111-111111111111',
'Complete', 'Current',
false, false, now() - interval '30 days',
null, null
),
(
'seed-pending-demo',
'Seed Pending Demo',
'Pending dev hack for testing the admin approval dashboard.',
'Intentionally unapproved. Log in as admin@hackdex.local to review on the dashboard.',
'poke_emerald', '', '0.1',
'33333333-3333-3333-3333-333333333333',
'English', false, null, null,
'Beta', 'None',
false, false, now() - interval '30 days',
null, null
),
(
'seed-pending-ready',
'Seed Pending Ready',
'Pending hack with patch, covers, and tags — ready for admin review.',
'Has patch and gallery metadata so approve UI shows no missing-screenshot or missing-patch warnings.',
'poke_emerald', '', '0.2',
'33333333-3333-3333-3333-333333333333',
'English', false, null, null,
'Beta', 'None',
false, false, default,
null, null
),
(
'seed-all-downloads',
'Seed All Downloads',
'Approved hack with All download permission across published versions.',
'Tests direct BPS download on every published version row.',
'poke_emerald', '', '2.0',
'22222222-2222-2222-2222-222222222222',
'English', true, now(), '11111111-1111-1111-1111-111111111111',
'Alpha', 'All',
false, false, default,
null, null
),
(
'seed-current-only',
'Seed Current Only',
'Approved hack with Current download permission.',
'Direct download only on the current published patch version.',
'poke_emerald', '', '2.0',
'22222222-2222-2222-2222-222222222222',
'English', true, now(), '11111111-1111-1111-1111-111111111111',
'Demo', 'Current',
false, false, default,
null, null
),
(
'seed-draft-version',
'Seed Draft Version',
'Approved hack with a published current version and an unpublished draft.',
'Tests draft/publish UI in the version editor.',
'poke_emerald', '', '2.0',
'22222222-2222-2222-2222-222222222222',
'English', true, now(), '11111111-1111-1111-1111-111111111111',
'Complete', 'Current',
false, false, default,
null, null
),
(
'seed-archived-version',
'Seed Archived Version',
'Approved hack with an archived older version and a current release.',
'Tests show-archived and restore flows on the versions page.',
'poke_emerald', '', '2.0',
'22222222-2222-2222-2222-222222222222',
'English', true, now(), '11111111-1111-1111-1111-111111111111',
'Complete', 'Current',
false, false, default,
null, null
),
(
'seed-third-party',
'Seed Third Party',
'Approved hack credited to an external original author.',
'Tests third-party author and permission display on the hack page.',
'poke_emerald', '', '1.0',
'22222222-2222-2222-2222-222222222222',
'English', true, now(), '11111111-1111-1111-1111-111111111111',
'Complete', 'Current',
false, false, default,
'Famous Hacker', 'Famous Hacker'
),
(
'seed-xdelta-demo',
'Seed Xdelta Demo',
'Approved hack with a published xdelta patch.',
'Seeded development data using the shared example xdelta for Pokémon Emerald. Run npm run seed:storage after db reset.',
'poke_emerald', '', '1.0',
'22222222-2222-2222-2222-222222222222',
'English', true, now(), '11111111-1111-1111-1111-111111111111',
'Complete', 'Current',
false, false, default,
null, null
);
UPDATE public.hacks SET
assigned_admin = '11111111-1111-1111-1111-111111111111',
verification_contact_info = 'creator2@hackdex.local / Discord: creator2'
WHERE slug = 'seed-pending-ready';
UPDATE public.hacks SET
box_art = 'https://images.launchbox-app.com/1fde111d-8805-47ba-af07-03b436e4dfde.jpg',
social_links = '{
"discord": "https://discord.gg/example",
"twitter": "https://twitter.com/example",
"pokecommunity": "https://www.pokecommunity.com/threads/example.1/",
"github": "https://github.com/example/seed-emerald-demo"
}'::jsonb
WHERE slug = 'seed-emerald-demo';
UPDATE public.hacks SET
custom_version_name = '1.0 + 2.0'
WHERE slug = 'seed-all-downloads';
-- Nine patcher-only sandboxes (3 per owner account)
DO $$
DECLARE
v_admin uuid := '11111111-1111-1111-1111-111111111111';
v_creator uuid := '22222222-2222-2222-2222-222222222222';
v_creator2 uuid := '33333333-3333-3333-3333-333333333333';
v_owner record;
v_slug text;
v_num int;
v_idx int := 0;
v_tag_set_idx int;
v_tags int[];
v_tag_id int;
v_pos int;
v_cover int;
BEGIN
FOR v_owner IN
SELECT * FROM (VALUES
('admin', v_admin),
('creator', v_creator),
('creator2', v_creator2)
) AS owners(label, uid)
LOOP
FOR v_num IN 1..3 LOOP
v_idx := v_idx + 1;
v_slug := 'seed-patcher-only-' || v_owner.label || '-' || v_num;
v_tag_set_idx := (v_idx - 1) % 3;
IF v_tag_set_idx = 0 THEN
v_tags := ARRAY[27, 71, 49];
ELSIF v_tag_set_idx = 1 THEN
v_tags := ARRAY[16, 71, 90];
ELSE
v_tags := ARRAY[31, 71, 49];
END IF;
INSERT INTO public.hacks (
slug, title, summary, description, base_rom, patch_url, version,
created_by, language, approved, approved_at, approved_by,
completion_status, patches_download_permission, downloads,
is_archive, published, tags_updated_at
) VALUES (
v_slug,
'Seed Patcher Only ' || initcap(v_owner.label) || ' ' || v_num,
'Editable sandbox with patcher-only download permission (None).',
'Approved sandbox for testing in-browser patcher with no direct BPS download.',
'poke_emerald', '', '2.0',
v_owner.uid,
'English', true, now(), v_admin,
'Complete', 'None', 0,
false, false, default
);
INSERT INTO public.patches (parent_hack, version, filename, bucket, published, published_at, changelog)
VALUES
(v_slug, '1.0', 'seed-shared.bps', 'patches', true, now(), 'Initial sandbox version.'),
(v_slug, '1.1', 'seed-shared.bps', 'patches', true, now(), 'Minor sandbox update.'),
(v_slug, '2.0', 'seed-shared.bps', 'patches', true, now(), 'Current sandbox version.');
UPDATE public.hacks
SET current_patch = (
SELECT id FROM public.patches
WHERE parent_hack = v_slug AND version = '2.0'
LIMIT 1
)
WHERE slug = v_slug;
v_pos := 0;
FOREACH v_tag_id IN ARRAY v_tags LOOP
v_pos := v_pos + 1;
INSERT INTO public.hack_tags (hack_slug, tag_id, "order")
VALUES (v_slug, v_tag_id, v_pos);
END LOOP;
FOR v_cover IN 1..3 LOOP
INSERT INTO public.hack_covers (hack_slug, url, position)
VALUES (v_slug, v_slug || '/cover-' || v_cover || '.png', v_cover);
END LOOP;
END LOOP;
END LOOP;
END $$;
-- Patches for non-sandbox hacks (BPS rows omit format; default is bps)
INSERT INTO public.patches (parent_hack, version, filename, bucket, published, published_at, changelog) VALUES
('seed-emerald-demo', '1.0', 'seed-shared.bps', 'patches', true, now(), 'Initial seeded version for local development.'),
('seed-pending-ready', '0.2', 'seed-shared.bps', 'patches', true, now(), 'Ready-for-review pending version.'),
('seed-all-downloads', '1.0', 'seed-shared.bps', 'patches', true, now(), 'First published version.'),
('seed-all-downloads', '2.0', 'seed-shared.bps', 'patches', true, now(), 'Current published version.'),
('seed-current-only', '1.0', 'seed-shared.bps', 'patches', true, now(), 'First published version.'),
('seed-current-only', '2.0', 'seed-shared.bps', 'patches', true, now(), 'Current published version.'),
('seed-draft-version', '1.0', 'seed-shared.bps', 'patches', true, now(), 'First published version.'),
('seed-draft-version', '2.0', 'seed-shared.bps', 'patches', true, now(), 'Current published version.'),
('seed-draft-version', '2.1', 'seed-shared.bps', 'patches', false, null, 'Unpublished draft version.'),
('seed-archived-version', '1.0', 'seed-shared.bps', 'patches', true, now(), 'Archived older version.'),
('seed-archived-version', '2.0', 'seed-shared.bps', 'patches', true, now(), 'Current published version.'),
('seed-third-party', '1.0', 'seed-shared.bps', 'patches', true, now(), 'Third-party credited version.');
INSERT INTO public.patches (parent_hack, version, filename, bucket, published, published_at, changelog, format) VALUES
('seed-xdelta-demo', '1.0', 'seed-shared.xdelta', 'patches', true, now(), 'Xdelta seeded version.', 'xdelta');
UPDATE public.patches
SET archived = true, archived_at = now()
WHERE parent_hack = 'seed-archived-version' AND version = '1.0';
UPDATE public.hacks SET current_patch = (SELECT id FROM public.patches WHERE parent_hack = 'seed-emerald-demo' AND version = '1.0' LIMIT 1) WHERE slug = 'seed-emerald-demo';
UPDATE public.hacks SET current_patch = (SELECT id FROM public.patches WHERE parent_hack = 'seed-pending-ready' AND version = '0.2' LIMIT 1) WHERE slug = 'seed-pending-ready';
UPDATE public.hacks SET current_patch = (SELECT id FROM public.patches WHERE parent_hack = 'seed-all-downloads' AND version = '2.0' LIMIT 1) WHERE slug = 'seed-all-downloads';
UPDATE public.hacks SET current_patch = (SELECT id FROM public.patches WHERE parent_hack = 'seed-current-only' AND version = '2.0' LIMIT 1) WHERE slug = 'seed-current-only';
UPDATE public.hacks SET current_patch = (SELECT id FROM public.patches WHERE parent_hack = 'seed-draft-version' AND version = '2.0' LIMIT 1) WHERE slug = 'seed-draft-version';
UPDATE public.hacks SET current_patch = (SELECT id FROM public.patches WHERE parent_hack = 'seed-archived-version' AND version = '2.0' LIMIT 1) WHERE slug = 'seed-archived-version';
UPDATE public.hacks SET current_patch = (SELECT id FROM public.patches WHERE parent_hack = 'seed-third-party' AND version = '1.0' LIMIT 1) WHERE slug = 'seed-third-party';
UPDATE public.hacks SET current_patch = (SELECT id FROM public.patches WHERE parent_hack = 'seed-xdelta-demo' AND version = '1.0' LIMIT 1) WHERE slug = 'seed-xdelta-demo';
-- hack_tags (all complete hacks except seed-pending-demo)
INSERT INTO public.hack_tags (hack_slug, tag_id, "order") VALUES
('seed-emerald-demo', 31, 1),
('seed-emerald-demo', 71, 2),
('seed-emerald-demo', 90, 3),
('seed-emerald-demo', 49, 4),
('seed-pending-ready', 31, 1),
('seed-pending-ready', 59, 2),
('seed-pending-ready', 71, 3),
('seed-all-downloads', 26, 1),
('seed-all-downloads', 71, 2),
('seed-all-downloads', 79, 3),
('seed-current-only', 27, 1),
('seed-current-only', 49, 2),
('seed-current-only', 71, 3),
('seed-draft-version', 31, 1),
('seed-draft-version', 71, 2),
('seed-draft-version', 90, 3),
('seed-archived-version', 28, 1),
('seed-archived-version', 49, 2),
('seed-archived-version', 71, 3),
('seed-third-party', 45, 1),
('seed-third-party', 71, 2),
('seed-third-party', 31, 3),
('seed-xdelta-demo', 38, 1),
('seed-xdelta-demo', 71, 2),
('seed-xdelta-demo', 90, 3);
-- hack_covers (17 complete hacks × 3 covers; seed-pending-demo has none)
DO $$
DECLARE
v_slug text;
BEGIN
FOREACH v_slug IN ARRAY ARRAY[
'seed-emerald-demo',
'seed-pending-ready',
'seed-all-downloads',
'seed-current-only',
'seed-draft-version',
'seed-archived-version',
'seed-third-party',
'seed-xdelta-demo'
] LOOP
INSERT INTO public.hack_covers (hack_slug, url, position) VALUES
(v_slug, v_slug || '/cover-1.png', 1),
(v_slug, v_slug || '/cover-2.png', 2),
(v_slug, v_slug || '/cover-3.png', 3);
END LOOP;
END $$;
-- Custom patcher versions for seed-all-downloads (published 1.0 then 2.0)
INSERT INTO public.hack_patcher_patches (hack_slug, patch_id, sort_order)
SELECT 'seed-all-downloads', p.id, spec.sort_order
FROM (VALUES ('1.0', 1), ('2.0', 2)) AS spec(version, sort_order)
JOIN public.patches p ON p.parent_hack = 'seed-all-downloads' AND p.version = spec.version;
-- patch_downloads: dated unique (patch, device_id) rows across ~30 days.
-- Trigger increments hacks.downloads; do not also hardcode downloads on hacks.
INSERT INTO public.patch_downloads (patch, device_id, created_at)
SELECT
p.id,
'seed-device-' || spec.slug || '-' || spec.ver || '-' || gs.n,
now() - (gs.n * 30.0 / spec.cnt) * interval '1 day'
FROM (
VALUES
('seed-emerald-demo', '1.0', 30),
('seed-all-downloads', '1.0', 9),
('seed-all-downloads', '2.0', 18),
('seed-current-only', '2.0', 12),
('seed-third-party', '1.0', 15)
) AS spec(slug, ver, cnt)
JOIN public.patches p ON p.parent_hack = spec.slug AND p.version = spec.ver
CROSS JOIN LATERAL generate_series(0, spec.cnt - 1) AS gs(n);
-- Seeded hacks count as submitted (drafts are made in the app); otherwise the pending ones show up as private drafts.
UPDATE public.hacks SET submitted_at = created_at WHERE slug LIKE 'seed-%' AND submitted_at IS NULL;