diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1ee62ee --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,71 @@ +# 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).