Commit graph

99 commits

Author SHA1 Message Date
Adam Brown
584cf62271 Allow Windows reserved names in scene/group titles (#586)
Scene and group titles are stored wrapped as `order~name~id`, so a leading
dot or a Windows reserved word (CON, PRN, COM0-9, LPT0-9, ...) can never
collide on disk for them — yet name validation rejected them as if the title
were a raw filesystem basename. Project names, which become directories
verbatim, keep the strict rules.

- ProjectNameValidator.validate gains usedAsRawFilename (default true); the
  leading-dot and reserved-name checks now only apply to raw filenames.
  Trailing dot/space stay rejected for everyone (the on-disk encoder strips
  them). Reserved set extended with COM0/LPT0.
- Thread the flag through ProjectsRepository.validateFileName and
  SceneRepository.validateSceneName.
- Consolidate the six naming dialogs onto a shared rememberNameValidation
  hook keyed by NameKind (Project vs SceneItem), so strictness lives in one
  place instead of a per-call-site boolean.
2026-06-22 01:10:52 -07:00
Adam Brown
a10cdb1d20
Short-id project URLs (fix name round-trip 404s) + shared project-name validation (#628)
Promote the client's project/file-name validation rules out of :common's
ProjectsRepository and into a shared ProjectNameValidator in :base, resolving
the long-standing TODO on the old length-only stub.

Project URLs are now /story/{slug}-{id} (and /a/{penName}/{slug}-{id}), where the
trailing id is a stable 6-char base62 hash of the project's uuid and the slug is
purely cosmetic and never parsed back. This dissolves the slug round-trip bug:
the slug can be as pretty as we like because resolution matches the embedded id
against the user's projects, not the name. A bare /story/{id} (no slug) also
resolves, for short URLs.
2026-06-21 18:02:56 -07:00
Adam Brown
31b98876f7
Pre-sync change probe: skip syncing unchanged projects (#584)
On app open, sync made ~4 HTTP round-trips per project even when nothing
had changed. Add a batched pre-sync probe: the client sends a project-wide
content hash for each eligible project in one request, and the server
returns which ones still match so the client can skip syncing them.

- base: ProjectContentHasher (entity hashes + project-data hash, computed
  identically on client and server) and probe request/response DTOs
- server: POST /projects/{userId}/sync_probe, recomputing each project's
  hash from stored state; skips projects with an in-flight sync session;
  400 on a malformed body
- client: cache the hash in the project journal (written at FinalizeSync,
  invalidated on any content mutation via one chokepoint); probe is
  best-effort and degrades to a full sync on any failure
- backstop: never skip a project with pending entity or project-data work,
  so a stale cache can't drop a change

Writing activity is excluded by design (per-device, conflict-free).

Tests: ProjectContentHasher, client/server probe units, and an e2e suite
covering client/server hash agreement, the session gate, auth, and 400s.
2026-06-10 23:15:50 -07:00
Adam Brown
f779996fee
A previous refactor narrowed the TOML loader catches to IOException +
SerializationException, dropping the IllegalArgumentException branch that
readJsonOrNull already has. tomlkt doesn't route every decode failure
through SerializationException: a stale stats.toml (old schema, e.g. a
non-integer key in Map<Int, Int>) throws a raw NumberFormatException.

That uncaught exception killed the parallelMap worker for the affected
project, nulled its result slot, and filterNotNull() dropped it from the
project selection list — while sync, a different path, still showed it.

Add a readTomlOrNull helper mirroring readJsonOrNull that absorbs the full
set tomlkt can throw on bad input (SerializationException,
IllegalArgumentException incl. NumberFormatException, IllegalStateException
from parser errors, IOException), with an onError callback so callers keep
their site-specific logging. Schema-version checks run after decode, so they
can't rescue a decode that throws first.

Migrate every TOML read site to it: ProjectStatisticsCacheReader,
StatisticsDatasource, ProjectDataDatasource, ProjectsListComponent,
ReferenceIndexDatasource, WritingActivityDatasource, and
SceneMetadataDatasource (which previously had no error handling at all).
Stale caches are now treated as a miss and recalculated.

Add ReadTomlOrNullTest covering each exception family and a
ProjectStatisticsCacheReaderTest reproducing the original crash.
2026-06-06 22:43:53 -07:00
Wavesonics
52dbde1daf Suppress generic-exception catches at must-not-crash boundaries
Sync, background jobs, API/email send, and UI boundaries legitimately
catch broadly; annotate each with @Suppress and a reason, and log the
exception where it was previously swallowed.
2026-06-06 15:42:35 -07:00
Adam Brown
4537909826
Add prose diff visualization to draft compare and conflict merge UIs (#525)
New ProseDiff module: word-level Myers diff (kotlin-multiplatform-diff)
over markdown text, with syntax stripped via the JetBrains parser and
offsets mapped back to source. Merges adjacent edits into single hunks,
emits anchors at each boundary for scroll sync, and includes a git-style
slider that aligns pure insert/delete blocks to line boundaries. Covers
plain-text diffing, prepared/reusable inputs, and an OffsetMap that maps
a position on one side to its counterpart.

Draft compare: highlights DELETED (red/strikethrough) and INSERTED
(green/underline) spans via non-destructive RichSpans in each pane's
rendered coordinate space, recomputed off-thread on a 500ms debounce.
Synchronized scrolling (compose-texteditor 2.0.7) drives either pane from
the other off raw scroll pixels with an echo guard. Both editor states are
hoisted so highlights and sync work in compact and expanded layouts and
survive theme changes. A DIFF toggle gates highlights and sync.
2026-05-30 09:49:33 -07:00
Adam Brown
bbfc27efa2
Track last-edited scene per project and surface in stats
Adds `created` and `lastEdited` timestamps to SceneMetadata, stamped
on every autosave tick in SceneEditorRepository.recordSceneActivity.
StatisticsService rolls them up into a new "Last Edited" tile on the
project home dashboard, and the Android widget reads the same field
from the stats cache.

The timestamps ride the sync wire on ApiProjectEntity.SceneEntity and
participate in EntityHasher.hashScene. On download, local values are
preserved when the server didn't ship them. No conflict resolution —
last write wins.

EntityHasher hash functions had their defaults removed so call sites
can't silently miss a newly added field; this caught a latent bug in
the second markForSynchronization hash call that was relying on
defaults for archived/confirmedReferences/dismissedReferences.
2026-05-17 22:57:10 -07:00
Adam Brown
244cc78e3c
Drop iosX64 target (removed from Compose Multiplatform 1.11) 2026-05-13 21:58:48 -07:00
Adam Brown
1d39309d53
Add Scenes to the tag system!
This is a ton of work to plumb it all through the sync system
2026-05-12 22:23:34 -07:00
Adam Brown
93d0df771b
base: skip install-git-hooks in git worktrees
In a worktree, .git is a file pointer rather than a directory, so the
Copy task's destination ancestor validation fails at configuration time.
Register a no-op stub instead — hooks install when the main checkout
builds.
2026-05-07 23:12:16 -07:00
Adam Brown
1bb68a5b6f
Added tags to timeline events 2026-05-07 23:10:15 -07:00
Adam Brown
9e0049465d
Added tags to the NoteContent entity 2026-05-07 00:43:25 -07:00
Adam Brown
585c9919c5
Apply project theme to project window UI
Renames ProjectTheme.color1/color2 to primary/secondary and pipes them
into a Material 3 ColorScheme via material-kolor's dynamicColorScheme.
The user's secondary seeds both the Material 3 secondary and tertiary
palettes, the Rainbow palette style keeps them in distinct hue lanes
instead of collapsing toward the primary, and surfaceTint is redirected
to secondary so elevated surfaces (cards, dialogs, top bars) pick up
the accent.

The override is hoisted to the platform entry points
(ProjectEditorWindow.AppContent on desktop, ProjectRootActivity.Content
on Android) so a single ProjectThemeOverride wraps the NavigationRail,
ProjectRootUi, FAB stack, and confirm-close dialogs. ProjectRootUi and
ProjectRootFab no longer self-wrap.

All five project FABs (notes, encyclopedia, timeline, scene group,
scene) explicitly draw containerColor/contentColor from
secondaryContainer so the second color is the obvious accent on the
heaviest visible chrome.
2026-04-28 20:59:15 -07:00
Adam Brown
b1189d50dd
Add per-project synced settings (author, theme, word goal)
New project-data sync phase runs before entity transfer. The blob is a
single per-project row on the server, hash-checked for conflicts; the
client surfaces a per-field pick-a-side resolver UI when both sides
edited concurrently. Author name is a text field, custom theme uses a
clickable color swatch backed by skydoves/colorpicker-compose, and the
word-count goal is cadence + count. Each opt-in section nulls out its
data when unchecked so the wire signal is unambiguous.
2026-04-28 19:31:45 -07:00
Adam Brown
e3e9a4a6e5
Track per-device writing activity with client-side sync
Counts words written per save (multiset diff vs. an in-memory pre-edit
baseline so deletions don't reduce credit), rolls them into per-device
sessions logged at scenes/.activity/{deviceId}.toml, and syncs them as a
new step in the project sync pipeline. Server is treated as dumb
per-(project, deviceId) blob storage; client does GET → merge own slot
locally → POST. Cross-device conflicts can't happen because no device
ever writes another device's slot.
2026-04-28 16:08:43 -07:00
Adam Brown
f04a2001d4 Move entity hashing onto ApiProjectEntity itself
Collapse the two duplicate "given a complete ApiProjectEntity, compute
its hash" implementations into a single canonical fun hash() declared
on the sealed interface, with one implementation per subclass living
right next to the field declarations. Behaviorally equivalent - all
hash outputs are byte-for-byte unchanged because the new method bodies
call the same EntityHasher.hashX functions the wrappers used to call,
with the same arguments.

The hash impls now live in the same data class as the field
declarations, so adding a field forces the author to update the hash
method four lines below it. The fan-out hash bug class - which has
bitten us three times across this work - can no longer appear by way
of two parallel implementations drifting apart, because there is now
only one path for entity-shaped callers.

Production changes:
  - ApiProjectEntity: added abstract fun hash(): String to the sealed
    interface. Implemented per subclass (Scene, Note, TimelineEvent,
    EncyclopediaEntry, SceneDraft) by delegating to the corresponding
    EntityHasher.hashX function.
  - ServerEntitySynchronizer: hashEntity is now open with a default
    impl that delegates to entity.hash(). Subclasses inherit it.
  - 5 server synchronizers (Scene, Note, TimelineEvent, Encyclopedia,
    SceneDraft): deleted their hashEntity override and unused
    EntityHasher import.
  - EntityHasherExt.kt: deleted. Was a 53-line wrapper that was the
    source of the Tier 2 silent-failure bug.
  - ProjectEntityDatabaseDatasource and ProjectRoutes: three call
    sites switched from EntityHasher.hashEntity(e) to e.hash().

Test changes:
  - 25+ test call sites migrated from EntityHasher.hashEntity(X) to
    X.hash() across e2e tests, server-side synchronizer tests,
    EntityHashSensitivityTest, and EntityHasherExtTest.
  - Orphan EntityHasher / hashEntity imports stripped.
  - EntityHasherExtTest is preserved (now asserts entity.hash() equals
    a direct EntityHasher.hashX(...) call with the entity's fields as
    explicit arguments). The data-driven parity check still catches
    "hash impl swapped two argument bindings," which the structural
    EntityHashSensitivityTest does not.

Net diff: -78 lines across 19 files. 811 tests pass.

Layer 2 (the markForSynchronization assemblers in client repositories
that construct hash inputs from disparate local sources) is still
duplicated but is now defended end-to-end by the Tier 3 E2E tests. A
future refactor could collapse those by constructing transient
ApiProjectEntity instances and calling .hash(), at the cost of one
extra allocation per dirty-mark; deferred for now.
2026-04-27 20:26:20 -07:00
Adam Brown
6bfd47afb6 Cover and fix sync hash field-coverage gaps
Add Tier 2 server-side sync coverage. Two more hash bugs surfaced and
got fixed in the process - both of the same shape as the hash collision
caught by Tier 1: a serialized field on the DTO was silently absent
from the digest, so two clients with different values for that field
would never converge through sync.

Bugs fixed:

  EntityHasherExt.hashEntity (the wrapper used by
  ProjectEntityDatabaseDatasource.storeEntity to compute the hash
  persisted in the database) was not forwarding the new
  confirmedReferences, dismissedReferences, or aliases parameters to
  the underlying hashScene/hashEncyclopediaEntry calls. The
  per-synchronizer hashEntity overrides were correct, but the
  storage-path wrapper was not - so the server's persisted hash always
  disagreed with the client's hash for any entity with non-empty new
  fields. Data round-tripped fine via JSON, but every sync would
  report drift, trigger a re-download, and never settle. Fixed by
  forwarding the missing fields.

  EntityHasher.hashSceneDraft was not digesting SceneDraftEntity.sceneId
  (the FK linking a draft to its scene). This bug pre-dates the
  reference-index work and was found by the new sensitivity test on
  its very first run. Fixed by adding sceneId to hashSceneDraft (with
  a default of 0 for backward source compatibility) and threading it
  through the four call sites.

Tests added/strengthened:

  EntityHashSensitivityTest (new, server) - one test per
  ApiProjectEntity subtype that uses the @Serializable descriptor as
  the source of truth for "what fields exist," then asserts every
  field affects the hash. The descriptor coverage check is the
  forcing function: adding a field to a DTO immediately fails this
  test until the author declares a mutation for it, which then fails
  the sensitivity loop until the hasher is updated. No more silent
  passes from the parallel-paths-drift-together failure mode that let
  the EntityHasherExt bug ship.

  EntityHasherExtTest - added a SceneEntity with non-empty
  confirmedReferences and dismissedReferences plus an
  EncyclopediaEntryEntity with non-empty aliases. The pre-existing
  parity test was structurally correct ("extension call must equal
  direct call") but trivially passing because both sides used
  defaults. Now exercising real values, it would have caught the
  EntityHasherExt bug.

  ServerSceneSynchronizerTest, ServerEncyclopediaEntrySynchronizerTest
  - createNewEntity / createExistingEntity now include non-empty refs
  / aliases, so the inherited Hash Entity, Save Entity, and Load
  Entity tests automatically extend their coverage to the new fields.
2026-04-27 19:36:36 -07:00
Adam Brown
5198f4698f Cover sync hashing of new reference and alias fields
Add Tier 1 sync regression coverage for the reference index work, and
fix a hash-collision bug surfaced by the new tests.

Bug fix in EntityHasher.hashScene:
  Confirmed and dismissed reference sets were digested through identical
  d.update(ref, buf) calls with no boundary marker between them, so
  {confirmed=[7], dismissed=[]} hashed identically to
  {confirmed=[], dismissed=[7]}. A user confirming an entry then
  dismissing it would produce no hash change, the server would not
  detect a change, and other clients would never sync the transition.
  Each section is now prefixed with its size to delimit them
  unambiguously. (Side effect: this changes the hash for all scenes,
  including those with empty refs, forcing a one-time re-push on first
  sync after upgrade.)

Tests added:
  - EntityHasherTest: 6 cases for the silent-failure class - hashScene
    is sensitive to confirmedReferences and dismissedReferences, sets
    are order-independent, the two sets contribute distinctly,
    hashEncyclopediaEntry is sensitive to aliases, and alias order is
    significant (List, not Set).
  - SceneSynchronizerTest: verify ClientSceneSynchronizer.getEntityHash
    feeds metadata.confirmedReferences into the hash so refactors that
    drop the field are caught at test time.
  - ClientEncyclopediaSynchronizerTest (new file): verify reIdEntity
    calls referenceRemapper.remapEntryReferences after re-IDing the
    entry, defending the cross-entity sync wiring so encyclopedia ID
    conflicts during sync don't silently leave stale references in
    scene metadata.
2026-04-27 19:06:32 -07:00
Adam Brown
a85a634c6b Add data fields for encyclopedia entry references
Groundwork for tracking which scenes encyclopedia entries appear in.
Adds aliases to encyclopedia entries and confirmedReferences /
dismissedReferences to scene metadata, plumbed through sync DTOs,
hashes, and client/server synchronizers so the new state round-trips.
2026-04-26 22:55:47 -07:00
Adam Brown
cd1c491c0b Improved Server setup error messaging
Add client-side validation for email, password, and URL, along with shared validation utilities

Server error messages can now properly bubble up to the UI
2026-01-13 23:50:47 -08:00
Adam Brown
4ada7b3f14 Sync protocol can now heal stale server data 2026-01-12 14:26:40 -08:00
Adam Brown
a5b57ca23d Implement scene archiving!
Also dramatically improve test fidelity around client sync code
2026-01-11 21:41:06 -08:00
Adam Brown
aa63ed57f3 Lots of polish around Login and Server Setup
Explanitory callouts and such
2026-01-08 18:38:43 -08:00
Adam Brown
4554655345 Add client version check to about 2026-01-06 20:40:43 -08:00
Adam Brown
56dad1bf64 Migrate to Android KMP plugin 2025-12-21 11:51:40 -08:00
Adam Brown
2bb490aa3f
Frontend rewrite to Hmx (#435) 2025-12-19 01:28:28 -08:00
Adam Brown
e97d5dd344 Update Kotlin datetime and other libs 2025-12-01 22:20:36 -08:00
Adam Brown
47e076ca28 android.defaults.buildfeatures.buildconfig is deprecated 2025-05-29 01:17:51 -07:00
Adam Brown
2fa3029fc7 Use Kotlin STD Uuid 2024-10-06 15:37:20 -07:00
Adam Brown
b28ecb605c
At rest encryption (#367)
* Implemented initial at-rest encryption
* Add `loadHash()` ProjectDatabaseDatasource
* Added cipher to story_entity rows
* Implemented KDF for entity encryption key
* User existing hash rather than recalculate it
* Optimized `getUpdateSequence`
2024-10-06 15:15:24 -07:00
Adam Brown
df108cf659 Adding rename Project API endpoint
Adding tests for it, and I upgraded to JUnit 5 at the same time
2024-10-02 21:23:43 -07:00
Adam Brown
407177d231
Server entity database refactor (#350)
* Moving all data into the server database
* Server projects now have UUIDs
* Client is now sending the project server ID
* Implemented Projects sync E2E test
* E2E sync test now does uploading and downloading!
* JVM 21
2024-10-01 21:58:12 -07:00
Adam Brown
25738598e6 Finished extracting ProjectDatasource
Working on ServerEntitySynchronizerTests
2024-07-09 21:45:40 -07:00
Adam Brown
63361e351b Setting up server filesystem test 2024-07-04 00:23:26 -07:00
Adam Brown
fff324a6c0 Fix server not serving scenes
Older scenes previously saved on the server would fail to serialize because they were missing fields.
2024-06-17 21:15:06 -07:00
Adam Brown
82f0cbbac6 Gradle DSL update 2024-06-10 17:57:24 -06:00
Adam Brown
07fc5c2a71
Decompose 3.0 upgrade (#263)
* Remove parcelize
2024-05-25 22:38:30 -08:00
Wavesonics
dcf8c64655 iOS gradle tweaks 2024-01-14 23:44:51 -08:00
Wavesonics
f480fc4653 Added scene metadata to server sync 2024-01-13 02:32:49 -08:00
Wavesonics
78c9ba0fae Initial data migration implementation 2023-09-29 23:01:23 -07:00
Wavesonics
66cf647091 Upgrade Koin 2023-09-14 22:39:57 -07:00
Wavesonics
391c3fa818 KMP update 2023-09-08 00:37:44 -07:00
Wavesonics
992843fb91 Fixes for library upgrades 2023-09-07 23:34:47 -07:00
Wavesonics
28c14ae8cd Added About App screen 2023-09-03 18:23:51 -07:00
Wavesonics
432f531d8b Switched TOML libraries, hopefully it fixes #40 2023-09-02 00:41:24 -07:00
Wavesonics
5dd246d968 Encyclopedia tags work
Lots of it.
Add and remove tags on Entries. Click on tags to sort by them.
Tags now stored as Sets, and validated to avoid invalid characters
2023-09-01 20:52:06 -07:00
Wavesonics
21c681d11a Fix Encyclopedia edit failure toast
In doing so, I added a lot more test infrastructure
Added CResult, which is similar to SResult, and will be used in more places to report user facing error
2023-08-30 21:19:44 -07:00
Wavesonics
28114a3d7d Localized project sync log strings 2023-08-25 17:21:47 -07:00
Wavesonics
c9f5518381 Fixed SceneHash not including path 2023-08-22 21:08:23 -07:00
Wavesonics
0909caee0f API adminRoutes now translated 2023-08-18 12:11:24 -07:00