Files
raven/devdocs/modernization/NET10-PORT.md

59 KiB
Raw Blame History

AyaNova 8 back end → .NET 10

The plan and running record for the port. Tick items and add a log line in the same commit as the change they record. Anything found along the way that is out of scope goes under "Deferred", not into the branch.

Starting point AyaNova 8.2.4: ASP.NET Core 8, EF Core 8
Target net10.0, every back-end package at its current release
Front end Minimal or no change. Rebuild only if needed (open question)
Branch net10-port in raven (and raven-client and raven-test-integration and raven-launcher)
Baseline tag net8-baseline, on the last commit before the port
Release version 8.3.0
Release timing Tentatively within two weeks of 2026-10-23, after John's move of Sockeye and the ayanova.com business server to a new droplet (weekend of 2026-10-24) has settled. The case database's "next release" cases (including the SuperUser delete bug) must be done first. After 8.3.0: a security review of the whole codebase and an MCP interface for AyaNova 8

Where this plan comes from

Sockeye, the internal licensing server, is a near-copy of this codebase. It was ported from .NET 6 to .NET 10 in September–October 2026 and released as 8.1.0. This plan is that port's carry-forward list (Appendix A of the Sockeye migration runbook), arranged as a checklist.

AyaNova starts from .NET 8, not .NET 6. So every item is "check whether this applies", not "this will apply". Several items will turn out to be already done or not present. Mark those n/a with a one-line reason rather than deleting them.

When an item is unclear, look at how Sockeye did it:

git -C ..\sockeye log --oneline net6-baseline..8.1.0
git -C ..\sockeye show <commit>

Where AyaNova differs from Sockeye: customers choose their own database role names and passwords, every customer uses reports, and AyaNova verifies licences rather than issuing them. Items touching those three areas carry more weight here than they did in Sockeye.

Scope

  • In: target framework; all NuGet packages in server/AyaNova; code changes those require; the Sockeye-found fixes in section 3, each as its own commit.
  • Out: front-end dependency changes; refactoring (Roadmap step D); new features; PostgreSQL major upgrades.
  • Undecided: see "Open questions".

Working method

  • Version bump first. One concern per commit.
  • Prove each gate on the unmodified code before changing anything, so a later failure points at the port, not at the test.
  • Packages that must move together: framework-matched Microsoft.* packages all go to 10.x in one step. (The BouncyCastle/MimeKit clash from Sockeye does not arise here: AyaNova is already on BouncyCastle.Cryptography 2.x with MailKit/MimeKit 4.x.)
  • Publish server\AyaNova\AyaNova.csproj directly, never the solution, so no test project can reach the artifact.

0. Before changing anything

  • Record any port work already done in earlier sessions (branch, commits) here first, and reconcile it with this list. Done: branch net10-port and the 8.3.0 version bump (c2f2f922); no code changes yet
  • raven and raven-client clean on main and pushed
  • Annotated tag net8-baseline on the pre-port commit in raven and raven-client. Present locally in both; confirm on the remote with git ls-remote --tags origin
  • Branch net10-port created and pushed with upstream (origin/net10-port exists in raven)
  • First commit: version bump in every place listed in devdocs/build/BUILD.md → "Version numbers" (c2f2f922, 8.3.0)
  • Dev machine has the .NET 10 SDK and keeps the .NET 8 runtime for baseline comparisons. SDK 10.0.103 (no 8 SDK; the 10 SDK builds net8.0); runtimes 8.0.24 and 10.0.3

