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

212 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
## 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:
```powershell
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
- [x] 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
- [x] 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`
- [x] Branch `net10-port` created and pushed with upstream (`origin/net10-port` exists in `raven`)
- [x] **First commit: version bump** in every place listed in `devdocs/build/BUILD.md` → "Version numbers" (`c2f2f922`, 8.3.0)
- [x] 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)
- [x] **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
- [x] **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
- [x] `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
- [x] `raven-launcher`: `net8.0` → `net10.0` (no package references). `raven-launcher` `d2079c9`
- [x] **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`)
- [x] **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.**
- [x] `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
- [x] **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)
- [x] **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`)
- [x] **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
- [x] **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
- [x] **SMTP.** n/a: `util/Mailer.cs` already uses MailKit's `SmtpClient` (`:1`, `:84`); no `System.Net.Mail`
- [x] **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
- [x] `BrowserFetcher.DefaultChromiumRevision`: n/a, already `DownloadAsync()` with no argument (`ReportBiz.cs:800`, case 4586)
- [x] `IJSHandle.ExecutionContext`: n/a, the only use is commented out (`ReportBiz.cs:858-860`). Evaluate results are `JsonElement?`: check any remaining `Evaluate*` call sites
- 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
- [x] 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
- [x] 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
- [x] 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`
- [x] **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
- [x] **BinaryFormatter.** n/a: not used anywhere in `server/AyaNova`
- [x] **`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`.** 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
- [x] **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`)
- [x] **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
- [x] **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.
- [x] **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
- [x] **`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`
- [x] **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
- [x] **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.** 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.** 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.** 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.** 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.** `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. 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
- [ ] 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
## 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). How `aspnetcore-runtime-10.0` is obtained on 24.04 needs checking **[unverified]**. On 26.04 it is in Ubuntu's own archive
- Self-hosted Linux customers (server and desktop zips are framework-dependent)
- Windows LAN: `lan.iss` prompts for the ASP.NET Core Hosting Bundle. Update its text and link to .NET 10
- Windows standalone and the launcher are self-contained: no runtime needed on the customer's machine
- [ ] Customer manual (`docs/`): system requirements and upgrade instructions for self-hosted customers
- [ ] Build VM: install the .NET 10 SDK; update `devdocs/build/DEV-VM-SETUP.md` and `devdocs/build/BUILD.md`
- [ ] Final gate runs on the finished code
- [ ] Merge, per repo:
```powershell
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
- 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
- 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
- `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
- **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 |