A post whose server is behind a CDN has no place of its own: the globe groups such posts per CDN in labels on a ring above the Arctic, and hovering one draws its arc to the CDN's group and a ring where the server was before the CDN. The card's CDN chip names the CDN, its domain, how PrivaPub found it (published ranges, response headers or network), the place before the CDN and the CDN's servers week by week (/api/privapub/v1/cdns/:domain); the author's tooltip lists the server's changes of place and CDN week by week (/instances/:host/history). Both are read on the first hover and kept for the session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
382 lines
25 KiB
Markdown
382 lines
25 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## What this is
|
|
|
|
decePubClient is a Blazor WebAssembly PWA (.NET 10). It is meant as a Pleroma-FE-like client for **PrivaPub**, the
|
|
owner's ActivityPub server, whose repo is `thepra/SocialPub`. It is deployed as a static site at
|
|
https://decepub.thepra.dev, and its API base address is `https://privapub.thepra.dev`
|
|
(`AppConfiguration:ApiBaseAddress` in `wwwroot/appsettings.json`).
|
|
|
|
It talks to PrivaPub through both of its APIs with one sign-in (see **PrivaPub wiring** below). The feed page has three
|
|
columns: navigation, the feed with a tab per kind of feed, and a 3D globe that follows the posts in view. Nerd stats sit
|
|
in tooltips wherever there is data. Development builds point at a local PrivaPub on `http://127.0.0.1:6970`
|
|
(`wwwroot/appsettings.Development.json`, never published).
|
|
|
|
`Docs/Insomnia_APIs.json` is an old API collection for a localhost backend. Don't treat it as PrivaPub's API.
|
|
|
|
## Sibling repo dependency
|
|
|
|
The project references `../SocialPub/PrivaPub.ClientModels/PrivaPub.ClientModels.csproj`, so SocialPub must be
|
|
checked out next to this repo. CI clones it there. That project supplies:
|
|
- the shared DTOs;
|
|
- `Policies` (IsAdmin, IsUser, IsModerator);
|
|
- the `FieldsNameResource` and `ErrorsResource` localization resources.
|
|
|
|
Before wiring anything to the API, read `../SocialPub/CLAUDE.md`. PrivaPub's route names (`/peasants/...`, `/mouth`,
|
|
`/anus`, ...) are deliberate. Never "correct" them. The API surface is the Mastodon client API plus PrivaPub's private
|
|
`/clientapi` and `/api/privapub/v1`.
|
|
|
|
To run against a local PrivaPub: start a mongod, run SocialPub's `PrivaPub` with the Development command in its
|
|
CLAUDE.md on `127.0.0.1:6970` (`Registrations__Mode=Open` lets `/clientapi/user/signup` create a root), then create
|
|
personas with `/clientapi/avatar/private/insert`. Federated posts come from SocialPub's pasture
|
|
(`tools/pasture/run.sh up gts akkoma`, then `tools/pasture/interop.sh gts akkoma`), whose API is `127.0.0.1:6971`.
|
|
Neither DB-IP nor PrivaPub locate private addresses, so for the globe set `RemoteInstance.Geo` in mongo and
|
|
`Statistics:Geo:Self` in PrivaPub's configuration.
|
|
|
|
## Commands
|
|
|
|
```sh
|
|
dotnet build decePubClient.csproj # what CI runs (-c Release) on every push to master and on PRs
|
|
dotnet run # dev server: https://localhost:7046 / http://localhost:5046
|
|
dotnet publish decePubClient.csproj -c Release -o publish # static site lands in publish/wwwroot
|
|
```
|
|
|
|
`global.json` pins SDK 10.0.100 with `rollForward: latestFeature`. There are no tests and no linter.
|
|
|
|
## CSS: Tailwind 4 + daisyUI 5, vendored, offline
|
|
|
|
`Styles/app.css` is compiled into `wwwroot/css/app.css` by the Tailwind CSS standalone binary with the daisyUI
|
|
plugin. `app.css` is gitignored and regenerated on every build.
|
|
|
|
**Nothing in the build or the CI may download from github.com.** Tools live in the repo, and the CSS build runs
|
|
without network access. NuGet restore still uses nuget.org.
|
|
- `tools/tailwind/` holds the pinned tools:
|
|
- Tailwind 4.3.3 as `tailwindcss-linux-x64.xz`;
|
|
- daisyUI 5.7.47 (`daisyui.mjs`, `daisyui-theme.mjs`);
|
|
- daisyUI's component reference, `daisyui-llms.txt`.
|
|
- `.gitattributes` keeps these files byte-exact.
|
|
- `tools/tailwind.sh prepare` checks them against the sha256 values pinned in the script, and unpacks the binary into
|
|
the gitignored `.tools/tailwind/`. It needs `xz`.
|
|
- Only linux-x64 is vendored.
|
|
- The script's header explains how to upgrade or add a platform.
|
|
- The `TailwindCompile` target in the csproj runs it, so `dotnet build` / `publish` are enough, in CI as well. Debug
|
|
builds use `--optimize`, Release builds `--minify`. Pass `-p:SkipTailwind=true` to skip it.
|
|
- `IncludeTailwindOutput` adds the generated file to `Content` before static web assets are resolved. Without that, a
|
|
clean-clone publish would ship `app.css` without its `.gz`/`.br` copies and outside `service-worker-assets.js`.
|
|
The deploy workflow checks for `css/app.css.gz`.
|
|
- `bash tools/tailwind.sh watch` rebuilds the CSS live next to `dotnet watch`.
|
|
- Tailwind scans every file `.gitignore` doesn't exclude (minus `tools/`, `wwwroot/vendor/`, `Docs/`), and only
|
|
generates the classes it finds written out **whole**. Never build a class name by concatenation or interpolation (`"btn-" + size`, `$"btn-{size}"`). Write
|
|
every variant as a literal, e.g. in a switch arm.
|
|
|
|
Ionicons 4.5 (`wwwroot/vendor/ionicons.min.css`, `<i class="ion-md-...">`) is loaded as its own stylesheet.
|
|
|
|
## Theme and neumorphism
|
|
|
|
- `Styles/theme.css` holds two daisyUI themes, `neo-light` (default) and `neo-dark` (`prefersdark`). Every colour
|
|
is an `hsl()` of `--neo-hue-light` / `--neo-hue-dark` / `--neo-chroma` (0 for the grey themes).
|
|
- `wwwroot/js/theme.js` sets those variables and `data-theme` on `<html>` from `PageSettings`. It runs in `<head>`
|
|
from localStorage, so there is no flash before Blazor boots. After that, CascadingState calls it through
|
|
`AppStatusService.ApplyTheme` whenever the settings change. With `PreferSystemTheming` it leaves `data-theme` off,
|
|
and the OS decides.
|
|
- Change the theme flags through `PageSettings.SetDarkMode` / `SetGray` / `SetHue` and
|
|
`CascadingState.UpdatePageSettings`, which keep the flags consistent:
|
|
- choosing light or dark leaves system theming;
|
|
- picking a hue leaves grey.
|
|
- `AppStatusService.IsDarkTheme()` tells which variant is showing.
|
|
- Settings → General edits every theme setting. NavMenu's toggle and slider are shortcuts to the dark and hue ones.
|
|
- `Styles/neo.css` holds the neumorphic layer:
|
|
- `neo-raised` / `neo-inset` / `neo-flat`, with sizes `neo-2xs` / `neo-xs` / `neo-sm` / `neo-md` (these replace the
|
|
old `neomorph is-nxsmall` classes);
|
|
- `neo-gradient(-pressed)` and `neo-hue-track`;
|
|
- `@utility <daisy-component>` overrides that give btn, input, select, card, … the neo look. daisyUI puts its rules
|
|
in sublayers of `utilities`, so these overrides beat **every** daisyUI rule. Each override must exclude the
|
|
daisyUI variants it shouldn't touch, e.g. `:not(:where(.btn-ghost, .btn-link, ...))`, and call sites drop a neo
|
|
shadow with `shadow-none!`.
|
|
- Never write `@utility collapse`. It brings back Tailwind's own `collapse` utility (`visibility: collapse`), and
|
|
every collapse disappears.
|
|
|
|
## daisyUI Blazor components (`Components/Daisy/`)
|
|
|
|
Every daisyUI component has a `D`-prefixed wrapper (`DButton`, `DModal`, `DDropdown`, `DInput`, ...), grouped by
|
|
daisyUI's categories into folders whose namespaces are imported in `_Imports.razor`. `/styleguide` (not linked) renders
|
|
all of them in every theme. When you add or change one:
|
|
- Map each enum to its classes in a switch with whole literal class names.
|
|
- Value-holding fields derive from `DInputBase<TValue>`, Blazor's `InputBase`, so `@bind-Value` and EditForm
|
|
validation work. Everything else derives from `DComponentBase` (`Class`, pass-through attributes, `Cx(...)`).
|
|
- Open/active state lives in C# (`@bind-IsOpen`, `@bind-Active`) and is rendered with daisyUI's `*-open` modifier
|
|
classes. No JS interop.
|
|
- What daisyUI does with scripts has a script-free equivalent here: `DCheckbox.Indeterminate` renders
|
|
`aria-checked="mixed"`, and `DCarousel`'s navigation links to the slides' ids, which Blazor scrolls to.
|
|
- `DCalendar` is the one exception: it wraps the vendored Cally web component (`wwwroot/vendor/cally.js`).
|
|
- Images and links in `/styleguide` stay local (`wwwroot/imgs/styleguide/`).
|
|
- Don't name a parameter after an HTML attribute (`Title`, `Style`, `Id`, `Label`, ...). Blazor matches attributes
|
|
to parameters without regard to case, so the parameter would swallow the attribute.
|
|
- Components don't use `Localizer` or `CascadingState`; their user-visible strings are parameters.
|
|
|
|
## Architecture
|
|
|
|
**Startup (`Program.cs`).**
|
|
- DI registration happens here.
|
|
- 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 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. 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`);
|
|
- `IsOnline` / `IsOffline`, polled every 10 seconds;
|
|
- the session: `IsSignedIn`, `Personas` (the root's), `PersonaId` and `Persona` (its Mastodon `Account`), with
|
|
`LoadSession`, `SwitchPersona` and `SignOut`. A persona change re-renders every subscriber; pages compare
|
|
`PersonaId` and reload what belongs to the persona. The JWT is renewed hourly when needed;
|
|
- `ProcessError` / `ProcessWarning`, which log through `ILoggingService` and show a toast;
|
|
- `LogFromJs`, which JS calls back into through a `DotNetObjectReference`.
|
|
|
|
**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 that nothing uses. 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`).**
|
|
- Routes are constants in `Models/APIs.cs` (b2b.next naming: `GET_`/`POST_`/`DELETE_`, `_M_` for routes built from
|
|
parameters), relative to `ApiBaseAddress`.
|
|
- The route picks the credentials: `clientapi/` sends the root's JWT, `api/` the current persona's Mastodon token
|
|
(exchanged when missing, exchanged once more and retried when PrivaPub refuses it), `oauth/` nothing.
|
|
- It also picks the JSON: `SUtility.MastodonSerializer` (snake_case, `MastodonJsonContext`) for `api/` and `oauth/`,
|
|
`SUtility.DefaultSerializer` (camelCase, enums by name like PrivaPub, `ClientJsonContext`) for `clientapi/`. A payload
|
|
can be an object (JSON), name/value pairs (a form) or a `Func<HttpContent>` (multipart), built once per attempt.
|
|
- Auth levels: `Get` / `Post` / `Delete` need credentials and end the session when PrivaPub refuses them; `GetAnon` /
|
|
`PostAnon` add them when there are some; `*TotallyAnon` send none (the login must not carry a JWT: PrivaPub
|
|
redirects it).
|
|
- `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` (it understands `WebResult`, validation ProblemDetails and Mastodon's
|
|
`{"error": ...}`) and bodies with `DefaultReadFromJsonAsync` / `MastodonReadFromJsonAsync`; a body that doesn't fit
|
|
comes back as the default and is written to the browser console. `PageCursors()` reads `max_id`/`min_id` from `Link`.
|
|
- Every request PrivaPub answers is recorded in `RequestStats` (method, route template, status, duration, size, rate
|
|
limit headers; never a body or a token), for the nerd stats.
|
|
- PrivaPub's `WebResult` gets `IsInvalid` from `Extensions/WebResultExtensions.cs`, a C# 14 extension property.
|
|
|
|
**PrivaPub wiring.**
|
|
- **One sign-in** (`Services/AuthService.cs`, `Pages/Login.razor`): the root signs in on `/clientapi/user/login`, then
|
|
each persona the client uses gets its own Mastodon token in exchange for the JWT (PrivaPub's `PersonaExchange`,
|
|
RFC 8693 on `/oauth/token`, first-party client id `AppConfiguration:ClientId`). `AuthData` keeps the JWT and the
|
|
persona tokens; logout revokes every persona token, then forgets the session and IndexedDB.
|
|
- **Services** (`Services/`): `TimelineService` (every feed as `FeedPage`s of `FeedPost`s, paged by `max_id`/`min_id`;
|
|
circles are read as ids from `/clientapi` and hydrated through `/api/v1/statuses?id[]=`; located posts come from
|
|
`/clientapi/post/nearby` and have no Mastodon actions), `StatusService` (publish, favourite, boost, bookmark, delete,
|
|
threads, mute, block; located and group posts go through `/clientapi/post/insert`), `MediaService` (uploads),
|
|
`GroupService`, `InstanceService` and `ProvenanceService`.
|
|
- **Mastodon DTOs** are client-side, in `Models/Mastodon/` (snake_case). `/clientapi` DTOs come from
|
|
`PrivaPub.ClientModels`.
|
|
- **Where a post is** (`InstanceService.Locate`, `Models/FeedPost.cs`): its own place (Pixelfed's, `privapub.place`),
|
|
its event's venue, the reader for a located post, its author's server (to 0.1°, or a point in its country from
|
|
`Helpers/CountryPoints.cs` when DB-IP located it only to a country), or, for a CDN-fronted server, its CDN's group
|
|
(`LocationSource.Cdn`: no point, a `CdnKey` and the place `Before` the CDN). Server descriptions are cached a day in
|
|
localStorage (`ServerInfos`); a server's weekly history (`/instances/:host/history`) and a CDN's page (`/cdns/:key`)
|
|
are read on the first hover and kept for the session. PrivaPub's own host is the signed-in persona's account host,
|
|
since the API may be reached under another name.
|
|
- **The feed page** (`Pages/Index.razor`): `FeedTabs`, `Content` (the card, which does its own actions), `Composer`,
|
|
`GlobePanel`. Older pages load as the end comes near (`wwwroot/js/feed.js`); newer ones are polled every 45 s while
|
|
the page is visible (PrivaPub has no streaming) and wait behind a "new" button.
|
|
- **The globe** (`wwwroot/js/globe.js`, an ES module imported by `GlobePanel`) runs on globe.gl, vendored in
|
|
`wwwroot/vendor/globe/` with its textures (`SOURCES.md` has versions, licences and hashes; the service worker never
|
|
precaches it). It looks at the spherical mean of the located posts in view, pulses and draws an arc to a hovered
|
|
post, follows the theme (day/night texture), honours `prefers-reduced-motion` and pauses while hidden. CDN-fronted
|
|
posts are labels per CDN on a ring at 68°N, never a place; hovering one draws its arc there and a ring where the server
|
|
was before the CDN. The posts'
|
|
places reach it in one call per feed change; cards are matched by `data-post-id`.
|
|
- **Nerd stats** (`Components/Nerd/NerdTip.razor`, rows from `NerdRow`): a post's ids, times and provenance (read on
|
|
hover), its author and server with the server's changes of place and CDN week by week, its place or its CDN (how
|
|
PrivaPub found it, the place before it, the CDN's servers week by week), media and counts; the feed's paging and last request; the globe's camera,
|
|
points and frame rate.
|
|
|
|
**Localization (`Services/CoalescingStringLocalizer.cs`).**
|
|
- 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`, `PublicCacheData`, `FeedFilter` and the
|
|
server cache `ServerInfos`, keyed by `nameof(Type)`.
|
|
- **IndexedDB** (DnetIndexedDb, database `data`) is reached through `IStorage`. Its schema is declared in
|
|
`GenericExtensions.AddIndexedDb()`, and you must bump `WithVersion` when you change it.
|
|
- The schema is at version 3: the `ClientLogs` store that `IStorage.AddLog` writes (version 2 added it; version 3 left
|
|
the mock feed's `Message` store out).
|
|
|
|
**Auth.** `TokenAuthStateProvider` holds `AuthData` (localStorage, cached in memory) and builds the auth state from
|
|
it: the root's name, its policies as claims, and the current persona. Pages that need a session carry
|
|
`[Authorize]`; `RedirectToLogin` sends the reader to `/login?returnUrl=…`.
|
|
|
|
**JS interop.**
|
|
- `wwwroot/main.js` defines `window.*` helpers. `Services/AppStatusService.cs` calls them, mostly through the
|
|
synchronous `IJSInProcessRuntime`.
|
|
- Each helper reports its errors back through `window.logFromJs`.
|
|
- The other scripts (IndexedDB, HeadElement, rxjs) are loaded by `<script>` tags in `wwwroot/index.html`; the feed's
|
|
modules (`js/feed.js`, `js/globe.js`) are imported on demand through `IJSObjectReference`.
|
|
|
|
**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<WebResult>` for mutations, `Task<List<T>>` (`[]` on error) for lists, `Task<T>` 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<FeedPost> posts = []; // DO
|
|
@posts.Count // DO
|
|
@(posts?.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 StatusService.Delete(Status.Id);
|
|
if (result.IsInvalid)
|
|
{
|
|
Toast.AddResponseError(result);
|
|
return;
|
|
}
|
|
|
|
Toast.AddResponseSuccess(Localizer["Message deleted."]);
|
|
await OnDeleted.InvokeAsync(Post);
|
|
```
|
|
|
|
**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 for `/clientapi` and the Mastodon API (snake_case DTOs), forms for `/oauth`.
|
|
|
|
**In-page errors.** `ErrorViewer` takes `AdditionalError` for one message and `AdditionalErrors` for a list. Never
|
|
join a list into one string with `<br/>` 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);
|
|
- `username` and `current-password` on the login form, so password managers fill it;
|
|
- 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.
|
|
- Never add a key that differs from an existing one only by case ("users" next to "Users"): resource lookups collide
|
|
on them and one of the two silently shows the other's text. Reuse the existing key.
|
|
- 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 `/clientapi` or localStorage type in `ClientJsonContext.cs` (camelCase: `theme.js` reads
|
|
`pageSettings` straight from localStorage), every Mastodon API type in `MastodonJsonContext.cs` (snake_case).
|
|
|
|
**Wiring a feature to PrivaPub:**
|
|
1. For `/clientapi`, 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. For the
|
|
Mastodon API, add the shape to `Models/Mastodon/`.
|
|
2. Register them in `ClientJsonContext` or `MastodonJsonContext`, and the route in `Models/APIs.cs`.
|
|
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 `/// <summary>` 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
|
|
would download that action from github.com. Never add a `uses:` step.
|
|
|
|
`.gitea/workflows/deploy.yml` runs on a `v*` tag or a manual dispatch, on a self-hosted runner that is also the web
|
|
host. The steps:
|
|
1. Publish the site.
|
|
2. Write `build.json` (commit, ref, time).
|
|
3. Snapshot the live directory to `/var/backups/decepub.thepra.dev`.
|
|
4. `rsync --delete` into `/var/www/decepub.thepra.dev`.
|
|
5. Check that `build.json` serves the new commit and that a deep client route returns 200 (the SPA fallback). Otherwise,
|
|
roll back to the snapshot.
|
|
|
|
The nginx vhost and headers are in `deploy/nginx/` (it sends `X-Robots-Tag: noindex, nofollow`), and
|
|
`deploy/max/setup.sh` is the one-time root setup on the server. If you add a file that must never be cached, add it to the `expires -1` locations in the vhost.
|
|
|
|
## Conventions
|
|
|
|
- Indent with tabs.
|
|
- `Nullable` is disabled and `ImplicitUsings` is enabled.
|
|
- Global Razor usings are in `_Imports.razor`.
|
|
- Styling uses the D components, Tailwind utilities and the `neo-*` utilities. Use `gap-*` rather than `space-x/y-*`
|
|
on flex containers; the feeds use `flex-col-reverse`, which `space-*` gets wrong.
|
|
- `SUtility.IfTrueThen(cond, "class")` is the helper for conditional classes.
|