Gates (prove on the unmodified code)

  • Integration suite baseline. Run raven-test-integration against the unmodified build and record the counts in the log below. Expected: everything passes except the three known server-side failures listed in that repo's CLAUDE.md. Result: 103 tests, 96 passed, 2 failed (ContractCrud.CRUD, CustomerWithWorkOrders_CannotBeDeleted), 5 skipped (including DirectMessage_SendAndDelete). Needs a freshly generated test database: a reused one fails 32 tests at the superuser login
  • Licence verification gate. Write a test that runs every v8 row of Sockeye's alicense fixture through AyaNova's own verification routine. The fixture is customer data: it stays outside the repo, at C:\data\sockeye-fixtures\resign.json (a fixed path, as in Sockeye's sockeye.tests; the environment variable was dropped there). Must be green on unmodified code. A regression here would reject licences customers already have installed. Done: ayanova.tests\LicenceGateTests.cs (not in raven.sln; run with dotnet test ayanova.tests). It calls the production License.Parse by reflection for each of the 726 v8 rows, and checks each outcome against what the key itself requires on a perpetual build (verify and parse, or rejection as not licensed for this build). Plus a tamper test, a build-timestamp sanity check and an MExBB check. Set AYANOVA_LICENCE_GATE_OUT to write per-row outcomes (ids only) for diffing two builds. How AyaNova verifies:
    • Library: BouncyCastle (util/License.cs:15-17). PemReader reads a SubjectPublicKeyInfo PEM (BEGIN PUBLIC KEY, not PKCS#1) into an AsymmetricKeyParameter; SignerUtilities.GetSigner("SHA256WITHRSA") verifies (License.cs:968-978)
    • Public key: hardcoded RSA-2048 PEM, "our real 2016 public key", License.cs:957-965
    • Signed payload: the JSON between [KEY and KEY], with whitespace outside strings stripped by regex (License.cs:950); base64 signature between [SIGNATURE and SIGNATURE] (:951)
    • Other checks in Parse (License.cs:926-1062): LicenseFormat == "8"; DBID equals the server's schema id; perpetual key only on a perpetual build and subscription key only on a subscription build; MExBB, which rejects a perpetual non-trial key whose maintenance expiry is before the build date, read from the PE linker timestamp (util/FileUtil.cs:861-882). That timestamp is only a real date because <Deterministic>False</Deterministic> (AyaNova.csproj:12). Turning determinism on, or getting the CA2022 change at FileUtil.cs:875 wrong, would reject perpetual licences with E1020. The gate must cover MExBB, not only the signature
    • When it runs: at boot, BootSafetyCheck before any schema update (Startup.cs:603), then InitializeAsync (Startup.cs:633); after every fetch, FetchKeyAsync → InstallAsync → InitializeAsync, either manually (LicenseController.cs:125) or from CoreJobLicense (generator/CoreJobLicense.cs:80). Fetch interval (CoreJobLicense.cs:21-34): perpetual build 30 min while unlicensed, trial or expired, otherwise 24 h; subscription build 30 min; DEBUG 1 or 5 min. The installed key is not re-verified on a schedule; expiry is checked in CoreJobSweeper, according to the comment at CoreJobLicense.cs:10
    • Test access: Parse is private static and depends on the static ServerDbId and on the running assembly's timestamp. The test needs InternalsVisibleTo or reflection
  • Report baseline. Render the advanced reports on the unmodified build and keep the PDFs for comparison. Sockeye shares only one report with AyaNova, so Sockeye's testing says little about AyaNova's reports. John's decision (2026-10-22): no recorded baseline; John verifies the reports visually himself (see section 4)
  • E2E baseline. Cypress run (raven-test\e2e) against a devtest server on the unmodified build. Skipped for now (2026-10-22): not practical at this point. Run later as a smoke test on the .NET 10 build (section 4)

1. Framework and packages

  • net8.0 → net10.0, with every framework-matched Microsoft.* package to 10.x in the same step (AyaNova.csproj:3). Done for the server: JwtBearer, Mvc.NewtonsoftJson, EntityFrameworkCore, ApiDescription.Server, HealthChecks.EntityFrameworkCore → 10.0.13; Npgsql.EntityFrameworkCore.PostgreSQL → 10.0.3 (required by EF 10); .vscode/launch.json program paths → net10.0. Builds with no new code changes. raven-launcher.csproj:5 still to do
  • raven-launcher: net8.0 → net10.0 (no package references). raven-launcher d2079c9
  • Hosting model. n/a: Program.cs:330-351 already uses Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(...) with UseStartup<Startup>. Only a commented-out BuildWebHost line remains (:308)
  • Asp.Versioning. Done: 8.1.0 → 10.2.1 (as Sockeye), explicit .AddMvc() added between AddApiVersioning and AddApiExplorer. New analyzer suggestions AV0015 (use UrlSegmentApiVersionReader only) and AV0016 (AssumeDefaultVersionWhenUnspecified) deliberately not acted on: both would change which requests deployed clients can make. Was already: Asp.Versioning.Mvc and .ApiExplorer 8.1.0 (AyaNova.csproj:36-37), wired at Startup.cs:200-217 as AddApiVersioning(...).AddApiExplorer(...). Remaining: bump to 10.x and confirm AddApiExplorer still registers MVC versioning without an explicit .AddMvc(). Leave AV0014 warnings: do not add [ApiController]
  • Npgsql / EF Core 10.
    • Npgsql.EnableLegacyTimestampBehavior: n/a, commented out (Program.cs:26)
    • ConfigureWarnings(w => w.Throw()) is present (Startup.cs:179). Every new EF warning becomes an exception naming its ID: fix the query, or move that ID to .Log(...)
    • Exercise LINQ-based queries screen by screen. Data-list filters build raw SQL and are unaffected
  • BouncyCastle. Already BouncyCastle.Cryptography 2.3.1 (AyaNova.csproj:28) with MailKit/MimeKit 4.5.0; no Portable.BouncyCastle in the graph. Version bump only: now 2.7.0, moved together with MailKit 4.18.1, as in Sockeye. The Sockeye note about PemReader and PKCS#1 key pairs does not apply: AyaNova reads a public key (see the licence gate)
  • Swashbuckle 10 / OpenAPI.NET 2. Done: all six Swashbuckle.AspNetCore* 6.5.0 → 10.3.1 (Sockeye: 10.2.3), explicit Microsoft.OpenApi reference removed (now 2.7.5 transitively), and the three code changes below. Applies. Microsoft.OpenApi.Models merged into Microsoft.OpenApi: using at Startup.cs:15 and ConfigureSwaggerOptions.cs:6. Bearer requirement (Startup.cs:235-244) becomes c.AddSecurityRequirement(document => new OpenApiSecurityRequirement { [new OpenApiSecuritySchemeReference("Bearer", document)] = [] }). Remove the explicit Microsoft.OpenApi 1.6.14 reference (AyaNova.csproj:47)
  • NLog 6. Done: NLog and NLog.Web.AspNetCore 6.2.2; ArchiveNumbering removed; ArchiveFileName kept and ArchiveSuffixFormat = "-{0}" added (Program.cs); log viewer sorts archives by file date, newest first. Tested in a scratch program and on the server, against a folder seeded with NLog 5 archives -0 to -8: without ArchiveFileName, NLog 6 writes straight into numbered files (no log-ayanova.txt) and appends to the old -8; with it but no suffix format, archives become log-ayanova_NN.txt (NLog 6 ignores {#}) and the old -0 to -8 are never deleted. Sockeye 8.1.0 ships that second form. With both settings: log-ayanova.txt stays current, archives are log-ayanova-N.txt as the manual (docs/.../ops-log.md) says, numbering continues after the old files, and the old files are cleaned up. NLogBuilder.ConfigureNLog is already replaced (Program.cs:206, LogManager.Setup().LoadConfiguration). ArchiveNumbering = Rolling (Program.cs:136) is gone in 6. Archive numbers keep incrementing and the un-numbered file is always current. The log viewer sorts by file name as a string (LogFilesController.cs:120, OrderByDescending(z => z)), so -10 sorts after -2. Fix the sort in the same commit
  • Newtonsoft.Json. Mvc.NewtonsoftJson 10.x sets a 13.0.3 floor (NU1605). Already resolved 13.0.3 transitively; now a direct reference at 13.0.4 (current; Sockeye pins 13.0.3)
  • JwtBearer 10.x. The IdentityModel part is n/a: it resolves 7.1.2, not the vulnerable 6.10.0
  • SMTP. n/a: util/Mailer.cs already uses MailKit's SmtpClient (:1, :84); no System.Net.Mail
  • PuppeteerSharp 17 → 25.x. Reporting is AyaNova's main Puppeteer surface (biz/ReportBiz.cs). Done: 25.15.0 (Sockeye: 25.12.0); compiles with no API changes; launch arguments fixed (below). Downloads Chrome for Testing 155 (was 124). The download location and failure handling are separate section 3 items
    • BrowserFetcher.DefaultChromiumRevision: n/a, already DownloadAsync() with no argument (ReportBiz.cs:800, case 4586)
    • IJSHandle.ExecutionContext: n/a, the only use is commented out (ReportBiz.cs:858-860). Evaluate results are JsonElement?: check any remaining Evaluate* call sites
    • Launch chrome-headless-shell, not full Chrome (found on the Ubuntu 24.04 droplet). Full Chrome 155 would not start under the www-data service: chrome_crashpad_handler: --database is required, because Chrome now insists on writing config and crash-reporter files under the user's home (/var/www, not writable); Chrome 124 in 8.2.4 did not. Measured on the 1 GB droplet printing a report-sized page, 5 runs each: full Chrome (with writable folders) 4.1–4.9 s and about 1.6 GB summed RSS per render, headless shell 1.0–1.5 s and about 620 MB, PDFs within 0.2% in size. A browser starts for every render, so the shell is chosen on merit (John's decision). Also XDG_CONFIG_HOME/XDG_CACHE_HOME are pointed at report-browser/browser-home (temp folder when no data path) so a full Chrome given in AYANOVA_REPORT_RENDER_BROWSER_PATH starts too
    • The default download is Chrome for Testing, into Chrome/ and ChromeHeadlessShell/ beside the app. AyaNova 8.2.0 already moved from Chromium to Chrome to follow Puppeteer's default, so the browser choice is settled
    • Launch arguments must be separate array elements. Fixed: defaults are new[] { "--no-sandbox" } (--headless dropped, LaunchOptions.Headless already adds it, as in Sockeye); the customer setting is split on whitespace. A customer value containing a quoted argument with spaces would now be split; none is known. Browser default locale: the old --headless argument ran Chrome's old headless mode, which takes its default language from the OS (en-CA → Chrome's en-GB, day-first); current Chrome headless always defaults to en-US. Report scripts that format without a locale (stock sample report 80 calls toLocaleDateString() bare) switched from 22/10/2026 to 10/22/2026. Fixed by adding --lang=<server CultureInfo.CurrentCulture> unless the customer setting already has a --lang; skipped when the server culture is invariant. Stock helpers in ay-report.js pass the client's language explicitly and were never affected. Both paths were wrong: the defaults are one element "--headless --no-sandbox " (ReportBiz.cs:812, :829), and AYANOVA_REPORT_RENDER_BROWSER_PARAMS is passed whole as one element (:819-822)
    • AppArmor restricts unprivileged user namespaces on Ubuntu 24.04+, which affects the Chrome sandbox
  • Remaining packages to current releases. Done: Bogus 35.5.1 → 35.6.5 (evaluation seeder), jose-jwt 5.0.0 → 5.3.0 (login tokens), QRCoder 1.5.1 → 1.8.1 (2FA setup QR). dotnet list package --outdated: nothing; --vulnerable --include-transitive: none. MailKit 4.5.0 carried a moderate advisory (NU1902); on net10.0 the audit also reported high-severity NU1903 for MimeKit 4.5.0 and transitive System.Security.Cryptography.Pkcs 8.0.0. Mail done: MailKit/MimeKit 4.18.1, BouncyCastle 2.7.0, and System.Security.Cryptography.Pkcs 10.0.13 pinned directly (MimeKit only requires 10.0.0, which has advisories). dotnet list package --vulnerable --include-transitive: none. After the upgrade, check what still pulls in System.Text.Json 5.0.2
  • Warnings: CA2022 done with ReadAtLeast(bytes, bytes.Length, throwOnEndOfStream: false) rather than ReadExactly, which would throw on a short file on the licence path. ASP0019: n/a. It flags Headers.Add and recommends Append or the indexer; Startup.cs already uses Append and no ASP0019 warning is reported (the line below had it backwards). Was: CA2022 → ReadExactly at FileUtil.cs:875 (licence build-date path: covered by the licence gate); ASP0019 → header indexer instead of Headers.Append at Startup.cs:514
  • MiniProfiler. Present, not absent. Removed (John's decision, as in Sockeye 6dd2b09): both packages, AddMemoryCache/AddMiniProfiler/UseMiniProfiler, the /profiler/results download-token workaround in the auth middleware, the unused using in ReportBiz.cs, three commented-out using lines, and the credit in docs/.../appendix-os-credits.md. Client: ops-profile.vue and its route remain, but the menu entry was already commented out (initialize.js:785) and the page was retired after the beta (changelog.md:519); John may remove them. Was: MiniProfiler.AspNetCore.Mvc and .EntityFrameworkCore 4.3.8 (AyaNova.csproj:48-49); AddMiniProfiler(...).AddEntityFramework() (Startup.cs:62-74); auth workaround for /profiler/results (Startup.cs:393-441); UseMiniProfiler() (Startup.cs:530); using StackExchange.Profiling.Internal (ReportBiz.cs:14). Nothing in raven-client. Keep and upgrade, or remove: see Open questions
  • BinaryFormatter. n/a: not used anywhere in server/AyaNova
  • System.GC.Server. n/a: not set, so the Web SDK default (true) stays. On one vCPU the runtime uses workstation GC anyway, and DATAS keeps server GC lean since .NET 9
  • InvariantGlobalization. Decision (John, 2026-10-22): leave it off. The server formats with the OS culture (the report browser --lang depends on it), and turning it on would change date and number handling. Was: Not set. Decide on EF Core string comparison and date-parsing grounds. Note DataListSqlFilterCriteriaBuilder.cs:835 reads CurrentCulture.NumberFormat. Still open in Sockeye too

2. Configuration behaviour to check

  • URL binding precedence. n/a: UseUrls(ServerBootConfig.AYANOVA_USE_URLS) (Program.cs:340), read from config.json, environment and command line (Program.cs:38, ServerBootConfig.cs:167-170). The documented AYANOVA_USE_URLS is the one that wins over ASPNETCORE_URLS; a command-line --urls still overrides it through UseConfiguration (Program.cs:341)
  • Logging providers. appsettings.json Debug and Console levels Trace → Warning. appsettings.Development.json left: only read when ASPNETCORE_ENVIRONMENT=Development, which nothing sets. No journal flood: Program.cs:348 calls ClearProviders() before UseNLog(). appsettings.json:4-13 still sets the Debug and Console providers to Trace, with no effect today. Fix the levels in the repo
  • Data Protection keys. n/a: nothing uses Data Protection; authentication is JWT only (Startup.cs:278-302)

3. Fixes found through Sockeye (each its own commit)

These are bugs, not port mechanics, so each is a separate commit that can be reverted alone. Sockeye included its equivalents in the port branch. John decides per item whether they go in this branch or a follow-up release. UPDATE: John has decided these all go in this branch and some more additional cases in the case database that are urgent, exact cases TBD.

  • Case 4676: data-list SQL injection — priority. Confirmed: DataListSqlFilterCriteriaBuilder appended the raw filter value for Roles and for every numeric type (Enum, MemorySize, Decimal, Currency, InternalId, Integer). Text and tag values escape '; TimeSpan goes through int.Parse. Any authenticated user who can open a data list could reach this. Sockeye's fix (3427cd8) is incomplete: it checks only Decimal/Currency and Integer, and leaves InternalId, Enum, MemorySize and Roles open (still open in Sockeye 8.1.0). AyaNova's fix parses every numeric type as an invariant decimal (Decimal, Currency) or long (the rest), and Roles as a long, then appends the re-formatted number. An invalid value throws ArgumentException, which the API returns as an error before any SQL runs
  • DbUtil.AdminConnectionString. Fixed with the builder below (Sockeye 8.1.0 still has the Replace). Confirmed: DbUtil.cs:99 is _dbConnectionString.Replace(_dbName, "postgres"), used by the boot check (:192), create (:286) and drop/recreate (:334). Any customer whose role name, password or host contains the database name as text gets a mangled admin connection, and the boot check authenticates as the wrong user. Fix: new NpgsqlConnectionStringBuilder(cs) { Database = "postgres" }.ConnectionString
  • Backup pg_dump call. Fixed (Sockeye 8.1.0 still has the old call): new RunProgram.RunNoShell (no shell, ArgumentList, per-process environment, both streams read together, timeout kills the process tree, non-zero exit code returned as an error); backup passes --host/--port/--username/--dbname/--format=custom/--file/--no-password with the password in PGPASSWORD. The old RunProgram.Run now has no callers (see Deferred). Confirmed: CoreJobBackup.cs:68 builds postgresql://user:password@host:port/db with the raw password, and :94 adds -Fc > file; RunProgram runs it through cmd.exe /C (RunProgram.cs:55) or /bin/sh -c (:82-89). A password with spaces or @ : / ? # % breaks backups. Also, on Linux stderr is not redirected (RunProgram.cs:86-92), so a failed pg_dump returns empty output and is reported as a successful backup (CoreJobBackup.cs:97). Fix: password in PGPASSWORD in the process environment, connection parameters as separate arguments through ProcessStartInfo.ArgumentList (no shell), output with -f, stderr and exit code checked
  • Drop-and-recreate database. Fixed: before the DROP, DropAndRecreateDbAsync checks rolcreatedb OR rolsuper for the current user; without it, boot stops with an E1000 message naming the user and the setting, and the database is untouched. Confirmed: DbUtil.cs:342-345 drops, then creates; triggered by AYANOVA_PERMANENTLY_ERASE_DATABASE (Startup.cs:567-570). Without CREATEDB it drops the database and then fails to create it. Test the permission before dropping, or change the order
  • Report browser download location. Fixed: downloads into <AYANOVA_DATA_PATH>/report-browser (derived ServerBootConfig.REPORT_RENDER_BROWSER_DOWNLOAD_PATH, not a new setting; empty when no data path is set, then beside the app as before) through BrowserFetcherOptions.Path, and launches the downloaded executable explicitly. AYANOVA_REPORT_RENDER_BROWSER_PATH still takes precedence. Existing installs keep an unused old download beside the app until it is removed. Confirmed: new BrowserFetcher() with the default path beside the app (ReportBiz.cs:800). Point it under the data directory in code. Beside the app it is read-only under a hardened systemd unit (ProtectSystem=strict), and a redeploy that replaces the app folder deletes it
  • Report failures: return a specific error. Done: download and launch each in their own try; on failure HandleBrowserFailure logs at Error (was Debug via the job processor) and writes rendererror.exception ("Report rendering browser could not be downloaded to … / started from … with arguments …: (reason)"), which the client already shows as JobFailed: …; no client change. The Sockeye crash is n/a: rendering runs as a job, and JobsBiz.ProcessJobAsync catches every exception, logs it through NLog and marks the job Failed (JobsBiz.cs:390-439), so the process survives. What remains: a browser download or launch failure (ReportBiz.cs:800, :838, outside the render try) reaches the client only as a generic job failure. Catch it in DoRenderJob and return a render error that says what failed
  • Data-list date filters, small fixes. Fixed: >= and < use ZeroSeconds; != is < ZeroSeconds OR > MaxSeconds; the DataListField null check runs first and in all builds, throwing ArgumentException naming the column. (An unknown column sent to /data-list is in practice rejected earlier by DataListSqlSelectBuilder, with its own "DEV ERROR" message; unchanged.) Sockeye 8.1.0 still has the old code. Confirmed: >= (:790-793) and < (:795-798) use MaxSeconds and should use ZeroSeconds; date NotEqual (:806-814) uses MaxSeconds on both sides and should exclude the whole minute (< ZeroSeconds OR > MaxSeconds). DataListField is dereferenced at :24 and :27 before the DEBUG-only null check at :33, so release builds throw a NullReferenceException on an unknown column, not only DEBUG builds
  • Relative date tokens use the server's time zone (*today*, *yesterday* …). Confirmed: ClientToday = clientTimeStamp.Date (:399-400) has Kind=Unspecified, and ToUniversalTime() (:1060, :1065) converts it with the server's zone. Wrong for any user in a different zone from the server. The fix builds the range from clientTimeStamp.Offset, with care for ranges that cross a DST change. This changes behaviour and affects the integration tests' TIME_ZONE_ADJUSTMENT. Probably better as a follow-up release

Not port items, listed so they are not forgotten: the three server bugs the integration suite is waiting on (case 4653, case 4654, the contract issue).

4. Test under realistic conditions

  • Database role. Done on the Ubuntu 24.04 droplet (2026-10-23): role "AyaNova", no superuser, no CREATEDB, password p @:/?#% x; boot, on-demand backup (818 entries), 22/22 reports and the integration suite all pass. Scheduled backup uses the same code path as on-demand. Run under a non-superuser role named like the database, with a password containing spaces and @ : / ? # %. Boot, on-demand backup, scheduled backup, restore. Sockeye ran as postgres for years and never exercised either bug in section 3
  • Restore with a dedicated role. Done on the droplet: createdb --owner=AyaNova, pg_restore --no-owner --no-privileges --role=AyaNova; every table owned by the role, integrity check and licence pass at boot. A plain superuser restore (dropdb/createdb/pg_restore as postgres) of an 8.2.4 dump into 8.3.0 also works. Manual comparison still to do. createdb -O <role>, then pg_restore --no-owner --no-privileges --role=<role>. Without --role, a superuser restore leaves every table owned by postgres, and the integrity check, which counts through information_schema, fails. Compare with the restore instructions in the customer manual (docs/) and note any change needed
  • Reports, all advanced reports, compared with the baseline PDFs. Done: Windows (page counts = PuppeteerSharp 17), Ubuntu 24.04 and 26.04 droplets, 22/22 each; John checked the PDFs visually; ldd clean on both Ubuntu versions; no orphaned browser processes after a cancelled render. Was: On Windows and on Linux. On Linux also: ldd on the downloaded browser binary for missing libraries (install exactly what is reported; several Ubuntu 24.04+ names carry a t64 suffix); the sandbox under the service account; no orphaned chrome processes after a cancelled render
  • Integration suite: same result as the baseline. Locally on every step; on both droplets with UserWithActivityShouldNotBeDeleteable excluded (it deletes SuperUser when SuperUser has no related records, see Deferred)
  • Licence verification gate: green
  • Cypress E2E (raven-test\e2e) against a devtest server running the .NET 10 build
  • build-release.bat produces all five packages; install and start each one: Windows standalone, Windows LAN, Linux server, Linux desktop, subscription Linux. All five built (2026-10-23, from 4a011f65+). Linux done on droplet devtestn10 (Ubuntu 24.04): subscription zip as an in-place upgrade of 8.2.4, perpetual server and desktop zips booted fresh (see log). Windows standalone done in Windows Sandbox (upgrade 8.2.4 → 8.3.0, uninstall, fresh install; see log); fresh install needed the standalone.iss PostgreSQL folder fix. Windows LAN still to do

5. Release

  • Every host that runs a framework-dependent build needs the .NET 10 runtime before it gets the release:
    • Hosted subscription droplets (Ubuntu 24.04). Confirmed on the test droplet: apt-get install -y aspnetcore-runtime-10.0 gets 10.0.13 from Ubuntu's own archive (noble-updates/main, noble-security/main), no PPA. Installing it replaces dotnet-host-8.0 with dotnet-host-10.0 (the shared dotnet command); the 8.0 runtime stays installed and unused. Tested upgrade 8.2.4 → 8.3.0 in place: stop service, install runtime, the manual's file swap, start; boots and keeps the licence. 26.04: to be confirmed on the same droplet rebuilt
    • Subscription server setup script (sockeye-client/src/views/biz-subscription-server.vue, around lines 1350-1423; filled in per server and copied to the clipboard from the "Script" menu, used for every new subscription droplet): apt-get install -y aspnetcore-runtime-8.0 (line 1377) → the .NET 10 runtime from whichever source the test droplet confirms; remove the mkdir/chmod g+rwx of /var/ayanova/Chrome and ChromeHeadlessShell (lines 1396-1397, 1402-1403), the browser now downloads to /var/ayanova/data/report-browser. Verify by running the updated script on the test droplet. Part of this port (John, 2026-10-22). Runtime and Chrome-folder change committed in sockeye-client main (2026-10-23); do not deploy a Sockeye client built from it before 8.3.0 is released (the script installs the released zip; 8.2.4 needs .NET 8). 26.04 support and hardening committed in sockeye-client main (64d8f62, 2026-10-23), tested end to end on a fresh 26.04.1 droplet: PostgreSQL via postgresql-common's apt.postgresql.org.sh (apt-key is gone in newer Ubuntu); libgdk-pixbuf2.0-0 → libgdk-pixbuf-2.0-0 (the only library name missing on 26.04); unique random PostgreSQL password and JWT secret per server, written to PostgreSQL, config.json and /root/ayanova-secrets.txt (600); config.json 640 root:www-data; John's 52no-phasing apt setting
    • Self-hosted Linux customers (server and desktop zips are framework-dependent)
    • Windows LAN: lan.iss prompts for the ASP.NET Core Hosting Bundle. Text and link updated to .NET 10 (2026-10-23); installer test still in section 4
    • Windows standalone and the launcher are self-contained: no runtime needed on the customer's machine. But they carry the .NET runtime of the build machine's SDK: SDK 10.0.103 → runtime 10.0.3 (the Sandbox test logged .Net Version - 10.0.3; Linux hosts run 10.0.13). Before the final release build, update the workstation's .NET 10 SDK to the newest release so the standalone package ships the current runtime with its security fixes. 8.2.4 did the same with .NET 8.0.13. Done 2026-10-23: SDK 10.0.402 installed with winget (winget upgrade did not find the Visual Studio-installed 10.0.103); test publish of the standalone server and the launcher bundles runtime 10.0.13. Standing note added to devdocs/build/BUILD.md
  • Customer manual (docs/): system requirements and upgrade instructions for self-hosted customers. Report browser move (section 3): manual already updated in the same commit (ops-config-report-render-browser-path.md, ops-install-linux-server.md, ops-upgrade-linux-server.md, ops-upgrade-linux-desktop.md). .NET 10 runtime install and upgrade steps (Windows LAN and IIS, Linux server and desktop), Ubuntu 24.04/26.04, backup password note and the 8.3.0 changelog draft: 111d7922 (2026-10-23). Changelog wording to be finalised by John before release
  • Build machines: the workstation has the .NET 10 SDK (10.0.402, plus 10.0.103 from Visual Studio). The backup laptop needs it before it can build 8.3.0 (winget install --id Microsoft.DotNet.SDK.10 -e --source winget; the .NET 8 SDK is no longer needed); tracked with the laptop's other catch-up items in devdocs/build/BUILD.md. devdocs/build/DEV-VM-SETUP.md and devdocs/build/BUILD.md updated (2026-10-23). The build VM is deprecated (John, 2026-10-23): n/a
  • Final gate runs on the finished code
  • Merge, per repo:
    git switch net10-port; git status; git push; git fetch origin
    git switch main; git pull
    git log --oneline net10-port..main          # must print nothing: main untouched
    git merge --no-ff net10-port -m "Merge net10-port: .NET 10 port"
    git diff net10-port main                    # must print nothing: main is exactly what was tested
    git push origin main
    git tag -a <version> -m "<release note>"
    git push origin <version>
    git ls-remote --tags origin
    
    --no-ff keeps a merge commit, so the whole port can be backed out with git revert -m 1 <merge>
  • Rebuild the client from main, publish, and test the published artifact itself
  • Rollout: own test droplet first, then customer droplets by time zone, as in the weekly routine
  • After release: delete net10-port locally and on the remote

Open questions

  • Release version number (Sockeye's port was released as 8.1.0) Answer: 8.3.0 will be the version
  • raven-launcher (.NET 8, self-contained): move to .NET 10 in the same release? Answer: Yes. It has no package references, so this is the target framework only
  • raven-test-integration: move to net10.0 (separate commit) or leave on net8.0? It only talks HTTP to the server Answer: Move to net10.0 for consistency
  • Does the client need a rebuild in this pass, or can its current output be carried across? Answer: yes it will have minor changes as well as version number to 8.3.0
  • Section 3: which fixes go in this branch, which in a follow-up
  • How the .NET 10 runtime gets onto Ubuntu 24.04 hosts. Today's setup script uses Ubuntu's own archive (aspnetcore-runtime-8.0); to be settled on the test droplet
  • MiniProfiler (section 1): keep it and upgrade to 4.5.x (check that MiniProfiler.EntityFrameworkCore supports EF Core 10), or remove it in its own commit? Answer: remove it, never used

Deferred

Found during the port, out of scope. One line each.

  • License.DoFetchAsync (License.cs:629-661) rethrows at x == 3, so the fifth server (Sockeye) is never tried, and it can return null to a caller that dereferences it
  • Stray raven-client\ayanova\cypress.json: the Cypress suite lives in raven-test\e2e
  • Race, affects production after every restart: FormFieldReference.FormFieldReferenceList (biz/FormFieldReference.cs:57-59) builds a static dictionary lazily with no lock, assigning it before filling it. A concurrent first request sees it half-built and throws FormFieldReferenceList: <type> is not valid. Seen twice on .NET 10, each time on the first form-field use after boot: VendorCrud.CRUD, then FormCustom.FormCustomUpdate. Fix: build the dictionary fully before publishing it (Lazy<T> or a static initialiser). Pre-existing, not a port change. John's decision: its own case, fixed with the other cases going into this release, not under the port
  • raven-launcher Program.cs:98 calls PGProcess.StandardInput.Close() but standard input was never redirected, so it throws InvalidOperationException at shutdown: the "Postgres shut down" step never runs and the launcher exits with an error when the server stops. Postgres was started detached by pg_ctl start, so it probably stays running. Pre-existing; candidate for its own case. Confirmed in the Sandbox test (8.2.4 and 8.3.0): after "Shut down server" the launcher crashes with exactly this exception and 7 postgres processes stay running. Every standalone upgrade then stops at Setup's "files in use" page listing PostgreSQL Server, and "automatically close the applications" fails ("Setup was unable to automatically close all applications"): the customer must end the processes or restart Windows first. One-line fix (stop PostgreSQL with pg_ctl stop instead of writing to its standard input). John, 2026-10-23: own case, fixed with the "next release" cases before 8.3.0
  • Standalone install needs the Visual C++ runtime (pre-existing, same in 8.2.4; John, 2026-10-23: own case, fixed with the "next release" cases before 8.3.0): the bundled PostgreSQL (local-postgres\bin\pg_ctl.exe, postgres.exe) needs VCRUNTIME140.dll, which neither the installer nor local-postgres provides. A Windows without it (a new PC, Windows Sandbox) shows "pg_ctl.exe - System Error: VCRUNTIME140.dll was not found" and AyaNova never starts. Confirmed fix: copying vcruntime140.dll, vcruntime140_1.dll and msvcp140.dll (x64, 14.44) into local-postgres\bin made PostgreSQL start. Ship those three in dist\win-x64\postgres-standalone\bin (app-local, allowed by Microsoft's redistribution terms) rather than running vc_redist.x64.exe (it crashed Windows Sandbox mid-install)
  • Standalone config.json (launcher template) has no AYANOVA_JWT_SECRET, so every boot logs a warning and generates a random one (logins are lost on restart). Pre-existing; noted from the Sandbox test logs. John, 2026-10-23: own case, fixed with the "next release" cases before 8.3.0
  • Standalone and LAN installers share the same Inno Setup AppId ({B2BB79DB-0690-497C-B92B-955F7B468429}) although they are different products (AyaNova vs AyaNova server): installing one over the other would be treated as the same application. Pre-existing; not tested. Both also install to the same folder ({autopf}\ayanova) and use port 7575, so the two flavours could never sit side by side on one machine anyway; the shared AppId only changes what happens when one flavour is installed over the other (e.g. an evaluator moving from standalone to LAN on the same PC): Setup treats it as an upgrade of the same program. Changing either AppId now would make that flavour's next installer miss existing installs (second Add/Remove entry over the same folder). Standalone is thought to be used almost only for evaluation (John). John, 2026-10-23: keep both AppIds as they are; the Windows install pages now say only one type per computer, uninstall the other first (backup and restore to switch)
  • Sockeye 8.1.0 has the NLog 6 archive naming problem fixed here for AyaNova: archives are log-sockeye_NN.txt, old -0 to -8 archives are never deleted, and its log viewer still sorts by name (it also filters on ayanova.txt, not sockeye.txt, so the current file is listed twice). Same fix applies: ArchiveSuffixFormat = "-{0}" and the date sort
  • SuperUser can be deleted (serious, pre-existing, own case; John's decision 2026-10-23: fixed with the other "next release" cases before 8.3.0 ships, not under the port): UserBiz.ValidateCanDelete (biz/UserBiz.cs:1055) only refuses a delete when the user has related records; nothing protects user id 1, although updates to it are blocked (UserBiz.cs:946). On the droplet, whose sample data left SuperUser with no related records, the integration test UserWithActivityShouldNotBeDeleteable (DELETE User/1) succeeded and removed the account; AYANOVA_SET_SUPERUSER_PW cannot bring it back (it only resets an existing user 1). Same code in 8.2.4; likely also why superuser could not log in on the reused local database at the first baseline attempt. Any user with delete rights on Users can do this
  • RunProgram.Run (and its RunWindows/RunLinuxShell/RunOSX helpers) has no callers since the backup moved to RunNoShell. It runs through cmd.exe /C or /bin/sh -c and logs its full command line on error: delete it rather than reuse it
  • Own case after the port (John): automated provisioning from Sockeye. Goal: from a trial subscription server request, one menu item in Sockeye (after John's manual approval) provisions the whole server through the DigitalOcean API, ready for login: droplet, DNS, firewall, setup, licence. Replaces today's manual steps (header comments of the setup script), which are slow and error-prone. Not part of this port
  • Linux desktop manual (ops-install-linux-desktop.md), pre-existing, same in 8.2.4: sudo mkdir ~/.ayanova-data creates the folder in the user's home, but sudo ./AyaNova runs as root, so AYANOVA_DATA_PATH ~/.ayanova-data resolves to /root/.ayanova-data and the user's folder is never used (data, logs and backups end up under /root). Found in the 8.3.0 desktop zip test. Fix the manual (absolute data path, or not running as root) after the release
  • Own case after the port (John): the subscription server setup script is hard-coded in biz-subscription-server.vue, so every change needs a new Sockeye client build. Make it editable without a rebuild
  • Improvement, own case after the port (John to create): bring back logging of JavaScript error details from report page console errors, as Sockeye's port did. AyaNova logs only ERROR: {message text}; the per-argument detail (arg instanceof Error ? arg.message : arg) is commented out at biz/ReportBiz.cs:873-876 since case 4586, because IJSHandle.ExecutionContext was removed. Sockeye 8.1.0 (server/biz/ReportBiz.cs:575) evaluates on the handle instead: args.Message.Args.Select(arg => arg.EvaluateFunctionAsync("(arg) => arg instanceof Error ? arg.message : arg")), which also needs the console handler to be async again

Log

Date Step Result
2026-10-22 Survey of sections 1–3 against the code (read-only) Findings recorded per item: n/a items ticked with reasons, evidence added, MiniProfiler found present
2026-10-22 Integration baseline, eb607d34 (unmodified, .NET 8.0.24, fresh test database) 103 tests: 96 passed, 2 failed (the two known server bugs), 5 skipped
2026-10-22 Case 4676 data-list SQL injection fix (.NET 8) Integration suite same as baseline. API probe: valid Integer, Decimal and Roles filters return rows; 0 OR 1=1, 0); select pg_sleep(3); --, 1)=1 OR (1 and similar are rejected with ArgumentException before any SQL runs
2026-10-22 AdminConnectionString via NpgsqlConnectionStringBuilder (.NET 8) Boot database check passes; integration suite same as baseline. Old vs new on sample strings: old turned role AyaNova and password AyaNova@1 x into postgres, and host ayanova.example.com into postgres.example.com; new changes only Database. The section 4 role test still applies
2026-10-22 Retarget net10.0 + framework-matched Microsoft.* 10.0.13 + Npgsql EF 10.0.3 Builds, 0 errors (warnings: CA2022 FileUtil.cs:875; NU1902/NU1903 MailKit/MimeKit/Pkcs). Boots on .NET 10.0.3, licence accepted. Suite, first run after boot: 95 passed, 3 failed: the two known plus VendorCrud.CRUD, caused by the existing FormFieldReference start-up race (see Deferred); VendorCrud passes alone. Full rerun: 96 passed, 2 failed (known), 5 skipped = baseline. No EF warning-as-error thrown. Licence, report and E2E gates to run next on this build
2026-10-22 Licence gate (ayanova.tests) on 6d6d4d9a (.NET 10) and on unmodified eb607d34 (.NET 8 runtime, temporary worktree) Both 730/730 passed. 726 v8 rows: 389 verify and parse, 337 rejected as not licensed for this build (225 subscription keys, 112 perpetual with maintenance before the build date), 0 signature, format or parse errors. Per-row outcomes identical between the two builds
2026-10-22 Report and E2E baseline gates Not run, by decision: reports to be verified visually by John; E2E deferred to a later smoke test
2026-10-22 raven-launcher → net10.0 (d2079c9) Release-style publish (-r win-x64 --self-contained, trimmed): no warnings, 42 files / 20.3 MB (was 37 / 18.7 MB on .NET 8). Smoke run in a scratch layout: prints version 8.3.0, parses config.json, reaches the pg_ctl.exe start. Installer test still in section 4
2026-10-22 MailKit/MimeKit 4.5.0 → 4.18.1, BouncyCastle 2.3.1 → 2.7.0, Pkcs 10.0.13 pinned Builds (only CA2022 left); no vulnerable packages. Licence gate 730/730, per-row outcomes identical to before. Mail end to end: test-smtp-settings sent through Mailer (connect, AUTH, send) to a local SMTP sink: well-formed text/plain; charset=utf-8 message received. Integration suite = baseline. PDF-attachment path not exercised (report emailing)
2026-10-22 Swashbuckle 6.5.0 → 10.3.1, OpenAPI.NET 1.6 → 2.7.5 Builds (only CA2022). /swagger/v8/swagger.json compared with the pre-change document: same 315 paths, 462 operations (ids, parameters, request bodies, response codes) and 137 schemas; same global Bearer requirement and scheme. Only change: openapi 3.0.1 → 3.0.4. /api-docs UI serves. Integration suite = baseline
2026-10-22 NLog 5.3 → 6.2.2; archive suffix -{0}; log viewer sorted by file date Builds (only CA2022). Server at Trace level, logging to a scratch folder seeded with NLog 5 archives -0 to -8, through suite runs: log-ayanova.txt current, 9 archives log-ayanova-23 to -31, old files removed; GET log-file lists the current file, then -31 down to -23. Integration suite = baseline. In one of six Trace-level runs LogFiles.MostRecentLogShouldFetch most likely failed: a just-rolled current file can hold only TRACE/DEBUG lines (not reproduced; not at Info level)
2026-10-22 PuppeteerSharp 17 → 25.15.0; launch arguments as separate elements Builds (only CA2022). Rendered all 24 work order and customer reports through the API (render-job, poll, download) for records 1 and 2: on PuppeteerSharp 17 (baseline), on 25 with default arguments, and on 25 with AYANOVA_REPORT_RENDER_BROWSER_PARAMS="--no-sandbox --disable-gpu". 24/24 ok each time, page counts identical; PDFs 5–20% smaller with Chrome 155. Integration suite = baseline. PDFs kept in the scratchpad for John's visual check
2026-10-22 John's visual check of the PuppeteerSharp 25 PDFs; --lang fix Identical apart from report 80: date month/day flipped (default locale, see section 1). Probe of Chrome 124/155 with each argument set confirmed the cause. With --lang=<server culture>: John confirmed report 80 shows 22/10/2026 again; 22/22 render, page counts = PuppeteerSharp 17, launch arguments logged as --no-sandbox --lang=en-CA. Integration suite: 95 passed, 3 failed: the two known plus FormCustom.FormCustomUpdate, which the server log shows was the FormFieldReference start-up race again (Vendor is not valid at the first form-field use after boot; see Deferred), not this change. Never render reports 81 (sends real email via the server's SMTP settings) or 86 (writes to the work order) in tests; 81 ran three times on 2026-10-22 and caused bounces
2026-10-22 Asp.Versioning 8.1.0 → 10.2.1, explicit .AddMvc() Builds; new warnings AV0015/AV0016 left (see section 1) besides CA2022. swagger.json identical to the Swashbuckle 10 document (315 paths, 462 operations, 137 schemas, 0 differences). api/v8/…, api/v8.0/… and ?api-version=8.0 return 200 with api-supported-versions: 8.0; api/v9/… returns 404 (8.1 response for v9 not checked). Integration suite = baseline
2026-10-22 MiniProfiler removed Builds (only AV0015/AV0016/CA2022). No StackExchange.Profiling/MiniProfiler/AY_PROFILER left in the server. /profiler/results-index now gets the client app's index.html fallback like any unknown path; unauthenticated API call still 401. Integration suite = baseline
2026-10-22 CA2022: FileUtil.GetLinkerTimestampUtc reads with ReadAtLeast CA2022 gone (only AV0015/AV0016 left). Licence gate 730/730 incl. build-timestamp and MExBB checks; per-row outcomes identical to the first .NET 10 run (389 ok / 337 rejected for this build). Server boots and accepts the (trial) licence
2026-10-22 Bogus 35.6.5, jose-jwt 5.3.0, QRCoder 1.8.1, Newtonsoft.Json 13.0.4 direct; appsettings.json levels; ASP0019 n/a Builds (only AV0015/AV0016). All packages current, none vulnerable. Login JWT still HS256 and accepted on every authenticated call; GET auth/totp returns a valid 183×183 QR PNG and leaves 2FA disabled. Integration suite = baseline. Bogus (evaluation seeder) compiled only: running the seeder replaces the test data
2026-10-22 Backup pg_dump without a shell; password in PGPASSWORD On-demand backups through the API into a scratch folder with a space in its name, checked with pg_restore --list. Old code as postgres: 803 entries. New code as postgres: 803 entries. Failing stand-in pg_dump (exit code 2): reported as BACKUP ERROR, no file, no password in the log. Temporary superuser role "AyaNova" (named like the database) with password p @:/?#% x: old code failed (too many command-line arguments (first is "@:/?#%")), left a 0-byte backup file and echoed part of the password into the log; new code 803 entries, no errors. Role dropped afterwards. Integration suite = baseline. Linux not tested here: the stderr gap there is closed by the same code (no shell, exit code checked)
2026-10-22 Drop-and-recreate: CREATEDB check before the drop Throwaway non-superuser role ayatest_nocreatedb owning throwaway database ayatest_erase, server data path in scratch (the erase also clears the attachments folder). Without CREATEDB: boot stops with the new E1000 message, database still there. Old sequence by hand as the same role: DROP DATABASE succeeds, CREATE DATABASE fails "permission denied", database gone. With CREATEDB: erased and recreated (oid 145595 → 145596), schema 20 built, boot complete. Database and role dropped afterwards. Integration suite not rerun: the change is only reached with the erase setting
2026-10-22 Report browser downloaded under the data path Browsers in bin moved aside; server with an empty scratch data folder. 22/22 reports render; Chrome and ChromeHeadlessShell 155 downloaded into <data>/report-browser, launched from there (logged), nothing written to bin. Second run: first render 1.6 s (was 25 s with the download), no new files: download reused
2026-10-22 Specific error for report browser download / launch failures Download blocked (a file named report-browser in the data folder): job failed with rendererror.exception "…could not be downloaded to "…\report-browser" (check internet access and that the folder is writable): … Cannot create …", Error in the log, server still up. Launch with AYANOVA_REPORT_RENDER_BROWSER_PATH pointing nowhere: "…could not be started from "C:\no-such-folder\chrome.exe" with arguments '--no-sandbox --lang=en-CA' …: Browser was not found…", server still up. Normal: 22/22 reports, page counts = PuppeteerSharp 17; integration suite = baseline
2026-10-22 Data-list date filters >=, <, !=; null check WorkOrderServiceDate through /data-list, 590 work orders. Minute with 6 records off second 0, before: >= 142 (= >), < 448 (= <=), != 590 (all). After: >= 148, < 442, != 584; identities >= = >+K, < = <=−K, != = all−K hold. Same identities hold for a minute with 5 records exactly on second 0. Integration suite = baseline
2026-10-22 raven-test-integration → net10.0 (2c0cfc4), no package changes Builds with no warnings; suite runs as net10.0 against the .NET 10 server: 96 passed, 2 known failures, 5 skipped = baseline. FluentAssertions kept at 5.10.3 (8+ is commercially licensed)
2026-10-23 Ubuntu 24.04.5 test droplet (net10port.onayanova.com, 1 GB): 8.2.4 on .NET 8 set up by John like a customer (subscription trial licence, sample data, PostgreSQL 18.6 from PGDG) aspnetcore-runtime-10.0 10.0.13 from noble-updates/main. In-place upgrade to 8.3.0: boots on .NET 10.0.13, schema 20, licence kept. Reports first failed to launch (full Chrome 155, crashpad needs a writable home; see section 1); fixed by launching chrome-headless-shell (measured 3–4× faster and smaller): 22/22 on Windows (page counts = PuppeteerSharp 17, 1.14 s avg vs 1.8 s) and on the droplet; ldd no missing libraries; cancelled render leaves no browser processes. Integration suite through an SSH tunnel: 28 failures, all from SuperUser being deleted by UserWithActivityShouldNotBeDeleteable (pre-existing bug, see Deferred); after restoring the pre-upgrade dump and excluding that test: 95 passed, 2 known failures, 5 skipped = baseline. On-demand backup 818 entries. Non-superuser role "AyaNova" named like the database with password p @:/?#% x, database restored with --role: boot, backup, 22/22 reports, suite = baseline
2026-10-23 John's PDF check of the headless-shell renders; Linux date format compared with 8.2.4 PDFs identical apart from tiny font-size variations. Browser-default-locale dates (bare toLocaleDateString(), Chrome's <span class="date"> in PDF headers) are en-US on the droplet (10/23/2026, 10/23/26, 7:34 AM) but day-first on Windows (en-CA). Cause: Linux services run with LANG=C.UTF-8 (DigitalOcean default), so .NET sees an invariant culture and no --lang is added. Same reports rendered on John's 8.2.4 devtest server (Ubuntu 24.04, same LANG): 10/23/2026 and 10/23/26, 8:13 AM, so unchanged from 8.2.4. A hosted customer wanting another default can set LANG in the service unit; not a port item
2026-10-23 Fresh Ubuntu 26.04.1 droplet (rebuilt net10port, updated by John), updated setup script with the 8.3.0 build Archive: aspnetcore-runtime-10.0 10.0.13 in resolute/resolute-updates main (no .NET 8 at all, so 8.2.4 cannot run on 26.04); PostgreSQL 18 in Ubuntu's archive but the PGDG repository is kept for one source everywhere; apt-key absent (apt 3.2); one Chrome library renamed. Script exit 0: time zone, swap, unattended upgrades, no-phasing, .NET 10.0.13, PostgreSQL 18.6 (pgdg26.04), unique secrets in place and accepted by PostgreSQL, config.json 640, AyaNova 8.3.0 boots unlicensed on a new database (schema 20), nginx + Let's Encrypt certificate, HTTPS 200. 11 updates deferred by the no-phasing setting, as designed. Section 4 tests on 26.04 pending John's licence and sample data
2026-10-23 Section 4 on the 26.04 droplet (licensed and sample data by John) 22/22 reports (0.9–2.7 s); only ChromeHeadlessShell downloaded; ldd no missing libraries; cancelled render: browser processes gone within about 25 s (cleanup slower than on 24.04, nothing orphaned). On-demand backup 818 entries. Suite excluding the SuperUser-delete test (SuperUser again had no related records): 95 passed, 2 known failures, 5 skipped = baseline. Restricted role "AyaNova" (no superuser, no CREATEDB, password p @:/?#% x), database restored with --role: boot, backup 818 entries, 22/22 reports, suite = baseline; config.json stays 640 root:www-data
2026-10-23 lan.iss Hosting Bundle prompt → .NET 10 Message says 10.x, link opens dotnet/10.0. standalone.iss has no runtime prompt (launcher and server are self-contained). Not yet built into an installer
2026-10-23 lan.iss and standalone.iss: no more {app}\Chrome / {app}\ChromeHeadlessShell folders The report browser downloads to <data>\report-browser (%ProgramData%\ayanova, users-modify in both installers), so the program-folder browser folders were created empty and unused. Uninstall now deletes those folders themselves (was only their contents), clearing what 8.2.x left behind and letting the program folder be removed. Check in the section 4 installer test: fresh install has no Chrome folders; upgrade from 8.2.4 then uninstall removes them
2026-10-23 Build tooling: build-release.bat logs all output to build-output.txt (UTF-16, overwritten each build, git-ignored) as well as the console; sets PYTHONUTF8=1 (mkdocs-material 9.7 banner otherwise crashes MkDocs when output is piped). raven-client vue.config.js: Sass quietDeps Wrapper tested on a dummy script: live console output, stderr captured, Unicode intact. Client build: Sass deprecation warnings 475 → 33 (the rest are Vuetify component styles compiled as the entry stylesheet, which quietDeps cannot tell apart); CSS output byte-identical to the 8.3.0 release build
2026-10-23 Linux packages from build-release.bat on a fresh Ubuntu 24.04.5 droplet (devtestn10): 8.2.4 subscription server set up by John with the deployed setup script, trial licence, sample data, one report rendered on 8.2.4 Subscription zip, the manual's steps 1.5 and 2 with the built zip: .NET 10.0.13 from noble-updates, boots 8.3.0, schema 20, trial licence kept, 0 errors; 22/22 reports (1.5–2.8 s, first 7.5 s with the download); browser in data/report-browser (ChromeHeadlessShell only, 272 MB); old Chrome/ChromeHeadlessShell removed with the manual's command. The E1090 in the log is the 8.2.4 server, restarted by needrestart during apt-get upgrade and cancelled mid-boot by the manual's systemctl stop; not 8.3.0. DataProtection ephemeral-key warnings at boot: same in 8.2.4. Perpetual server zip, fresh folder, own database, port 7576, as www-data: boots unlicensed (E1020), creates the database, 0 errors, login page 200. Desktop zip, the desktop manual's steps as an ordinary user (sudo ./AyaNova): AyaNova apphost runs on .NET 10, boots, creates the database, 0 errors, login page 200; data goes to /root/.ayanova-data (see Deferred). Test databases, folders and user removed afterwards
2026-10-23 standalone.iss: create the 13 empty folders PostgreSQL 14 needs in a new database (pg_notify, pg_tblspc, pg_twophase, pg_stat, pg_stat_tmp, pg_commit_ts, pg_dynshmem, pg_replslot, pg_serial, pg_snapshots, pg_logical\mappings, pg_logical\snapshots, pg_wal\archive_status) Release blocker found in the Sandbox test: fresh standalone installs built on this PC failed, pg_ctl "could not start server", postgres.log "could not open directory pg_notify". dist\win-x64\postgres-empty-database was checked out from Git on 2026-02-19 and Git does not keep empty folders; compared with a reference initdb of the bundled PostgreSQL 14, exactly these 13 were missing. 8.2.4 (built on the old machine) is not affected; upgrades are not affected (existing database kept). Fixed in the installer rather than on disk so a fresh checkout cannot break it again. Fixed installer: fresh install in a clean Sandbox starts PostgreSQL first time, schema 20 created, boot complete
2026-10-23 Windows standalone installer in Windows Sandbox (clean Windows 11, C:\temp\ayanova-sandbox\ayanova-test.wsb, checks by check.ps1): 8.2.4 production installer, trial licence with sample data (John), upgrade with the 8.3.0 build, uninstall, fresh install 8.2.4 install: needed the Visual C++ runtime first (see Deferred; vc_redist.x64.exe crashed the Sandbox, the three DLLs copied into local-postgres\bin worked); report 10 Customer Work Order 1.6 s, Chrome/ChromeHeadlessShell in the program folder. Shutdown: launcher crash at Program.cs:98, 7 postgres left running (see Deferred). Upgrade to 8.3.0: Setup "files in use" (PostgreSQL Server ×7), automatic close failed, installer cancelled itself; after the leftover processes were gone, upgrade went through: 8.3.0 (a223a2bc), net10.0, schema 20, trial licence and data kept, PostgreSQL recovered from the unclean stop; report 10 rendered in 13.9 s with the browser download, browser in %ProgramData%\ayanova\report-browser; runtime 10.0.3 (see section 5). Uninstall (delete data: yes): Chrome/ChromeHeadlessShell removed with the program folder, data folder removed; only the hand-copied runtime DLLs left behind. Fresh install: failed to start PostgreSQL (missing empty folders, fixed in standalone.iss, see the previous row); with the fixed installer: starts first time, schema 20 created, no Chrome folders