Skip to content

Fix frontend correctness & robustness issues found in code review - #13

Open
b-eyselein wants to merge 15 commits into
mainfrom
frontend-review-fixes
Open

b-eyselein wants to merge 15 commits into
mainfrom
frontend-review-fixes

Conversation

@b-eyselein

Copy link
Copy Markdown
Contributor

Fixes for 15 issues found during a review of the frontend (ui/src/), one commit per finding. No security issues — all findings are correctness/robustness bugs, several involving silent data loss.

XML editor (ui/src/xmlEditor/)

  • Invalid half-edit of a split word (faa9bf5f) — editing one half of a word spanning a line break into an invalid state no longer silently leaves the parent showing a stale, submittable result.
  • Header date UTC shift (742b5a0c) — event/merge dates in HeaderEditor no longer silently drift by the browser's UTC offset on save.
  • Dangling Hurrian concordance entry on delete (064b3542) — deleting a morphological analysis now removes its concordance attestation, matching the sibling update/toggle functions.
  • Incomplete isSelected() variant match (e2d75cee) — now exhaustive across all 6 MorphologicalAnalysis variants; previously silently dropped 4 of them from the concordance.
  • Bulk delete leaves stale cuneiform cache (16c95bc3) — multi-select delete now marks the preceding <lb> dirty for refetch, matching single-node delete.
  • Export mutated live state in place (9241ec6b) — reCountNodeNumbers now returns a renumbered copy instead of mutating the shared React-state tree.
  • Cuneiform refetch in render body (402207e9) — moved into a useEffect, with failure handling so it can't loop forever on a rejected fetch.

Rest of frontend

  • "Add line" wiped the merge column (b863f09b) — Array.prototype.splice mutates in place; the code was discarding the array by reassigning it to splice's return value.
  • Stale leftTxtId leaking across merges (8247f221) — reset at the start of each merge instead of persisting as module state.
  • Silent failure after XML download (e89d8d11) — DocumentApproval now awaits the approval mutation before downloading, and surfaces mutation errors in the UI.
  • No password-confirmation check on registration (8047e413) — added the same match check ResetPasswordForm already has.
  • Swapped diff colors (ca1a3b9c) — XmlComparator now colors added/removed lines consistently with the left/right legend.
  • Error banner styled as a button (7d64f426) — CreateManuscriptForm now uses the same message-banner class as every other form.
  • App crash on corrupted localStorage (93a0304a) — loadFromLocalStorage now falls back to the default value on a parse failure instead of crashing store initialization.
  • Query fired from render body (13b4536f) — Home.tsx and UserManagement.tsx now trigger their initial lazy query from a mount-only useEffect instead of directly in the render body.

Testing

  • npm run lint — clean.
  • npm run build — compiles successfully (full TypeScript type-check included).
  • Manual smoke-test of the XML editor and document merger recommended before merge, given how much of this touches interactive editing state.

🤖 Generated with Claude Code

https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3

Björn Eyselein and others added 15 commits September 1, 2026 10:19
onPartChanged only forwarded the parsed word to the parent when both
the pre-lb and post-lb halves parsed successfully; otherwise it just
logged to console and left the parent's state untouched. If the parent
had a valid parse from an earlier edit, it kept showing that stale
result with a working submit button even though the currently visible
textarea content was actually invalid - submitting would silently save
the old value. Now propagates a proper error result so the parent shows
the invalid state and withholds the submit button, matching how
WordContentEditor already handles single-part invalid input.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
isoDate() parses a datetime-local form value as local time and
converts it to UTC on submit, but the three date fields (event date,
merge date, nested-merge date) populated their defaultValue with a raw
slice of the stored UTC ISO string, displayed verbatim with no
local-time conversion. Submitting the form without touching the date
field re-interpreted the displayed UTC value as local time, silently
shifting the stored timestamp by the browser's UTC offset. Added
toDatetimeLocalValue() to convert to local time when populating the
field, so the round-trip through isoDate() is consistent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
deleteMorphology unset the mrp attribute directly without removing the
corresponding Hurrian concordance attestation, unlike toggleMorphology
and updateMorphology which both register a removal (deferred until the
global update button is clicked). For a Hurrian word with a selected
analysis already in the concordance, deleting the morphology removed
the XML attribute but left a stale attestation referencing an analysis
that no longer exists. Now registers the same deferred removeAttestation
call as the sibling functions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
…riants

