Skip to content

Repository files navigation

OP Tracker

A One Piece TCG inventory tracker and deck builder.

  • Inventory -- track how many copies of each card you own.
  • Deck Builder -- build 50-card decks with a Leader, filtered/checked against configurable format rules (e.g. "Standard" bans Block 1).

Stack: Next.js (App Router, TypeScript) + PostgreSQL + Prisma + Auth.js (credentials login) + Tailwind CSS.

How legality works

Format rules live in the Format table (prisma/seed.ts seeds "Standard" and "Extra Regulation"), not hardcoded -- edit them in the DB (or via Prisma Studio: npm run db:studio) as official rules rotate.

Each card's block is computed locally from its set code (src/lib/blockMap.ts), not pulled from the card data API, since "block" is tournament-rule metadata rather than raw card data. The current mapping (OP01-OP04 = Block 1, OP05-OP08 = Block 2, etc., with Standard banning the oldest block each April) was confirmed via web search in August 2026 -- verify it's still current, and fill in EXPLICIT_BLOCK_OVERRIDES for ST-*/EB-* sets, before relying on it.

Deck validation logic (src/lib/rules.ts) checks: exactly one Leader, exactly deckSize main-deck cards, max copies per card, main-deck cards share a color with the Leader, and no banned blocks/card codes. It runs both in the deck editor UI (for live feedback) and can be reused server-side.

Card data source

scripts/sync-cards.ts pulls cards from apitcg.com's /api/products endpoint (filtered to tcg=one-piece&type=card) into the local Card table.

Before relying on this: the base URL, endpoint shape, auth header, and pagination in src/lib/cardApi.ts are confirmed against apitcg.com's own docs. The individual card object's field names, though, are still best-effort -- this was built in a sandbox that couldn't reach apitcg.com/api.apitcg.com at all, so there was no way to fetch a real sample response. Run the sample check below and fix normalizeCard() in that file if anything's off.

  1. Register (free) at https://apitcg.com/register, grab your key from the Developer Platform, and set APITCG_API_KEY in .env.
  2. Sanity-check the schema: npm run sync:cards -- --sample prints one raw card as JSON.
  3. Compare it to normalizeCard() in src/lib/cardApi.ts and adjust field names if they don't match.
  4. Run a full sync: npm run sync:cards (or -- --set=OP01 to test one set first).

Until you've done this, npm run db:seed loads a handful of hand-typed placeholder cards so the app is usable (see the disclaimer at the top of prisma/seed.ts -- their stats aren't guaranteed accurate and get overwritten by a real sync).

Local development

Prerequisites: Node 22+, a PostgreSQL database.

cp .env.example .env
# fill in DATABASE_URL, AUTH_SECRET (npx auth secret), AUTH_URL=http://localhost:3000

npm install
npm run db:migrate      # creates tables
npm run db:seed         # sample cards + Standard/Extra Regulation formats
npm run dev

Accounts and the admin dashboard

There's no public self-registration -- an admin creates every account from /admin (a "User" nav tab appears there for admins). Since there's no account yet on a fresh database, promote the first one directly:

npx tsx -e "
import { PrismaClient } from './generated/prisma/client';
new PrismaClient().user.update({ where: { email: 'you@example.com' }, data: { role: 'ADMIN' } }).then(() => process.exit());
"

(that user must already exist -- sign in once won't work since there's no register page; create it the same way, with role: 'ADMIN' set directly on prisma.user.create, or ask an existing admin to add you from /admin).

From /admin, create accounts by email -- each gets a random one-time temporary password shown once for you to hand off, and they're forced to set their own on first login. Admins can also deactivate accounts, reset a forgotten password, or promote/demote roles (the last remaining admin can't be demoted or deactivated, so you can't lock yourself out).

Deploying to your GCP VM

.github/workflows/build-and-release.yml builds the app on every push to main and publishes the result as a GitHub release (tag deploy). A systemd timer on the server polls that release every 5 minutes and swaps it in -- the VM never runs npm run build itself, so git push is all you need day to day.

On the VM (as the user that will run the app, not root):

# 1. Prerequisites: Node 22+, PostgreSQL reachable from this VM, the `gh` CLI
#    (https://cli.github.com -- apt/dnf package `gh`, so it lands on the
#    system PATH the systemd unit already expects).

# 2. Make a working directory (no longer a git clone -- deploy.sh downloads
#    the built release instead of compiling from source)
mkdir -p ~/op-tracker
cd ~/op-tracker

# 3. Configure
cp <path-to-repo>/.env.example .env
# fill in DATABASE_URL (pointing at your Postgres), AUTH_SECRET, and
# AUTH_URL=https://your-domain-or-ip, APITCG_API_KEY if you're syncing cards,
# and GH_TOKEN=<a fine-grained PAT with this repo's Contents: Read-only
# permission> -- gh CLI picks this up automatically, no `gh auth login` needed

# 4. First-time deploy (also copies deploy.sh in from the repo)
cp <path-to-repo>/deploy/deploy.sh .
chmod +x deploy.sh
OP_TRACKER_REPO_DIR="$PWD" ./deploy.sh

# 5. Install the systemd --user units (from the repo checkout, not this dir)
mkdir -p ~/.config/systemd/user
cp <path-to-repo>/deploy/op-tracker.service <path-to-repo>/deploy/op-tracker-deploy.service <path-to-repo>/deploy/op-tracker-deploy.timer \
   ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now op-tracker
systemctl --user enable --now op-tracker-deploy.timer

# 6. Let user services run without an active login session
sudo loginctl enable-linger "$USER"

From then on, pushing to main triggers a GitHub Actions build; within 5 minutes the timer's deploy/deploy.sh notices the new release, downloads the artifact, reinstalls production dependencies, runs migrations, and restarts the op-tracker service. It then polls /api/health for up to ~30s -- if the app doesn't come up healthy, it automatically rolls back to the previous release (each build is tagged deploy-<sha> and the workflow keeps the last 5). A rollback still counts as a failed run (check journalctl if one happens -- the site recovered, but something in the new build needs fixing before it's safe to retry). Logs: journalctl --user -u op-tracker-deploy -f and journalctl --user -u op-tracker -f.

Put a reverse proxy in front of port 3000 for TLS if you're exposing this beyond your own network -- with real login credentials on this app, don't run it over plain HTTP publicly. If you don't own a domain, see deploy/README-https.md for a free walkthrough using Caddy + sslip.io (a real, trusted cert with no domain purchase needed).

Change the polling interval by editing OnUnitActiveSec in deploy/op-tracker-deploy.timer; the target repo via OP_TRACKER_REPO and the health-check URL (match whatever port op-tracker.service actually runs on) via OP_TRACKER_HEALTH_URL, both in deploy/op-tracker-deploy.service.

Project structure

prisma/schema.prisma      Data model: User, Card, InventoryItem, Deck, DeckCard, Format
prisma/seed.ts            Sample cards + default formats
src/lib/rules.ts          Deck legality engine (shared by UI + reusable server-side)
src/lib/blockMap.ts       Set code -> legality block
src/lib/cardApi.ts        apitcg.com client + normalizer
scripts/sync-cards.ts     Card catalog sync script
src/app/inventory/        Inventory tab
src/app/decks/            Deck builder tab
deploy/                   systemd units + poll-and-deploy script for the GCP VM

About

an application to track inventory of one piece cards you collect

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages