CLAUDE.md 7.8 K · 143 lines · raw · history

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.