Commit Graph
330 Commits
Author SHA1 Message Date
rmcrackan 819193fe03 Merge pull request #1958 from dmatlock171/parallel-downloads-rebased
Parallel downloads rebased
2026-08-24 13:41:58 -04:00
Cursor Agentandrmcrackan cc40d5e494 fix: an unreadable Books folder no longer poisons AudibleFileStorage
Building the Books file cache from a static field initializer meant any failure
became a TypeInitializationException, which the runtime caches for the life of
the process and rethrows at every later reader of the type. A USB drive that
started failing mid-session therefore crashed Libation on every launch after
that, in the startup logging, before the window appeared.

See issue #1984.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-24 14:22:08 +00:00
Allamagoosa 29a9cb3873 Fix the sidebar entry, a test race, and comments left pointing at deleted members
The docs page was linked from docs/index.md but missing from the VitePress sidebar, where every sibling feature page is listed.

In the dispatch test fake, Started.Enqueue ran before the counter incremented, so WaitForStarted could return in the gap and the HighWaterMark assertion could observe one book fewer than had started.

Comments still named TrackedQueue.Current and .Active after both were deleted, the Configuration doc comment still called itself the bound on a spinner when nothing binds it any more, and the ClaimAbort comment said the winner was the book that answered the dialog when it is really whichever book gets there first.
2026-08-21 06:33:31 -07:00
rmcrackan 0b03be5d02 Merge pull request #1980 from rmcrackan/cursor/fix-quickfilters-not-loaded-c279
Fix QuickFilters not loading from disk after restart (#1979)
2026-08-19 16:17:29 -04:00
Cursor Agentandrmcrackan 10c1cc2a60 Restore MaxSampleRate clamping lost with migrate_to_v11_6_5
Commit 065118cf also deleted migrate_to_v11_6_5, which clamped
MaxSampleRate into LAME's supported [Hz_8000, Hz_48000] range on every
startup (the fix for #1116). Its replacement, ValidateEnumSettings, only
rejects values that fail to parse - but AAXClean.SampleRate defines
Hz_7350, Hz_64000, Hz_88200 and Hz_96000, so a hand-edited or
pre-v11.6.5 Settings.json could carry a valid-but-unsupported rate
straight into the encoder.

Clamp in the property getter and setter instead of a startup hook,
following the DailyDownloadLimitQuantity pattern.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 20:12:44 +00:00
Cursor Agentandrmcrackan 49e8164b6f Use System.Threading.Lock for the QuickFilters locker
Matches the existing usage in PersistentDictionary and
LibationAvalonia.Program; the lock statement now emits
Lock.EnterScope() instead of Monitor.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 20:12:44 +00:00
Cursor Agentandrmcrackan 1d9b41c2a4 Fix QuickFilters not loading from disk after restart (#1979)
Commit 065118cf deleted Migrations.migrate_to_v11_5_0, which despite its
name ran on every startup and was the only code that read
QuickFilters.json into QuickFilters.InMemoryState. Since v13.7.6, saved
quick filters were never loaded after a restart, and adding a new filter
overwrote the file.

Make QuickFilters load its state lazily from disk on first access so it
no longer depends on a startup hook. Restore the pre-v11.5.0 format
fallback (plain string filters without names) that the deleted migration
provided, and add regression tests covering load formats and
restart persistence.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 19:46:21 +00:00
Cursor Agentandrmcrackan 6a166b23fb Merge master
LibationFileManager.csproj conflicted because both sides edited adjacent
PackageReference lines: this branch bumped AudibleApi, master's #1974 bumped
Microsoft.Extensions.Configuration.Json. Kept both.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 18:55:42 +00:00
Cursor Agentandrmcrackan 71e8d8eea1 Adopt Dinah.Core 10.2.5.1 and AudibleApi 11.0.4.1
Picks up the .NET 10.0.11 dependency floors both now declare.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 18:51:50 +00:00
Cursor Agentandrmcrackan 4a138bb42f Move the .NET 10 package references to 10.0.11
Microsoft.EntityFrameworkCore.Sqlite 10.0.11 is the first 10.0.x servicing
release to depend on SQLitePCLRaw 2.1.12, which drops the pre-3.50.2 SQLite
build flagged by GHSA-2m69-gcr7-jv3q (CVE-2025-6965). DataLayer's Sqlite
reference is where that chain enters the solution, so bumping it clears
NU1903 from all 23 projects that transitively saw it.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 17:55:55 +00:00
Cursor Agentandrmcrackan 099b01af14 Ask Windows which folders are synced instead of guessing from OneDrive variables
The previous check read the OneDrive environment variables and matched path
prefixes, so it missed Dropbox, Google Drive and iCloud, and had its own
guesswork to get wrong.

Every sync engine that serves files on demand registers a sync root with the
Cloud Files API, and CfGetSyncRootInfoByPath answers directly whether a path
sits under one. That covers every provider without naming any of them, and it
replaces the environment lookups, path canonicalisation and prefix matching
rather than adding to them. Asking for the provider info class also gets the
provider's own name, so the message can say Dropbox rather than a generic phrase.

The buffers are plain byte arrays read by hand: nothing here can be verified
away from Windows, so there is no marshalling to get subtly wrong.

Detection cannot produce a false positive now, since the answer is Windows'
rather than an inference from a folder name. The warning still appears only in
the incomplete-upgrade message, where something has already failed, so a folder
someone deliberately keeps fully downloaded never prompts anything.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-18 17:55:06 +00:00
Cursor Agentandrmcrackan a0ca574974 Detect Application Control enforcement and stop upgrading into a broken install
An in-app upgrade overlays files Windows has never seen. Smart App Control
blocks unsigned files it does not recognise, so upgrading in place under
enforcement is precisely how a working install becomes one that cannot start,
which is what #1873, #1876 and #1967 all describe.

Read VerifiedAndReputablePolicyState under HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy
to find out. The read needs no elevation and cannot raise a UAC prompt: UAC
prompts only on an explicit elevation request, and HKLM is readable by standard
users. Only the value 1 counts as enforcing. A missing key, a missing value, or
anything unrecognised counts as not enforcing, because the cost of guessing
wrong is telling someone to disable Smart App Control, which cannot be undone,
on a PC that was never blocking anything.

When enforcing, the upgrade notification becomes a notice with the download
link instead of an update prompt, and the flow stops before downloading even if
a UI ignores that. Classic honoured no such flag at all, so its dialog now takes
one; its two prompt labels had to be promoted from designer locals to fields to
carry the explanation.

The blocked-file dialog now states the setting it found rather than asking the
user to go and look, and startup logs the state, the install folder, and any
cloud sync root containing it, so a report answers these without a round trip.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-18 16:26:56 +00:00
Cursor Agentandrmcrackan d240ab12ec Present waiting for signed builds as an option now that signing is in motion
Libation has applied to the SignPath Foundation. Turning Smart App Control off
is irreversible, so pointing users at it as one of only two ways out reads
differently when signed builds may be close. The docs now list waiting first and
carry the status, hedged on approval since there is no date.

The dialog says nothing about signing progress and defers to the docs instead:
a shipped string cannot be corrected if the application falls through. It no
longer nudges toward turning Smart App Control off without mentioning that
the page has the current options.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-18 15:50:36 +00:00
Cursor Agentandrmcrackan 6fa65238db Apply the same Smart App Control correction to the in-app dialog
The 'Libation blocked by Windows security' dialog is generated from
GetApplicationControlBlockedMessage, so it repeated the broken Unblock-File
command verbatim and offered a reinstall that cannot clear an Application
Control block. Explain the missing signature instead, and tell the user how to
check which mode Smart App Control is in and what turning it off costs.

Point the incomplete-upgrade message at its own docs anchor now that the two
failures have separate sections.

Tests cover the message text because it is the whole deliverable here: one
asserts Unblock-File cannot come back, one checks the blocked file and cause
are named, and one pins each message to the docs section it links to.

Refs #1967

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-18 15:38:19 +00:00
Cursor Agentandrmcrackan e6e8ba73e0 Upgrade to Dinah.Core 10.2.4.1 and AudibleApi 11.0.3.1
Seven references across six projects, all now on published versions.

What the app gains is from Dinah.Core: OsSecretStore.Create bounds how
long it waits for the backend, so a Linux or macOS start with a keyring
that never answers falls through to the portable master key path instead
of hanging there. IdentityTokenStorageWiring.ResolveSecretStore is the
caller, and it runs at startup whenever no key file or env var is set.

AudibleApi 11.0.3.1 carries no code change for us - it is the release
where its nuspec finally declares the Dinah.Core floor its own code needs.

Dinah.Core.WindowsDesktop and Dinah.EntityFrameworkCore move to 10.2.4.1
as well, keeping every Dinah package on one version.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 17:53:53 +00:00
rmcrackan 14c56e4052 Merge pull request #1960 from rmcrackan/cursor/adopt-secretstring-packages-d9d1
Adopt Dinah.Core 10.2.2.1 and AudibleApi 11.0.0.1
2026-08-16 23:58:07 -04:00
Cursor Agentandrmcrackan 9ea66ddaee Point at AudibleApi 11.0.2.1
The last reference still on 11.0.0.1. 11.0.2.1 is the release whose nuspec
declares the Dinah.Core floor its code actually needs, so the package now
agrees with what this repo resolves anyway.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 03:37:20 +00:00
Cursor Agentandrmcrackan c813d6ee98 Drop AsScalar; SecretString reports its own shape now
Dinah.Core 10.2.2.2 gives SecretString a Redacted property, so a
destructured secret carries its length without the logger being told
anything. That was the only thing AsScalar was buying - safety never
depended on it - so the registration and its explanation go away, and the
knowledge lives in the type instead of in this file.

MaskedLogEntryPolicy stays: an ILogMasked has no equivalent property, and
without the policy one logged as {@Account} is still written out property
by property.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 02:26:46 +00:00
Cursor Agentandrmcrackan 4ac94d1ff2 Say AsScalar instead of restating ToString
Same output, but it states the intent: leave a SecretString whole rather
than transform it. The comment now separates the two lines by what they
do, because they are not the same kind of thing - the policy is the
protection, and without it a masked object is written out property by
property, while this line only decides whether a secret reads as its
length or as {"HasValue":true}.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 02:08:57 +00:00
Cursor Agentandrmcrackan 742e58d2e8 Mask the account on the paths that do not go through an exception
Account.ToString() returned "id - locale", so interpolating an account or
logging a non-destructured {Account} published the address. It now returns
the masked entry, with a DebuggerDisplay keeping the real values visible
while debugging. Nothing in the UI relied on it: both scan dialogs build
their own labels.

For structured logging, an ILogMasked type is reduced to its masked entry
by a destructuring policy, which covers the {@DebugInfo} shape most of
Libation's logging uses. And DecryptKey - the activation bytes - is now a
SecretString, so it has no plaintext for a reflective dump to find at all.
Its JSON stays the bare string it always was, so existing settings files
load unchanged.

A registered policy that nobody notices is missing protects nothing, so
the tests write through a logger built by ConfigureLogging itself rather
than a hand-made one. Deleting either registration fails them: the masked
object comes out whole, and a destructured secret renders as
{"HasValue":true} instead of its length.

The contribute guide now states the rule, since the reason for all of
this is invisible from the code alone: log files get attached to public
issues, so treat what goes in them as published.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 02:04:09 +00:00
Cursor Agentandrmcrackan 337cb3169a Adopt Dinah.Core 10.2.2.1 and AudibleApi 11.0.0.1
AudibleApi 11 holds token, key, and cookie values in a SecretString
rather than a string, so nothing public exposes plaintext for a reflective
logger to find. Picking it up is a breaking upgrade: the seven package
references move, and the nine places that read a secret now call Reveal().

Two of those needed thought rather than a mechanical edit. Mkb79Auth
exports to and imports from audible-cli's JSON format, which is plaintext
by definition, so the cookie projections reveal explicitly in both
directions and the file format is unchanged. And the account's own
DecryptKey stays a plain string here: converting it is separate work.

This is the dependency bump only. The log leak it enables fixing - an
AuthenticationRequiredException carrying a live Account, whose address and
activation bytes Serilog.Exceptions writes into a shared log - is still
open, and none of the account-side masking has landed yet.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-17 01:49:21 +00:00
Cursor Agentandrmcrackan 4187712c8d Add HasSubtitle and TitleHasColon search fields
<title short> stops at the first colon, so it shortens Audible titles that
contain one just as readily as it drops Audible's subtitle, and distinct books
then collapse onto the same name. A colon cannot be searched for: the analyzer
discards punctuation and Lucene reads a colon in a query as a field separator.
Two bool index fields find the affected books instead.

Document how the two title tags differ, since <audible title> already drops
Audible's subtitle without ever cutting a title, and how to audit for names
that actually collide in a spreadsheet export.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-16 23:43:53 +00:00
Allamagoosa 325af3a491 Address the remaining review points on concurrent processing
Faulted book tasks were dropped unobserved. The reaping pass removes any
completed task from the active set before the closing WhenAll can
rethrow, so an exception out of ProcessOneAsync - which can happen via
GetFailureActionAsync in its finally - went nowhere. In the sequential
loop it reached the outer catch and was logged. It is logged again.

The bad-book dialog did not survive concurrency. The license and
Widevine messages are guarded to appear once per run, but the "skip this
book?" dialog is per book and its "apply to all remaining books" answer
lands in a shared BadBookSessionContext. Three books failing together
put three modals on screen racing to set the same override. The dialog
is now serialised on the session, and each book re-checks the override
after its turn comes: if the book ahead answered "apply to all", the
question is not asked again.

Machine capability no longer overwrites the stored concurrency setting.
MaxAllowedConcurrentDownloads clamped both the getter and the setter, so
a user who chose 8 and then opened the same config on a two-core machine
read back 2 - and had 2 written over their 8. The stored value is now
bounded only by the hard limit, which is machine-independent; processor
count bounds the spinner and is applied at the point of use, where the
loop decides how many books to run.
2026-08-16 14:14:51 -07:00
Allamagoosa 986dda5eaa Review #1885: Avalonia parity, and cap concurrency by processor count
Chardonnay had parallel downloads with no way to configure them, since
the queue logic lives in shared UI code but each UI supplies its own
controls. Adds the Auto-scroll toggle and the 'At once' spinner to
Chardonnay's queue panel, bound to the same view model properties the
WinForms panel uses.

Also uses Environment.ProcessorCount as the spinner's ceiling rather
than its default: min(ProcessorCount, 10). Downloading is bound by
Audible's license throttling rather than local CPU, so core count says
nothing about how many concurrent downloads will succeed - it only
bounds how many decrypts can usefully run at once. The default stays 3.

Spinner bounds are bound rather than hardcoded, so the two UIs cannot
drift apart.
2026-08-16 14:08:49 -07:00
Allamagoosa 2db63a8edd Review #1885: one concurrency setting with a safe default and a numeric control
Replaces the MultiThreadEnabled checkbox and the ProcessorCount default
with a single MaxConcurrentDownloads value where 1 means serial - the
behaviour Libation had before parallel downloads existed.

The old pairing could not express 'off' at all: the bool was hardcoded
true in the view model constructor and the int setter clamped to a
minimum of 2, so there was no way back to one-at-a-time downloads.
Collapsing both into one value makes that state unreachable rather than
merely fixed, and removes the risk of the two settings disagreeing.

Bounds live in Configuration as named constants: minimum 1, default 3,
maximum 10. Audible throttles license requests, so the default is
deliberately conservative and the cap keeps users from choosing a number
that produces license denials instead of speed.

WinForms swaps the 'Parallel downloads' checkbox for an 'At once:'
spinner that reads its bounds from those constants.
2026-08-16 14:08:49 -07:00
SirBiggin d93d325913 Added Multi Threading for downloads and Auto Scroll to view current downloads 2026-08-16 14:08:39 -07:00
Cursor Agentandrmcrackan 4d8075927f fix(logging): roll the log on size, not only on the calendar month
The default Serilog config set rollingInterval only, so Serilog's own defaults
applied: no size-based roll and a 1 GB ceiling after which the sink silently
stops writing. A busy install (many accounts scanned several times an hour)
reaches tens of MB in a month, past the point where the log can be attached to
a bug report.

Add fileSizeLimitBytes, rollOnFileSizeLimit and retainedFileCountLimit to the
default File sink, and fill in whichever of the three an existing Settings.json
is missing so installs that already have a Serilog section benefit too. Only
absent keys are written, so a hand-tuned config is left alone.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-16 16:03:27 +00:00
Cursor Agentandrmcrackan 562b563477 feat(cli): stop a liberate run at a per-run download limit
Adds three mutually exclusive options to `liberate`: --limit-books,
--limit-mb and --limit-gb. Each stops the run once it has downloaded that
much, leaving the rest of the library un-liberated for the next run.

Requested in #1920: a scripted or scheduled run has no way to take only a
slice of a large library, so users resort to feeding the CLI a handful of
ASINs at a time. The GUI needs no equivalent, where selecting rows already
says exactly which titles to download.

Counting reuses the daily limit's history rows rather than a private tally,
so a book and a byte mean the same thing to both limits, and failed,
cancelled and pdf-only work is never counted. Only titles this run attempted
are counted, so a Libation window or a second container downloading at the
same time does not consume this run's allowance.

The limit is checked before each title rather than at the top of the run, so
a run whose books happen to end exactly at the limit reports nothing: nothing
was cut short. The daily download limit keeps applying on top, unchanged.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-14 19:46:47 +00:00
Cursor Agentandrmcrackan 3129221531 docs(limit): document the daily download limit and add a history seeding script
Adds a feature page covering the rolling window, what counts, the MB/GB
estimate and the Docker/CLI keys, plus a one-line pointer from getting started
where a new user with a large Plus library clicks Begin Book Backups.

Scripts/seed-download-history.cs seeds fake completed downloads so the limit
can be exercised without downloading, including dating rows just under 24 hours
old to turn the multi-day pause-and-resume behavior into a one minute test.

Also shortens the queue status text: the process queue column clips rather than
wrapping, so the resume time was being cut off.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-14 18:03:23 +00:00
Cursor Agentandrmcrackan 818510d1b0 feat(limit): add opt-in daily download limit core, history store and enforcement
Records every successful audiobook download in the library database (a new
DownloadHistory table) and, when the user opts in, stops downloading once the
rolling 24 hour window is full.

The history lives in the database rather than a file under LibationFiles
because in Docker only the database is on a volume; a file there is discarded
on every container restart.

The limit is checked immediately before each book downloads, never at queueing
time, so a full queue stays full and the user can raise or disable the limit
mid-run. When nothing in the queue can proceed the queue pauses and re-checks
every 15 seconds, recomputing settings, history and clock from scratch, so a
queue left running for days drip-feeds itself as downloads age out. The CLI
never waits: it skips covered titles and reports a count.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-14 17:16:47 +00:00
Cursor Agentandrmcrackan a5b1df0d29 Centralize best-effort file deletion in FileUtility
Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-12 18:55:42 +00:00
Cursor Agentandrmcrackan 90856b29a6 Add single-instance guard and make SQLite WAL/SHM cleanup data-safe
Concurrent Libation instances against one LibationFiles folder raced on the
SQLite database, Lucene search index, and log file, and the startup routine
unconditionally deleted LibationContext.db-wal/-shm - discarding committed
transactions when a prior run died abruptly (issue #1931).

- Add SingleInstance (named mutex keyed on the LibationFiles folder). Wire it
  into the Avalonia and WinForms startup so a second launch shows a message and
  exits before any database access, and holds the lock for the process lifetime.
- DeleteOpenSqliteFiles now skips cleanup when the DB is held by another process
  and preserves a non-empty (unrecovered) WAL so SQLite can recover it on open.

Also compare AccountId case-insensitively in AccountsSettings.GetAccount so a
capitalization difference no longer causes spurious 'No account found' errors.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-12 18:13:58 +00:00
Cursor Agentandrmcrackan da23a7ddb0 fix: use '/' in Unix mount prefix matching (Windows CI)
FindLongestMountPointPrefix is exercised on Windows CI with Unix-style
paths. Path.DirectorySeparatorChar is '\\' there, so '/var/home' never
matched '/var/home/...'. Hardcode '/' for Unix mount identity.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-09 17:55:38 +00:00
Cursor Agentandrmcrackan 16e6a6f659 bugfix #1919: resolve Unix mount points for disk-space preflight
Path.GetPathRoot always returns "/" for absolute Unix paths, so bulk
backup preflight queried composefs root free space (often 0 on Bazzite)
instead of the filesystem containing Books/In progress.

On Unix, symlink-canonicalize paths (so /home -> /var/home) and pick the
longest DriveInfo.GetDrives() mount prefix. Route GetBackupDriveSpaces
through the shared helper so grouping and free-space queries agree.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-09 17:51:36 +00:00
Robert McRackan 065118cf6c Fail fast on invalid Settings.json enums and broken Serilog structure: reject unknown enum values at startup with clear errors, accept case-insensitive names, migrate ZipFile sinks, and validate Serilog shape without blocking hand-edited custom sinks 2026-08-07 11:10:43 -04:00
Robert McRackan e0d49b57e1 Encrypt-fail plaintext fallback 2026-08-05 10:02:53 -04:00
Robert McRackan 4513a8007b Quiet Docker ConfigApp.dll noise and document login-external via /config 2026-08-03 09:09:59 -04:00
Robert McRackan 712568b188 Surface real CLI/Docker auth failures and fix login-external locales 2026-08-02 11:55:48 -04:00
Robert McRackan 5f4a75aade Detect identity decrypt failures by exception type 2026-08-01 11:25:56 -04:00
Robert McRackan a6d637c718 Harden Audiobookshelf Server URL handling and connection errors 2026-07-31 14:05:36 -04:00
John Doe b5e87847ff fix: Address review feedback - UI parity, soft-fail, token encryption, pagination, CLI wiring 2026-07-26 19:12:31 +02:00
John Doe ad61618cc9 Add Audiobookshelf auto-upload integration
- Persist settings: enabled, server URL, API token, library/folder IDs

- Add AudiobookshelfApiService for login, library listing, and multipart upload

- Add UploadToAudiobookshelf post-download processable

- Add settings tab to WinForms and Avalonia with library/folder dropdowns

- Match Avalonia layout to WinForms with aligned columns
2026-07-26 09:58:12 +02:00
Robert McRackan 20150cc4ed update dependencies 2026-07-25 22:03:23 -04:00
Robert McRackan abea874db8 Add Important settings UI for encrypted token storage 2026-07-25 21:32:42 -04:00
Robert McRackan bb18e9adc4 Add TokenStorageMethod preference and AudibleApi token-storage wiring 2026-07-25 21:17:11 -04:00
Robert McRackan a9e6f75443 update dependencies 2026-07-25 20:57:52 -04:00
rmcrackan d30086e53a bugfix 2026-06-17 10:38:24 -04:00
rmcrackan 82d4001f0a #1878 : Detect incomplete in-app upgrades, roll back install files, and show clear recovery messages 2026-06-17 10:18:52 -04:00
rmcrackan d955cb7605 #1873 , #1876 - Handle Windows Application Control blocking after in-app upgrades 2026-06-16 17:25:18 -04:00
rmcrackan b654a8b551 #1867 - Fix duplicate-ASIN crashes. New Hangover recovery 2026-06-12 11:48:32 -04:00