me-web? skeleton: Avalonia 12 app, server, packaging and CI
Build / Desktop, server and tools (push) Successful in 1m7s
Build / Desktop, server and tools (push) Successful in 1m7s
The first screen (online or not, a Check button, every language with a draft notice), the desktop and Android heads, the server's /api/info and /api/ip, a release tool that builds deb, rpm, a Windows exe, macOS zips, a signed APK and the server, and the Gitea workflows that run it. Rules adapted from passolo; AGPL-3.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
commit
7589121846
76 files changed
+3082
No files matched your search
+178
@@ -0,0 +1,178 @@
|
||||
# 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, `KnowledgeStore` and `HistoryStore` in the App.
|
||||
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 resx key in `src/MeWeb.App/Strings/Strings*.resx`; no
|
||||
literals in XAML or view models (the product name `me-web?` excepted).
|
||||
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` marks each language `reviewed` or `draft`; a
|
||||
draft shows its notice and the "suggest a correction" link. The root `FlowDirection` follows the language.
|
||||
126. **`?` appears only in display text.** File names, package ids and paths use `MeWeb`, `meweb` and
|
||||
`software.arasaka.meweb`.
|
||||
Reference in new issue
Block a user