Add TODO.md backlog and docs/DESIGN.md (original plan)
Track the project backlog in-repo and preserve the original pre-implementation design with a note on where the build diverged. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2ce89fa490
commit
830eb4f6dc
+108
@@ -0,0 +1,108 @@
|
||||
# WishNinja — Design (original plan)
|
||||
|
||||
> **Status:** This is the original pre-implementation design, kept for posterity. The build
|
||||
> followed it closely, but a few details evolved during implementation. For the current state of
|
||||
> the code, the README and the git history are authoritative. Notable deltas from this document:
|
||||
>
|
||||
> - **Item images are always stored locally.** Pasted URLs are downloaded server-side and saved as
|
||||
> files; the `ImageKind.ExternalUrl` value and `WishlistItem.ImageUrl` column were removed.
|
||||
> - **Ordering:** list queries order by `Id` (not `CreatedAt`), because SQLite cannot `ORDER BY`
|
||||
> a `DateTimeOffset` column.
|
||||
> - **Connection string:** derived from `WishNinja__DataPath` (default `/data`); override via
|
||||
> `ConnectionStrings__DefaultConnection` (not `ConnectionStrings__Default`).
|
||||
> - **CI registry auth:** uses a `REGISTRY_TOKEN` PAT secret, not the auto-provided token.
|
||||
> - **Seeder:** implemented as `Services/StartupInitializer.cs` (migrate + seed roles + admin),
|
||||
> not `AdminSeeder.cs`.
|
||||
> - **Container** runs as root for hassle-free Unraid bind-mount permissions (the "non-root" note
|
||||
> below was not adopted).
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The user runs an Unraid home server hosting many self-hosted apps and wants a **new gift
|
||||
wishlist manager** ("WishNinja") that ships as a **Docker image** and lives in their
|
||||
self-hosted **Gitea** instance. The working directory was empty — a clean greenfield build.
|
||||
|
||||
The app lets a closed group (family/friends) create wishlists, add items with links/prices/
|
||||
images, and **claim/reserve gifts so the owner never sees what's been claimed** (preserving the
|
||||
surprise) while other viewers do (avoiding duplicate gifts). Users are managed internally with
|
||||
email-based password resets.
|
||||
|
||||
### Confirmed decisions
|
||||
- **Stack:** ASP.NET Core **Blazor Web App** (interactive **server** render mode), **.NET 10 LTS**.
|
||||
- **Persistence:** **SQLite** via **EF Core**, single DB file on a mounted volume.
|
||||
- **Auth:** **ASP.NET Core Identity** with internal accounts + email password resets.
|
||||
- **Onboarding:** **Admin invites only** (no open self-registration).
|
||||
- **Claim privacy:** **Claims hidden from the list owner**; visible to other viewers.
|
||||
- **Item images:** **file upload + clipboard paste** (both stored locally on `/data`) **+ external URL**.
|
||||
- **Delivery:** Docker image, built via Gitea Actions → Gitea container registry, run on Unraid.
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Stack
|
||||
|
||||
| Concern | Choice |
|
||||
|---|---|
|
||||
| Framework | ASP.NET Core 10 (LTS), Blazor Web App, InteractiveServer render mode |
|
||||
| ORM / DB | EF Core 10 + SQLite (`Microsoft.EntityFrameworkCore.Sqlite`) |
|
||||
| Identity | ASP.NET Core Identity (cookie auth, roles: `Admin`, `User`) |
|
||||
| Email | `MailKit` for SMTP (password reset + invite emails) |
|
||||
| Data Protection | Keys persisted to `/data/keys` so cookies/antiforgery survive restarts |
|
||||
| Image storage | Local files under `/data/uploads`; served via a static-files mapping |
|
||||
| Container base | `mcr.microsoft.com/dotnet/aspnet:10.0` (Debian slim), built from `sdk:10.0` |
|
||||
|
||||
**Why Blazor Server interactivity:** all-C#, single container, no separate JS build, live claim
|
||||
buttons. Reverse proxy must allow **WebSocket upgrades** (SignalR) — documented in the README.
|
||||
|
||||
---
|
||||
|
||||
## Data Model (EF Core entities)
|
||||
|
||||
- **ApplicationUser : IdentityUser** — adds `DisplayName`, `CreatedAt`.
|
||||
- **Wishlist** — `Id`, `OwnerId` (FK user), `Title`, `Description`, `Visibility`
|
||||
(`AllMembers` | `SpecificUsers`), `IsArchived`, `CreatedAt`.
|
||||
- **WishlistShare** — `WishlistId`, `UserId` (rows only when `Visibility = SpecificUsers`).
|
||||
- **WishlistItem** — `Id`, `WishlistId`, `Name`, `Description`, `ProductUrl`, `ImageKind`,
|
||||
`ImagePath`, `Price`, `Priority`, `Quantity`, `SortOrder`, `CreatedAt`.
|
||||
- **Claim** — `Id`, `WishlistItemId`, `ClaimedByUserId`, `Quantity`, `Note`, `CreatedAt`.
|
||||
- **Invite** — `Id`, `Email`, `TokenHash`, `Role`, `InvitedByUserId`, `ExpiresAt`, `AcceptedAt`.
|
||||
|
||||
EF migrations applied automatically at startup for friction-free self-hosting.
|
||||
|
||||
---
|
||||
|
||||
## Core Features
|
||||
|
||||
### Wishlists & items
|
||||
- CRUD wishlists; set visibility (all members / specific users via `WishlistShare`).
|
||||
- CRUD items with name, description, product link, price, priority, quantity, image.
|
||||
- Item images via three inputs, all stored locally:
|
||||
- **File upload** → saved to `/data/uploads`, `ImageKind=Uploaded`.
|
||||
- **Clipboard paste** → JS-interop `paste` handler captures the blob, uploads via the same path.
|
||||
- **Image URL** → downloaded server-side and saved as a local file.
|
||||
- Upload validation: allowlist content types (png/jpeg/webp/gif), size cap, GUID filenames.
|
||||
|
||||
### Claim / reservation (owner-hidden)
|
||||
- Any member viewing a list they **don't own** sees each item's claim state and can claim/
|
||||
unclaim (respecting `Quantity`).
|
||||
- The **owner's view of their own list never loads or renders claim data** — enforced at the
|
||||
query layer, not just hidden in the UI, so it can't leak via timing/DOM. This is the single most
|
||||
important correctness rule and gets dedicated unit tests.
|
||||
|
||||
### Auth & onboarding (admin-invite-only)
|
||||
- Self-registration disabled. Admin invites by email → emailed invite link with a one-time token
|
||||
→ recipient sets `DisplayName` + password → account created with `User` role.
|
||||
- First-run **admin seeding** from env vars (`SEED_ADMIN_EMAIL` / `SEED_ADMIN_PASSWORD`).
|
||||
- Password reset via Identity's token flow + MailKit SMTP email.
|
||||
- Admin area: list/disable users, manage invites.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
- **Automated:** `dotnet test` — owner queries never return claim data; non-owner queries do;
|
||||
invite token single-use/expiry; image upload type/size validation; list queries run on SQLite.
|
||||
- **Container:** `docker compose up --build`; verify SQLite DB + uploads persist across restarts
|
||||
via the `/data` volume.
|
||||
- **Unraid:** deploy image, map appdata volume + port, place behind reverse proxy with WebSockets
|
||||
enabled, confirm Blazor interactivity (claim button) works through the proxy.
|
||||
Reference in New Issue
Block a user