Decryption now dispatches on each row's stored cipher tag instead of the
single DI-injected encryptor. Adds ContentEncryptorRegistry and an identity
PlaintextContentEncryptor (tag "none"); loadEntity resolves the row's
encryptor from its cipher column. NULL cipher resolves to plaintext
(pre-#367 rows); an unknown non-null tag fails loudly rather than reading
ciphertext as garbage. Store path is unchanged.
Tests: mixed-tag read (aesgcm/none/NULL), unknown-tag loud failure, and
cipher-orthogonal-to-hash invariant.
Frame tests around the catastrophic failure modes (unreadable data, mid-convergence
loss, silent corruption, false 'safe to delete' signal, auth breakage). Add the
grandfather golden-corpus test (incl. lossy-secret trap), crash/no-loss and
completion-signal gates, over-cap handling, and a convergence dry-run. Move the
plaintext read path into PR1 so NULL/plaintext rows are readable immediately.
Record the four settled decisions (NULL=plaintext, single-JSON keyring with
roles, File+Env explicit-selection providers with no auto-generation, blocking
pre-launch convergence). Drop the online background sweep in favor of
offline-only maintenance, add a read-only admin encryption-status view, and note
the design doc will be replaced by an admin tutorial when the feature ships.
Capture the design discussion for moving the server secret off the
co-located filesystem file toward a pluggable, versioned keyring with
per-row encryption modes (including no-encryption), online key rotation,
and a provable key-retirement signal.
formatForUrl maps spaces to dashes and decodeFromUrl maps every dash
back to a space, so a project whose name contains a literal dash (e.g.
a "2026-06-07" date) could not be reversed and its web page 404'd.
Add ProjectsRepository.findProjectByUrlName, which tries the exact
decoded name then matches the URL segment against each project
re-encoded with formatForUrl. formatForUrl output is unchanged, so
existing shared/published links keep working. Route the story and
review lookups through it, and compare slugs in reviewMatchesUrlProject.
FileKit's Compose remember*Launcher runs the native picker on a shared
Dispatchers.IO thread and calls CoInitializeEx(STA), which throws
"CoInitializeEx failed" (RPC_E_CHANGED_MODE) when that pooled thread was
already COM-initialized MTA. Intermittent and crashes the app.
Add retryingFileDialog and route every desktop-reachable picker through
FileKit's suspend API instead of the Compose launchers, retrying past the
transient COM failure and returning null rather than crashing. iOS/Android
keep the Compose launchers.
A project name the server accepts can contain characters this client
rejects (e.g. #). Such projects failed local creation during account
sync and were silently skipped, so they never appeared on the client
and could not be deleted from it.
Map server names through ProjectsRepository.toLocalSafeName when
creating or renaming local projects from server changes, and log a
failed local create as an error rather than a warning.
Also plug a test leak: SceneEditorRepository{Archive,Other}Test
mockkObject ProjectsRepository.Companion without unmocking, which
globally forced validateFileName to succeed for any later test.
Verifies that databases upgraded via Schema.migrate end up structurally
identical to a fresh install, and that data written under the v1 schema
survives migration to the latest version. Restores the schema/data
guarantees the SQLite migration tests provided before the Postgres move.
The checked-in v1_baseline.sql is the immutable upgrade-from snapshot;
new schema versions are picked up automatically via Schema.version.
The Recent Errors list rendered every entry as loud red, even though most
were scanner/bot probes hitting /api without a protocol header. Give those a
quieter treatment and surface the HTTP status each error resolved to.
- Record the resolved HTTP status on error_log (folded into the v4 migration;
status column added last to match ALTER's physical column order).
- Classify only deliberately-marked client/transport faults as 4xx via a new
HttpStatusException hierarchy; everything else stays 500 so genuine server
bugs aren't hidden as client errors. The protocol enforcer now throws
UnsupportedProtocolVersionException instead of a generic korlibs exception.
- Apply the classified status to /api responses only; web routes keep their
500 + servererror page behavior.
- Render 4xx as amber warnings and 5xx as red errors, with a status badge and
severity icon, on the errors page.
The public story route now reads the logged-in viewer's id from the session
and skips the reader tally when it matches the story owner, so an author
browsing their own published / shared story doesn't inflate their count.
The Error Rate graph read from MetricsCollector error counts recorded on
RoutingCallFinished, which never fires when a handler throws. Exceptions
mapped to 500 by StatusPages were therefore invisible to the time series
even though they were logged to the recent-errors list.
Record metrics on ResponseSent instead, so the final (exception-mapped)
status is observed, and key on the route template stashed during routing.
Two privacy-preserving usage metrics for the sync server, accumulated in
memory and flushed by the monitoring maintenance job:
- Unique active users (sync + web) over 24h/7d/30d on the admin dashboard.
- Best-effort unique readers of published / shared stories, identified by a
cookieless daily-salted visitor hash: IP + user-agent are hashed in-request
and never stored, and the salt rotates daily in memory, so reads can't be
linked across days or back to an IP. Authors see an all-time total plus a
30-day daily trend on their story page; admins see aggregate window counts.
Reader rows dedupe per (project, day) and are purged on the metrics retention
window, with each day's count rolled into a per-story lifetime total first so
the all-time number survives the purge.
Adds the user_activity and published_story_reader[_total] tables (schema v4),
consolidates UTC-day truncation into a shared helper, and formats the trend
labels in UTC to match the buckets.
Track unique logged-in users (real accounts, not anonymous visitors)
split by activity: syncing vs. authenticated web sessions.
- New user_activity table (dedup per user/type/hour) + migration 3.sqm,
DB version 4. Distinct counts via COUNT(DISTINCT user_id); the additive
histogram used for API metrics can't answer distinct-user counts.
- In-memory UserActivityCollector flushed by MonitoringMaintenanceJob,
mirroring MetricsCollector. Gated end-to-end by the master monitoring
switch (MonitoringConfig.enabled) and purged on metricsRetentionDays.
- Recorded at two hook points: sync (ProjectsRoutes.beginProjectsSync)
and any authenticated web render (Frontend.withDefaults).
- Overview dashboard shows 24h/7d/30d rollups (DAU/WAU/MAU) plus a daily
active-users trend line (Sync vs Web) over the last 30 days.
- Tablet variants: new TabletPreviewSurface provides a wide ScreenCharacteristics
so screens render their expanded layout; @Preview(widthDp/heightDp) sizes the
canvas to a landscape tablet. Added for ProjectStats, ProjectSettings,
TimeLineOverview, BrowseEntries, BrowseNotes, and ViewEntry.
- Naming: prefix every top-level screen preview (phone and tablet) with "Screen"
so they group together and are easy to pick out from component previews.
- Backgrounds: the shared Padded wrapper now applies the app theme + surface
background; ProjectCard and SceneItem previews now render against the app
background instead of the renderer's transparent default.
The compose-preview renderer invokes @Preview functions reflectively and
cannot access private top-level functions, so most existing previews failed
to render with IllegalAccessException. Drop `private` from every @Preview
function.
Also fix the remaining render failures:
- Wrap encyclopedia/scene previews that touched Koin in KoinApplicationPreview.
- Register a minimal coil3 ImageLoader in the preview Koin graph so AsyncImage
screens (ViewEntry, CreateEntry) resolve it.
Result: 88/88 previews render, 0 errors (was 19/88).
MarkdownEditField injects SpellCheckRepository, so every editor-based
preview (CreateNoteUi, CreateTimeLineEventUi, ...) crashed at render with
a missing Koin definition. Register a real SpellCheckRepository backed by
in-memory settings datasources in KoinApplicationPreview's base module.
Lazy single, so non-editor previews are unaffected.
Cover the destination screens behind the project-home, notes, timeline,
and story-editor routers with @Preview fixtures:
- ProjectStatsUi, ProjectSettingsUi (project home)
- CreateNoteUi (notes)
- TimeLineOverviewUi, CreateTimeLineEventUi, ViewTimeLineEventUi (timeline)
- OutlineOverviewUi (story editor dialog)
Each preview hand-rolls a fake component implementing the Decompose
interface so the screen renders without runtime dependencies.
Add tests over a real datasource/fake filesystem for setEntryImage
(store and null-clears), loadEntryImage/getEntryImagePath round-trip,
calculateEntryImageHash, removeEntryImage (success and failure branches),
and ensureEntriesLoaded caching.
Add tests for reIdEvent, updateEventForSync (replace and append),
storeTimeline flushing in-memory edits, getTimelineEvent, and the
server-synced markForSynchronization branch.
Drive the repository over a real GlobalSettingsStore (mocked datasources)
and a mocked platform spell-check factory: loads a checker for the
configured locale, skips unsupported locales, reloads on a locale change,
ignores unrelated settings changes, plus the toSpLocale mapping.
Add tests for getBackupsForProject, deleteBackup (success, missing,
failure), cullBackups (over and under budget), and the real
createBackup/restoreBackup zip round-trip over a fake filesystem.
* Exclude generated code from kover coverage reports
Generated SQLDelight query/DB code and Compose Multiplatform resource
accessors were counted against coverage, masking the real figure for
hand-written code (merged line coverage read ~47% with them, ~61%
without). Filter them out so the metric reflects code we actually own.
* Test PasswordResetRepository
Cover the password-reset flow end-to-end: enumeration-safe request
handling (unknown account, rate limiting, email-send failure), token
validation states (invalid/used/expired/valid), and password reset
(invalid token, weak password, and the success path that updates the
hash, logs out all devices, and consumes the token).
* Test the review invite and submitted mailers
Cover email composition for both review mailers: the rendered HTML and
text bodies carry the project, author, note, and review link; the
note-present/absent and expiry/no-expiry branches; the singular vs
plural suggestion tally; and that the email service's result (including
failures) is passed through to the caller.
* Test AccountsRepository token refresh
Cover refreshToken: a matching install token mints a fresh valid token,
while a missing token or a mismatched refresh hash fails.
Cover ProjectDataRepository end-to-end over a fake filesystem (load
caching, user edits invalidating the project hash, sync-only updates),
fill out every branch of ProjectDataSyncOperation (no-op, fast-forward,
upload, and conflict resolve/abort paths) by driving real repository and
broker collaborators, and exercise the EntitySynchronizers facade's
type dispatch (get, findEntityType, reIdEntry phantom skip, conflict
routing) for all entity types.
* Expand test coverage for sync synchronizers and ImmutableTree
Add a Classical-style suite for ImmutableTree covering the previously
untested query API (indexOf, findBy, isAncestorOf, getBranch, coordinate
round-trips, iterator exhaustion, equals/hashCode).
Rewrite ClientEncyclopediaSynchronizer tests to drive real repository,
service, and datasource collaborators over a fake filesystem instead of
mocks, asserting observable on-disk state, image base64 round-trips, and
create-vs-update behavior.
Extend ClientSceneSynchronizer tests with createEntityForId,
deleteEntityLocal, archived-scene-from-server, store-content failure,
and reIdEntity branches.
Extend EntityTransferOperation tests with the download/heal branches:
not-modified, not-found remote delete, stale-hash forced-upload heal,
failed-store logging, and the onlyNew upload path.
* Fail entity transfer when a downloaded entity cannot be stored
downloadEntry returned CResult.success() even when storeEntity failed,
so a failed download was logged but never marked the transfer as
unsuccessful, masking sync failures from the operation's allSuccess
tracking. Return a failure result in that case so the transfer reflects
it.
* Add tier 2 sync coverage: uploadEntity, transfer + scene branches
Cover the EntitySynchronizer.uploadEntity base method end-to-end through
the encyclopedia synchronizer's classical harness (success, force, plain
failure, conflict resolution, and failed resolution), the remaining
EntityTransferOperation download/upload failure branches, and the scene
group move-parent and unarchive-on-active paths.
The .site-header had backdrop-filter applied directly, which made it the
containing block and stacking context for the fixed mobile nav drawer
nested inside it. This trapped the drawer in the header's compositing
layer, so it rendered beneath page content like the home page hero image
regardless of its z-index.
Move the frosted-glass background to a ::before pseudo-element so the
header no longer establishes a backdrop-filter context around the drawer.
The visual appearance is unchanged, but the drawer now positions against
the viewport and stacks correctly above page content.
The markdown editor only exposed inline styles through format-bar buttons;
the underlying composetexteditor library handles editing/navigation shortcuts
(Ctrl+C/V/X/Z/Y, arrows, etc.) but has no bindings for bold/italic, so those
keys were simply dropped.