* Recover from an undecryptable auth token keyset instead of crashing
Exclude the encrypted token prefs from backups, and attach the R8 mappings
to GitHub releases.
* Reset the Keystore master key along with the token prefs
2.6.0 was built against Compose 1.11 and lacks the IME members 1.12 made
abstract, so focusing an editor on iOS threw IrLinkageError on
ImeComposeStateAdapter.text and killed the app.
URL, numeric, tag, word-list, and delete-confirmation fields inherited
sentence capitalization and autocorrect, which inserted a space after
'.' in server URLs.
The desktop implementation of calculateWindowSizeClass reads an AWT window,
which does not exist under the Tao backend, and its LocalWindow reference was
removed in Compose 1.12. Size classes now derive from layout constraints.
Signed-in readers can leave kudos at the end of a publicly published
story: up to four craft chips and one reaction. There are no comments
or ratings, so there is nothing to moderate.
- Kudos are anonymous. Authors see per-chip totals on their story page;
the public page shows a chip's name, never a count, once three readers
pick it.
- The reader card loads as its own HTMX fragment on the last page, so
the story page's ETag never depends on kudos. Private shares never
show it.
- Authors can turn kudos off per story; existing kudos are kept.
- New story_kudos and story_kudos_opt_out tables (migration 9.sqm).
Chip keys are a code enum, rows are purged in both directions on
account deletion, and soft-deleted givers drop out of the counts.
- Story URL resolution is shared between the story page, the read
beacon, and the kudos routes.
Only count runs of non-whitespace containing a letter or digit, so
horizontal rules, heading, bullet and blockquote markers are not words.
Move countWords to :base and use it on the server too.
Headers take the library's own transformation, free text gets a side
inset and sync log entries are capped at five lines so they stay inside
a round bezel. The capture splash holds until the project list loads,
list content modifiers are remembered per item, and one app theme is
shared by the manifest and the splash style.
Branded launch: core-splashscreen theme shows the launcher icon on black
for MainActivity and CaptureActivity.
Play listing names the Capture tile and the complication.
Every TransformingLazyColumn item now uses the Material 3 scroll
transformation so buttons, headers and text shrink and fade at the
bezel instead of being clipped on round displays. The custom
screenContentPadding helper is dropped; the scaffold's padding already
includes the horizontal inset.
Add a project-scoped user spelling dictionary (#939)
Writers can whitelist words per project: "Add to dictionary" on a flagged word in
the scene editor and focus mode, plus an add/remove word list in Project Settings
under Spell checking.
Words live in ProjectData.dictionaryWords so they sync with the project. A conflict
confined to the word list merges both sides by union with no resolver; other
conflicting fields still go through the existing resolver with the dictionary
unioned. The union is verbatim, so a word a newer build stored under laxer rules
is never deleted server-side. Sync writes also re-apply any project-data edit made
after the phase snapshot, so an edit landing mid-sync is no longer clobbered.
ProjectDictionaryService now feeds user words to the checker alongside encyclopedia
words, independent of the encyclopedia toggle. HdHairlineTagField and the new
HdHairlineWordListField share an extracted HdHairlineChipInput.
Encyclopedia entry titles and scene draft names were still on the old
restricted set while project and scene names had moved to the shared
ProjectNameValidator. Both now use that validator, so punctuation like
`. , ! ? : ( ) & - "` works, and draft names accept non-Latin letters.
Both filename formats move to the `~` delimiter, since `-` is now a legal
name character:
encyclopedia type~id~name.toml, images type~id~image.ext
drafts sceneId~draftId~name~timestamp.md
Names round-trip through encodeForFilename/decodeFromFilename, so
OS-forbidden characters become lookalikes on disk. Legacy patterns are
kept for reads, and path resolution falls back to the legacy filename,
because unlike scenes these two rebuild the filename from the def rather
than scanning the tree.
Migration2_3 renames existing files and PROJECT_DATA_VERSION goes to 3.
It canonicalises names through the encoder, otherwise a name ending in a
space (which the old rules allowed) would migrate to a file that no
rebuilt path could ever match. Draft names are now trimmed at the save
and sync boundaries the way entry names already were.
A synced name containing a path separator is no longer rejected outright;
it is encoded to a lookalike and stays one path segment, matching how
scene names already behave. Containment rests on the isWithin guard. The
reserved `~` delimiter is still rejected.
A standalone watch client that dictates notes into subscribed projects
and story ideas, then syncs them on its own session.
- New :wear module: capture activity, tile and complication, project
subscriptions, sync log, and a WorkManager periodic plus
after-capture sync behind a single SyncCoordinator
- Pairing over the Wearable Data Layer: the phone confirms the request
and mints a session for the watch's install via the new
/api/account/pair_install endpoint. Play Services builds only; the
F-Droid build ships without it
- Manual sign-in on the watch, with an Android 17 local network
permission check before contacting a LAN server
- Plaintext sync is opt-in: typing an http:// URL selects it, with a
warning in server setup. HTTPS stays the default, and legacy
settings always restore as HTTPS
- Account sync extracted into SyncAccountUseCase, shared by the phone
project list and the watch
- temporaryProjectTask ref-counts concurrent users so one task no
longer closes a scope another is still using
- Wear version codes are offset from phone codes so both can ship in
one Play listing
- Crash handler and FileLogger moved into common for reuse
The iOS build now ships under the same App Store record as macOS, so the
home page and README link to id6770841038.
* Offer the iOS app with a smart app banner on the home page
* Say why a login failed instead of answering a bare 401
A failed login gave nobody anything to work with. StatusPages matched status 401
and re-responded with a bare status for API calls, discarding the HttpResponseError
the route had just written, and the client's own 401 branch discarded whatever
body did survive in favour of a generic string. Two layers erasing the same
message, so a self-hoster saw "401 Unauthorized" in the log, an empty body on the
wire, and a generic message in the app.
StatusPages now leaves API responses alone, since the routes answer 401 with
their own body, and the client parses the body for every status and only falls
back to a generic message when the server sent nothing usable.
On top of that:
- HttpResponseError carries an optional errorCode from a shared ApiErrorCode
vocabulary. It is optional and the shared serializer ignores unknown keys, so
it is compatible with servers and clients on either side of this change.
- Account creation failures use accurate statuses rather than a blanket 409:
400 for a policy or email violation, 409 for a real conflict, 403 for the
whitelist. Login answers 403 when the whitelist was the problem, since that is
not a credential failure.
- The login path logs which failure occurred. The response still cannot
distinguish an unknown account from a wrong password, because that would let
anyone enumerate users, but the operator's log now can.
The docs gain the password policy (8 to 64 characters, no complexity rule,
nothing stripped or truncated) and the log lines to look for, which is what the
reporter of #835 asked for.
* Allow the round-trip test accounts now that Allowed Users is always on
Only the first account on a server is exempt, so every later address the test
creates has to be on the list or account creation answers 403.
* Point the login-failure note at the Allowed Users section
The section was renamed, so the anchor was dead.
* Cover the account error statuses and the client's failure body
AccountErrorCodeTest walks the create and refresh failures a client branches on:
existing email, email pending deletion, malformed email, short password, unknown
refresh token. ApiFailureBodyTest covers the other end, where a server message
has to survive instead of being replaced by the generic one.
RecordingStrRes moves to its own file so both API tests can use it.
* Mark the password error codes as non-secrets for semgrep
* Add a Copy Diagnostics action to the About screen
Puts the startup banner (version, channel, OS/JVM) and the last 200 lines
of the current log on the clipboard, so a bug report can carry its own
diagnostics instead of asking the reporter to find, zip, and attach a log.
The blob always includes the banner: a missing log costs the reporter the
log, not the whole report.
The buttons move into a FlowRow so three of them wrap on a narrow window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Include the latest crash dump in Copy Diagnostics
* Only report the XDG session in the desktop banner on Linux
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Switch the desktop Ktor engine to OkHttp
`java.net.http.HttpClient`'s constructor opens an NIO `Selector`, whose
Windows wakeup pipe is an AF_UNIX socket. The MSIX sandbox rejects the
connect with EINVAL, so every Microsoft Store build hard-crashes on the
first network call, before any request is sent (JDK-8312215, open since
Java 17 with no fix in sight). OkHttp uses blocking socket IO and is
unaffected.
Ships unconditionally rather than gated on the distribution channel:
OkHttp works everywhere, and a second engine would be a second code path
to test forever. Android already uses it.
Includes the unchecked-IO mapping from `fix/unchecked-io-network-errors`,
so a client that cannot be built at all surfaces as a network error
rather than taking down the app.
* Add an Export Logs button to the desktop About screen
Opening the log directory hands the shell a path that does not exist under
an MSIX container: the app's writes to %LOCALAPPDATA% are redirected into
the package's LocalCache, but reads fall through, so the app sees the
directory and Explorer does not. Snap confinement will do the same on
Linux. Exporting a zip to a location the user picks works on every
distribution vehicle, which makes it the one support instruction that
never needs a per-channel caveat.
Also walk up to the nearest existing ancestor before handing a directory
to the shell, so the open button degrades instead of erroring.
* Bake the distribution channel into the build
Per-vehicle rules (self-update, store payment policy, "get the app" links,
sandbox-aware paths) were tracked by hand. `-Pchannel=<token>` now resolves
to a `DistributionChannel` constant, defaulting to DEV, and an unknown
token fails the build rather than quietly shipping a store binary as DEV.
Every release pipeline passes its own channel; the existing F-Droid build
flags map to FDROID without their call sites changing.
Flat enum rather than capability flags: every channel-conditional branch is
a greppable `when` on the type. Flags can emerge later from actual
duplication.
The startup banner and all three crash dumps now carry the channel. That
line pays for itself immediately: this investigation started from a crash
log that did not say which build produced it.
* Un-redirect container paths before handing them to the shell
MSIX filesystem redirection is asymmetric: the app's writes under
%LOCALAPPDATA% land inside the package container, but reads fall through,
so File.exists() is true from inside the container and the app is
satisfied. Explorer runs outside it and correctly reports nothing at the
literal path, which is the "location is not available" dialog the
open-logs button produced on the Store build.
Rewrite the path into the container for the Microsoft Store channel, both
where it is shown and where it is handed to the shell. The package family
name is hardcoded: Windows derives its hash suffix from the publisher ID,
so it is not in AppxManifest.xml and cannot be computed from it, and it
only changes if the Store identity does.
`Windows.Storage.ApplicationData.Current.LocalCacheFolder` is the native
answer but means a WinRT dependency on a KMP desktop target for one path
lookup. Worth revisiting only if more packaged-app APIs are needed.
* Cover the About log section with a render test, document the channel
* Suppress TooGenericExceptionCaught on the unchecked IO catch