mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-10-07 00:08:54 -05:00
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:
47
README.md
47
README.md
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
BIN
public/patches/example_patch.xdelta
Normal file
BIN
public/patches/example_patch.xdelta
Normal file
Binary file not shown.
173
scripts/seed-storage.mjs
Normal file
173
scripts/seed-storage.mjs
Normal 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
546
supabase/seed.sql
Normal 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);
|
||||
Reference in New Issue
Block a user