Files
WishNinja/CLAUDE.md
T
Jim BassoandClaude Opus 4.8 e1fc1f9a2b
Build & Push Docker image / test (push) Successful in 41s
Build & Push Docker image / docker (push) Successful in 1m4s
Add CLAUDE.md operational guide for session handoff
Captures the non-obvious operational knowledge (deploy via Unraid+NPM, Gitea
runner config, CI registry auth, and gotchas like the SQLite DateTimeOffset
ordering limit) so a fresh session starts informed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 15:55:01 -05:00

4.8 KiB

CLAUDE.md — WishNinja

Operational guide for working on this project. Read this first; it captures the non-obvious stuff that isn't visible from the code alone. For detail see README.md (setup/config), docs/DESIGN.md (original design), and TODO.md (backlog).

What this is

Self-hosted gift wishlist manager. ASP.NET Core Blazor Web App (.NET 10) + EF Core/SQLite

  • ASP.NET Core Identity. Closed group; admin-invite-only accounts. The defining feature: claims/reservations are hidden from the list owner to preserve the surprise.

Build / test / run

dotnet build -c Release
dotnet test                      # xUnit; tests run against in-memory SQLite
# run locally (DB/uploads/keys land in src/WishNinja/Data by default):
cd src/WishNinja && SEED_ADMIN_EMAIL=admin@local SEED_ADMIN_PASSWORD='Admin!2345' dotnet run

EF migrations are applied automatically at startup. To add one after changing entities:

dotnet-ef migrations add <Name> --project src/WishNinja/WishNinja.csproj --output-dir Data/Migrations

Conventions & invariants (don't break these)

  • Owner-hidden claims is the core correctness rule. Services/WishlistService.cs never returns claim data when the viewer is the list owner — enforced at the query layer, not the UI. Covered by tests in tests/WishNinja.Tests/WishlistServiceTests.cs; keep it that way.
  • Use IDbContextFactory<ApplicationDbContext> (short-lived contexts) in components and services — never inject a scoped DbContext into an interactive Blazor Server component (it lives for the whole circuit → stale data / concurrency bugs). Identity gets a scoped context resolved from the factory (see Program.cs).
  • All item images are stored locally under /data/uploads. The three inputs (file upload, clipboard paste, image URL) all converge on a local file; URLs are downloaded server-side (ImageService.SaveFromUrlAsync). There is no external-URL image kind.
  • SQLite can't ORDER BY DateTimeOffset. Order list queries by Id (autoincrement ≈ creation order). All date comparisons are done in memory after materializing. Don't add .OrderBy(x => someDateTimeOffset) to a DB query — it throws NotSupportedException at runtime.
  • The src/WishNinja/Data/ folder is source (DbContext, entities, migrations). .gitignore and .dockerignore must only exclude the runtime artifacts inside it (*.db, uploads/, keys/), never the whole folder. (Excluding it once broke the CI build — the namespace went missing.)

Configuration (env vars)

Container defaults WishNinja__DataPath=/data and listens on :8080. Key vars: SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD (first-run admin), WishNinja__BaseUrl (public URL for email links), WishNinja__Smtp__* (optional; if unset, emails are logged and invite links show in the admin UI). Full table in the README.

Deployment (production)

  • Repo: https://git.basso.land/jim/WishNinja (Gitea). Push to main → Gitea Actions runs tests, builds the Docker image, pushes git.basso.land/jim/wishninja:latest + a SHA tag.
  • CI (.gitea/workflows/build.yml): registry login uses the REGISTRY_TOKEN repo secret (a Gitea PAT with write:package) — the auto GITEA_TOKEN was rejected by the registry. Jobs runs-on: ubuntu-latest, which the runner maps to catthehacker/ubuntu:act-latest.
  • Runner: a gitea/runner container on Unraid. Its config (the catthehacker label mapping) lives on the host at /mnt/user/appdata/gitea-runner/config.yamlnot in this repo. Do not add -v docker.sock in that config's container.options; the runner mounts it automatically.
  • Host: Unraid box cybertron. Container WishNinja, host port 5481 → container 8080, volume /mnt/user/appdata/wishninja/data (DB + uploads + DataProtection keys persist here).
  • Reverse proxy: Nginx Proxy Manager → https://wish.basso.land. WebSockets Support must be ON (Blazor Server uses SignalR) or the app loads but interactivity breaks.
  • Redeploy: push → wait for CI green → Unraid Docker tab → WishNinja → Force Update (or docker pull …:latest then Apply Update). The /data volume (and the admin) persists.

Gotchas / tips

  • Diagnostics: a dedicated SSH key on the dev machine (~/.ssh/wishninja_unraid) can reach root@cybertron for docker logs WishNinja / docker ps when debugging the live container.
  • Git push to this Gitea sometimes fails with Failed to authenticate (stale cached credential). Clearing it unblocks the push: printf "protocol=https\nhost=git.basso.land\n\n" | git credential reject then push again.
  • CI triggers on every push to main, including docs-only commits (see TODO for a paths-ignore idea).