diff --git a/CLAUDE.md b/CLAUDE.md index 636df05..d3671ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` | | `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 | +| `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 @@ -232,6 +232,9 @@ The Android release key: alias `meweb`, certificate SHA-256 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 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; they are plain `TextBlock`s for that reason, so a copy never carries the invisible character (the cards' values, which are selectable, stay untouched). diff --git a/Directory.Build.props b/Directory.Build.props index 1269127..44aab95 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.15.3 + 0.15.4 me-web? Arasaka di Hvozdikov Yevheniy © 2026 Arasaka di Hvozdikov Yevheniy diff --git a/src/MeWeb.App/AppServices.cs b/src/MeWeb.App/AppServices.cs index bfac3bb..3dcf1fa 100644 --- a/src/MeWeb.App/AppServices.cs +++ b/src/MeWeb.App/AppServices.cs @@ -15,6 +15,8 @@ public static class AppServices public const string Secure = "secure"; 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) { var agent = $"me-web/{BuildInfo.Version}"; @@ -28,7 +30,7 @@ public static class AppServices return new ServerProbe(factory.CreateClient(Secure), factory.CreateClient(Plain)); }); services.AddSingleton(); - var folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MeWeb"); + var folder = Folder; services.AddSingleton(new HistoryStore(folder)); services.AddSingleton(new Leftovers(folder)); services.AddSingleton(new Submissions(folder)); diff --git a/src/MeWeb.App/Services/AppSettings.cs b/src/MeWeb.App/Services/AppSettings.cs index 03031ab..e48f5f0 100644 --- a/src/MeWeb.App/Services/AppSettings.cs +++ b/src/MeWeb.App/Services/AppSettings.cs @@ -11,8 +11,7 @@ public class StoredSettings public static class AppSettings { - static readonly string Folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MeWeb"); - static readonly string FilePath = Path.Combine(Folder, "settings.json"); + static string FilePath => Path.Combine(AppServices.Folder, "settings.json"); public static StoredSettings Read() { @@ -32,7 +31,7 @@ public static class AppSettings { try { - Directory.CreateDirectory(Folder); + Directory.CreateDirectory(AppServices.Folder); var temporary = FilePath + ".tmp"; using (var stream = File.Create(temporary)) JsonSerializer.Serialize(stream, settings, AppJsonContext.Default.StoredSettings); diff --git a/src/MeWeb.App/Services/ErrorLog.cs b/src/MeWeb.App/Services/ErrorLog.cs index 10a0e07..2c024f0 100644 --- a/src/MeWeb.App/Services/ErrorLog.cs +++ b/src/MeWeb.App/Services/ErrorLog.cs @@ -7,7 +7,7 @@ namespace MeWeb.App.Services; public static class ErrorLog { 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(); public static void Watch() diff --git a/src/MeWeb.Core/Checks/Verdicts.cs b/src/MeWeb.Core/Checks/Verdicts.cs index 1fb8852..2970547 100644 --- a/src/MeWeb.Core/Checks/Verdicts.cs +++ b/src/MeWeb.Core/Checks/Verdicts.cs @@ -337,14 +337,14 @@ public static class Verdicts { Port25: Core.Mail.PortState.Blocked } => (Outcome.Bad, "MailBlocked"), _ when listed.Count > 0 => (Outcome.Bad, "MailListed"), _ when mail.Listings.Any(l => l.Residential) => (Outcome.Caution, "MailResidential"), - _ when wide.Count > 0 => (Outcome.Caution, "MailNetworkListed"), { ReverseName: null } => (Outcome.Caution, "MailNoName"), { ReverseLooksDynamic: true } => (Outcome.Caution, "MailDynamicName"), + _ when wide.Count > 0 => (Outcome.Caution, "MailNetworkListed"), { Port25: Core.Mail.PortState.Open } => (Outcome.Good, "MailClean"), _ => (Outcome.Unknown, "MailUnknown"), }; - if (listed.Count > 0) finding.Args = [string.Join(", ", listed.Select(l => l.List))]; - else if (wide.Count > 0) finding.Args = [string.Join(", ", wide.Select(l => l.List))]; + if (finding.Summary == "MailListed") finding.Args = [string.Join(", ", listed.Select(l => l.List))]; + else if (finding.Summary == "MailNetworkListed") finding.Args = [string.Join(", ", wide.Select(l => l.List))]; finding.WithKey("FactPort25", mail.Port25 switch { Core.Mail.PortState.Open => "ValuePort25Open", diff --git a/src/MeWeb.Core/Net/LocalNetwork.cs b/src/MeWeb.Core/Net/LocalNetwork.cs index 86241f9..0ab8b87 100644 --- a/src/MeWeb.Core/Net/LocalNetwork.cs +++ b/src/MeWeb.Core/Net/LocalNetwork.cs @@ -105,7 +105,7 @@ public class SystemPlatform : IPlatform try { 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) { @@ -183,6 +183,12 @@ public class SystemPlatform : IPlatform 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) { try diff --git a/tools/Screenshots/Program.cs b/tools/Screenshots/Program.cs index 40b7056..a00b675 100644 --- a/tools/Screenshots/Program.cs +++ b/tools/Screenshots/Program.cs @@ -16,11 +16,12 @@ using Microsoft.Extensions.DependencyInjection; 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(); Directory.CreateDirectory(output); +var data = Directory.CreateTempSubdirectory("meweb-screenshots-"); +AppServices.Folder = data.FullName; var session = HeadlessUnitTestSession.StartNew(typeof(ScreenshotEntry)); await session.Dispatch(async () => { - var saved = AppSettings.Read(); var run = await App.Services.GetRequiredService().Run((_, _) => { }, CancellationToken.None); 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") }) @@ -48,9 +49,9 @@ await session.Dispatch(async () => }); } } - AppSettings.Write(saved); return 0; }, CancellationToken.None); +data.Delete(true); Environment.Exit(0); void Shoot(int width, int height, string name, Action prepare, Action arrange = null)