| 1 |
# CLAUDE.md |
| 2 |
|
| 3 |
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 |
|
| 5 |
## What this is |
| 6 |
|
| 7 |
`bes.is` — a Blazor Server app (single ASP.NET Core project, no test project) hosting a |
| 8 |
collection of small, mostly self-contained JS/HTML experiments/tools (RPN calculator, |
| 9 |
QR code scan/generate, a Netflix→Letterboxd converter, a JSON query playground, BRP |
| 10 |
lookup, etc.), each on its own route. |
| 11 |
|
| 12 |
## Commands |
| 13 |
|
| 14 |
Run from the repo root or `Blog/`: |
| 15 |
|
| 16 |
``` |
| 17 |
dotnet build # build |
| 18 |
dotnet run --project Blog # run locally (http://localhost:5296) |
| 19 |
dotnet watch --project Blog # run with hot reload |
| 20 |
``` |
| 21 |
|
| 22 |
There is no test project in the solution — don't invent `dotnet test` commands or test |
| 23 |
files unless asked to add a test project first. |
| 24 |
|
| 25 |
Publishing is done via the Rider run configuration "Publish Blog to custom server" (self-contained |
| 26 |
`linux-x64`, Release, ReadyToRun) — not part of normal dev workflow. |
| 27 |
|
| 28 |
## Architecture |
| 29 |
|
| 30 |
**One project, one layout, many independent page-features.** `Blog/Components/Pages/*.razor` |
| 31 |
are the routes (`@page "/Xyz"`), each linked from `SiteFooter.razor`. Most pages are a |
| 32 |
`.razor` file paired with a `.razor.js` file of the same name — this is the pattern to |
| 33 |
follow for any new interactive page. |
| 34 |
|
| 35 |
### The `.razor` + `.razor.js` pairing |
| 36 |
|
| 37 |
There is **no Blazor client runtime**. Every page is statically server-rendered and its JS is |
| 38 |
loaded as a plain ES module: |
| 39 |
|
| 40 |
```razor |
| 41 |
<script type="module" src="@Assets["Components/Pages/Foo.razor.js"]"></script> |
| 42 |
``` |
| 43 |
|
| 44 |
`.razor.js` files colocated with a component are static web assets, so `@Assets[...]` resolves |
| 45 |
them to a fingerprinted URL and `<ImportMap/>` in `App.razor` maps the bare `/common.module.js` |
| 46 |
style imports onto their fingerprinted files too. Never hand-write the plain path — go through |
| 47 |
`@Assets` or the module ships uncached. |
| 48 |
|
| 49 |
The module body *is* the page's setup code: it runs once, at top level, after the DOM is parsed |
| 50 |
(`type="module"` is deferred). There are no `onLoad`/`onUpdate`/`onDispose` hooks, because |
| 51 |
navigation is a real page load — the browser tears down listeners, timers and module state for |
| 52 |
you. Page logic is vanilla JS DOM manipulation, not Blazor data binding: grab elements with |
| 53 |
`getById` at top level and wire up listeners directly. |
| 54 |
|
| 55 |
If you ever reintroduce interactivity or enhanced navigation, this stops being true — module |
| 56 |
state would then outlive the DOM it points at, and every page script would need teardown again. |
| 57 |
|
| 58 |
`Log.razor`, `Panel.razor`, and the `StackOp*.razor` components live in `Components/_Shared/` |
| 59 |
and are reused across pages (e.g. `Log` renders a debug/error log panel that JS writes into via |
| 60 |
`writeDebug`/`writeError`; `Panel` is a bordered fieldset-with-legend used for docs/credits/ |
| 61 |
grouped controls). |
| 62 |
|
| 63 |
### `wwwroot/common.module.js` |
| 64 |
|
| 65 |
Shared JS helpers imported by page scripts: |
| 66 |
- `h(tag, attrs?, children?)` / `t(text)` — a small hyperscript-style DOM builder (no |
| 67 |
framework, just `document.createElement`/`appendChild`). Prefer this over manual DOM |
| 68 |
building or template strings in new page scripts. |
| 69 |
- `getById(id)` — like `document.getElementById` but throws instead of returning null. |
| 70 |
- `writeError` / `writeInfo` / `writeDebug` / `resetLog` — write into the `#log` element |
| 71 |
that `Log.razor` renders. |
| 72 |
- `debounce(fn, wait)`. |
| 73 |
|
| 74 |
Other `wwwroot/*.js` files (`dactal.js`, `qrcode.js`, `lz-string.module.js`, `jsonql-js/`) |
| 75 |
are third-party or semi-vendored libraries used by specific pages (e.g. `Query.razor.js` |
| 76 |
uses both `dactal.js` and `jsonql-js` to run two query languages against the same mock |
| 77 |
BRP dataset in `wwwroot/brp.json`). |
| 78 |
|
| 79 |
### rvrb feature (stats read off another BEAM node) |
| 80 |
|
| 81 |
`Rvrb.razor`/`.razor.cs`/`.razor.js` (route `/rvrb`) shows the status and stats of the |
| 82 |
rvrb Elixir bot (`~/Developer/elixir/rvrb`), which runs on the same server. `Services/RvrbService.cs` |
| 83 |
gets them by joining the bot's Erlang cluster: [BeamSharp](https://github.com/Besselking/BeamSharp) |
| 84 |
(referenced as a project from the sibling checkout, it is not on NuGet yet) makes this site a |
| 85 |
hidden Erlang node and calls `Rvrb.Stats.snapshot/0` on the bot the way any BEAM node would — |
| 86 |
no HTTP endpoint on the Elixir side. |
| 87 |
|
| 88 |
The node is started on the *first request* rather than at boot, and a failure is a value |
| 89 |
(`RvrbStatus.Unreachable`) rather than an exception: the site must not fail to start, or a page |
| 90 |
fail to render, because EPMD is down or the bot was deployed without distribution. `Models/RvrbSnapshot.cs` |
| 91 |
holds the shapes and `Services/RvrbSnapshotReader.cs` decodes the Erlang term into them by hand — |
| 92 |
the term is an Elixir map written by Elixir, not a serialized C# type. |
| 93 |
|
| 94 |
Configuration lives under `Rvrb` (`RvrbOptions`): `Node`, `LocalNode`, `CallTimeout`, `CacheFor` |
| 95 |
in `appsettings.json`, and `Cookie` — the shared Erlang cookie — from user secrets in development |
| 96 |
(`dotnet user-secrets set "Rvrb:Cookie" ...`) or `Rvrb__Cookie` in the environment. The bot's own |
| 97 |
side of this (enabling distribution on its release) is documented in the rvrb repo's README. |
| 98 |
|
| 99 |
### BRP feature (an external API behind a page) |
| 100 |
|
| 101 |
`BRP.razor`/`.razor.cs` and `BrpTestData.razor` are backed by `Services/BrpService.cs`, |
| 102 |
which calls an external "Haal Centraal BRP" lookup API (base address configured in |
| 103 |
`Program.cs` as `https://brp.bes.is/`) and caches responses via `HybridCache` |
| 104 |
(`Microsoft.Extensions.Caching.Hybrid`). `Models/BRPEntry.cs` and |
| 105 |
`Models/RaadpleegMetBurgerservicenummer.cs` model the request/response shapes. Test/mock |
| 106 |
data lives in `Resources/test-data.json` and `wwwroot/brp.json`. |
| 107 |
|
| 108 |
### Warframe drops feature (`/Warframe`) |
| 109 |
|
| 110 |
`Warframe.razor`/`.razor.cs`/`.razor.js` searches Digital Extremes' published drop tables: given a |
| 111 |
part it shows where it drops sorted by chance, and for prime parts the relics holding it plus the |
| 112 |
best places to farm those relics. |
| 113 |
|
| 114 |
`Services/DropTableParser.cs` turns <https://www.warframe.com/droptables> into |
| 115 |
`Models/WarframeDrops.cs`. The source is 4 MB of machine-generated HTML: twenty `<h3 id>` sections |
| 116 |
of one flat table each, no classes or ids on the rows. It walks rows with regexes instead of an HTML |
| 117 |
parser, using three row grammars (two-column reward tables, three-column bounty tables, three-column |
| 118 |
"by source" tables) that cover all twenty sections. Rows matching no grammar are skipped rather than |
| 119 |
thrown over. Current output: 3,481 items, about 200 ms to parse. |
| 120 |
|
| 121 |
`Services/WarframeDropService.cs` holds one parsed copy in a field behind a `SemaphoreSlim`, not in |
| 122 |
`HybridCache` like the rest of the site, because HybridCache serializes what it stores. This depends |
| 123 |
on the service being registered as a **singleton**: a typed `AddHttpClient<WarframeDropService>()` |
| 124 |
registration makes it transient and re-parses 4 MB per request, so it takes `IHttpClientFactory` and |
| 125 |
a named client instead. A failed refresh keeps the copy it already had. |
| 126 |
|
| 127 |
Configuration lives under `Warframe` (`WarframeOptions`): `DropTablesUrl`, `CacheFor` (12 h), |
| 128 |
`Timeout`. |
| 129 |
|
| 130 |
The page is server-rendered and driven by the query string (`?q=` search, `?item=` selection), so |
| 131 |
results are linkable and work without JS. `Warframe.razor.js` only fills the search box's |
| 132 |
`<datalist>` from `/api/warframe/names` (mapped in `Program.cs`). It does *not* import |
| 133 |
`/common.module.js`: that module binds to a `#log` element this page doesn't have. |
| 134 |
|
| 135 |
### Styling |
| 136 |
|
| 137 |
`wwwroot/app.css` is one hand-maintained stylesheet (no CSS framework, no build step), |
| 138 |
organized into numbered sections (Tokens → Reset/base → Typography → Layout → Controls → |
| 139 |
Components → Media) with CSS custom properties as the single source of design tokens |
| 140 |
(colors, spacing scale, fonts). It uses `light-dark()` and `color-scheme` for automatic |
| 141 |
dark mode — don't hardcode light/dark colors, extend the token set in section 1 instead. |
| 142 |
There are no scoped `.razor.css` stylesheets, so `App.razor` links `app.css` only; adding one |
| 143 |
means adding the `Blog.styles.css` bundle link back. |