From b9040bbd80f60afc8ef91f8ff68a30d364f95c60 Mon Sep 17 00:00:00 2001 From: thepra Date: Sat, 3 Oct 2026 15:26:21 +0200 Subject: [PATCH] Document the development rules imported from b2b.next The new "Development rules" section covers the rules and the patterns that go with them: - the quick-reference table; - CancellationToken and its exemptions; - state initialisation; - the page loading/offline pattern; - early return with toasts; - client service patterns, ErrorViewer, autocomplete values, localization, formatting and JSON; - the steps to wire a feature to PrivaPub; - what was deliberately not adopted, and why: the tw: prefix, server-only rules, b2b.next-specific features, and mandatory parameter docs. The architecture notes now describe the component base, toasts, ErrorViewer, HttpService, the mock data and the two cultures. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- CLAUDE.md | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 160 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2e9cc12..31fefa2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,10 +10,10 @@ https://decepub.thepra.dev, and its API base address is `https://privapub.thepra (`AppConfiguration:ApiBaseAddress` in `wwwroot/appsettings.json`). **It is still a prototype and is not connected to the server:** -- The feed comes from `Helpers/Faker.cs`. `Storage.GetMessages()` (`Services/IStorage.cs`) and `Pages/ExpandMessage.razor` - create mock posts. -- Every method in `Services/MessagesService.cs` is a `//TODO` stub. -- `HttpService` (`Services/IHttpService.cs`) is written but not registered in DI. +- The feed and threads come from `Helpers/Faker.cs`: `Storage.GetMessages()` returns the seed feed while IndexedDB is + empty, and `Faker.Thread` builds the messages around an expanded one. +- `MessagesService` has the real `Task` signatures but builds its results in memory. +- `HttpService` is registered but nothing calls it yet. - The nginx vhost sends `X-Robots-Tag: noindex` while the site shows mock posts. `Docs/Insomnia_APIs.json` is an old API collection for a localhost backend. Don't treat it as PrivaPub's API. @@ -118,23 +118,57 @@ all of them in every theme. When you add or change one: - The named `HttpClient` "default" uses `ApiBaseAddress` as its base address. It falls back to the host's base address when that setting is empty. - `SetDefaultCulture()` (`Extensions/ExtensionMethods.cs`) runs before the app starts. It picks the culture from the - saved `PublicCacheData` or from the browser language, and maps any `en*` to `en-GB`. + saved `PublicCacheData`, or on a first visit from the browser language. +- Only English (`en-GB`) and Italian (`it-IT`) have resources; any other language gets English. +- Settings → General changes the language, saves it, and reloads the app. -**`LayerComponents/CascadingState.razor`** wraps the router in `App.razor`. It is the app-wide state object, and pages -take it as `[CascadingParameter] CascadingState`. It handles: +**`LayerComponents/CascadingState.razor`** wraps the router in `App.razor`. It is the app-wide state object. It handles: - `PublicCacheData` (page settings: theme colours, dark/light/system theming, language), persisted in localStorage; - the theme, applied through `AppStatusService.ApplyTheme` (`wwwroot/js/theme.js`); -- an online check every 10 seconds; -- `ProcessError` / `ProcessWarning`, which components should call to report errors and warnings; +- `IsOnline` / `IsOffline`, polled every 10 seconds; +- `User`, which is `Faker.CurrentUser` until the login is wired; +- `ProcessError` / `ProcessWarning`, which log through `ILoggingService` and show a toast; - `LogFromJs`, which JS calls back into through a `DotNetObjectReference`. -**Components and pages** inherit `LocalizableComponentBase`, which supplies `Localizer`, an `IsLoading` flag and -`AfterRenderAsyncJobs`. `PagesBase` is an empty subclass of it, and only `Authentication.razor` uses it. +**Components and pages** inherit `LocalizableComponentBase`, the equivalent of b2b.next's `WidmannComponentBase`. It +supplies: +- `CascadingState`, `Localizer`, `Navigation` and `Toast`; +- `IsLoading`, which starts true, and `IsDefaultDisabled`; +- `AfterRenderAsyncJobs`, run once after the next render; +- `WhenOnline` / `StartOnlinePolling`. + +`PagesBase` is an empty subclass of it, and only `Authentication.razor` uses it. Never redeclare the base's members in a +component. + +**UI feedback.** +- `Services/ToastService.cs` is the snackbar: the SUtility extensions `AddResponseError(WebResult)` (silent on + 410/cancelled; connectivity failures show as warnings), `AddResponseError(string)`, `AddResponseSuccess` (closes + after 5 s), `AddWarning` and `AddException`. `Components/ToastHost.razor`, placed in `MainLayout`, shows the + messages. +- `Components/ErrorViewer.razor` shows in-page errors: the cascading `EditContext`'s validation messages, + `AdditionalError` and `AdditionalErrors`. + +**HTTP (`Services/IHttpService.cs`)**, registered but not called yet, since the app still runs on mock data: +- Auth levels: `Get` / `Post` / `Delete` need a valid token and sign out on 401; `GetAnon` / `PostAnon` add the token + if there is one; `*TotallyAnon` send none. +- `retryOnError` backoff: 1, 2, 4, 8, 16, 29 s, then 60 s, for at most 10 minutes. +- Failures never throw. They come back as responses carrying an invalid `WebResult`, with an `ErrorCode` from + `Models/FailureCodes.cs`; a cancelled request comes back as HTTP 410. +- Read failures with `ReadWebResult` and bodies with `DefaultReadFromJsonAsync` (`Extensions/ExtensionMethods.cs`). +- JSON goes through `SUtility.DefaultSerializer`, which consults `ClientJsonContext` first. +- PrivaPub's `WebResult` gets `IsInvalid` from `Extensions/WebResultExtensions.cs`, a C# 14 extension property. + +**Mock data.** `MessagesService` has the real signatures (`Task`, `Data` = the updated entity), but builds +its results in memory. `Helpers/Faker.cs` holds the current user, the seed feed and the thread around an expanded +message. **Localization (`Services/CoalescingStringLocalizer.cs`).** -- Lookup order: this repo's `Resources/AllStrings.resx`, then `FieldsNameResource`, then `ErrorsResource`. -- A missing key renders as the key itself, so a typo fails silently. +- Lookup order: this repo's `Resources/AllStrings.resx` (+ `.it.resx`), then `FieldsNameResource`, then + `ErrorsResource`. +- A missing key renders as the key itself, formatted with its arguments, so a typo fails silently. - The class lives in namespace `collAnon.Client.Services`, a leftover from another project. +- `Resources/ErrorMessages.resx` is read by `[Required]` / `[StringLength]` through `ErrorMessageResourceType`. A new key + needs its `public static string` property in `ErrorMessages.Designer.cs` too, or validation throws at runtime. **Client storage** comes in two layers: - **localStorage** (Blazored.LocalStorage, camelCase JSON) holds `AuthData` and `PublicCacheData`, keyed by @@ -162,6 +196,119 @@ take it as `[CascadingParameter] CascadingState`. It handles: **PWA.** Published builds use `wwwroot/service-worker.published.js`, and `service-worker.js` is the dev version. +## Development rules + +These rules are adapted from the owner's b2b.next project. They are enforced by review: the build doesn't check them, +so follow them in every change. + +| Area | Rule | +|---|---| +| All | `var` everywhere; uninitialised locals are `var x = default(T);` | +| All | Primary constructors for DI (`class Foo(IBar bar)`), not constructor + fields | +| All | Every async method takes a `CancellationToken` as its last parameter | +| All | Early return on error: guard, handle, `return`; never if/else for success vs failure | +| Models / state | Lists and arrays `= []`, never null; no `= string.Empty` / `""` defaults; explicit enum defaults | +| Components | No `@{ }` blocks in markup: locals become `@code` members | +| Components | `[Inject]` in `@code`, never `@inject` | +| Components | Disable controls with `IsDefaultDisabled` (plus local conditions), never bare offline state | +| Components | Every text/number input declares `autocomplete` | +| Services | `Task` for mutations, `Task>` (`[]` on error) for lists, `Task` with a default for values | +| Localization | Every user-facing string through `Localizer`, its key in **both** `AllStrings.resx` and `AllStrings.it.resx` | + +**CancellationToken.** It goes on every async method in services, `CascadingState` and helpers, defaulting to +`default`; pass it on to what you call. Exempt: Blazor lifecycle overrides, `[JSInvokable]` methods, framework overrides +(`GetAuthenticationStateAsync`), and private UI handlers bound from markup. + +**State initialisation.** Initialise page and component state (`= new()`, `= []`) and bind it directly. No passthrough +getters and no `?.` chains over state that can't be null: + +```csharp +List Messages { get; set; } = []; // DO +@Messages.Count // DO +@(Messages?.Count ?? 0) // DON'T +``` + +`= ""` is allowed only as a documented exception, with the reason in a comment (e.g. a decorative image's `Alt`). + +**Pages that load data.** Pages fetch in `LoadData` and keep lifecycle concerns in the lifecycle method: + +```csharp +protected override async Task OnInitializedAsync() +{ + if (CascadingState.IsOffline) + { + WhenOnline(LoadData); + StartOnlinePolling(); + IsLoading = false; + return; + } + + await LoadData(); + IsLoading = false; +} +``` + +- Reuse the base `IsLoading`; don't add per-section loading flags. +- A component that binds `IsDefaultDisabled` must set `IsLoading = false` once it is ready, even if it loads nothing; + otherwise its controls stay disabled. +- Never wrap a service call in try/catch. Services already catch and log, and return an invalid `WebResult`. Branch on + `result.IsInvalid` instead: + +```csharp +var result = await MessagesService.DeleteMessage(message); +if (result.IsInvalid) +{ + Toast.AddResponseError(result); + return; +} + +Messages.Remove(message); +Toast.AddResponseSuccess(Localizer["Message deleted."]); +``` + +**Client services** follow b2b.next's two patterns (see its `StoreService`): +- Create `var result = new WebResult();`, call `httpService` inside a try/catch, and on a non-success status + `result = await response.Content.ReadWebResult(response.StatusCode, localizer, ct)`. Log with `ILoggingService`. +- Return `result` (mutations) or `[]` (lists), and set `result.Data` on success. +- POST bodies are JSON payloads. + +**In-page errors.** `ErrorViewer` takes `AdditionalError` for one message and `AdditionalErrors` for a list. Never +join a list into one string with `
` or `\n`. + +**Autocomplete values:** +- `autocomplete="off"` for search, filter and free text; +- `new-password` when setting a password; +- `one-time-code` for OTP (DOtp sets it); +- omit it for login credentials; +- inside `Components/Daisy`, attributes pass through from the caller. + +**Localization:** +- Keys are English source strings. Grep both resx files for an existing key before adding one; match keys exactly. +- Use `Localizer[key, args]`, never `string.Format(Localizer[key], …)`. Don't write `.Value`. +- Enum members shown through `Localizer[value.ToString()]` are keys too: when you add a member, add its key. +- Language names in the language picker stay in their own language. + +**Formatting.** Date formats come from `SUtility.dateTimeFormat` / `timeFormat` / `dateTimeFormatExt`. Relative times +and sizes come from `GetPassedTime` / `GetFileSize` (`Extensions/GenericExtensions.cs`). No inline format strings for +display. + +**JSON.** Register every type that crosses HTTP or goes into localStorage in `ClientJsonContext.cs`. Keep it +camelCase: `theme.js` reads `pageSettings` straight from localStorage. + +**Wiring a feature to PrivaPub:** +1. Add the request/response DTOs to `PrivaPub.ClientModels` in the SocialPub repo: `View*` for responses, `*Request` + for requests, `*Form` for forms with validation. Lists `= []`, no string defaults. +2. Register them in `ClientJsonContext`. +3. Add a client service method that calls `IHttpService` against the server's route, following the patterns above. +4. Wire the page with early returns, toasts and/or an `ErrorViewer`. + +**Not adopted from b2b.next, on purpose:** +- **The `tw:` Tailwind prefix.** b2b.next needs it next to MudBlazor's CSS; here daisyUI is the only component CSS. +- **Server-only rules:** no 5XX responses, the controller pattern, `IOptionsMonitor`, data access and caching. +- **b2b.next-specific features:** Excel import/export, password policy, legacy porting rules, the `{culture}` route + placeholder (PrivaPub's routes are absolute `/clientapi/...`), and the release/API-status worker. +- **Mandatory `/// ` on every `[Parameter]`.** Document only what isn't obvious from the name. + ## Deployment Both workflows check the repo out with a plain `git fetch` from this Gitea, not `actions/checkout`, because the runner