Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
252da88
feat(db): add sticker export ledger
Railly Aug 3, 2026
61402ed
feat(stickers): enforce export eligibility
Railly Aug 3, 2026
1584ae7
feat(stickers): render outlined variants
Railly Aug 3, 2026
64e9f82
feat(stickers): publish complete reaction sets
Railly Aug 3, 2026
cdac719
feat(stickers): define shareable reaction URLs
Railly Aug 3, 2026
8f5f917
feat(stickers): add reaction explorer
Railly Aug 3, 2026
89c8cd1
feat(stickers): gate pet reaction entrypoints
Railly Aug 3, 2026
7570bf8
fix(i18n): include reaction explorer messages
Railly Aug 3, 2026
9d4b0f5
fix(stickers): purge before publication
Railly Aug 3, 2026
1790810
feat(stickers): expand Claude reaction demo
Railly Aug 3, 2026
cc24662
fix(stickers): retire legacy redirect caching
Railly Aug 3, 2026
d9b9c60
fix(stickers): isolate publication failures
Railly Aug 3, 2026
66d3e2b
fix(stickers): preserve locale in explorer links
Railly Aug 3, 2026
2c469ec
feat(stickers): share exact reaction links
Railly Aug 3, 2026
8d1ec49
feat(stickers): seed Claude reaction collection
Railly Aug 3, 2026
338b416
fix(stickers): retry publication purges
Railly Aug 3, 2026
cbd5965
fix(stickers): constrain cached artifact access
Railly Aug 3, 2026
e7994c6
fix(stickers): align review and legacy contracts
Railly Aug 3, 2026
b288565
fix(stickers): version published artifacts
Railly Aug 3, 2026
c1f539f
fix(stickers): cache access by pet
Railly Aug 3, 2026
4f8135c
feat: add compliant WhatsApp sticker exports
Railly Aug 3, 2026
d172d45
fix: harden sticker publication migration
Railly Aug 3, 2026
bd6905f
fix: harden sticker rollout and delivery
Railly Aug 3, 2026
fab9ce3
fix: close sticker review gaps
Railly Aug 3, 2026
d4a7f01
fix: close final sticker gate findings
Railly Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ yarn-error.log*
.hatch-runs/
batches/
public/version.json
public/sticker-review/
src/generated/current-build.ts

# CLI build artifacts (regenerated by `bun run build`)
Expand Down
119 changes: 119 additions & 0 deletions drizzle/0018_sticker_exports.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
ALTER TABLE "submitted_pets" ADD COLUMN IF NOT EXISTS "sprite_sha256" text;
ALTER TABLE "submitted_pets" ADD COLUMN IF NOT EXISTS "pet_json_sha256" text;
ALTER TABLE "submitted_pets" ADD COLUMN IF NOT EXISTS "zip_sha256" text;

CREATE TABLE IF NOT EXISTS "pet_export_approvals" (
"pet_id" text NOT NULL REFERENCES "submitted_pets"("id") ON DELETE CASCADE,
"scope" text DEFAULT 'stickers' NOT NULL,
"status" text NOT NULL,
"source_sha256" text NOT NULL,
"policy_version" text NOT NULL,
"reviewed_by" text NOT NULL,
"reason" text NOT NULL,
"reviewed_at" timestamp with time zone DEFAULT now() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "pet_export_approvals_pet_id_scope_pk" PRIMARY KEY("pet_id","scope")
);

