MeWeb.Core runs seven checks concurrently and reports each as it settles:
online or behind a login page, this connection, the public IPv4 and IPv6
seen from v4./v6.meweb, the provider from a built-in map, the router
(UPnP IGD, NAT-PMP and PCP, read-only), address sharing (router WAN
against the echo, TTL hops only as a hint), and the history kept on the
device. The app draws the path from this device to the internet and lights
it node by node, with a card per check and a sheet listing every host it
contacts.
The provider map is iptoasn's table compressed to 2.1 MB by
tools/knowledge.cs and built before every app release; eleven Italian
provider profiles carry each fact with who said it, the source and the
month. The server gains a download page (en, it) and /download/{platform}
from the latest Gitea release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
191 lines
13 KiB
Markdown
191 lines
13 KiB
Markdown
# PATTERNS.md — MeWeb house rulings
|
||
|
||
The canonical standards doc. **On any conflict about how to write code, a ruling here wins**; cite it in review as
|
||
"PATTERNS #n". `AGENT.md` is the operational contract, `CLAUDE.md` the quick reference and pitfalls.
|
||
|
||
Numbering is shared with the house lineage (SimpleSchool, passolo, odysseus, zigzag.academy): **#42, #51, #86–#93
|
||
mean the same thing in every repo**, and the text below follows passolo's wording wherever the subject is the same.
|
||
A number whose subject does not exist here is kept as a one-line **n/a** so the numbering never drifts. Rulings
|
||
only MeWeb needs start at **#120**.
|
||
|
||
Scope: `src/`, `tools/`. Out of scope: generated files (`obj/`, `*.g.cs`), third-party trees, the licence text.
|
||
|
||
---
|
||
|
||
## A. Dependency injection and services
|
||
|
||
1. **Primary-constructor DI is the default.** `class ServerClient(IHttpClientFactory factory)`. Constructors with
|
||
logic stay explicit; a constructor that only assigns fields is a primary constructor. Applies to server services,
|
||
App services and view models alike.
|
||
2. **Registration only through extension methods**, one group per area: `AddMeWebApp` (App), `AddMeWebHosting`,
|
||
`AddMeWebData` (server). `Program.cs` reads as a list of calls.
|
||
3. **Lifetime discipline.** Server: stateless per-request → `AddScoped`; process-global state, caches, rate counters →
|
||
`AddSingleton`. App: services singleton, view models transient.
|
||
4. **Interface fan-in via factory re-registration** when one instance serves two contracts; never register it twice.
|
||
86. **A service never takes `IHttpContextAccessor`** — the endpoint reads the caller's address and passes it in.
|
||
|
||
## B. Web boundary
|
||
|
||
5. **`WebResult` envelope + early return** on every server endpoint that can refuse; the happy path stays flat.
|
||
6. **Pure or internal code may return tuples** (`(value, error)`), never across the web boundary.
|
||
7. **`try/catch` placement: few at the top, few at the bottom, none in the middle.** Top: host startup, background
|
||
loops. Bottom: sockets, DNS, UPnP/PCP, file IO, platform APIs — the IO edges where a check meets the network.
|
||
8. **Endpoints are minimal-API handlers**, static methods mapped in one `Map*` extension per area, thin bodies that
|
||
call a service. (passolo's controller shape: n/a.)
|
||
9. **Routes centralized in `src/MeWeb.Shared/Endpoints.cs`** (`GET_Name`, `GET_M_Name(args)`), shared by server and
|
||
App. No `"/api/` literal anywhere else.
|
||
10. **Prefer the `WebResult` path over inline `try/catch`** in endpoints.
|
||
11. **n/a — SSE.**
|
||
12. **Subprocesses: argv only, never a shell string.** `ProcessStartInfo.ArgumentList`; no `sh -c`, no `cmd /c`
|
||
(`tracepath`, `ip`, `netsh` are run this way, if at all).
|
||
13. **Long-running server work is registered and stoppable** (probe runs, the per-address limiter).
|
||
53. n/a — no request-bound streams.
|
||
|
||
## C. Verification (there is no test project)
|
||
|
||
14. **Pure static helpers for IO-free logic** (verdicts, address classification, PTR patterns). They are exercised
|
||
by the CLI mode and the screenshot tool, not by a test project.
|
||
15. **n/a — xUnit theories.** No test project by owner decision.
|
||
16. **Hand-written fakes, no mocking framework.**
|
||
17. **Server endpoints are verified by the curl ladder** against a scratch database (`/verify`).
|
||
54. **UI work is verified by running the app and looking at it** — desktop, Android device, or the headless
|
||
screenshots from `tools/Screenshots` (evidence E2), a real device for E3. A compile is not a UI verification.
|
||
55. n/a — no model output.
|
||
63. **An inventory that lives in a check rots; keep one map and assert it against reality.**
|
||
64. **A sweep asserts what must hold and records what is a judgement call.**
|
||
65. **Prefer a source lint to a runtime check when the bug is visible in the source** (`tools/lint.cs`).
|
||
66. **A check that has always failed is a decision nobody made** — fix it or delete it.
|
||
67. **Two paths that resolve the same thing must degrade the same way** (the v4 and v6 echo, the system resolver and
|
||
the server-side blocklist fallback).
|
||
68. **Seed data before judging a surface** — the screenshot tool runs every language and two sizes.
|
||
69. n/a. 70. **Intermittent failures are usually an assertion on something transient** (network timing): fix the
|
||
assertion, not the retry count. 71. **A filter check asserts both directions.** 72. **Verify tooling against
|
||
every case it claims to cover.** 73. n/a — CSS selectors (corollary: `AutomationId`s are unique). 79. n/a.
|
||
80. n/a. 81. **Shared Mongo is not scratch space**: verification uses `meweb_<scratch>` and drops it. 82. **A new
|
||
verification mode is a rung of `/verify`.** 83. **A bulk change is bracketed by a full verification before and
|
||
after.**
|
||
|
||
## D. Configuration
|
||
|
||
18. **`record` types only for local, performance-oriented data.** Wire DTOs, options classes and everything in
|
||
`MeWeb.Shared` and `MeWeb.App` are classes.
|
||
19. **Config layering:** `appsettings.json` → environment variables (`Section__Key`) from
|
||
`/etc/meweb/meweb.env` on the box. **This repository is public: no secret is ever committed** (the house carve-out
|
||
of committing secrets does not apply here).
|
||
20. **Signing keys never touch git**: the Android keystore, the update-feed key and any packaging key live in Gitea
|
||
Actions secrets and are decoded into `$RUNNER_TEMP` by the release job.
|
||
87. **`IOptionsMonitor<T>` + `.CurrentValue` — never `IOptions<T>`.**
|
||
|
||
## E. Lifecycle and concurrency
|
||
|
||
21. **Three lifecycle idioms:** `IHostedService` for start/stop hooks, `BackgroundService` for periodic work with a
|
||
per-tick catch, explicit `IAsyncDisposable` for sockets and listeners.
|
||
22. **Disposable-lease mutex** (`TryBegin → IDisposable`) when an operation must not overlap (one probe run per
|
||
address).
|
||
23. n/a. 24. n/a.
|
||
88. **`CancellationToken` is the last parameter of every async method, and it is forwarded.** Relay commands take
|
||
one (the toolkit cancels them). Best-effort cleanup (removing a port mapping on exit) passes `CancellationToken.None`
|
||
on purpose.
|
||
|
||
## F. Data
|
||
|
||
25. **One data accessor per store**: `MongoContext` on the server, `KnowledgeBase` and `HistoryStore` in `MeWeb.Core`.
|
||
No SQLite: the provider map is a sorted brotli table read into arrays, the history a small JSON file, so no
|
||
native library has to ship on four platforms.
|
||
26. **Entity → view is a hand-written `ToView()`.** No mapper library.
|
||
56. **A multi-row mutation is one server call.**
|
||
74. **No in-between DTOs — a response DTO is pure data; `WebResult` carries ok/error.**
|
||
75. **A list is `[]` by construction end to end** — never `null`, never `?? []` at a use site.
|
||
84. **Never hand-roll a `JsonSerializerOptions`.** One source-generated context per assembly
|
||
(`MeWebJsonContext`, `AppJsonContext`) with Web defaults.
|
||
85. n/a.
|
||
89. **No `= string.Empty` property defaults — anywhere.**
|
||
|
||
## G. App (Avalonia)
|
||
|
||
27. **Client calls never throw** across the view-model boundary: a probe returns a result the view model can show;
|
||
transport failures become a state ("can't tell from here"), never an exception in a page.
|
||
28. **One screen = `XView.axaml` + `XViewModel`**, the view model resolved from DI.
|
||
29. n/a.
|
||
30. **Resilience engages on connection failure only**; a definite answer is never retried.
|
||
31. **`AutomationProperties.AutomationId` on every interactive element** (`<view>-<slug>`).
|
||
32. **Subscription discipline:** every `+=` has its `-=` in the matching unload/dispose; timers are `PeriodicTimer`
|
||
with a cancellation source cancelled on leave.
|
||
33. n/a.
|
||
34. **DTO naming by direction:** `View*` (response), `Upsert*` (create/update), `*Form` (any other input). Contracts
|
||
grouped by area in `MeWeb.Shared/<Area>Contracts.cs`.
|
||
35. n/a. 36. **Path safety** for anything served from disk by name. 37. **Layered gating: the UI hides; the server
|
||
refuses.** 57. n/a.
|
||
43. **Every submit-shaped screen is a real form** with a `*Form` object and one submit command whose `CanExecute`
|
||
follows validity and busy state.
|
||
44. **No converters or inline logic for display values:** the view model exposes `Show*` booleans and `*Formatted`
|
||
strings; XAML binds to them.
|
||
45. **No service locator in views**: a view receives its view model; `App.Services` is read only where Avalonia
|
||
builds the root view.
|
||
58. n/a. 59. n/a. 60. n/a — `prefers-reduced-motion` is not read (as on arasaka.software). 62. n/a. 76. n/a.
|
||
77. **Editable rows bind straight to their item.**
|
||
78. **No `record` types in the App** — classes only.
|
||
90. n/a.
|
||
91. **Never write `.Value` on a localizer indexer.** `Localizer.Instance[key]` returns a string.
|
||
93. **Never `try/catch` a client call — it already handed you the failure.**
|
||
|
||
## H. Logging, localization, packages, docs
|
||
|
||
38. **Structured logging with `{Placeholder}` templates**, never interpolated strings (Microsoft.Extensions.Logging,
|
||
as passolo).
|
||
39. **Localization-first:** every user-facing string is a key in `src/MeWeb.App/Strings/<code>.json`, one flat
|
||
file per language, English the fallback; no literals in XAML or view models (the product name `me-web?`
|
||
excepted). JSON rather than resx on purpose: 21 of the 113 official languages are unknown to ICU and to MSBuild's
|
||
satellite-culture list, and a file looked up by code needs neither. Keys change only through
|
||
`tools/strings.cs set`, and `tools/strings.cs check` (keys and `{n}` placeholders against English) runs in CI.
|
||
40. **Central Package Management** (`Directory.Packages.props`); Avalonia packages move together through
|
||
`AvaloniaVersion`.
|
||
41. **Docs as convention:** `AGENT.md` (operational), `CLAUDE.md` (quick reference + pitfalls), `PATTERNS.md`
|
||
(rulings), `README.md` (public, English).
|
||
42. **Code comments — strict ban.** No `//`, `/* */`, `///`, `<!-- -->` in owned source (C#, AXAML, csproj, props,
|
||
manifests, plist, scripts, workflows). Intent goes in names, structure and the markdown docs.
|
||
61. **Binaries only through `tools/setup.cs`** (versions and SHA-256 pinned in `tools/setup.json`).
|
||
|
||
## I. Version control
|
||
|
||
46. **Never create a branch.** Commit on `master`; never force-push, never rewrite pushed history.
|
||
|
||
## J. Styling and UI text
|
||
|
||
47. **Colors only through tokens** in `src/MeWeb.App/Resources/Colors.axaml`; no hex literal in a view. Every
|
||
text/background pair passes WCAG AA.
|
||
48. **One icon source** (`Resources/Icons.axaml`, Lucide geometries, ISC); no inline path data in a view.
|
||
49. **Action rows run `[destructive] … [Cancel] [Primary]`** — primary on the end side (mirrored in RTL).
|
||
50. **All UI text is sentence case — never Title Case.**
|
||
51. **A helper must earn its existence** — 3+ call sites or extracted pure logic (#14); otherwise inline it.
|
||
52. **Zero-call-site code is deleted.**
|
||
92. **`var` for every local**; a local with no initializer is `var x = default(T)`.
|
||
|
||
---
|
||
|
||
## K. MeWeb-only rulings
|
||
|
||
120. **Every outbound contact is declared.** The app talks only to the hosts listed in "What this app contacts"
|
||
(Arasaka's server, the STUN servers, the MX hosts tested on port 25, blocklists through the user's own
|
||
resolver). A new host is a change to that screen in the same commit. No telemetry, analytics or crash
|
||
reporting.
|
||
121. **A verdict states what was measured, not what is hoped.** Each card says what it tried, what answered, and
|
||
what that means; "can't tell from here" is a valid result and never rendered as success or failure.
|
||
122. **Nothing is changed on the user's network without consent**: a port mapping is asked for first, leased for
|
||
at most ten minutes, removed after the run and recorded so a leftover is removed at the next start.
|
||
123. **The probe only ever connects back to the caller's own address**, bound to a single-use ticket; it refuses
|
||
private, loopback, link-local, CGNAT, ULA and the server's own prefixes.
|
||
124. **Shared findings leave only after the person has read exactly what is sent**, and every submission can be
|
||
deleted with the id and token the app keeps.
|
||
125. **Every language ships; drafts say so.** `data/languages.json` (written by `tools/languages.cs` from CLDR:
|
||
every language official or de facto official in a UN member state, plus the six UN languages) marks each
|
||
`reviewed` or `draft` and carries its own `rtl` flag, because ICU does not know every script's direction. The
|
||
picker lists a language once its strings file exists; a draft shows its notice and the "suggest a correction"
|
||
link; the root `FlowDirection` follows `rtl`.
|
||
126. **`?` appears only in display text.** File names, package ids and paths use `MeWeb`, `meweb` and
|
||
`software.arasaka.meweb`.
|
||
127. **A provider fact carries who said it, where and when.** Every value in `data/isps/*.json` has `basis`
|
||
(`operator`, `users`, `one-user`, `press`, `research`), `source` (a URL) and `date` (`yyyy-MM` or `yyyy`), and
|
||
the card shows all three. A fact nobody published is left out, not guessed; the values describe what is usual
|
||
for the provider, never the person's contract.
|