- The path from this device to the internet is a vertical zig-zag of larger drawings (this phone or computer, the router or the mobile network, a shared exit, the provider's building, the globe) that pulse while their step is checked, joined by curved wires with the link and its speed, and a few details beside each: the model, the addresses inside and outside the router and which port-opening protocols answer, the AS number, the public addresses. - The results are grouped under the step they concern, in the path's order, with the same drawing as a heading; tapping a step in the path scrolls to its checks. - After a check, the status and "Check again" move to the end, beside sharing. - Memory reads the device's RAM: on Android it showed the runtime's heap limit (0.1 GB). Labelled "Memory (RAM)" in every language. Free disk is read for the app's own folder, so Android shows it too. The device's model is read on Android, Linux and Windows. - Two icons drew outside their box (the outside test's shield, the calls and games radio tower): joined Lucide elements started with a relative move. Fixed; twelve unused icons deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
10 KiB
CLAUDE.md — MeWeb quick reference
Read AGENT.md first and cite PATTERNS.md for code rules. This file holds commands, the map and the pitfalls.
Commands
dotnet build src/MeWeb.Desktop/MeWeb.Desktop.csproj
dotnet run --project src/MeWeb.Desktop
dotnet run --project src/MeWeb.Server # ASPNETCORE_URLS=http://127.0.0.1:5481
dotnet run tools/setup.cs -- --group jdk # pinned JDK 21 into tools/jdk
dotnet run tools/setup.cs -- --group packaging # pinned nfpm into tools/packaging
dotnet build src/MeWeb.Android/MeWeb.Android.csproj \
-p:JavaSdkDirectory=$PWD/tools/jdk/linux-x64/jdk-21.0.12+8 -p:AndroidSdkDirectory=$HOME/Android/Sdk
dotnet run --project tools/Screenshots -- dist/screenshots [en,it,...]
dotnet run tools/strings.cs -- check # every language against English
dotnet run tools/strings.cs -- set Key values.json # one key, every language at once
dotnet run tools/strings.cs -- merge keys.json # many keys: {"Key": {"en": "…", "it": "…"}}
dotnet run tools/strings.cs -- import fr fr.json # one language's drafts: {"Key": "…"}
dotnet run tools/knowledge.cs # provider map -> artifacts/knowledge/ip2asn.bin
dotnet run tools/languages.cs # regenerate data/languages.json from CLDR
dotnet publish src/MeWeb.Desktop -c Release -r win-x64 --self-contained -p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true
dotnet publish src/MeWeb.Desktop -c Release -r osx-arm64 --self-contained
The Android workload installs into the user SDK (dotnet workload install android); the system SDK in /opt/dotnet
is root-owned, so CI uses the runner user's own ~/.dotnet (passolo's tools/release.sh pattern).
Repository, CI and the variables they read
| Name | Where | What |
|---|---|---|
GITEA_URL |
shell, optional | Gitea base URL for tools/gitea/*.sh; default https://git.thepra.dev |
GITEA_ORG |
shell, optional | organisation; default Arasaka |
GITEA_REPO |
shell, optional | repository for set_secret.sh; default MeWeb |
| (git credential store) | ~/.git-credentials |
the only Gitea authentication the scripts use: git credential fill for git.thepra.dev |
MEWEB_KEYSTORE_B64 |
Actions secret | the Android keystore, base64 (tools/gitea/set_secret.sh) |
MEWEB_KEYSTORE_PASS |
Actions secret | its password (store and key share it), alias meweb |
GITEA_TOKEN |
Actions, automatic | the job token; release.sh uses it as RELEASE_TOKEN to create the release and attach files |
VERSION, COMMIT |
job env | written by .gitea/scripts/prepare.sh from Directory.Build.props and the commit |
MEWEB_KEYSTORE |
release step | a temporary file in $RUNNER_TEMP decoded by .gitea/scripts/android.sh, removed on exit |
build.yml runs on every push to master; release.yml on a v* tag (publishes the Gitea release) or by hand
(publish: false builds every platform and deploys the server only). Both run on the Arasaka org runner nuvola,
user gitea-runner, home /var/lib/gitea-runner: tools/sdk.sh installs the SDK from global.json into its
~/.dotnet (the system SDK in /opt/dotnet is older and root-owned), and the JDK, nfpm and the Android SDK are
cached in ~/.cache/meweb. There is deliberately no pull_request trigger: the runner is the production box.
The server's one-time root setup is deploy/max/setup.sh (user meweb, /var/www/meweb.arasaka.software,
/etc/meweb/meweb.env, the unit, the runner's sudoers line, the certificate, nginx), run on the box through
arasaka.software's tools/max/run.sh.
The Android release key: alias meweb, certificate SHA-256
5f52cff4f52f4e0b6da6cd044bfceaf78d1d42db24f2470602db7f19cbfdf846. Never regenerate it.
Map
| Path | What |
|---|---|
src/MeWeb.Shared |
Endpoints.cs (routes and hosts), wire contracts, MeWebJsonContext, generated BuildInfo |
src/MeWeb.Core |
the checks, no UI: Net/ (address kinds, local network, TTL route, STUN, the probe's Answerer, DNS, speed, the device), Router/ (UPnP IGD, NAT-PMP, PCP; PortMapper opens and removes mappings, Leftovers remembers them across a crash), Mail/ (port 25, reverse name, blocklists through the user's resolver), Knowledge/ (provider map and profiles), History/ (checks, shared submissions), Checks/ (CheckRunner, Verdicts, Reach, Uses, ServerProbe, SharePayload) |
src/MeWeb.App |
Avalonia views and view models, Services/Localizer.cs (+ {s:Tr Key}), Strings/<code>.json, Resources/Colors.axaml; Resources/Icons.axaml (Lucide, 24 units, for cards and lists) and Resources/Art.axaml (the path's drawings, 48 units: Art<Name>Base, up to three Beats that pulse in turn while the step is checked and take its verdict colour, and a Spin for the globe). The path is a vertical zig-zag of PathNodeViewModels, and the same nodes carry the result cards (Cards, regrouped by MainViewModel.Home), so a step in the path and its group of cards are one object |
src/MeWeb.Desktop |
desktop head (meweb executable), Windows manifest |
src/MeWeb.Android |
Android head (software.arasaka.meweb), resources, manifest |
src/MeWeb.Server |
meweb.arasaka.software: /api/info, /api/ip, the download page / and /privacy (en, it), /download/{platform} from the latest Gitea release, the probe (/api/probe/ticket, /api/probe/{id}) and shared findings (/api/share, files under /var/lib/meweb/shares, swept after KeepDays) |
data/languages.json |
113 languages: code, autonym, English name, rtl, reviewed or draft |
data/isps/*.json |
provider profiles: AS numbers, and per fact a value, who said it (basis), the source URL and the month |
tools/knowledge.cs |
downloads iptoasn's ip2asn-combined.tsv.gz (cached in tools/cache, 12 h) and writes the brotli table artifacts/knowledge/ip2asn.bin (~2 MB), embedded by MeWeb.Core when present; tools/release.cs runs it before every app build |
tools/setup.cs + setup.json |
pinned build tools |
tools/Screenshots |
headless renders of every view in every language |
branding/ |
the icon (SVG source, PNG, ICO) |
Pitfalls
-
A TTL ping needs an empty payload off-root. .NET on Linux falls back to the
pingutility withoutCAP_NET_RAWand refuses any custom buffer ("Unable to send custom ping payload");SendPingAsync(target, timeout, [], new PingOptions(ttl, false))works and reportsTimeExceededwith the hop's address. -
A 100.64.x.x hop is not proof of CGNAT. The owner's Dimensione line has a public IPv4 on the FRITZ!Box and still shows
100.64.255.255as hop 2. Only the router's own WAN address compared with the echo decides; hops are a hint. -
Routers answer the SSDP search they don't implement. The FRITZ!Box 5530 answers an IGD:2 search while describing itself as IGD:1; the version comes from the description's
deviceType. Its PCP answersNOT_AUTHORIZED(2). -
HeadlessUnitTestSession.Dispatchbuilds a freshApplicationeach call, andAttachDeveloperToolsthrows the second time: the screenshot tool does the real check and every shot inside oneDispatch. -
Fluent's
Expanderpaints its header from resources (ExpanderHeaderBackground, …), not fromBackground; theExpander.explainstyle overrides them inStyle.Resources. -
The probe never trusts the client for a target. A ticket is bound to the address the server saw, single use, 60 s; the run refuses any other caller address, private and CGNAT space, and
Probe:Refuse(the box's own addresses, so the owner cannot probe himself from home).Probe__AllowPrivate=trueexists for a local server only. -
Ask the outside STUN server first. When
stun.mewebis the caller's own public address (the owner's line), the first binding hairpins inside the FRITZ!Box with the port preserved and the next destination reuses that mapping, so the orderstun.nextcloud.comthenstun.mewebis what reads a symmetric NAT as hard. -
Shared findings are files, not Mongo. The box's Mongo has auth on and me-web? has no user there; a file per submission under the unit's
StateDirectory, with only the delete token's SHA-256, needs no credentials. -
An address inside right-to-left text reorders around
→.Texts.Isolatewraps every literal value in U+2068/U+2069. -
A Lucide icon joined from several SVG elements must start each one with an absolute
M. A leadingmis absolute only as a path's first command; joined after another element it becomes relative and draws outside the box. The shield and the radio tower did that until 0.3; check new geometry by walking its points against 24 (or 48). -
On Android
GC.GetGCMemoryInfo().TotalAvailableMemoryBytesis the runtime's limit, not the RAM (0.1 GB on a phone with several):AndroidPlatformreadsActivityManager.MemoryInfo.TotalMem, Linux/proc/meminfo. -
DriveInfoon Unix takes any path (it callsstatvfs): pass the app's folder, not/, or Android reads the read-only root and reports no free disk at all. -
An IPv6 address only wraps at a colon if it is allowed to: the path's detail lines put U+200B after each colon; they are plain
TextBlocks for that reason, so a copy never carries the invisible character (the cards' values, which are selectable, stay untouched). -
Avalonia 12 Android head:
[Application] MainApplication : AvaloniaAndroidApplication<App>plus a non-genericAvaloniaMainActivity; the main view comes throughIActivityApplicationLifetime.MainViewFactory, checked beforeISingleViewApplicationLifetime. -
ApplicationVersionmust be an integer: computed fromVersionthrough separateVersionMajor/Minor/Patchproperties; one nested property function left it unevaluated (XA0003). -
A headless Avalonia session does not end on its own while the pulse animation runs: the screenshot tool calls
Environment.Exit(0)after dispatching. -
The macOS apphost is ad-hoc signed by the .NET 10 SDK even on Linux (
LC_CODE_SIGNATUREpresent); Apple Silicon refuses unsigned arm64 code, so never strip it. -
The Windows single-file publish drops native
.pdbfiles beside the exe; onlymeweb.exeships.