Add a Gmail App Password walkthrough to the README and note in CLAUDE.md that production email is configured, including the Unraid-template persistence caveat. Co-Authored-By: Claude Opus 4.8 <[email protected]>
77 lines
5.1 KiB
Markdown
77 lines
5.1 KiB
Markdown
# 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).
|