me-web? 0.15.4: a desktop with a wireless mouse is no longer taken for a laptop
- 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:
1 parent
e112c1dc1e
commit
0c722c21d4
8 files changed
+24
-13
No files matched your search
@@ -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,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>
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user