# 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](README.md) (setup/config), [docs/DESIGN.md](docs/DESIGN.md) (original design), and [TODO.md](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 ```bash 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: ```bash dotnet-ef migrations add --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`** (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.yaml` — **not** 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).