# 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 <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.

**Production email is configured** via a Gmail **App Password** (`smtp.gmail.com:587` STARTTLS),
stored in the Unraid template (masked). Invites + password resets both use it. See the README
"Email (Gmail example)" section. Reminder: SMTP vars must live in the Unraid *template*, not just
the running container, or a Force Update wipes them.

## 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).
