- On Linux, a desktop computer with a wireless mouse, keyboard, headset or game controller was described as a laptop, with the advice to use a small always-on computer instead, because the battery inside the mouse was counted. Only the computer's own battery counts now. - The mail card now leads with what big mailboxes care about most: when your address's reverse name looks like a home line, or there is none, that comes first. A blocklist that names your provider's whole network is still shown below it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
33 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,...]
tools/release.sh notes # artifacts/release-notes.md alone, nothing built
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/strings.cs -- quote # the name in its language's marks in a sentence, bare alone
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); fdroid.yml daily at 05:23 UTC or by hand
(the F-Droid tools, below); packages.yml daily at 05:43 UTC or by hand (the deb and rpm tools, below). All four
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; then the intro planet's still pieces as DrawingImages with a fixed Viewbox: ArtHouse, ArtBlock, ArtPine, ArtOak, ArtPhoto, ArtLetter, ArtServer). 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, and quotes, the language's own quotation marks from CLDR (fixed by hand where CLDR has none or is wrong) |
data/glossary/<code>.json |
per language, the plain words chosen for about forty recurring ideas (provider, router, port, name server, …), so a card never calls one thing by two names; written with the translations and read by whoever reviews them |
data/isps/*.json |
provider profiles: AS numbers, and per fact a value, who said it (basis), the source URL and the month |
data/observed.json |
notes counted from shared findings and accepted by a person: per AS number and line (home, mobile), IPv4 shared or public, IPv6 offered or not, port 25, each with how many networks agree; embedded in every release |
src/MeWeb.Server/ObservedNotes.cs |
every six hours (with the share sweep) counts the shared findings into /var/lib/meweb/observed.json on the box: one vote per /24 or /48, the latest check of each network, the publisher's own network left out; nothing is served |
tools/observed.cs |
show prints that count, accept <file> AS<n>/<line> ... copies the ready notes (at least 5 networks, 4 in 5 agreeing) into data/observed.json |
src/MeWeb.Server/KnowledgeFeeder.cs |
refreshes the open network data under /var/lib/meweb/knowledge every six hours (iptoasn daily, DB-IP monthly, eight RouteViews RIBs weekly through bzcat (one or two collectors per continent, Collectors; each counts once per network in UpstreamTally, and a path's first AS, the collector's own neighbour, is never taken for an upstream), the AOSP carrier list weekly, the address registries' block files weekly (Registries: RIPE's and APNIC's inetnum and inet6num split files, AFRINIC's whole dump, LACNIC's lacnic.db)), builds ip2asn.bin, networks.bin, carriers.json, registry.bin (every block of the four, RegistryBuilder) and SOURCES.txt with MeWeb.Core/Knowledge/Sources.cs, and serves them at /api/knowledge |
tools/knowledge.cs |
takes the server's knowledge files (MEWEB_KNOWLEDGE_URL, default the public server; the release sets http://127.0.0.1:5480 after deploying the server first) into artifacts/knowledge/, checking each SHA-256, and falls back to building ip2asn.bin alone from iptoasn (--local forces that); MeWeb.Core embeds whatever is there; 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; it runs a real check, in a throwaway app folder (AppServices.Folder), so the developer's own history and settings stay untouched |
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. -
On mobile data there is no router of the user's. Android reports a gateway for the cellular link, so
CheckRunnerskips the router protocols on a mobile link, and a private or 100.64 address on the device with a different public echo is the operator's NAT (NatKind.Shared); before 0.4 a phone on Iliad read "router not answering" and "can't tell" everywhere downstream. Provider profiles describe home lines, so on mobile the card says so. -
The publisher's own network measures the house, not the line. When the public IPv4 equals the server's, or the IPv6 shares its /48 (
Addresses.SameNetwork,RunReport.OwnNetwork), everything aimed at the server loops back through the FRITZ!Box: the NAT type, the port burst, hairpin, lifetime, speed and round trip. Those cards then sayReachOwnNetworkand the burst is not sent; before 0.5.3 a home report read "hard NAT" next to "ports kept". -
One verdict decides who holds the IPv4.
Verdicts.Holder(NatOf(...))feeds both the address card and the provider card's "On this line" row, so a home router behind CGNAT can no longer read "your router" on one card and "shared with other customers" on the next. Provider notes that the check contradicts (sharing, port 25) get that row right under them, added when the provider card is settled again at the end of the run. -
The route is drawn, never trusted.
Route.Trace(8 TTLs) feeds the path's wires: hops after the first, up to the first public one, sit before the shared exit or the provider; public ones carry their AS number into the internet; unanswered runs collapse to "…". The NAT verdict never reads them (see the 100.64 pitfall). -
A note from shared findings never ships without a person. The consent screen and the privacy page promise "only totals without addresses, after a person has reviewed them, from at least five networks agreeing four in five";
ObservedFact.Readyis that rule,tools/observed.cs acceptthe review. Hand-written profile facts win over observed ones; observed facts fill the gaps, and on mobile data they replace the home-line profile entirely. -
The knowledge build must fit the unit's
MemoryMax=512M. It peaked at 551 MB with Server GC on a 32-core machine; workstation GC (the server's csproj), streaming hashes and streaming the iptoasn ranges into compact tuples brought it to about 335 MB. Re-measure (VmHWM) after touchingSources.csorAsnTable.Write. -
RIPE Database data is used by permission, on a condition. The RIPE NCC allowed it on 16 October 2026 (ticket #1122623) "as long as there is no commercial intent where the RIPE Database data is behind a paywall or used for monetization purposes". So it stays in this free app and its server: never behind a paywall, never in anything sold, never reused by the publisher's business. The request promised range, netname, country and status only (no persons, contacts,
descrorremarks), RIPE credited with a link, the extract's date and a note that it may lag;SOURCES.txt(the release notes) andRegistryCreditcarry that. The RIR's own allocations (ALLOCATED PA,ALLOCATED UNSPECIFIED,ALLOCATED-BY-RIR) are skipped: their names are registry-made (IT-FASTWEB-20101230). -
APNIC, AFRINIC and LACNIC came on the same terms, each with its own promise. The letters of 15 October 2026 (APNIC #6500381, AFRINIC #1066122, LACNIC #222611) promised what
Sources.cskeeps, so never add a field to one without asking that registry again. APNIC, which named its public split files for this purpose: range and netname, never the country (its rules bar mapping addresses to places); its member allocations stay (Japan, Korea and Taiwan exist there only as their national registries' allocations), its own placeholders go (ApnicPlaceholder:IANA-BLOCK,IANA-NETBLOCK-n,APNIC-AP,ERX-NETBLOCK, the other registries'*-CIDR-BLOCK,ROOT, which would otherwise label the whole internet), and so do the national registries' umbrella blocks (JPNIC-NET-JP,CNNIC,KRNIC-NET,TWNIC-TW,IDNIC-ID,VNNIC-NET, which name a registry, not a provider), while their members' own names stay (IDNIC-BIZNETGIO-ID). AFRINIC: range, netname and country, skippingALLOCATED UNSPECIFIEDandPOLICY-RESERVED(placeholders such asIANA-BLKandNET-154-1-0-0); its member allocations carry real names (MTNSA-MOBILE-BROADBAND). LACNIC'slacnic.dbnames no block: prefix, status and country only (not the city or the dates), so the card shows how the block was handed out in plain words (ValueBlock*, the status inregistry.bin's row byte). ARIN's region has no block names: its NetName and NetType come only through the Bulk Whois download behind an ARIN Online key, not wired in, and its public delegation file says only "allocated" and the country, which the provider map already has. -
Every registry block travels with the app, because the app works offline. The owner, 16 October 2026: "this is an offline first capable app it needs to hold value even in absence of connection".
registry.binholds all seven and a half million blocks of the four registries, flattened so the narrowest one wins (RegistryTable.Narrowest), in Brotli chunks of 16,384 rows behind an index: about 32 MB, opened in a millisecond, one chunk decompressed per lookup, nothing else resident. It is not insideMeWeb.Core, because the APK stores every assembly once per processor type: the desktop head and the screenshot tool embed it in their entry assembly, the Android head ships it as an uncompressed asset (AndroidStoreUncompressedFileExtensions) thatAndroidPlatform.OpenRegistryreads in place throughOpenFd.RegistryBuilderreads IPv4, then IPv6, with the names in a temporary file, into lists sized up front (6.5 million IPv4 and 2 million IPv6 rows; RIPE alone has 1.48 million IPv6 blocks, and a list that outgrows its size doubles): about 276 MB peak in October 2026, under the unit's 512 MB, and some four minutes on top of the RouteViews tally./api/ipstill answers each caller with its own block from the server's newer copy; without that answer the app looks the address up itself, and when the server cannot be reached at all it uses the router's public outside address or the STUN answer, so the provider card names the network and the block without the publisher's server. A change to any knowledge file's format bumpsKnowledgeFeeder.Formatand the"format":Nthatwait-knowledge.shwaits for: the feeder rebuilds only when a source changes or the format differs, and a release must not embed the previous build. -
The deb and rpm reach packages.arasaka.software from this repository's own release job. On tags,
.gitea/scripts/packages.shruns on thenuvolarunner (host mode, usergitea-runner, which owns/var/www/packages.arasaka.software). It imports the org secretPACKAGES_SIGNING_KEYinto a throwawayGNUPGHOME, refuses any key butF92C166E…3FB2, signs the rpm in place, hard-links the current pool into a new.snap/folder, adds this version, keeps the newest five, rebuilds and signs the apt (InRelease,Release.gpg) and rpm (repomd.xml.asc) metadata, carries over the files the previousrepomd.xmlnamed so a client mid-refresh still finds them, swaps theaptandrpmlinks, rewritesSHA256SUMS.txtfor the signed rpm, and checks the live site. Never copy a package over a pool file: the pools are hard links into older snapshots, so it isrmthencp.PACKAGES_ROOTandPACKAGES_KEEPexist for a test against a copy. YoloCICD, which used to do this, was erased on 23 Oct 2026.packaging/arasaka-repository.shis written into both maintainer scripts bytools/release.cs; it is now maintained here by hand, and any change to a file it writes needs the old content's SHA-256 kept inarasaka_repository_known, or installed copies are treated as edited by hand and left alone. The package owns/usr/share/arasaka-repository/meweb, the marker that keeps the repository configured while any Arasaka package is installed. The signing key's only copies are that secret and the owner's Nextcloud backup. The tools that build these two repositories are the box's own Ubuntu packages (apt-utilsforapt-ftparchive,createrepo-c,dpkg,gnupg,rpm), and a daily job keeps them current (owner, 31 Oct 2026: the same daily update as the F-Droid tools). The runner cannot install packages, so the update is a root timer on the box,packages-tools.timerat 05:13 UTC (source and setup in arasaka.software,deploy/packages.arasaka.software/). It upgrades only those packages, from Ubuntu's updates and security fixes, with package lists of its own, so it never brings the rest of the box forward. There is no two-week wait, as there is for fdroidserver: Ubuntu reviews and signs these updates itself.packages.ymlfollows at 05:43 UTC. It fails if the morning's update failed (it reads the result withsystemctl show, which needs no rights), then runs this script with no version. When the tools' versions or this script differ from what the live repositories were built from (built-frombeside each snapshot), it rebuilds and re-signs both from the current pools with no new package, swaps the links and checks the live site; otherwise it says "nothing new" and names the versions. Unlike fdroidserver, these tools cannot be kept beside an older copy, so a release uses whatever is installed; an update that breaks them fails the next morning's job first. -
The APK reaches an F-Droid repository on packages.arasaka.software from the same job (31 Oct 2026), after the release is published, so the repository can lag the release page but never lead it.
.gitea/scripts/fdroid.shinstalls fdroidserver with uv fromsetup.json(its download cache intools/fdroid, cached like the JDK), because the box has noensurepipand the apt package would bring 242 others. Nothing pins fdroidserver; a daily job keeps it current (owner, 31 Oct 2026: a daily service, not patching it from time to time).fdroid.ymlruns the same script with no version at 05:23 UTC: it resolves the newest fdroidserver, and the newest of everything it needs, published at least 14 days earlier (a bad upload has usually been pulled by then), against the box's ownpython3, into a hash-locked list; a new list gets its own install,fdroidserver-<hash of the list>in the cache's real path (an install's scripts carry the absolute path they were made at; 0.15.1 failed withfdroid: not foundon one made from a deleted folder). When that list or the listing inpackaging/fdroid/differs from what the live repository was built from (built-frombeside each snapshot), it rebuilds the repository with it, and only once that is live and verified does the install becomecurrentand the older ones go. A release usescurrentand downloads nothing, so a new fdroidserver that breaks fails the daily job, never a release; the job's log names the version. Every install runs with the signing key out of its environment. It signs the repository with the APK's own key (MEWEB_KEYSTORE_B64andMEWEB_KEYSTORE_PASS, aliasmeweb), so the F-Droid fingerprint is the certificate's5F52CFF4…FDF846and there is no second key to lose; it refuses any other. It builds a new snapshot in/var/www/packages.arasaka.software/.fdroid/from the live one (the APKs as hard links, sormthencpas above; the metadata, the icons,apkcache.jsonand the previousindex-v2.json, which is where fdroidserver reads each APK's first date; without the icons a rebuild with no new APK fails, because the cache skips extracting them), adds this version (a release) or nothing (the daily job), keeps the newest three, runsfdroid update, deletesrepo/status/(it names the build machine's paths and/etc/issue.net), swaps thefdroid/repolink and checks the live signature. The key was already never to be regenerated; now everyone who added the repository trusts only its fingerprint too.packaging/fdroid/holds the repository's settings, its icon (the Arasaka emblem) and the app's listing in English and Italian; the download page links to it and to Obtainium (Endpoints.FDroid, sourceFDroidRepo). -
The server cannot ping:
NoNewPrivilegesdropsping's file capability andping_group_rangeis closed, so the trace back to a caller usestracepath(UDP, unprivileged) and keeps only public hops outsideProbe:Refuse, so the box's own network never reaches an app. -
Port facts are a lower bound. Sixteen bindings see a few ports of a provider's block; the block estimate widens with each check on the same public address (
HistoryStore.PortRange), so "up to about N customers" is an upper bound that only falls over time. The mapping lifetime test (30 s and 120 s) runs after the check and updates the card; a NAT that reuses the same port after expiry reads as "kept". -
5G non-standalone reports LTE as the data network type; the display-info callback (Android 12+) gives the override the status bar shows. The radio type needs
READ_BASIC_PHONE_STATE(Android 13+), asked during the first check on mobile data. -
Download speed is timed after the first quarter of the sample, once TCP has ramped up: on mobile the sample is 2 MB and the whole-transfer figure was mostly the ramp.
-
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. -
A wireless mouse is a battery to Linux.
/sys/class/power_supplylists the batteries of mice, keyboards, headsets and game controllers too (typeBattery,scopeDevice); only one withoutscopeDevice makes a laptop. Until 0.15.4 the owner's desktop read as a laptop because of an MX Master 4 (MW-VRST-EK5A). -
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. -
Launchers and the Android 12 splash crop the icon to their own shape. The launcher icon is the adaptive
mipmap-anydpi-v26/icon.xml(background colour,drawable/icon_foreground.xml,drawable/icon_monochrome.xmlfor themed icons), and the splash uses the same foreground. Both vectors holdbranding/meweb.svg's drawing scaled into the central 66 of 108 units around its smallest enclosing circle; a change to the drawing goes into all three files. The plainmipmap-xxxhdpi/icon.pngis only for Android 7.x. -
Release notes are the commits since the previous tag.
tools/release.cswrites "What's new" from each commit's subject (without theme-web? x.y:prefix, quoted or not) and body, dropping trailers and any line that mentions Claude; the release step needsfetch-depth: 0for the tags. So a commit body is written for the people who download the release: plain words, one bullet per change they would notice. -
The name is quoted inside a sentence, in each language's own marks, and bare where it stands alone. “me-web?” in English, «me-web?» in Italian, „me-web?“ in German, « me-web? » (no-break spaces) in French, 「me-web?」 in Japanese: unquoted in a sentence, the question mark reads as its end ("What me-web? found"). Alone, as a title or a label, the marks only clutter it (the owner, 30 October 2026): the window and launcher name (
AppName,Product, the Android label, the desktop file, the macOS bundle), the page titles and the download page's heading, the version line (VersionLine, "me-web? 0.15.0 · free software"), the README's heading and the commit prefix (me-web? 0.15.0: ...).tools/strings.cs checkfails on a bare name in a sentence and on a quoted one in the keys of itsalonelist;quoteputs each right. In right-to-left languages theLocalizerwraps the name in U+2066/U+2069 (left-to-right isolate), the English fallback included: without it Avalonia lays out the lone title ”me-web?“ asme-web?”“, and inside a sentence the?jumps to the other end of the name. A left-to-right mark after the name was tried and does not fix the lone title. -
Avalonia 12 refreshes an indexer binding only on
PropertyChanged("Item").{s:Tr Key}bindsLocalizer.Instance[Key]; the WPF name"Item[]"and""refresh nothing, so until 0.7.1 changing the language left every{s:Tr}text in the old one while the view models'*Formattedstrings followed. The screenshot tool draws eachstart-<code>shot by switching from English on a live view, which is the check that would have caught it. -
The first screen is three cards in one scroll: the outcome, how it works, then the question and the choice. Picturing the outcome alone lowers the drive to act, and an obstacle only motivates while it looks beatable (Oettingen's mental contrasting), so the third card names the difficulty with hope and ends on the action.
Views/IntroSceneis one small planet in threeSteps: 0, a phone far away sends a photo into the house and on to the parents' tablet (heart); 1, a house with its own number and a small always-on computer in a callout lets a visitor in (green door, tick); 2, that house next to a block of flats with one number for all, where the visitor stops at the main door (question mark). The visitor is a letter, because a letter needs an address; it and the photo fly along the dotted routes with a comet trail (a gradient stroke whose ends are set each frame inRelativeUnit.Absolute), and the world around them keeps moving (stars, town lights on the night side, clouds, chimney smoke, swaying trees). The still drawings areDrawingImages inArt.axaml; only what moves is a control, and it moves through itsRenderTransform(scale, rotate, translate), neverCanvas.SetLeft, so a frame does not lay the canvas out again. The third card asks whether the reader's home has an address of its own and offers two answers: "Yes, let's find out" runs the check, "Not for me" showsGoodbyefor two seconds and closes the app (IPlatform.Quit, the desktop lifetime'sShutdown). The six uses follow as chips, the QR codes wait folded at the bottom. -
As the reader scrolls, the first two cards fold into bubbles.
MainView.Foldreads each card's layout position against the scroll offset (neverTranslatePointon the card, whose ownRenderTransformit is changing), shrinks and fades the card and grows its bubble in the strip over the scroller; a bubble glides back to its card. Once the reader has pressed the check (StoredSettings.StorySeen), the app opens folded on the third card.Views/Animatedis the frame loop the scenes share;Restingparks a scene that is folded or off screen, so it starts again from the beginning when it comes back. The screenshot tool callsHold()to draw each at itsPostermoment, because the headless clock is real time. -
Fluent's
ButtonandItemsControlboth clip to their own bounds. The path's verdict aura and running pulse reach past the 90-pixel node button, and the nodes sit on the pathItemsControl's left and right edges, so both turnClipToBoundsoff (Button.hopwith itsContentPresenter, and the path'sItemsControl). With either one left on, the glow is cut straight; 0.14.0 shipped with theItemsControlcut. -
Nothing that is being checked is left empty. From the moment a check starts, every use has a card with its title, a "Checking" chip, two skeleton lines and a sweeping glint (
UseViewModelwith no finding), and every running check card shows the same; the verdicts replace them as they arrive. While a check runs, the share card sits at the end in a muted "getting them ready" state (ShowSharePreparing) and turns into the offer when the findings are complete. -
Values worth copying are read-only text boxes with a copy button inside (
Views/CopyField): addresses, the router, the network, the reverse name, the route, the STUN answers and a submission's code. The box is left-to-right in every language and copies the raw value, never the bidi marksTexts.Isolateadds to display text. Which facts get one is theCopyablelist inFactViewModel. -
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.