Add local seed data so a db reset covers current features (#91)

* Add local db seed

* Expand local db seed for main schema and dev testing fixtures.

Seed the production tag catalog, 19 hack permutations for download/version/archive flows, shared MinIO patch storage, and placehold.co cover uploads.

* Update local seed fixtures for current schema and xdelta patches.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jared Schoeny
2026-09-04 13:31:30 -06:00
committed by GitHub
parent f84350be82
commit fbe25aa4cd
5 changed files with 768 additions and 0 deletions

View File

@@ -117,6 +117,53 @@ Typical flow:
- Set the `PATCHES_BUCKET` environment variable to the name of your patches bucket (e.g., `patches`)
- Set the `NEXT_PUBLIC_HACK_COVERS_DOMAIN` environment variable to the public URL of your covers bucket (e.g., `http://localhost:9000/covers`)
### Seed data
After Supabase and MinIO are running, reset the database and upload dev fixtures:
```
npm run db:reset
```
This runs `supabase db reset` (migrations only — `[db.seed]` is disabled for CI), applies [`supabase/seed.sql`](supabase/seed.sql) via the local Supabase Postgres container, then uploads the shared BPS and xdelta patches plus cover images to MinIO via [`scripts/seed-storage.mjs`](scripts/seed-storage.mjs).
You can also run the steps separately:
```
supabase db reset
docker exec -i supabase_db_pokemon-romhack-platform psql -U postgres -d postgres -v ON_ERROR_STOP=1 < supabase/seed.sql
npm run seed:storage
```
**Dev login accounts** (password `Password1` for all):
| Email | Username | Role |
|-------|----------|------|
| `admin@hackdex.local` | `admin` | Admin (`claims_admin`) |
| `creator@hackdex.local` | `creator` | Regular creator |
| `creator2@hackdex.local` | `creator2` | Owns pending hacks |
**Seeded hacks (highlights):**
| Slug | Status | Purpose |
|------|--------|---------|
| `seed-emerald-demo` | Approved | Baseline discover, download, patch (`poke_emerald`); box art, social links; edit tags to see "New" badges |
| `seed-pending-demo` | Pending | Missing patch/screenshot warnings; unassigned |
| `seed-pending-ready` | Pending | Ready-for-review (patch + covers); assigned to admin; verification contact on the unverified owner |
| `seed-patcher-only-{account}-{1\|2\|3}` | Approved | Nine editable sandboxes (3 per account), patcher-only (`None` permission) |
| `seed-all-downloads` | Approved | Direct download on all published versions (`All`); custom patcher over 1.0 + 2.0 |
| `seed-current-only` | Approved | Direct download on current version only (`Current`); `Demo` completion status |
| `seed-draft-version` | Approved | Unpublished draft + publish UI |
| `seed-archived-version` | Approved | Archived older version + restore |
| `seed-third-party` | Approved | Third-party author display |
| `seed-xdelta-demo` | Approved | Published xdelta patch (`format = xdelta`) |
BPS patch rows share `seed-shared.bps`. The xdelta row on `seed-xdelta-demo` uses `seed-shared.xdelta`. Complete hacks have ≥3 cover images (`{slug}/cover-1.png` …) fetched from placehold.co during `seed:storage`.
To test in-browser patching on `seed-emerald-demo`, link a local **Pokémon Emerald** ROM (CRC32 `1f1c08fb`). The BPS fixture is [`public/patches/example_patch.bps`](public/patches/example_patch.bps), uploaded as `seed-shared.bps`. The xdelta fixture is [`public/patches/example_patch.xdelta`](public/patches/example_patch.xdelta), uploaded as `seed-shared.xdelta`.
**Note:** `seed:storage` requires `S3_*`, `PATCHES_BUCKET`, and `COVERS_BUCKET` in `.env.local` or `.env.development.local`, plus `NEXT_PUBLIC_HACK_COVERS_DOMAIN` for viewing covers in the app. It needs network access to placehold.co for cover images. If `PATCH_TOKEN_SECRET` and `PATCHES_DOWNLOAD_BASE_URL` are set, patch downloads route through the Cloudflare Worker instead of MinIO — unset those for local MinIO testing.
### Install & run
```

View File

@@ -7,6 +7,8 @@
"build": "next build --turbopack",
"start": "next start",
"db:pull": "supabase db pull && supabase migration up && supabase gen types --local > src/types/db.ts",
"db:reset": "supabase db reset && docker exec -i supabase_db_pokemon-romhack-platform psql -U postgres -d postgres -v ON_ERROR_STOP=1 < supabase/seed.sql && npm run seed:storage",
"seed:storage": "node --env-file=.env.local --env-file=.env.development.local scripts/seed-storage.mjs",
"discord:register": "node scripts/register-discord-commands.mjs",
"postinstall": "patch-package"
},

Binary file not shown.

173
scripts/seed-storage.mjs Normal file
View File

@@ -0,0 +1,173 @@
/**
* Upload dev seed fixtures to MinIO.
*
* Shared constants (keep in sync with supabase/seed.sql):
* SEED_SHARED_BPS = seed-shared.bps
* SEED_SHARED_XDELTA = seed-shared.xdelta
* SEED_APPROVED_SLUG = seed-emerald-demo
* SEED_PENDING_SLUG = seed-pending-demo
*
* Usage:
* npm run seed:storage
*
* Requires MinIO/S3 env vars (S3_ENDPOINT, S3_PORT, etc.) and PATCHES_BUCKET.
* Cover seeding also requires COVERS_BUCKET and network access to placehold.co.
*/
import { existsSync } from "node:fs";
import { stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Client } from "minio";
export const SEED_SHARED_BPS = "seed-shared.bps";
export const SEED_SHARED_XDELTA = "seed-shared.xdelta";
const PATCHES_BUCKET = process.env.PATCHES_BUCKET || "patches";
const COVERS_BUCKET = process.env.COVERS_BUCKET || "covers";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BPS_FIXTURE_PATH = path.resolve(__dirname, "../public/patches/example_patch.bps");
const XDELTA_FIXTURE_PATH = path.resolve(__dirname, "../public/patches/example_patch.xdelta");
/** Hacks that get ≥3 cover images (all complete seeded hacks). */
const COMPLETE_HACK_SLUGS = [
"seed-emerald-demo",
"seed-pending-ready",
"seed-patcher-only-admin-1",
"seed-patcher-only-admin-2",
"seed-patcher-only-admin-3",
"seed-patcher-only-creator-1",
"seed-patcher-only-creator-2",
"seed-patcher-only-creator-3",
"seed-patcher-only-creator2-1",
"seed-patcher-only-creator2-2",
"seed-patcher-only-creator2-3",
"seed-all-downloads",
"seed-current-only",
"seed-draft-version",
"seed-archived-version",
"seed-third-party",
"seed-xdelta-demo",
];
const COVER_PALETTES = [
{ bg: "1a1a2e", text: "ffffff" },
{ bg: "16213e", text: "e94560" },
{ bg: "0f3460", text: "ffffff" },
{ bg: "2d6a4f", text: "ffffff" },
{ bg: "40916c", text: "d8f3dc" },
{ bg: "5c4d7d", text: "ffffff" },
];
function requireEnv(name) {
const value = process.env[name];
if (!value) {
console.error(`Missing required environment variable: ${name}`);
console.error("Load env via npm run seed:storage (uses --env-file flags) or export vars manually.");
process.exit(1);
}
return value;
}
async function ensureBucket(client, bucket) {
const exists = await client.bucketExists(bucket);
if (!exists) {
console.log(`Creating bucket "${bucket}"...`);
await client.makeBucket(bucket, "");
}
}
function coverObjectKey(slug, index) {
return `${slug}/cover-${index}.png`;
}
function placeholdUrl(slug, index) {
const palette = COVER_PALETTES[(slug.length + index) % COVER_PALETTES.length];
const label = slug.replace(/^seed-/, "").replace(/-/g, " ");
const text = encodeURIComponent(`Cover ${index}\n${label}`);
return `https://placehold.co/240x160/${palette.bg}/${palette.text}/png?text=${text}`;
}
async function uploadPatchFixture(client, fixturePath, objectKey, expectedHint) {
if (!existsSync(fixturePath)) {
console.error(`Fixture not found: ${fixturePath}`);
console.error(expectedHint);
process.exit(1);
}
const { size } = await stat(fixturePath);
console.log(`Uploading ${fixturePath} (${(size / 1024 / 1024).toFixed(1)} MB)`);
console.log(` → ${PATCHES_BUCKET}/${objectKey}`);
await client.fPutObject(PATCHES_BUCKET, objectKey, fixturePath, {
"Content-Type": "application/octet-stream",
});
}
async function uploadSharedPatches(client) {
await uploadPatchFixture(
client,
BPS_FIXTURE_PATH,
SEED_SHARED_BPS,
"Expected public/patches/example_patch.bps (Pokémon Emerald dev patch).",
);
await uploadPatchFixture(
client,
XDELTA_FIXTURE_PATH,
SEED_SHARED_XDELTA,
"Expected public/patches/example_patch.xdelta (Pokémon Emerald xdelta fixture).",
);
}
async function uploadCovers(client) {
let uploaded = 0;
for (const slug of COMPLETE_HACK_SLUGS) {
for (let i = 1; i <= 3; i++) {
const objectKey = coverObjectKey(slug, i);
const url = placeholdUrl(slug, i);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`placehold.co fetch failed for ${slug} cover ${i}: ${response.status}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
await client.putObject(COVERS_BUCKET, objectKey, buffer, buffer.length, {
"Content-Type": "image/png",
});
uploaded++;
}
}
console.log(`Uploaded ${uploaded} cover images to ${COVERS_BUCKET}/`);
}
async function main() {
const endPoint = requireEnv("S3_ENDPOINT");
const port = parseInt(requireEnv("S3_PORT"), 10);
const accessKey = requireEnv("S3_ACCESS_KEY_ID");
const secretKey = requireEnv("S3_SECRET_ACCESS_KEY");
const useSSL = process.env.S3_USE_SSL === "true";
const client = new Client({ endPoint, port, accessKey, secretKey, useSSL });
await ensureBucket(client, PATCHES_BUCKET);
await ensureBucket(client, COVERS_BUCKET);
await uploadSharedPatches(client);
await uploadCovers(client);
console.log("Done.");
console.log("");
console.log("To test in-browser patching:");
console.log(" 1. Open /hack/seed-emerald-demo");
console.log(" 2. Link a local Pokémon Emerald ROM (CRC32: 1f1c08fb)");
console.log(" 3. Patch and download");
}
main().catch((err) => {
console.error("seed:storage failed:", err.message || err);
if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
console.error("Is MinIO running and reachable at S3_ENDPOINT:S3_PORT?");
}
process.exit(1);
});

546
supabase/seed.sql Normal file
View File

@@ -0,0 +1,546 @@
-- 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);