isSelected() only handled 2 of the 6 MorphologicalAnalysis variants and
defaulted to false for the rest, unlike the exhaustive isSelected in
nodePostprocessing.ts. This function gates whether a selected Hurrian
analysis is added to the concordance, so for a word whose selected
analysis was one of the 4 unhandled variants (e.g.
SingleMorphAnalysisWithMultiEnclitics), it silently never got added.
Mirrored the exhaustive match from nodePostprocessing.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
deleteMarkedNodes (bulk delete) never set lbCuneiformDirtyPath, unlike
single-node deleteNode and initiateInsert, which both recompute the
preceding <lb>'s cuneiform when a 'w' node is added/removed. Bulk-
deleting one or more word nodes via multi-select left the preceding
<lb>'s cached rendered-cuneiform attribute stale, with no refetch
triggered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
…ting

reCountNodeNumbers mutated XmlElementNode attributes in place on
whatever tree it was given, including the live React-state tree passed
directly from XmlDocumentEditor's state.rootNode via writeXml. Because
object identity didn't change, React never re-rendered to reflect the
new numbering, and any other code holding a reference to the same
pre-renumbering tree (e.g. a diff view) could see inconsistent state.
It now returns a renumbered copy, and writeXml uses that copy for
serialization instead of mutating its input.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
The lb-cuneiform refetch ran unconditionally in the component's render
body instead of inside a useEffect, with no .catch. Any unrelated
re-render before the promise resolved could re-fire a duplicate fetch
for the same path, and a rejected fetch left lbCuneiformDirtyPath set
forever, so the component kept re-issuing the same failing fetch on
every render with no visible error. Moved into a useEffect keyed on
the dirty path, and cleared the dirty flag (with a logged error) if
the fetch fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
addLine reassigned firstLines/secondLines to the return value of
splice(), which is the array of *removed* elements - empty, since this
is an insert (deleteCount 0), not the mutated array. splice() already
mutates in place, so clicking "add line" on either side of the
document merger wiped the entire column instead of inserting one line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
leftTxtId is module-level mutable state written in mergeLine and read
in iterateTxtId, but was never reset at the start of a merge. If a
merge reached an unmatched left-only line before any mergeLine call
had executed in that session, leftTxtId still held a value left over
from a previous, unrelated merge performed earlier on the same page,
silently populating the newly exported line's txtid attribute with
stale data. Reset it at the top of mergeLines(), the single entry
point for a merge operation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
onExport downloaded the exported XML before awaiting the approval
mutation, and the mutation's error was destructured but commented out
- only console.error'd, never shown in the UI. If the mutation then
failed (network error, session expiry, FK violation), the executive
editor already had the downloaded file and saw no error, and could
believe the document was approved when server-side approval never
persisted. Now awaits the mutation first and only downloads on
success, and renders the mutation error if it occurs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
The Yup schema validated password and passwordRepeat independently
(length only) and submitted both values without comparing them, unlike
ResetPasswordForm which checks the two match before submitting. A user
who mistyped the repeat field got an account created with a password
that differed from what they believed they confirmed, with no error
shown. Added a .oneOf([yup.ref('password')]) check on passwordRepeat,
surfaced through the existing ErrorMessage for that field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
XmlComparatorContainer colors the left file's name red
(leftColorClass) and the right file's name green (rightColorClass).
diffLines(left, right) marks a line "added" when it exists only in
the right file and "removed" when it exists only in the left file, but
DiffDisplay had these swapped, so every diff rendered with
right-only lines in red and left-only lines in green - backwards from
the legend above it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
The mutation error was rendered with redButtonClasses (solid red
button styling) instead of redMessageClasses (the message-banner style
used identically by LoginForm, RegisterForm, ForgotPasswordForm, and
ResetPasswordForm), producing a visually broken/inconsistent error
display compared to every other form in the app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
loadFromLocalStorage called JSON.parse on stored data with no
try/catch, run synchronously during Redux store initialization for
every persisted slice, including the user auth slice. A corrupted or
non-JSON localStorage entry (partial write, manual edit, browser
extension interference) threw during configureStore's initial state
construction, crashing the entire app on load with no fallback - the
user couldn't even reach the login page without manually clearing
localStorage. Now falls back to the default value on a parse failure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
Home.tsx and UserManagement.tsx both invoked a GraphQL lazy query
directly in the render body (gated by `!query.called`) instead of
inside a useEffect. Running a query trigger as a side effect during
render violates React's render-purity rules; under React 18
StrictMode's double-invocation of component bodies in development, or
any re-render occurring before Apollo's `called` flag propagates back
into state, the query could fire more than once for the same mount.
Moved both into a mount-only useEffect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01528tdgcQt4KEUmzGX9RAm3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant