me-web? 0.15.4: a desktop with a wireless mouse is no longer taken for a laptop
Build / Desktop, server and tools (push) Successful in 1m14s
Release / Every platform, the server, the release (push) Successful in 8m38s

- 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>
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-31 23:24:15 +01:00
1 parent e112c1dc1e
commit 0c722c21d4
8 files changed
+24 -13

No files matched your search

+4 -1
View File
@@ -77,7 +77,7 @@ The Android release key: alias `meweb`, certificate SHA-256
| `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` | | `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/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/setup.cs` + `setup.json` | pinned build tools |
| `tools/Screenshots` | headless renders of every view in every language | | `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) | | `branding/` | the icon (SVG source, PNG, ICO) |
## Pitfalls ## Pitfalls
@@ -232,6 +232,9 @@ The Android release key: alias `meweb`, certificate SHA-256
phone with several): `AndroidPlatform` reads `ActivityManager.MemoryInfo.TotalMem`, Linux `/proc/meminfo`. phone with several): `AndroidPlatform` reads `ActivityManager.MemoryInfo.TotalMem`, Linux `/proc/meminfo`.
- **`DriveInfo` on Unix takes any path** (it calls `statvfs`): pass the app's folder, not `/`, or Android reads the - **`DriveInfo` on Unix takes any path** (it calls `statvfs`): pass the app's folder, not `/`, or Android reads the
read-only root and reports no free disk at all. read-only root and reports no free disk at all.
- **A wireless mouse is a battery to Linux.** `/sys/class/power_supply` lists the batteries of mice, keyboards, headsets
and game controllers too (`type` Battery, `scope` Device); only one without `scope` Device 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; - **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 `TextBlock`s for that reason, so a copy never carries the invisible character (the cards' values, they are plain `TextBlock`s for that reason, so a copy never carries the invisible character (the cards' values,
which are selectable, stay untouched). which are selectable, stay untouched).
+1 -1
View File
@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>0.15.3</Version> <Version>0.15.4</Version>
<Product>me-web?</Product> <Product>me-web?</Product>
<Company>Arasaka di Hvozdikov Yevheniy</Company> <Company>Arasaka di Hvozdikov Yevheniy</Company>
<Copyright>© 2026 Arasaka di Hvozdikov Yevheniy</Copyright> <Copyright>© 2026 Arasaka di Hvozdikov Yevheniy</Copyright>
+3 -1
View File
@@ -15,6 +15,8 @@ public static class AppServices
public const string Secure = "secure"; public const string Secure = "secure";
public const string Plain = "plain"; public const string Plain = "plain";
public static string Folder { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MeWeb");
public static IServiceCollection AddMeWebApp(this IServiceCollection services, IPlatform platform) public static IServiceCollection AddMeWebApp(this IServiceCollection services, IPlatform platform)
{ {
var agent = $"me-web/{BuildInfo.Version}"; var agent = $"me-web/{BuildInfo.Version}";
@@ -28,7 +30,7 @@ public static class AppServices
return new ServerProbe(factory.CreateClient(Secure), factory.CreateClient(Plain)); return new ServerProbe(factory.CreateClient(Secure), factory.CreateClient(Plain));
}); });
services.AddSingleton<KnowledgeBase>(); services.AddSingleton<KnowledgeBase>();
var folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MeWeb"); var folder = Folder;
services.AddSingleton(new HistoryStore(folder)); services.AddSingleton(new HistoryStore(folder));
services.AddSingleton(new Leftovers(folder)); services.AddSingleton(new Leftovers(folder));
services.AddSingleton(new Submissions(folder)); services.AddSingleton(new Submissions(folder));
+2 -3
View File
@@ -11,8 +11,7 @@ public class StoredSettings
public static class AppSettings public static class AppSettings
{ {
static readonly string Folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MeWeb"); static string FilePath => Path.Combine(AppServices.Folder, "settings.json");
static readonly string FilePath = Path.Combine(Folder, "settings.json");
public static StoredSettings Read() public static StoredSettings Read()
{ {
@@ -32,7 +31,7 @@ public static class AppSettings
{ {
try try
{ {
Directory.CreateDirectory(Folder); Directory.CreateDirectory(AppServices.Folder);
var temporary = FilePath + ".tmp"; var temporary = FilePath + ".tmp";
using (var stream = File.Create(temporary)) using (var stream = File.Create(temporary))
JsonSerializer.Serialize(stream, settings, AppJsonContext.Default.StoredSettings); JsonSerializer.Serialize(stream, settings, AppJsonContext.Default.StoredSettings);
+1 -1
View File
@@ -7,7 +7,7 @@ namespace MeWeb.App.Services;
public static class ErrorLog public static class ErrorLog
{ {
const long MaxBytes = 256 * 1024; const long MaxBytes = 256 * 1024;
static readonly string FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MeWeb", "errors.log"); static string FilePath => Path.Combine(AppServices.Folder, "errors.log");
static readonly Lock Gate = new(); static readonly Lock Gate = new();
public static void Watch() public static void Watch()
+3 -3
View File
@@ -337,14 +337,14 @@ public static class Verdicts
{ Port25: Core.Mail.PortState.Blocked } => (Outcome.Bad, "MailBlocked"), { Port25: Core.Mail.PortState.Blocked } => (Outcome.Bad, "MailBlocked"),
_ when listed.Count > 0 => (Outcome.Bad, "MailListed"), _ when listed.Count > 0 => (Outcome.Bad, "MailListed"),
_ when mail.Listings.Any(l => l.Residential) => (Outcome.Caution, "MailResidential"), _ when mail.Listings.Any(l => l.Residential) => (Outcome.Caution, "MailResidential"),
_ when wide.Count > 0 => (Outcome.Caution, "MailNetworkListed"),
{ ReverseName: null } => (Outcome.Caution, "MailNoName"), { ReverseName: null } => (Outcome.Caution, "MailNoName"),
{ ReverseLooksDynamic: true } => (Outcome.Caution, "MailDynamicName"), { ReverseLooksDynamic: true } => (Outcome.Caution, "MailDynamicName"),
_ when wide.Count > 0 => (Outcome.Caution, "MailNetworkListed"),
{ Port25: Core.Mail.PortState.Open } => (Outcome.Good, "MailClean"), { Port25: Core.Mail.PortState.Open } => (Outcome.Good, "MailClean"),
_ => (Outcome.Unknown, "MailUnknown"), _ => (Outcome.Unknown, "MailUnknown"),
}; };
if (listed.Count > 0) finding.Args = [string.Join(", ", listed.Select(l => l.List))]; if (finding.Summary == "MailListed") finding.Args = [string.Join(", ", listed.Select(l => l.List))];
else if (wide.Count > 0) finding.Args = [string.Join(", ", wide.Select(l => l.List))]; else if (finding.Summary == "MailNetworkListed") finding.Args = [string.Join(", ", wide.Select(l => l.List))];
finding.WithKey("FactPort25", mail.Port25 switch finding.WithKey("FactPort25", mail.Port25 switch
{ {
Core.Mail.PortState.Open => "ValuePort25Open", Core.Mail.PortState.Open => "ValuePort25Open",
+7 -1
View File
@@ -105,7 +105,7 @@ public class SystemPlatform : IPlatform
try try
{ {
return Directory.Exists("/sys/class/power_supply") && Directory.EnumerateDirectories("/sys/class/power_supply") return Directory.Exists("/sys/class/power_supply") && Directory.EnumerateDirectories("/sys/class/power_supply")
.Any(d => File.Exists(Path.Combine(d, "type")) && File.ReadAllText(Path.Combine(d, "type")).Trim() == "Battery"); .Any(d => Attribute(d, "type") == "Battery" && Attribute(d, "scope") != "Device");
} }
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{ {
@@ -183,6 +183,12 @@ public class SystemPlatform : IPlatform
return product.StartsWith(maker, StringComparison.OrdinalIgnoreCase) ? product : $"{maker} {product}"; return product.StartsWith(maker, StringComparison.OrdinalIgnoreCase) ? product : $"{maker} {product}";
} }
static string Attribute(string folder, string name)
{
var path = Path.Combine(folder, name);
return File.Exists(path) ? File.ReadAllText(path).Trim() : null;
}
static string Dmi(string name) static string Dmi(string name)
{ {
try try
+3 -2
View File
@@ -16,11 +16,12 @@ using Microsoft.Extensions.DependencyInjection;
var output = args.Length > 0 ? args[0] : Path.Combine("dist", "screenshots"); var output = args.Length > 0 ? args[0] : Path.Combine("dist", "screenshots");
var codes = args.Length > 1 ? args[1].Split(',', StringSplitOptions.RemoveEmptyEntries) : Languages.All.Select(l => l.Code).ToArray(); var codes = args.Length > 1 ? args[1].Split(',', StringSplitOptions.RemoveEmptyEntries) : Languages.All.Select(l => l.Code).ToArray();
Directory.CreateDirectory(output); Directory.CreateDirectory(output);
var data = Directory.CreateTempSubdirectory("meweb-screenshots-");
AppServices.Folder = data.FullName;
var session = HeadlessUnitTestSession.StartNew(typeof(ScreenshotEntry)); var session = HeadlessUnitTestSession.StartNew(typeof(ScreenshotEntry));
await session.Dispatch(async () => await session.Dispatch(async () =>
{ {
var saved = AppSettings.Read();
var run = await App.Services.GetRequiredService<CheckRunner>().Run((_, _) => { }, CancellationToken.None); var run = await App.Services.GetRequiredService<CheckRunner>().Run((_, _) => { }, CancellationToken.None);
Console.WriteLine($"run: {string.Join(", ", run.Findings.Select(f => $"{f.Id}={f.Outcome}"))}"); Console.WriteLine($"run: {string.Join(", ", run.Findings.Select(f => $"{f.Id}={f.Outcome}"))}");
foreach (var (width, height, label) in new[] { (1100, 1500, "desktop"), (400, 8000, "phone") }) foreach (var (width, height, label) in new[] { (1100, 1500, "desktop"), (400, 8000, "phone") })
@@ -48,9 +49,9 @@ await session.Dispatch(async () =>
}); });
} }
} }
AppSettings.Write(saved);
return 0; return 0;
}, CancellationToken.None); }, CancellationToken.None);
data.Delete(true);
Environment.Exit(0); Environment.Exit(0);
void Shoot(int width, int height, string name, Action<MainViewModel> prepare, Action<MainView> arrange = null) void Shoot(int width, int height, string name, Action<MainViewModel> prepare, Action<MainView> arrange = null)