CREATE TABLE IF NOT EXISTS "pet_sticker_publications" (
"pet_id" text PRIMARY KEY NOT NULL REFERENCES "submitted_pets"("id") ON DELETE CASCADE,
"source_sha256" text NOT NULL,
"artifact_version" text NOT NULL,
"states" jsonb NOT NULL,
"formats" jsonb NOT NULL,
"profiles" jsonb NOT NULL,
"treatments" jsonb NOT NULL,
"object_count" integer NOT NULL,
"total_bytes" integer NOT NULL,
"manifest_sha256" text NOT NULL,
"status" text NOT NULL,
"cleanup_status" text DEFAULT 'not_required' NOT NULL,
"cleanup_error" text,
"published_at" timestamp with time zone DEFAULT now() NOT NULL,
"revoked_at" timestamp with time zone,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);

ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "source_sha256" text;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "artifact_version" text;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "states" jsonb;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "formats" jsonb;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "profiles" jsonb;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "treatments" jsonb;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "object_count" integer;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "total_bytes" integer;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "manifest_sha256" text;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "status" text;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "cleanup_status" text;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "cleanup_error" text;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "published_at" timestamp with time zone;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "revoked_at" timestamp with time zone;
ALTER TABLE "pet_sticker_publications" ADD COLUMN IF NOT EXISTS "updated_at" timestamp with time zone;
UPDATE "pet_sticker_publications" SET
"source_sha256" = COALESCE("source_sha256", ''),
"artifact_version" = COALESCE("artifact_version", 'legacy'),
"states" = COALESCE("states", '[]'::jsonb),
"formats" = COALESCE("formats", '[]'::jsonb),
"profiles" = COALESCE("profiles", '[]'::jsonb),
"treatments" = COALESCE("treatments", '[]'::jsonb),
"object_count" = COALESCE("object_count", 0),
"total_bytes" = COALESCE("total_bytes", 0),
"manifest_sha256" = COALESCE("manifest_sha256", ''),
"status" = COALESCE("status", 'revoked'),
"cleanup_status" = COALESCE("cleanup_status", 'pending'),
"published_at" = COALESCE("published_at", now()),
"updated_at" = COALESCE("updated_at", now());
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "source_sha256" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "artifact_version" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "states" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "formats" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "profiles" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "treatments" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "object_count" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "total_bytes" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "manifest_sha256" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "status" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "cleanup_status" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "published_at" SET NOT NULL;
ALTER TABLE "pet_sticker_publications" ALTER COLUMN "updated_at" SET NOT NULL;

CREATE INDEX IF NOT EXISTS "pet_export_approvals_scope_status_idx" ON "pet_export_approvals" ("scope","status");
CREATE INDEX IF NOT EXISTS "pet_export_approvals_source_idx" ON "pet_export_approvals" ("pet_id","source_sha256");
CREATE INDEX IF NOT EXISTS "pet_sticker_publications_status_idx" ON "pet_sticker_publications" ("status");
CREATE INDEX IF NOT EXISTS "pet_sticker_publications_source_idx" ON "pet_sticker_publications" ("pet_id","source_sha256");

INSERT INTO "pet_collections" (
"id",
"slug",
"title",
"description",
"cover_pet_slug",
"featured",
"updated_at"
)
VALUES (
'claude',
'claude',
'Claude',
'Claude Code pets curated for reaction stickers.',
'claude-crab',
false,
now()
)
ON CONFLICT ("slug") DO NOTHING;

INSERT INTO "pet_collection_items" ("collection_id", "pet_slug", "position")
SELECT 'claude', pet."slug", candidate."position"
FROM (VALUES
('claude-crab', 0),
('claude-spectacles-3', 1),
('claude-spectacles-4', 2),
('clawd-music', 3),
('clawd-2', 4),
('clawd-4', 5),
('clawd-3', 6),
('clawdex', 7)
) AS candidate("slug", "position")
INNER JOIN "submitted_pets" pet ON pet."slug" = candidate."slug" AND pet."status" = 'approved'
ON CONFLICT ("collection_id", "pet_slug") DO NOTHING;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"previews:snapshot:apply": "bun --env-file=.env.local --env-file=.env.production.local --conditions react-server scripts/publish-pet-previews.ts apply",
"stickers:snapshot:check": "bun --env-file=.env.local --env-file=.env.production.local --conditions react-server scripts/publish-pet-sticker-artifacts.ts check",
"stickers:snapshot:apply": "bun --env-file=.env.local --env-file=.env.production.local --conditions react-server scripts/publish-pet-sticker-artifacts.ts apply",
"stickers:review": "bun --env-file=.env.local --env-file=.env.production.local --conditions react-server scripts/render-pet-sticker-review.ts",
"cost:report": "bun scripts/vercel-cost-report.ts --project petdex",
"cost:apply-route-buckets": "bun scripts/apply-route-cost-buckets.ts",
"auto-tag": "bun scripts/auto-tag.ts",
Expand Down
20 changes: 20 additions & 0 deletions scripts/apply-sticker-exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";

import { neon } from "@neondatabase/serverless";

import { splitSqlStatements } from "@/lib/sql-statements";

const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error("DATABASE_URL is not set");

const sql = neon(databaseUrl);
const migration = await readFile(
join(process.cwd(), "drizzle/0018_sticker_exports.sql"),
"utf8",
);

const statements = splitSqlStatements(migration);
await sql.transaction(statements.map((statement) => sql.query(statement, [])));

console.log("sticker export schema applied");
Loading