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]>
4.8 KiB
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.csnever returns claim data when the viewer is the list owner — enforced at the query layer, not the UI. Covered by tests intests/WishNinja.Tests/WishlistServiceTests.cs; keep it that way. - Use
IDbContextFactory<ApplicationDbContext>(short-lived contexts) in components and services — never inject a scopedDbContextinto an interactive Blazor Server component (it lives for the whole circuit → stale data / concurrency bugs). Identity gets a scoped context resolved from the factory (seeProgram.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 byId(autoincrement ≈ creation order). All date comparisons are done in memory after materializing. Don't add.OrderBy(x => someDateTimeOffset)to a DB query — it throwsNotSupportedExceptionat runtime. - The
src/WishNinja/Data/folder is source (DbContext, entities, migrations)..gitignoreand.dockerignoremust 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 tomain→ Gitea Actions runs tests, builds the Docker image, pushesgit.basso.land/jim/wishninja:latest+ a SHA tag. - CI (
.gitea/workflows/build.yml): registry login uses theREGISTRY_TOKENrepo secret (a Gitea PAT withwrite:package) — the autoGITEA_TOKENwas rejected by the registry. Jobsruns-on: ubuntu-latest, which the runner maps tocatthehacker/ubuntu:act-latest. - Runner: a
gitea/runnercontainer on Unraid. Its config (thecatthehackerlabel mapping) lives on the host at/mnt/user/appdata/gitea-runner/config.yaml— not in this repo. Do not add-v docker.sockin that config'scontainer.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 …:latestthen Apply Update). The/datavolume (and the admin) persists.
Gotchas / tips
- Diagnostics: a dedicated SSH key on the dev machine (
~/.ssh/wishninja_unraid) can reachroot@cybertronfordocker logs WishNinja/docker pswhen 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 rejectthen push again. - CI triggers on every push to
main, including docs-only commits (see TODO for apaths-ignoreidea).