Commit graph

87 commits

Author SHA1 Message Date
Adam Brown
d1ea053c28
Bake the changelog into the app instead of checking GitHub on load (#850)
The app called the GitHub releases API from ProjectSelectionComponent.init
to decide whether to show an "update available" dialog. That is an
unprompted network request before the user has done anything, which has
been raised as a privacy concern.

The release notes are already in the repo at release time, so bake them
in instead. prepareForRelease writes the new CHANGELOG.md entry to
common/src/commonMain/composeResources/files/changelog.md and commits it
alongside the version bump, so the resource cannot drift from the version
in libs.versions.toml.

On load, ChangelogRepository compares the baked entry's version against
lastSeenChangelogVersion in global settings and shows a "What's New"
dialog when they differ. Dismissing records the version. Fresh installs
are seeded as already-seen, so only upgrading users get the popup;
the old lastDismissedUpdateVersion key is dropped, which is safe because
the TOML serializer ignores unknown keys.

About drops its update check entirely and gains two buttons: Changes,
which reopens the dialog with no network, and GitHub Release, which opens
the browser.

VersionCheckRepository stays for the protocol mismatch dialog, which only
appears after the user has connected to a sync server and exists to tell
them which version to get. It keeps its automatic check and is now that
repository's only consumer.

Links in the notes are clickable via linkifyChangelog, which handles
[label](url) and bare urls, including urls containing parentheses. It is
deliberately not a markdown renderer: entries lead with [New] / [Fix]
tags that a real parser mangles.
2026-08-04 19:04:59 -07:00
Adam Brown
1b312bc3b2
Allow a project to set the language it's written in (#838)
* Add per-project language setting (#754)

An optional BCP-47 language on ProjectData, picked from a searchable
list of all platform locales in project settings. New projects default
to the device locale; the Alice example project is en-US.

Spell check is gated per project: when the project language does not
leniently match the dictionary locale, the dictionary is withheld
(ProjectSpellCheckRepository) and project settings explain why.

The public story page emits <html lang> and JSON-LD inLanguage from the
declared language, and EPUB export prefers it over the device locale.
The hasher contributes zero bytes when unset so existing sync hashes
stay stable.

* Fix review findings in the project-language feature

createProject now only seeds the default language for genuinely new
projects (seedDefaultLanguage), so account sync materializes server
projects with the never-synced baseline intact, and the seed is
language-only so it cannot gate spell check against a same-language
dictionary. The hasher's language block gets a -1 marker plus length
prefix so it can never collide with a tags block, and the initial
write goes through the shared saveStoredProjectData path.

The Locale type now retains the script subtag, keeping zh-Hans/zh-Hant
style locales distinct in the picker. The picker's clear row is pinned
above the list so it survives an empty search, watchSpellCheckAllowed
delivers on the main dispatcher, and the public story page hashes the
stored project-data hash into its validator instead of parsing the
blob per request, applying the language override after withDefaults so
chrome links keep the viewer's locale.

* Enforce single-owner persisted formats

The tags write in PromoteIdeaUseCase rewrote project_data.toml from
scratch, erasing the language seed createProject had just written: the
exact hazard of a second inline writer. It now read-modify-writes
through the datasource's scope-less helpers, and ProjectsListComponent's
hand-rolled reader delegates to a new blocking readStoredProjectData.

The rule is written down (ARCHITECTURE.md hard constraint 7, CLAUDE.md)
and enforced by PersistedFormatOwnershipTest, which fails the build when
raw TOML I/O appears outside a Datasource file. Migrators are exempt by
role; the two remaining legacy offenders are allowlisted as a burn-down
that can only shrink.

* Burn down the last raw TOML I/O outside datasources

ProjectStatisticsCacheReader now delegates to a scope-less
readProjectStatistics helper in StatisticsDatasource, and the example
project's fabricated activity log goes through writeDeviceLog in
WritingActivityDatasource, which also becomes the single owner of the
.activity path convention.

With no offenders left, PersistedFormatOwnershipTest drops its
burn-down allowlist entirely: only Datasource files and migrators may
touch persisted TOML formats from here on.

* Pass seedDefaultLanguage in the Android instrumented-test harness

* Pass seedDefaultLanguage in the round-trip sync HeadlessClient
2026-08-03 22:22:00 -07:00
Adam Brown
70d902894c
Add optional Terms of Service gate for account creation (#742)
* Add optional Terms of Service gate for account creation

Self-hosters can set an undocumented `termsOfService` path in ServerConfig
pointing at a plaintext file. When set, account creation is gated: the server
answers POST /api/account/create with 451 + the TOS text and a content-hash
version. The client shows a scrollable dialog; accepting resubmits with the
accepted version, declining discards the provisional server settings.

Disabled by default (null path); existing servers are unaffected.

* Cover TOS repository and 451 client handling; harden error-body parsing

Adds direct tests for TermsOfServiceRepository (FakeFileSystem: absent/missing/
blank/populated file, stable and content-derived version) and ServerAccountApi
(201 success, 451 -> TermsOfServiceRequiredException, malformed 451 -> default
failure).

Also broadens error-body parse handling: ktor raises ContentConvertException
(not kotlinx SerializationException) on malformed JSON, so both the create-account
451 path and the shared defaultFailureHandler now catch it and fall back to a
graceful failure instead of letting it escape as an unhandled coroutine exception.

* Fail fast when termsOfService points at a missing or blank file

A configured but unreadable/empty TOS path previously made challenge() return
null, silently disabling the terms gate and letting accounts be created with no
terms at all. resolveServerConfig now validates the path at startup and aborts
(as it already does for unparseable config), so a misconfiguration can't quietly
drop the legal gate.

* Resolve a relative termsOfService path against the config file's directory

A bare `termsOfService = "tos.txt"` previously resolved against the server's
working directory, so a terms file sitting next to config.toml wasn't found.
Relative paths now resolve against the config file's own directory; absolute
paths are unchanged.
2026-07-16 21:02:48 -07:00
Wavesonics
ded3dcd504 Slim down DI bootstrap/dispatcher doc comments 2026-07-15 15:56:58 -07:00
Wavesonics
6f3051fa8d Fix iOS build: move qualified dispatchers to a plugin-less module
Under compileSafety=true the Koin compiler plugin emits a synthetic
dsl_single hint for every definition in a plugin-processed module{},
encoding the named() qualifier as a value-parameter NAME. Kotlin/Native's
IdSignature ignores parameter names, so the three same-type qualified
CoroutineContext dispatchers (main/default/io) collapse to one signature
and fail klib serialization with a SignatureClashDetector AssertionError.

Move the three dispatcher definitions into a plugin-less module in :base
(dispatcherModule) and include it from mainModule. No hints are generated
for them, so the clash is gone and compileSafety stays on for every target
including iOS. All dispatcher consumers use runtime inject(named()), never
auto-wiring, so nothing the plugin validates depends on these definitions;
the DISPATCHER_* constants stay in :common so no consumer changes.
2026-07-15 11:57:44 -07:00
Adam Brown
1fe142725b Migrate :common DI to Koin compiler plugin DSL with compile-time safety
Adopt the Koin Kotlin compiler plugin (io.insert-koin.compiler.plugin 1.0.2)
and convert :common's DI from classic DSL to the plugin DSL
(org.koin.plugin.module.dsl), enabling compile-time DI graph validation
(compileSafety = true) while keeping the centralized module{} layout.

- Convert singleOf/factoryOf/scopedOf(::X) -> single<X>()/factory<X>()/scoped<X>()
  across mainModule (root + ProjectDefScope block), migratorModule, and the
  android/desktop/ios platformModule + exampleProjectModule actuals.
- Provider functions and when/config bodies use create(::fn) to keep auto-wiring
  and validation; explicit-lambda and named() bindings stay classic.
- Add koin-annotations (BOM-managed) for @Provided; mark SandboxFileAccess
  @Provided (its definition lives in the desktop app module, external to :common).
- Relocate the iOS startKoin into a plugin-less bootstrapKoin() helper in :base so
  common's iOS compilation is no longer a full-graph (A3) aggregator.

Green with compileSafety=true on desktop, android, common metadata, and
desktopTest (1404 tests). iOS/Native is blocked by an upstream plugin bug: the
generated dsl_single hint functions encode named() qualifiers as parameter names,
which K/N's IdSignature ignores, so same-type qualified definitions (the three
CoroutineContext dispatchers) clash during klib serialization. Documented inline.
2026-07-14 23:37:54 -07:00
Adam Brown
94dae03333 Update repo references to Darkrock-Studios org
The repository moved from github.com/Wavesonics/hammer-editor to
github.com/Darkrock-Studios/hammer-editor. Update all URLs across
source, build scripts, web templates, docs, store metadata, and
test fixtures.
2026-07-13 16:31:11 -07:00
Adam Brown
b4e4a08702
Story Ideas: account-level idea capture with sync (#720)
Quick-capture story ideas as tagged markdown blobs in a new Project
Selection tab, stored one file per idea in .ideas/ and promotable into
a project. Offline-first; syncs as a phase inside the account sync
session (shape-agnostic server storage, hash-baseline conflicts,
tombstone/outbox deletion, ideasStateHash skip for unchanged sets).
Unifies idea + project tag suggestions behind AccountTagService.
2026-07-05 01:49:04 -07:00
Adam Brown
54f04c99ef Add Project Tags with tag search on the project list
Projects can now be tagged in Project Settings, with suggestions drawn
from the user's other projects. Tags show as chips on project rows, and
a new search bar on the project list filters by name and #tag using the
same query syntax as Global Search (parser extracted to a shared
data/search module). Conflict resolution gets a Tags row, picked as a
unit like the other project-data fields.

Sync safety:
- Tags hash with zero bytes when empty, so all existing hashes (synced
baselines and server rows) stay byte-identical; golden-pin tests
enforce this.
- The server now stores project data as an opaque blob with a
client-supplied hash (like entities), validating only that the
payload decodes; undecodable rows heal via re-upload. Adding fields
to ProjectData no longer requires server changes.
- Fast-forward records the hash of what was actually stored, so an
out-of-date client can no longer strip and delete fields a newer
build added. Documented in SYNCING-PROTOCOL.md.
- HAMMER_PROTOCOL_VERSION bumped to 3: older servers decode project
data destructively and would silently drop tags.

UI: the redundant projects-page heading is removed; search reveals via
a masthead toggle. New design-system pieces: HdClearGlyph and
HdCollapseGlyph (drawn glyphs, replacing misaligned text "×" and the
ambiguous double-X in search strips) and an HdSearchRow molecule now
shared by all four searchable screens.
2026-07-03 23:12:04 -07:00
Adam Brown
f62e4f577b
Merge pull request #681 from Wavesonics/fix/527-moved-paragraph-edge-cases
feat: detect moved paragraphs in prose diff (blue highlight)
2026-06-27 00:03:58 -07:00
Adam Brown
bddc3cb9a3 Fix moved-paragraph diff edge cases (swallowed edits, degenerate LIS)
Rework the move-detection post-pass into a single paragraph-level pass and
replace the overlap-based stable/moved gating with proper range subtraction.

- A hunk that merged a moved paragraph with an adjacent edit now surfaces the
  edit: delete/insert spans are the hunk minus the moved/stable ranges, instead
  of dropping the whole hunk when it merely touched a stable/moved paragraph.
- A fully reordered match set (LIS backbone of one) is treated as all-moved, so
  a swap like A,B <-> B,A marks both paragraphs moved instead of electing one as
  a spurious stable anchor.

Drops the separate pure-hunk detection path in favor of the unified detector.
2026-06-26 23:47:25 -07:00
Adam Brown
efb0f62c41
Identify projects by projectId in sync API URLs (#679)
Replace the project name in sync endpoint paths with the server-issued
projectId. The server resolves the ProjectDefinition from the DB by id;
the create endpoint takes the name as a query param. Updates client APIs,
tests, and the syncing protocol doc.

* Harden sync projectId refactor: protocol bump, 410 for missing project, path encoding

- Bump HAMMER_PROTOCOL_VERSION so version-mismatched clients fail fast via the
  protocol gate instead of hitting silent 404s on the renamed routes.
- requireProjectDef responds 410 Gone (not 404) for a missing project, so the
  download_entity client can't mistake a vanished-mid-sync project for a deleted
  entity and silently abandon undownloaded entities.
- Re-apply encodeUrlPathSegment to the projectId path segment and restore the
  path-encoding test so a reserved character can't escape the URL template.
2026-06-26 22:21:33 -07:00
Adam Brown
6db8d8936d Wire moved highlight through conflict UIs and tidy diff internals
- Thread movedStyle into SceneConflict and SceneDraftConflict so relocated
  paragraphs render blue instead of falling back to delete/insert; make the
  style a required argument so it can't be silently dropped again.
- Add a semantic `moved` color to HammerExtendedColors (theme-keyed, dark-safe)
  and drop the hard-coded Color(0xFF3A5FA0) duplicated across two files.
- Extract the duplicated split-highlight effect in DraftCompareUi into a shared
  DiffHighlightEffect.
- Hoist the whitespace Regex to a single instance and compute each hunk's
  move/stable overlaps once in the emit loop instead of rescanning.
2026-06-26 00:20:29 -07:00
Matt Van Horn
9ad403993a
feat: detect moved paragraphs in prose diff 2026-06-25 02:08:00 -07:00
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
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
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
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
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
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
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
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