diff --git a/CHANGELOG.md b/CHANGELOG.md index 0146bdc..f5cfbd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,19 @@ phase plan these entries follow. ### Added +- **The licences are readable from inside the app, not just present as files in the folder.** + "Licence and notices" on the Advanced page's Application section opens Offstream's own MIT + licence, the predecessor's copyright notice it carries, the bundled LGPL ffmpeg and its source + offer, TagLib#, and VB-CABLE's origin and donationware terms — with the running build's version + and commit at the top, so a bug report can name the build it came from. The text is embedded in + the executable rather than read from beside it: the MIT and LGPL notices are obliged to travel + with the software, and a loose file does not survive a zip unpacked selectively or a copy of just + the `.exe`. A test asserts the window still shows what `LICENSE` and `NOTICE` actually say, so the + two cannot drift. +- **Credit for the track details, beside the provider that supplies them.** Choosing Spotify or + Last.fm now says whose data is being written into the recordings — an attribution Spotify's + Developer Terms require and that the app had never carried anywhere. It is empty when no provider + is selected, because crediting a service the app is not calling would be a false statement. - **A release pipeline, with the git tag as the only place a version number lives.** Pushing `v1.2.3` builds, tests, publishes, signs and attaches a self-contained `win-x64` zip and its SHA-256 to a GitHub release; the changelog becomes the release notes. Nothing in the repository records a diff --git a/docs/MODERNIZATION-PLAN.md b/docs/MODERNIZATION-PLAN.md index 97da836..af05ec3 100644 --- a/docs/MODERNIZATION-PLAN.md +++ b/docs/MODERNIZATION-PLAN.md @@ -841,8 +841,27 @@ error paths and the scope list. - **No update mechanism of any kind in v1** (question 4). Not an updater, and not an in-app version check either: the installer registers the releases page with Windows (`AppUpdatesURL`) and that page is where a new version is announced. An in-app check is a later version's problem, and adding one now would be a network call, a settings toggle and a pair of resource strings spent on something the release notes already say. - **Tag-driven GitHub Actions release pipeline** — `v*` tag → publish → package → attach installer and portable zip to a GitHub release. +**Status (2026-08-15).** Built and green: the tag-driven pipeline, the per-user installer, the pinned ffmpeg with its source archive, the inert signing step, and the in-app notices. **Not done, and not doable from here:** the clean-VM install → record → uninstall pass. It needs a virtual machine and a person at it, and no amount of CI substitutes — the runner installs nothing and uninstalls nothing. Until that pass happens, the installer is *known to compile and known to produce an executable*, which is a weaker claim than the exit criterion makes. + **Exit:** clean-VM install → record → uninstall, no leftovers; third-party notices complete and accurate for whatever actually shipped. *(The original exit criterion said install → record → **update** → uninstall. Question 4 removed the update leg rather than reinterpreting it: there is nothing in the app to exercise. What replaces it is the installer's own upgrade path — installing a newer build over an older one and finding one entry in Apps & Features, not two — which is what a stable `AppId` buys and the only update behaviour v1 has.)* +**Phase 8 findings (2026-08-14 / 15):** + +- **The tag is the only place a version is written down.** `Directory.Build.props` carries a `VersionPrefix` and a `VersionSuffix` of `dev`; a tagged build passes `-p:Version=1.2.3`, which overrides both. The alternative — a `` bumped in lockstep with the tag — is a file that eventually is not bumped, producing a build that claims a version nobody released. Worth knowing about the override: `-p:Version` beats *both* properties, so the `dev` suffix is dropped for tagged builds without a condition anywhere in the pipeline. Verified by building: no arguments gives `0.1.0-dev`, and `-p:Version=1.2.3-rc.1` gives `AssemblyVersion 1.2.3.0` with `InformationalVersion 1.2.3-rc.1+`. +- **Releasing is two steps, and deliberately not one.** The changelog section for the version is closed in an ordinary pull request; only then is the tag pushed. The workflow re-checks that the section exists **before it builds**, so the failure arrives in thirty seconds rather than after a full publish. +- **The release notes originally fell back to `[Unreleased]` when the version's section was missing.** That works exactly once and then republishes the previous release's entries forever. There is no fallback now — a missing section fails the run. +- **`$array -notmatch $pattern` is not `-not ($array -match $pattern)`.** It returns the elements that failed to match, which on any real changelog is a long, non-empty, entirely truthy array. The changelog check was written that way and would have thrown on every tag, including correct ones. Caught by extracting the script and running it both ways before merging, which is the only reason it was caught at all. +- **Only `ffmpeg.exe` is bundled, not `ffprobe.exe`.** `FFmpegLocator` resolves a probe path and one test asserts it; nothing at runtime ever runs it. Shipping just the encoder saves 108 MB. The build is pinned by digest in `build/windows/ffmpeg.json`, and `fetch-ffmpeg.ps1` **throws** on a fresh download whose digest does not match rather than updating the pin — the message says so, because the tempting fix is the wrong one. +- **The pinned build is genuinely LGPL, checked rather than assumed.** Its configure line has `--enable-version3` and no `--enable-gpl`, which makes it LGPL-3.0-or-later. `NOTICE` had implied LGPL-2.1 and claimed "no GPL build is bundled" at a time when nothing was bundled at all. Distributing an LGPL binary obliges distributing its source: every release **attaches** the source archive rather than offering to supply it, because a written offer has to outlive whatever was going to host it. +- **The installer is compiled in CI, not only at tag time.** A script that runs once per release is a script that breaks once per release. CI stages the existing publish output with a stand-in `ffmpeg/ffmpeg.exe` and compiles the real `.iss`. +- **Inno Setup 6.7.1 is preinstalled on `windows-latest`; the Windows SDK is not.** `iscc.exe` needs no install step; `signtool.exe` is not on `PATH` and has to be found under `Windows Kits\10\bin`. The signing script was exercised as far as this machine allows — it locates a certificate from the environment and refuses to proceed silently — but has **never signed anything**, because there is no SDK here and no certificate yet. +- **`PrivilegesRequired=lowest` moves `{autopf}` to `%LOCALAPPDATA%\Programs`,** which is what makes the per-user install admin-free. `AppMutex` is `Local\Offstream`, the same string as `OffstreamPaths.InstanceMutex`, so an install over a running copy asks rather than fails. `AppId` must never be regenerated: a new one turns an upgrade into a second, parallel installation. +- **Uninstall offers to remove `%APPDATA%\Offstream` and defaults to No,** and says plainly that recordings live elsewhere and are never touched. The one thing an uninstaller must not do is delete the output of the app. +- **The licence text is embedded in the executable rather than read from beside it.** Both obligations here — the predecessor's MIT notice, and the bundled LGPL ffmpeg — require the notice to travel with the software, and a loose file does not survive a zip unpacked selectively or a copy of just the `.exe`. `LICENSE` and `NOTICE` are `EmbeddedResource` items in `Offstream.App.csproj`, and a test asserts the shown text still contains both files' contents, so the window and the repository cannot drift. +- **Attribution and licensing are two different obligations and live in two different places.** Spotify's Developer Terms want content credited; that line belongs **beside the provider picker on Settings**, where it is obvious what it refers to, and it is empty when no provider is selected because crediting a service the app is not calling is a false statement. Licences belong in the notices window. Folding the first into the second would have satisfied neither well. +- **The notices entry point sits on the section header, and the first attempt was wrong.** A version line and a button under the ffmpeg path pushed both off the bottom of the window at its minimum size — the shell's `MinHeight` is a written promise that no setting is ever unreachable without scrolling, and this quietly broke it. Raising the floor was not available either: at 150% scaling the window is already 1050 physical pixels tall. The header line is the one place in a full card with room that costs nothing vertically. **Anything added to the Advanced page from here needs the same arithmetic done first.** +- **`System.IO` is imported explicitly in `Services/ThirdPartyNotices.cs`.** WPF compiles a throwaway project first to resolve XAML type references, and that pass does not inherit `ImplicitUsings` — so a type reached through them fails to compile there while compiling fine in the real assembly. The error names a `_wpftmp.csproj` that does not exist on disk. + ### Phase 9 — Hardening (5+ days, ongoing) - 12-hour soak; memory and handle profiling. - Fault injection: device removed mid-recording, disk full, ffmpeg killed, Spotify closed, sleep/resume. diff --git a/src/Offstream.App/Offstream.App.csproj b/src/Offstream.App/Offstream.App.csproj index 4ca5fb4..5198681 100644 --- a/src/Offstream.App/Offstream.App.csproj +++ b/src/Offstream.App/Offstream.App.csproj @@ -26,6 +26,23 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Offstream.App/Views/NoticesWindow.xaml.cs b/src/Offstream.App/Views/NoticesWindow.xaml.cs new file mode 100644 index 0000000..22c3e8b --- /dev/null +++ b/src/Offstream.App/Views/NoticesWindow.xaml.cs @@ -0,0 +1,17 @@ +using Wpf.Ui.Controls; + +namespace Offstream.App.Views; + +/// +/// Offstream's own licence and the third-party notices that ship with it. +/// +/// +/// No constructor dependencies and no ViewModel: everything on it comes from +/// through x:Static, so the window is created +/// directly by whichever view opens it rather than resolved from the container. Nothing here +/// needs to outlive the dialog. +/// +public partial class NoticesWindow : FluentWindow +{ + public NoticesWindow() => InitializeComponent(); +} diff --git a/src/Offstream.App/Views/Pages/AdvancedPage.xaml b/src/Offstream.App/Views/Pages/AdvancedPage.xaml index 8825814..724688f 100644 --- a/src/Offstream.App/Views/Pages/AdvancedPage.xaml +++ b/src/Offstream.App/Views/Pages/AdvancedPage.xaml @@ -441,7 +441,38 @@ Padding="20,16,20,4" Style="{StaticResource OffstreamCardStyle}"> - + + + + + + + + + + + diff --git a/src/Offstream.App/Views/Pages/AdvancedPage.xaml.cs b/src/Offstream.App/Views/Pages/AdvancedPage.xaml.cs index 0182fee..ef553e2 100644 --- a/src/Offstream.App/Views/Pages/AdvancedPage.xaml.cs +++ b/src/Offstream.App/Views/Pages/AdvancedPage.xaml.cs @@ -1,3 +1,4 @@ +using System.Windows; using System.Windows.Controls; using Offstream.App.ViewModels; @@ -13,4 +14,17 @@ public AdvancedPage(AdvancedViewModel viewModel) DataContext = viewModel; InitializeComponent(); } + + /// + /// Opens the licence and third-party notices. + /// + /// + /// Opening a window is a view's job, not a ViewModel's — a command for this would put a + /// reference into , which is the thing + /// the MVVM convention in CLAUDE.md exists to prevent. ShowDialog rather than + /// Show: it is what makes the Close button's IsCancel close the window, and it + /// stops a second copy opening behind the first. + /// + private void OnShowNotices(object sender, RoutedEventArgs e) => + new NoticesWindow { Owner = Window.GetWindow(this) }.ShowDialog(); } diff --git a/src/Offstream.App/Views/Pages/SettingsPage.xaml b/src/Offstream.App/Views/Pages/SettingsPage.xaml index 6bfc385..38dba0a 100644 --- a/src/Offstream.App/Views/Pages/SettingsPage.xaml +++ b/src/Offstream.App/Views/Pages/SettingsPage.xaml @@ -222,6 +222,19 @@ + + +