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
7.5 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), Router/ (UPnP IGD, NAT-PMP, PCP, read-only), Knowledge/ (provider map and profiles), History/, Checks/ (CheckRunner, Verdicts, ServerProbe) |
src/MeWeb.App |
Avalonia views and view models, Services/Localizer.cs (+ {s:Tr Key}), Strings/<code>.json, Resources/Colors.axaml |
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 / (en, it) and /download/{platform} from the latest Gitea release |
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. -
An address inside right-to-left text reorders around
→.Texts.Isolatewraps every literal value in U+2068/U+2069. -
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.