# MilkyShots – Agent Guide **Gitea repo owner:** MilkyShots ## Prerequisites - **.NET 10 SDK** (pinned in `global.json`; `allowPrerelease: true`) - **PostgreSQL with pgvector extension** - **SCSS** compiled by `AspNetCore.SassCompiler` MSBuild task (no Node.js needed; compiles during `dotnet build`) - Run `dotnet tool restore` before first migration — installs `dotnet-ef` ## SCSS SCSS source is in `MilkStream.Client/Styles/`. Compiled via `AspNetCore.SassCompiler` MSBuild target: ```bash dotnet build # compiles SCSS automatically ``` - Config: `MilkStream.Client/Styles/sasscompiler.json` (expanded in Debug, compressed in Release) - Source maps disabled - Bootstrap 5.3.7 SCSS vendored in `Styles/bootstrap/` - Theme variables adjustable in `Styles/_variables.scss` (`$accent` for primary color) - Aggressive gradient mixins in `Styles/_gradients.scss` - Works in Docker — MSBuild task runs during `dotnet build`/`dotnet publish`, no hosted service ## Projects (5 in solution) | Project | Role | Entrypoint | |---|---|---| | **Butter** | Shared class library (DTOs, enums, MIME types) | — | | **Lactose.Analyzers** | Roslyn analyzer — enforces `DateTime.UtcNow` (not `DateTime.Now`) as error MS001 | — | | **Lactose** | ASP.NET Core Web API — controllers, EF Core, repos, background jobs | `Lactose/Program.cs` | | **MilkStream** | Blazor WASM host — serves WASM files + dynamic `/appsettings.json` | `MilkStream/Program.cs` | | **MilkStream.Client** | Blazor WASM client — Razor components, SCSS, frontend services, shared UI components (ModalFrame, EmptyState) | `MilkStream.Client/Program.cs` | ## Build & run ```bash dotnet build MilkyShots.sln # .NET 10, C# 12 dotnet run --project Lactose # API on :5162 (host) / :8080 (container) dotnet run --project MilkStream # WASM host on :5269 (host) / :8080 (container) ``` **Database:** Default credentials in `appsettings.json`. Docker compose spins up all services. **Port gotcha:** `appsettings.json` defaults `DatabaseAddress:Port` to **3306** (legacy MySQL port). PostgreSQL runs on 5432. The raw connection string must be overridden, or use `docker-compose.yml` which maps host 3306 → container 5432 automatically. ## Infrastructure gotchas - **MilkStream `/appsettings.json`:** Dynamically generated from server config — maps **before** `UseStaticFiles`, so it shadows the static file in `MilkStream.Client/wwwroot/`. This endpoint provides `LactoseBaseUrl` to the WASM client. - **Auth requires claims transformation:** `RefreshTokenTransformation` (registered as `IClaimsTransformation`) must succeed on every authenticated request, or all endpoints return 403. - **Microsoft.EntityFrameworkCore.Tools v8.0.15 vs EF Core Design v10.0.9:** The Tools package is outdated but the `dotnet-ef` CLI (from `dotnet-tools.json`) and Design package are both v10.0.9 — CLI should work fine. - **WASM client DI:** `LoginService` is **Singleton**; all other services are `Scoped`. Two `HttpClient` registrations: one unauthenticated (default) for login, and one named `"MilkstreamClient"` which has `JwtTokenRefresher` as a `DelegatingHandler`. The handler transparently refreshes expired tokens pre-flight and retries once on 401 — authenticated services must **never** handle token refreshes manually. Do not add `SendWithRefreshAsync` or similar wrappers; rely solely on the named client. - **HTTPS redirection is commented out** in Lactose — API does not enforce HTTPS. - **`SharedWith.Any()` EF Core translation:** Cannot be translated to SQL through many-to-many join tables (`AlbumAsset`, `AssetUser`). When visibility filtering includes `SharedWith.Any(u => u.Id == userId)`, you must materialize records first with `.Include().ThenInclude()` chains, then filter client-side in C#. The two-pass pagination approach preserves page-limited loading — see `PersonRepository.SearchQuery` and `AlbumRepository.SearchQuery` for the pattern. - **DateTime analyzer:** `Lactose.Analyzers` treats `DateTime.Now` as error MS001, `DateTime.Today` as error MS002, `DateTime.Date` as warning MS003, and non-UTC `SpecifyKind` as error MS004. Always use `DateTime.UtcNow`. All timestamp columns use `timestamp with time zone`. ## XML docs enforced `Directory.Build.props` at solution root enables `GenerateDocumentationFile` and treats **CS1591** (missing XML comment) as error. All public APIs must have XML docs. Repo implementations use `` — keep interface docs in sync. ## Conventions - **`HttpPut` = create, `HttpPost("{id}")` = update** — not REST-idiomatic; do not "fix" without team buy-in - **Soft delete only** — set `DeletedAt`, never hard-delete (models: Asset, User) - **Repository `Save()` must be called explicitly** after insert/update/delete - **Enum prefix `E`** (e.g. `EAccessLevel`, `EAssetType`, `EJobStatus`) - **JWT access token**: 10 min, refresh token: 60 min (constants in `LactoseAuthService.cs`) - **Pagination is zero-based** everywhere — `Page = 0` is the first page. All repositories use `Skip(page * pageSize)`. All controllers validate `Page < 0` (reject negative) and `PageSize < 1 || PageSize > 250` (clamp 1–250). - **Conventional Commits** enforced via `cliff.toml`; changelog generated with `git-cliff` - **Granular commits** — commit each logical change separately (e.g., DTO change, controller logic, UI component) with a descriptive commit message - **Reuse API endpoints** — prefer adding optional query parameters to existing endpoints over creating new routes. New endpoints are a last resort when the existing ones fundamentally cannot serve the need. ## Modularity Reusable UI components live in `MilkStream.Client/Components/Shared/`. When a markup pattern appears in 2+ places, extract it into a shared component. See issue #52 for the tracking list of candidate components. ## CORS gotchas - `CorsAllowedOrigins` env var **replaces** the JSON array entirely (semicolon-separated) - MilkStream browser origin must be listed in Lactose's CORS - `LactoseBaseUrl` on MilkStream = URL the browser uses to reach Lactose ## EF Core / Migrations Migrations in `Lactose/Migrations/` — generated, do not hand-edit. ```bash dotnet ef migrations add --project Lactose dotnet ef database update --project Lactose ``` **Database indexes for performance** should be defined using the **fluent API** in `LactoseDbContext.OnModelCreating` via `HasIndex().HasFilter()`. Do not use the `[Index]` data annotation — the `Filter` parameter is not available in the current package configuration (EF Core Tools v8.0.15 vs Design v10.0.9 mismatch). Example: ```csharp modelBuilder.Entity().HasIndex(e => new { e.IsPubliclyShared, e.OwnerId }) .HasDatabaseName("IX_Assets_VisibleForSearch") .HasFilter("\"DeletedAt\" IS NULL"); ``` - **Trigram GIN indexes** on `People.Name` and `Albums.Title` use `HasMethod("gin")` with `HasOperators("gin_trgm_ops")` for case-insensitive `ILike` search. - **pgvector and pg_trgm** extensions are enabled in `OnModelCreating` via `HasPostgresExtension`. ## Tests No unit/integration test project exists. API endpoint testing is done via **REST Client `.http` files**: - **`Lactose/WepApiTest.http`** — 67 numbered tests covering all API endpoints across 10 sections (Auth → User CRUD → Person → Album → Tag → Asset → Stats → Settings → Cleanup). Tests verify authorization at every level (anonymous, user, curator, admin). Self-contained — all test data is created at runtime and cleaned up. - **`Lactose/http-client.env.json`** — provides pre-filled variable values (`userName`, `curatorName`, etc.) for the REST Client extension. - Open in VS Code/Rider with the REST Client extension and run individual tests or the full suite. - Server must be running locally (default `http://localhost:5162`). - Paginated list tests (people, albums, users) use `?page=0&pageSize=5` — change these carefully. ## PR workflow - Before starting work, ask the user whether they want the fix in a PR or directly on `develop` (for quick/trivial fixes). - Larger work always goes in a PR. - Always mark the PR as **draft** until the user confirms they are satisfied and have tested it works. - When making the PR ready (removing draft status), mark it ready for review and request: - **REDCODE** when the PR contains frontend changes - **Fastwind** when the PR contains backend changes - **Both** when both areas are touched ## Data sizes Approximate record counts in the database: | Entity | Count | |---|---| | Assets | 1,000,000+ | | Albums | 10,000+ | | People | 1,000+ | | Users | 100+ | | Maintainers | 10+ | | Curators | single digit | | Admins | single digit | ## Gitea labels Labels live at two levels: - **Org level** (`MilkyShots` org): generic issue types (`bug`, `enhancement`, `help wanted`, `question`, `duplicate`, `invalid`, `wontfix`) - **Repo level** (`MilkyShots/MilkyShots`): project-specific labels (`area:frontend`, `area:backend`, `page:*`, `type:refactor`, `priority:*`, etc.) - When labelling issues, check both levels with the `label_read` tool (`list_org_labels` / `list_repo_labels`).