Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
190 lines
12 KiB
Markdown
190 lines
12 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 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 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.
|
|
|
|
## 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 intended API surface is the Mastodon client API plus PrivaPub's
|
|
private `/clientapi`.
|
|
|
|
## 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 from the browser language, and maps any `en*` to `en-GB`.
|
|
|
|
**`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:
|
|
- `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;
|
|
- `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.
|
|
|
|
**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.
|
|
- The class lives in namespace `collAnon.Client.Services`, a leftover from another project.
|
|
|
|
**Client storage** comes in two layers:
|
|
- **localStorage** (Blazored.LocalStorage, camelCase JSON) holds `AuthData` and `PublicCacheData`, 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 2, which added the `ClientLogs` store that `IStorage.AddLog` writes. Before it, every log
|
|
write failed silently.
|
|
|
|
**Auth.**
|
|
- `TokenAuthStateProvider` builds the auth state from the `AuthData` token in localStorage.
|
|
- `SetToken` is mostly commented out, so there is no real login flow yet.
|
|
- `Pages/Logout.razor` signs out through `TokenAuthStateProvider.LogoutAsync`, which clears the token and the local
|
|
database.
|
|
- `AddApiAuthorization`, `Pages/Authentication.razor` (`/authentication/{action}`) and `RedirectToLogin` are leftovers
|
|
from the Blazor OIDC template. That route's `RemoteAuthenticatorView` throws `InvalidCastException`, because the
|
|
registered `AuthenticationStateProvider` is `TokenAuthStateProvider`. The Login links that point there need a real
|
|
login page first.
|
|
|
|
**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, auth, rxjs) are loaded by `<script>` tags in `wwwroot/index.html`.
|
|
|
|
**PWA.** Published builds use `wwwroot/service-worker.published.js`, and `service-worker.js` is the dev version.
|
|
|
|
## 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/`, 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.
|