An accepted reword or insert now renders its replacement's inline
emphasis in the author's preview, so *word* reads as italic like the
rest of the manuscript instead of showing literal asterisks. The mobile
breakpoint targeted a class that is never rendered; point it at the real
paragraph class so justified text left-aligns on phones.
SceneDraftsDatasource.reIdScene moved the scene's drafts directory
unconditionally, so re-IDing a scene with no drafts threw
FileNotFoundException and failed the entire sync during ID conflict
resolution.
The no-email copy-link button now falls back to execCommand when
navigator.clipboard is unavailable (plain-HTTP self-hosting), instead of
throwing and copying nothing. Each scene-group's select-all button now
flips to 'Clear all' once its whole subtree is checked — matching the
master toggle — so it no longer silently clears those scenes while still
labelled 'Select all'.
Escape now dismisses just the frontmost layer, so closing a dialog opened
over an in-progress suggestion popup no longer also throws away the
popup's typed text. Popups and tooltips clamp by their actual half-width
instead of a fixed margin, so on a narrow phone they stay inside the
manuscript card rather than spilling off the edge. A selection endpoint
landing in inserted ink (which carries no source offset) snaps to the
nearest real text node, so text adjacent to existing ink can still be
selected.
A single in-flight guard serializes accept/reject and the bulk
accept-all/reject-all, so a fast second click can't race and leave the
chip showing the opposite of what the server stored. The commit dialog
can no longer be dismissed mid-POST into a detached node — it locks until
the response lands, then any dismissal reloads. Suggestion saves capture
their scene before awaiting (a response can't land in the wrong scene if
the editor switches mid-flight) and keep the popup open with the typed
text when a save fails. Failed saves, edits, and commits now show the
server's localized message instead of a generic toast.
The author review page, status POST, and commit POST now verify the
review actually belongs to the project named in the URL, so one of a
user's reviews can't render or be acted on under a different project's
URL. The public reviewer page does a single token lookup reused for the
owner redirect, the friendly error kinds, and the open transition (was
two or three hashes), and the panel cards fetch every review's scene
progress in one grouped query instead of one per card.
Links in review and password-reset emails came from the request's
scheme/host/port, so a reverse proxy produced internal links and anyone
holding a review token could poison the author's notification email with
a forged Host header. A new optional publicUrl server config wins over
the request origin. Also: review dates format with an explicit English
locale instead of the JVM default, the invite's plaintext body is built
line-by-line (a note at column zero defeated trimIndent and left literal
tabs everywhere), and the two review mailers share one bundle/template
helper with the same English fallback the pages use.
The review keys exist only in the English bundle so far, so any visitor
with a non-English locale got a MissingResourceException 500 on the
review page — and on the story page once a review existed. Missing keys
now fall back to English, and withMessages overlays the locale's
translations on the full English key set so untranslated keys render in
English instead of as blank template vars.
A delete or reword spanning an existing insert caret corrupted the
committed text (the right-to-left splice applied the inner insert first,
then the outer edit swallowed it and resurrected part of the deleted
range) — both sides now reject the pair at creation. Same-position
inserts commit in their display order via an id tiebreaker, and inserted
words get a trailing smart space so they don't glue to the next word.
Sync session slots are now claimed atomically (claimSession replaces the
check-then-create in begin-sync and review minting), the internal session
is kept alive across long mint loops, and every review status UPDATE is
guarded on the current status so a racing revoke, submit, or resolve can't
be silently overwritten. Commits run entirely inside the exclusive session
with a re-check, mint all drafts before overwriting any scene, and a failed
request creation cleans up after itself.
Generous server-side limits on the anonymous token endpoints (replacement
10k, reason 5k) and the request dialog (note 2k, label 100), mirrored as
maxlength on the inputs. Comments must carry text at creation, matching
the existing edit rule.
The very first time a review link is opened, a welcome explains what this
is and how to mark up — with the key point highlighted: nothing reaches
the author until Submit Revisions. Expired and revoked links now get
specific pages that name the author and story (and say what to do next)
instead of the generic broken-link card.
A 'Mark scene as done' toggle at the end of each manuscript records the
editor's progress (and advances to the next unread scene); done scenes get
a check in the scene rail. While the review is out, the author's panel
card shows 'N of M scenes read' with a slim progress bar.
The person marking up the manuscript is an editor, not a reviewer (in
publishing, a reviewer critiques the finished book). The exchange itself
stays an editorial review, since it is the author who reviews the edits.
The submitted-notice email now reads like publishing too: your manuscript
is back, with the editor's edits.
In author mode, hovering a suggestion's ink pops a small card above it:
the reviewer's reasoning, plus Accept/Reject (Resolve for comments) while
the suggestion is undecided. The tooltip survives the mouse crossing onto
it via a short grace timer, and its arrow bridges the hover gap. Decided
ink shows just the note; the undo stays on the gutter card.
Following the capability link with the author's own session no longer
impersonates the reviewer (or marks the request opened): it redirects to
the author review page once submitted, or to the story page while the
review is still out. Other sessions and anonymous visitors get the
reviewer editor as before.
The story-page panel links submitted reviews to a session-authenticated
page that reuses the manuscript renderer: pending suggestions keep their
pen ink while decided ones preview the outcome (accepted rewords read as
tinted real text, accepted deletes ghost out, rejected inks fade away).
Cards carry Accept/Reject buttons with undoable status chips plus
accept-all/reject-all, and Apply Revisions confirms with a tally before
reporting the per-scene outcome. Submitting a review now also emails the
author a link to the page, and the reviewer's submit dialog names them.
Authors can accept/reject each suggestion (resolve for comments) while a
review is submitted. Committing applies the accepted edits to each scene
snapshot, mints the revised text as a new draft inside the exclusive sync
session, and overwrites the working scene through the synchronizer when it
still matches the snapshot; diverged scenes keep the draft for Draft
Compare. The request is then marked resolved.
The native confirm broke the manuscript feel. Submitting now opens the
design-system dialog with the suggestion tally and a Submit & Lock
action, dismissible by cancel, overlay click, or Escape. The masthead
button reads Submit Revisions.
Delete committed instantly from the selection menu, so there was no
moment to say why. It now opens the reason-only form (reason optional,
save always enabled), matching the other suggestion types.
Tapping ink now highlights and scrolls to its gutter card and vice
versa, instead of jumping straight into editing; each card grows a
pencil action that opens the prefilled edit popup, next to the existing
remove action.
Also fixes selections that silently produced no popup: real drag
selections often end on element nodes at styled-run boundaries, and the
handler only accepted text-node endpoints. Endpoints now resolve to the
nearest text position, and the mouseup listener moved to the document so
releasing a drag outside the manuscript still offers the menu.
The tiled strike background restarted its angled stroke every 140px,
which read as a sawtooth of short disjoint lines across long deletions.
Stretch a single line across each box fragment instead, with
box-decoration-break: clone repainting it per wrapped line — every
physical line of struck text gets exactly one straight pen stroke.
Position the caret absolutely from a collapsed Range's rect, like the
selection popup, rather than splitting a text node to host an inline
marker. The manuscript DOM stays untouched, which removes the
source-offset re-registration the split needed.
Clicking to insert (or choosing Insert after from a selection) now
renders a blinking green caret at the exact spot the insertion will
land, removed when the popup closes or the suggestion saves. Splitting
a text node to place the caret registers the tail at its own source
offset so selection mapping stays exact.
Hovering inked text rings and lifts its gutter card; hovering a card
rings the ink in the manuscript. Both use the suggestion's own type
colour via a CSS variable, so a red delete links in red and a blue
reword in blue.
Tapping a suggestion's ink in the manuscript or its gutter card now
opens the form popup over the ink, prefilled with the current values:
rewords and inserts edit their replacement and reason, comments edit
their text, and deletes get a reason field they previously had no way
to set. Saving posts to a new tokenized update endpoint that keeps the
anchors untouched, validates per type, and refuses edits once the
review is submitted.
Building the strings island by hand in the template ran every string
through Mustache's HTML escaping, so apostrophes surfaced as ' in
confirm dialogs, and a quote in any translation would have broken the
JSON. The server now encodes the strings map with the JSON serializer
like the review data. Both islands also escape '<' so user content
containing </script> can't break out of the script element.
The manuscript now renders inline markdown emphasis (bold/italic, both
marker styles, nested) instead of showing raw markers. A conservative
parser in review-logic.js turns each paragraph into styled display runs
that remember their source positions; rendered text nodes register their
source offset so selections over styled text still anchor suggestions in
raw source space, exactly as before. Pen-ink strikes and comment
highlights span styled runs, and gutter quotes show marker-free text.
Removing a suggestion now asks for confirmation first.
The editor's three-column layout (scene nav, manuscript, gutter) was
constrained by the 860px container sized for the old read-only page, so
the manuscript was squeezed to a narrow ribbon. Widen the container and
manuscript so the prose reads at a comfortable measure with the nav and
gutter flanking it.
The tokenized review page is now an interactive editor. The server sends
each scene's paragraphs and existing suggestions as a JSON island;
review.js renders the manuscript with pen-ink markup (varied strike
angles, handwritten replacements, yellow-marker comments) and a gutter
of suggestion cards. Selecting text offers reword/delete/insert/comment;
clicking places a caret to insert. Each suggestion autosaves through new
tokenized endpoints (create/delete/submit) that validate offsets against
the immutable snapshot, reject overlaps, anchor to paragraph-local
offsets, and flip the request to In Progress on first edit. Submit locks
the review. Suggestion logic is the unit-tested review-logic.js; the
endpoints have e2e coverage.
Introduces review-logic.js: DOM-free suggestion logic (segment
splitting, overlap guards, smart-spacing, applying accepted edits) plus
deterministic per-suggestion pen-ink strike styling, loadable both as a
browser script and a Node module. Unit-tested with Node's built-in
runner via a new :server:jsTest Gradle task, wired into CI as an
explicit gate with Node set up in the build job.
The shared dialog component uses a translucent fill and a 50%-opacity
actions bar (a frosted-glass effect). The review dialog is tall enough
to overlap the page footer when scrolled, so that translucency let the
footer's icons and dark background show through. Give this dialog and
its actions bar fully opaque backgrounds; other dialogs are short and
centered, so they keep the frosted look.
Adds a sticky 'All scenes' master toggle at the top of the scene picker
that selects or clears every scene at once, with its label flipping to
'Clear all' when everything is selected. Fixes a horizontal scrollbar
that appeared once the dialog body gained a vertical scrollbar: the
two-column email/label row needed min-width:0 so its inputs shrink with
the dialog instead of overflowing.
The dialog's stopPropagation kept clicks from reaching the
document-level delegated handlers, so the group toggles never fired;
closeReviewDialog already ignores bubbled inner clicks, making it
redundant. Dialog width now 92% like other dialogs instead of touching
the viewport edges. Argless i18n keys aren't run through MessageFormat,
so doubled apostrophes rendered literally.