Skip to content

Release v2.5.0-bv2#14

Open
Che4ter wants to merge 177 commits into
bv-mainfrom
release/2.5.0-bv2
Open

Release v2.5.0-bv2#14
Che4ter wants to merge 177 commits into
bv-mainfrom
release/2.5.0-bv2

Conversation

@Che4ter

@Che4ter Che4ter commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Post-merge steps

  • Tag bv-main with v2.5.0-bv2 to trigger the AKS prod deploy pipeline (manual approval gate)
  • Delete release/2.5.0-bv2 branch

fatpeppapig and others added 30 commits May 22, 2026 14:54
[FIX] Fixed order of authentication methods for timeline filter endpoint.
Frontend (old jQuery/Vite UI):
- Add Milkdown (Crepe) WYSIWYG editor toggle alongside the ACE markdown
  editor in case notes, with inline image rendering and drag-to-resize.
- Route pasted/dropped images through the existing get_extension_from_mime
  + upload_interactive_data flow so file extensions are preserved.
- iris<->milkdown markdown adapters (fence-protected); dirty-tracking,
  silent save, collaboration guard (disable GUI when others are viewing).
- vite base '/static/' so chunk URLs resolve; add @milkdown/crepe dep.

Backend (DOCX report export):
- IrisDocxGenerator: promote whole-paragraph {{x|markdown}} out of w:t runs
  and render markdown images as inline drawings, producing Word-valid OOXML
  (fixes "Word can't open the file" from block elements inside a w:t leaf).
- validate_docx_text_leaf_nodes schema guard on generated reports.
- for_docx image preprocessing: absolutize datastore image links, carry
  width via iriswidth, scale to a % of the page text width; convert <br/>
  to newlines so they don't render as literal text.
- Thread for_docx flag through cases_export_to_json / reporter paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the case-notes Milkdown integration on the case SUMMARY editor
(cases.description), alongside the existing ACE markdown editor.

- case.summary.js: GUI/ACE toggle reusing window.IrisMilkdown; routes saves
  through the existing CRC32-checked /case/summary/update path. On exit
  (Close editor or toggle-back) Milkdown's markdown is always copied into ACE
  and a save is flushed; sync_editor() CRC-dedupes so an unchanged summary
  produces no POST and Crepe's markdown normalization is a one-time event.
- Single-editor semantics: GUI entry is blocked while a collaborator is
  actively editing (ACE delta collaboration is real-time, Milkdown can't
  consume the deltas); a remote save won't clobber unsaved GUI edits.
- A short post-create "settle" window + dirty flag throttle the autosave
  timer so the initial normalization isn't autosaved.
- case.html: GUI toggle button (shown when the editor is open), Milkdown
  container/root, milkdown_editor.css link, milkdown_editor.js module script.
- nginx CSP: add `font-src 'self' data:` so the code-block editor font
  (embedded data: URI) loads instead of being blocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bute flattening

DOCX export (docx_report_generator.py):
- Override MarkdownImageDocxRenderer.render_block_code to syntax-highlight fenced
  code blocks with Pygments instead of a single grey run. Emits one coloured OOXML
  run per token (adjacent same-colour tokens merged), reusing the template's
  monospace inline_code run props and inserting <w:color> in schema-safe order via
  lxml. Falls back to the plain renderer when there is no lexer or no colour, caps
  very large blocks, expands tabs, and trims the fence's trailing newline. Output
  stays Word-valid (text-only <w:t> leaves).

Custom attributes (marshables.py):
- Add a custom_attributes_merge post_load hook to CaseSchemaForAPIV2 mirroring
  CaseSchema, so v2 API case writes rehydrate flattened {tab:{field:value}} input
  into the full definition structure (type/value/mandatory/options) via
  merge_custom_attributes. Without it the v2 API persisted the flattened shape
  directly, which dropped the field metadata and made the attribute tabs render
  blank. Payloads without custom_attributes pass through untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y empty cells

Two table issues in the Word export, both from Milkdown-authored tables:

- Empty cells (report_db.py): Milkdown serialises an empty table cell as <br/>.
  The for_docx preprocessing converted <br/> to '\n' on every line, which injected
  a newline mid-row and shattered the GFM table (it parsed as a paragraph) whenever
  any cell was empty. On table-row lines (start with '|') collapse <br/> to a space
  instead, so the cell stays empty and the row stays intact -- tables now render
  whether cells are populated or not.

- Compressed tables (docx_report_generator.py): the base renderer emits tblW=auto
  with a single grid column, so Word shrank tables to their content. Override
  render_table to emit each markdown table at 100% width (tblW pct 5000), fixed
  layout, with one equal grid column per markdown column, keeping the ReportMain
  style and explicit borders. Only markdown-rendered tables are affected; the
  template's own layout tables are left untouched. Output stays Word-valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n WYSIWYG), remove ACE

Phase 1 — replace the ACE editor on case NOTES with a playground-style dual pane:
editable CodeMirror markdown source on one side, live editable Milkdown (Crepe)
WYSIWYG on the other, two-way synced. Edit either side; the other updates.

- milkdown_editor.js: add setMarkdown() as a SILENT, no-history full replace
  (parserCtx + editorViewCtx + Slice, tr.setMeta('addToHistory', false)) so the
  async/debounced markdownUpdated listener skips programmatic updates — no feedback
  loop, no undo-history pollution. (Not replaceAll, which would emit + enter history.)
- milkdown_split.js (NEW) window.IrisSplitEditor: a SplitEditor controller mounting a
  CodeMirror 6 source pane + the Milkdown pane, with origin-lock bidirectional sync
  (source→milkdown debounced ~250ms via setMarkdown; milkdown→source on markdownUpdated),
  flush() (applied before every canonical read), getMarkdown()/setMarkdown()/destroy()/
  focus(), draggable divider, Split/Source/Preview view toggle, focus-class handling,
  and narrow-screen reset. getMarkdown() flushes then returns Milkdown's canonical markdown
  (single source of truth for saving).
- case_notes_v2.html: replace the ACE editor + showdown preview + markdown toolbar + GUI
  toggle with the split markup (#note_split / #note_source / #note_divider / #milkdown_root,
  .iris-view-toggle, pane labels), data-theme on .note-body, #btn_save_note, split module script.
- case.notes.js: drive note_split instead of ACE; flush+silent-save before note switch;
  save/download/sync/revision-revert use canonical Milkdown markdown; revision preview is
  read-only showdown HTML; remove ACE delta collaboration (keep presence + save-conflict);
  remove ACE-only paths (edit_innote/toggle_max_editor/restore_ace_view/toggle_editor_mode).
- case_notes_event_handlers.py: fix notes presence (emit note_id on join-notes; add leave-note).
- milkdown_overrides.css: split-pane visual design (panes/divider/labels/focus tint/view
  modes/dark+light/responsive), scoped so the source CodeMirror never leaks into Crepe's
  code-block CodeMirror.

Built by Codex (code) + Kimi (UX/CSS) under orchestration; reviewed via CodeRabbit +
Playwright (two-way sync, no loop, toggle, save-persist, DOCX round-trip, 0 console errors).
Collaboration stays presence + save-conflict; real-time Yjs co-editing is Phase 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ump on edit

- milkdown_overrides.css: bound #note_split height (max-height: calc(100vh - 200px))
  and give each pane its own overflow-y, so the source (CodeMirror) and Milkdown panes
  scroll INDEPENDENTLY like the Milkdown playground instead of the whole page scrolling
  them together (they never match height once images differ).
- milkdown_split.js: stop the source pane jumping to the top while typing in it. Milkdown's
  markdownUpdated is async/debounced, so the synchronous origin flag couldn't guard the
  delayed callback — handleMilkdownChange now also skips the source rewrite when the source
  editor is focused (or the content is unchanged), and replaceSourceDoc preserves scrollTop +
  selection (clamped) for genuine WYSIWYG-origin updates. Two-way sync preserved.

Verified via Playwright: independent scroll (scrolling one pane doesn't move the other),
source edit keeps its scroll position, source<->Milkdown sync both directions, no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch the code-block syntax-highlight palette from 'default' to 'xcode' so exported
code reads like a light IDE theme (magenta keywords, red strings, green comments, blue
numbers) on the white page — closer to the editor's look while staying print-friendly.
Verified: export still Word-valid (0 <w:t> leaf violations).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ode blocks

Crepe's code-block language list was unset (empty). Configure
featureConfigs[Crepe.Feature.CodeMirror].languages with the standard
@codemirror/language-data registry plus a lightweight KQL/Kusto StreamLanguage
(keywords, tabular/scalar operators, built-in functions, // comments, strings,
numbers + timespans, pipe/operators). Registered as LanguageDescription name
"Kusto" with aliases kql/kusto, so analysts can pick Kusto in the code-block
language picker and ```kql / ```kusto fences highlight in the editor. Round-trips
as ```kql, which the DOCX export already highlights via Pygments' Kusto lexer.
Shared milkdown_editor.js, so notes (now) and the summary editor (Phase 1.5) both
get it. ImageBlock upload config preserved.

Verified (Playwright): code block recognized, syntax highlighting active, round-trips
as kql, build clean, no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (Phase 1.5)

Give the case SUMMARY the same true split-pane editor (CodeMirror source <-> Milkdown
WYSIWYG) as notes, by generalizing the split editor into a reusable, instance-agnostic
component and integrating it into the summary page.

- milkdown_split.js: IrisSplitEditor.create() now takes EXPLICIT refs
  {container, sourcePane, previewPane, divider, viewToggle, ...} instead of hardcoded
  note_* / global view_* ids; the view toggle may live outside the container and its
  buttons resolve via [data-view]; inner mount divs renamed to
  .iris-split-source-mount / .iris-split-preview-mount; listener cleanup + all behaviour
  (sync, silent setMarkdown, flush, divider drag, view modes, focus, source-jump fix) kept.
- milkdown_overrides.css (Kimi): generalized #note_* selectors to .iris-split* classes and
  moved theming to a .iris-split-shell[data-theme] wrapper, so both editors are styled;
  source CodeMirror rules stay scoped to .iris-split-source; reusable shell height that
  works full-page (notes) and inside the summary card.
- case_notes_v2.html / case.notes.js: notes markup carries the shell + pane role classes +
  data-view toggle; notes passes explicit refs to create(). No behaviour change.
- case.html / case.summary.js: summary gets the split in EDIT mode wrapped in
  .iris-split-shell; the read-only showdown preview (#ctrd_casesum/#targetDiv) stays for the
  CLOSED state; edit_case_summary toggles open/close (create on open; flush+save+destroy on
  close). Full ACE sweep. sync_editor keeps CRC32 conflict detection but reads the flushed
  summary_split.getMarkdown() and applies remote via setMarkdown(), with applyingRemote /
  unsaved-edit guards so a remote save can't clobber local edits. Presence + save-conflict
  kept; ACE delta collaboration dropped (Phase 2 = Yjs).

Built by Kimi (CSS) + Codex (code) under orchestration; reviewed by me via Playwright
(notes regression + summary split: open/sync both ways/save-persist/closed read-only
preview, 0 console errors). User confirmed it works. CodeRabbit review of the diff pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…abbit)

Address CodeRabbit findings on the Phase 1.5 summary integration (case.summary.js):
- sync_editor() now returns a promise that settles when the update AJAX actually
  completes; close_summary_split() awaits the real save and keeps the split editor
  OPEN (notify) if the save fails, instead of destroying it mid-flight (no lost edits).
- On a failed/non-success /case/summary/fetch, reset the #last_saved indicator and
  notify, so the UI no longer hangs on "Syncing..".
- wait_for_split_editor() times out after 10s (notify) so opening can't hang forever;
  open_summary_split() resets its in-flight flag on failure.
- Guard error.responseJSON.message with a fallback (no crash on network/non-JSON errors).

Verified: summary save still persists, build clean, no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rder

The grey code-block background came from the template's 'Code' paragraph style and didn't
suit the light Pygments 'xcode' token colours. Override it in render_block_code: inject a
direct paragraph shading (#F6F8FA, GitHub-light) + a subtle border (#D0D7DE) into the code
paragraph properties, in schema-safe CT_PPr order (pStyle, pBdr, shd). Token colours unchanged.
Verified: pPr order correct, shd fill F6F8FA, border present, 0 <w:t> leaf violations (Word-valid).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stand up a dedicated Yjs websocket service for real-time note collaboration (Phase 2a):
- source/app/collab_server.py: ASGI app on pycrdt-websocket. Rooms keyed by ws path
  'note-<id>'. Binary CRDT persistence via an SQLite YStore (server_data/collab/yjs.sqlite3).
  FAIL-CLOSED auth in the connection gate: decode the IRIS Flask session cookie with the same
  SECRET_KEY (SecureCookieSessionInterface), resolve the active user, map note->case, and require
  ac_fast_check_user_has_case_access(full_access); reject (accept-then-close 1008) otherwise.
  No reliance on iris_current_user (no Flask request context) — DB lookups under app.app_context().
  Healthz at /healthz and /collab/healthz.
- docker-compose.bv.yml: new 'collab' service reusing the app image, running
  uvicorn app.collab_server:app on :5001, with the IRIS env (SECRET_KEY/salt/DB), server_data
  volume, db dependency, and a healthcheck.
- docker/nginx/nginx.conf: /collab/ location proxying to the collab service with ws upgrade +
  no buffering + long timeouts.
- source/requirements.txt: pycrdt-websocket==0.16.3, uvicorn[standard]==0.49.0.

Built + run + tested via docker (Codex) and independently verified by me (real browser):
collab healthy, proxied /collab/healthz 200, AUTHORIZED admin session -> open + binary Yjs sync,
NO session -> closed 1008 (fail-closed), binary store persists updates. Client wiring is 2a-B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire real-time collaborative editing into the notes editor (opt-in, non-collab path unchanged):
- milkdown_editor.js: create(..., { collab:{room,user} }) registers @milkdown/plugin-collab on
  crepe.editor, creates a Y.Doc + y-websocket WebsocketProvider to same-origin /collab/<room>,
  binds via collabServiceCtx (bindDoc/setAwareness/connect), seeds with applyTemplate(initialMarkdown)
  (only if the room is empty), sets local awareness (user name + color), exposes collab status, and
  tears down the provider/doc on destroy.
- milkdown_split.js: in collab mode the CodeMirror SOURCE pane is read-only (WYSIWYG is the
  collaborative surface); source->Milkdown pushes are blocked while Milkdown->source mirroring stays.
- case.notes.js: enables collab per note (room "note-<id>" + current user awareness) and skips the
  old save-note delta + sync_note conflict/overwrite prompt while collab is active.
- deps: @milkdown/plugin-collab@7.21.2, yjs, y-protocols, y-websocket.

Verified (two browser contexts, same note): live A<->B CRDT sync both ways, source read-only during
collab, provider connected, build clean. NOTE: live content currently persists via the Yjs binary
store only; persisting back to note_content + coalesced revisions is 2a-C.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eding/persist fixes)

Persist live collab edits back to note_content and keep coalesced note revisions:
- case_notes_routes.py: NEW /case/notes/<id>/collab/persist (writes note_content via update_note
  WITHOUT a revision) and /collab/snapshot (creates a NoteRevisions snapshot, IDEMPOTENT — skips if
  content == latest revision, so concurrent clients coalesce to one). Both full_access + case-scoped.
- case_notes_db.py: SELECT ... FOR UPDATE row lock around revision creation to dedupe concurrent snapshots.
- case.notes.js: debounced persist (reads CURRENT markdown at fire time), coalesced snapshot triggers
  (manual Snapshot button / ~60s idle / last-leaver via Yjs awareness count on unload), and collab-aware
  revert (applies to the live Y.Doc so it broadcasts, then persists + snapshots).
- milkdown_editor.js: getMarkdown() serializes the live collab content; markdownUpdated forwards the
  listener's current markdown; awareness-count/last-client helpers.

Also fixes two bugs found in review:
- Stale debounced persist: now reads fresh markdown at fire time (was sending a stale captured value).
- CONTENT DUPLICATION on reopen: seeding now waits for the WebsocketProvider 'sync' and only
  applyTemplate-seeds when the synced prosemirror fragment is empty (was seeding before sync, so a
  persisted room got re-seeded and doubled every open). Verified: marker count stays 1 and note_content
  length is stable across repeated reopens; fresh note stable; two-client live sync intact.

Verified (Playwright + DB): persist writes note_content without revision spam, snapshot creates exactly
one revision and is idempotent, no reopen duplication, no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend Yjs collab to the case summary (mirrors notes collab; summary persists to
Cases.description with NO revisions per decision D5):
- collab_server.py: accept 'summary-<case_id>' rooms (in addition to 'note-<id>'),
  authorizing directly via ac_fast_check_user_has_case_access(case_id, full_access)
  (the room IS the case). Same fail-closed behavior; note rooms unchanged.
- case_routes.py: NEW POST /case/summary/collab/persist writing markdown to
  Cases.description only (full_access + case-scoped, Flask context). No revision snapshots.
- case.summary.js: open the summary split editor with collab { room:'summary-<case_id>', user },
  skip the CRC conflict path while collab is active (CRDT handles concurrency), debounced-persist
  to the new endpoint, final-persist on close/unload; non-collab fallback retained; sync-gated
  seeding (from IrisMilkdown) reused so reopen doesn't duplicate.

Verified (Playwright + DB, two browsers, case 1 summary): A<->B live sync both directions,
source pane read-only during collab, edits persist to cases.description exactly once (no
reopen duplication), unauthorized/invalid summary rooms rejected (1008), note rooms still work,
no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Layer the real-time collaboration UI on top of working notes/summary collab:
- milkdown_overrides.css (Kimi): collab UI styles — remote cursor caret + floating name pill
  (.ProseMirror-yjs-cursor) + selection wash, presence bar (.iris-collab-bar/.iris-collab-avatars/
  .iris-collab-avatar[.iris-collab-more]), connection status dot (.iris-collab-status .is-*),
  shell connection-state top border (.collab-*), read-only "Live view" badge
  (.iris-collab-liveview-badge / .is-collab-readonly). Dark + light, no animations.
- milkdown_editor.js: normalize provider state to syncing|live|reconnecting|offline, emit live
  awareness users, set local awareness user {name,color} so y-prosemirror draws remote cursors.
- milkdown_split.js: build the presence bar + avatar/status DOM, toggle shell state classes with
  ~3s anti-flap hold, add the source read-only class + live-view badge during collab.
- case.notes.js / case.summary.js: mount the presence bar (notes #ppl_list_viewing, summary
  #content_typing), disable the old per-document Socket.IO typing/viewer presence while collab is
  active (awareness replaces it), keep notes save as "Snapshot", summary save as plain persist,
  revert toast "Reverted to revision #…".

Verified (two browsers, notes + summary): remote cursor + name pill render, avatar stack + status
dot ("Live · … editing") update live, shell collab-live, source read-only + badge, Snapshot button,
old Socket.IO typing suppressed, no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…when solo

Two regressions from the Phase 1.5 generalization / Phase 2 collab:
- HEIGHT: the blanket `.iris-split-shell { max-height: 600px }` clamped the FULL-PAGE notes
  editor to ~50% of the viewport. Notes shell now fills the viewport again; the 600px cap is
  scoped to the summary shell (#summary_split_shell) which lives in a collapsible card. (Kimi)
- SOURCE EDITABILITY: source CodeMirror was `readOnly.of(collabActive)` — and collab is always on,
  so the Source/Split panes were permanently read-only. Now a CodeMirror Compartment makes source
  read-only only when `collabActive && other collaborators are present` (!isLastCollabClient());
  SOLO editing keeps Source/Split fully editable (edits propagate to WYSIWYG + persist). The
  source->Milkdown sync and the "Live view" badge / .is-collab-readonly class now track the actual
  read-only state, not just collabActive. (Codex)

Verified (Playwright): notes pane full-height (938px of 1000 viewport), solo source editable +
syncs to WYSIWYG, multi-user makes source read-only with badge. (Known follow-up: a joining
client can take a few seconds to register an existing peer before its source locks — tightening next.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e lock ~2s, was ~9s)

Presence + the source read-only state were recomputed only on awareness 'change' events, so a
client joining second didn't see an already-present peer until a slow periodic resend (~9s),
leaving a window where its source pane stayed editable (clobber risk).

Now recompute presence/read-only from provider.awareness.getStates() on sync / status-connected /
change / update / a short bootstrap timer, re-broadcast local awareness on sync+connect and when a
new peer is observed, and key source-read-only off "other users present" (excluding self by client
id). Clear bootstrap timers + broadcast null awareness on pagehide/beforeunload for faster solo recovery.

Verified (two browsers): the second joiner locks its source read-only within ~2s (both panes
read-only when 2 present); editable again when one leaves; no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Solo presence: when connected and editing ALONE, the status dot is now a muted neutral
  '.is-solo' (grey, Kimi) with the text "Connected" and NO avatar chips — so it no longer implies
  a live collab session when you're by yourself. When another collaborator is present it switches
  to the green 'is-live' dot with "Live · <N> editing" + avatars. Connection-trouble states
  (syncing/reconnecting/offline) still override regardless of presence. Applies to notes + summary.
- Removed the redundant #last_saved save icon from the notes header: it duplicated the Snapshot
  button's action and is unnecessary now that saving is automatic. Cleaned up its JS references.
  Snapshot button + auto-persist unaffected.

Verified (Playwright): solo -> is-solo/"Connected"/no avatars/no #last_saved/Snapshot works/auto-save
persists; two browsers -> is-live/"Live · … editing"/avatars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ull-height)

The earlier height fix replaced the bounded height with an unbounded flex-fill chain that
wasn't anchored to the viewport, so the shell grew with content and the whole page scrolled
as one (combined scroll). Re-bound the notes editor to the viewport: .note-body now caps at
max-height: calc(100vh - 188px) with overflow:hidden through .iris-split-shell / .iris-split,
so the editor fills the page top-to-bottom AND each pane scrolls internally (source via the
CodeMirror .cm-scroller, preview via .iris-split-preview .milkdown overflow-y:auto). Summary
shell keeps its own 600px card bound.

Verified (Playwright, typed tall content): scrolling the source pane leaves the preview + page
at 0, scrolling the preview leaves the source + page at 0 — independent per-pane scroll, no page
scroll, full-page height.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… WYSIWYG

Typing in the right (Milkdown WYSIWYG) pane mirrors the new markdown into the left (CodeMirror
source) pane via replaceSourceDoc()'s full-doc replace. The manual scrollTop capture/restore was
being overridden by CodeMirror asynchronously scrolling the preserved selection (sitting near
position 0, since the user isn't editing source) back into view — so the source pane jumped to top.

Fix: replaceSourceDoc() now captures sourceView.scrollSnapshot() and re-applies it as a transaction
effect (CM6's canonical scroll preservation, which wins over scroll-into-view), still clamps the
selection, and re-asserts scrollTop/scrollLeft in a requestAnimationFrame as a backstop (cancelled
on the next replace / on destroy).

Verified (Playwright): scroll source to 250 + type in WYSIWYG -> source stays 250 (was 0); editing
source directly still doesn't jump (89->89); independent per-pane scroll intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se-flush)

The summary auto-saves (debounced persist + force-persist on close + beforeunload flush) and has no
revisions, so the explicit Save button was redundant. In edit mode the #sum_refresh_btn ("Save") is
now hidden — only "Close editor" remains. In view mode it stays as "Refresh" (reloads the read-only
summary). Close still force-persists to cases.description.

Verified (Playwright + DB): edit mode shows only "Close editor" (no Save); typing then "Close editor"
persists the change to cases.description; view mode keeps "Refresh".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ft gutter

The preview-pane ProseMirror padding had been tightened, but Crepe renders its
block handle (the + insert button and drag grip) in the left padding gutter.
The handle is 66px wide (two 32px operation-items + 2px gap), positioned just
left of the text. Because .milkdown uses overflow-y:auto for independent
scrolling, the CSS spec coerces overflow-x to auto too, so a gutter narrower
than the handle clips its leftmost icon (the +) and the handle was effectively
invisible / unclean.

Restore enough left padding for the full handle plus margin: 84px desktop,
80px narrow. Independent per-pane scrolling and the source/CodeMirror pane are
untouched.
- updateSourceReadOnly(): flush a pending debounced source-pane edit
  into Milkdown/collab *before* flipping the source pane read-only,
  instead of discarding pendingSourceMd when a second collaborator
  joins.
- case.summary.js / case.notes.js: add a synchronous-XHR fallback save
  on beforeunload/pagehide for the non-collab path, so a pending
  autosave isn't silently dropped when the tab is closed within the
  debounce window.

Manual repros to verify: (1) type in the source pane, then have a
second client join the same note/summary - text should not be lost;
(2) edit a note or summary with collab inactive, close the tab within
the autosave delay, reopen - edit should be persisted.
- case_note_collab_persist now routes through notes_update() instead
  of calling update_note() directly, so collab edits get a revision
  (with row lock via update_note_revision's with_for_update()),
  history entry, module hooks, and activity tracking like the regular
  save path.
- Drop raw exception text (data=str(e)) from the collab note persist
  and snapshot error responses; keep app.logger.exception for
  server-side diagnostics.
- summary_collab_persist now emits the same 'save' socket event as the
  legacy desc_fetch path, so other open sessions get a live update.
- authorize_scope now verifies the case exists for summary-<case_id>
  collab rooms, matching the implicit check note-<id> rooms get via
  the Notes lookup.
milkdown_split.js and milkdown_editor.js import codemirror,
@codemirror/lang-markdown, @codemirror/state, @codemirror/view,
@codemirror/language-data, @lezer/highlight, and y-prosemirror
directly, but none were declared in ui/package.json - they only
resolved as transitive deps of @milkdown/crepe / @milkdown/plugin-collab
(the latter via an unconstrained "*" for y-prosemirror), which broke
hoisting/build in a clean install. Add explicit dependency entries
pinned to the already-locked (and currently latest) versions; the
lockfile diff is limited to the new top-level entries.
Deduplicate the colab color palette, content hashing, current-user
lookup, sync-XHR POST, and split-editor-ready wait helpers that were
copy-pasted between case.summary.js and case.notes.js into a new
ui/src/lib/collab_editor_session.js module. Notes now also gets the
load-timeout + notify_error fallback that summary already had for
wait_for_split_editor.

Validated with scoped eslint (146 errors, down from 151, all
pre-existing no-undef/no-unused-vars patterns consistent with the
rest of the codebase) and a successful `npm run build`.
…ng, presence-state fix, wysiwygPane rename

- Add a withOrigin(origin, fn) helper and use it for the four
  manual this.origin save/restore sequences, making them
  exception-safe.
- Wrap the IrisMilkdown.create() bootstrap call in try/catch,
  running registered cleanup functions before rethrowing on
  failure; case.notes.js now retries without collab on a failed
  IrisSplitEditor.create(), matching case.summary.js's existing
  fallback.
- handleCollabAwareness() now skips renderCollabPresence() while a
  collab state transition is debounced, avoiding a status dot/text
  desync.
- Rename previewPane/previewMount to wysiwygPane/wysiwygMount in
  milkdown_split.js and update the two call sites in
  case.summary.js/case.notes.js (CSS class names unchanged).

Validated with scoped eslint (147 errors, +1 pre-existing
no-unused-vars pattern mirrored from case.summary.js's collab
fallback) and a successful npm run build.
whitekernel and others added 30 commits July 3, 2026 14:11
…c gap

- Add merge migration for the second alembic head introduced by the
  upstream sync (war_room_id_to_user_activity vs our prior merge point).
- Import WarRoom explicitly in models.py so it's registered in the
  declarative registry before UserActivity.war_room is mapper-configured
  (was crashing gunicorn boot with an unresolvable relationship).
- Add worker-src 'self' blob: to the CSP header so Ace editor's
  syntax-highlighting web worker (blob: URL) isn't blocked.
…26-07

# Conflicts:
#	source/requirements.txt
user_activity.war_room_id had no ON DELETE behavior, so Postgres
blocked deleting any war room with logged activity against it — which
is every real war room, since creating one is itself logged. The
existing war_room_delete() code already assumed the FK would go NULL
(see its comment), it just wasn't configured that way at the DB level.
The auto-merge kept both our and upstream's copies of two blocks that
should have collapsed into one: the MFA brute-force lockout helpers in
login_routes.py (plus a duplicate `import time`), and the alert-update
readonly-fields guard in alerts_routes.py. Functionally harmless since
Python just re-defines the same names, but confusing and worth
cleaning up. Kept the better-commented (upstream) copy in each case;
verified py_compile and a container boot after each removal.
… port 443

Running as www-data (re-added in bc6f071 for hardening) blocks binding to
privileged ports without this capability, causing the container to exit
immediately whenever INTERFACE_HTTPS_PORT is left at its default 443 (as
in .env.tests.model) instead of a dev override like 8443. This broke the
Test API, Database migration tests, and End to end tests CI jobs, which
all depend on the nginx container starting successfully.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- business/auth.py: remove duplicate `import time` left by the 3-way
  merge (same pattern already fixed in login_routes.py/alerts_routes.py)
- access_control/utils.py, alembic/versions/e5d79b8c4a55_...py: drop
  stray extra blank lines left where overlapping merge hunks joined

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cleans up ruff UP032 findings in our own docx report generation code
(not present upstream). Report generation tests still pass.
/collab/ used a static proxy_pass to "collab", resolved at nginx
startup. CI's docker-compose.dev.yml (upstream's own file) has no
collab service, so nginx refused to start at all:
  nginx: [emerg] host not found in upstream "collab"

Switch to a variable + Docker's embedded resolver so DNS is resolved
per-request instead. nginx now starts fine without collab present;
only requests to /collab/ fail if it's genuinely absent, which is
correct since it's an optional BV-only feature.

Verified locally: nginx stays up with collab stopped, and successfully
proxies through to it once collab is running again.
…ross models, migrations, business, REST, schemas, hooks, celery tasks, tests
…NOT NULL crash

- custom_dashboard.owner_id had no ON DELETE rule, so deleting a
  dashboard's owner raised a raw ForeignKeyViolation/500 instead of
  orphaning the (possibly shared) dashboard. Set ondelete='SET NULL'.
- AuthorizationGroupSchema.group_permissions had no load_default, so
  full loads (e.g. group creation) with the key omitted hit a NOT NULL
  violation. Add load_default=0.
flask-cors, SQLAlchemy, alembic, cryptography, pyotp, bleach had
drifted to slightly older patch versions than the parallel
dependency-update work on the upstream PR branch.
Merging the 3 new upstream commits re-forked the alembic head
(dashboard_owner_fk vs collab_doc_seeder_version) - add the merge
migration to resolve it.

Also document a disposable iris-test podman stack for running the
test suite without touching real dev-stack data/volumes.
Merge dashboard/permissions fixes, dep pin alignment, and upstream sync into bv-develop
…filter values

- 'owner' (case owner username) and 'asset_type' were left unsanitized
  by the previous sanitation pass in this same table config.
- sanitizeHTML was being applied unconditionally for every DataTables
  render call (display/sort/filter/type), causing the search box and
  sort order to operate on the HTML-encoded value instead of the raw
  one. Sanitize only for the 'display' type via a shared helper.
- Guard asset_type/owner renderers against a null value (asset_type_id
  and case owner are both nullable).
user and action come from modification_history, which embeds the
acting user's username verbatim. They were appended as raw HTML via
jQuery .append(), so a username containing HTML/script executed when
an analyst opened the alert history modal.
alert_title was only quote-escaped before being embedded in the
popover's HTML content, so a title containing unquoted HTML (e.g. an
img tag with an onerror handler) still executed. Run it through
sanitizeHTML before the quote-escaping used for the attribute context.
…olumns

Use the same falsy check (!data) as the owner/asset_type renderers
instead of a strict === null comparison, so an undefined state value
falls back to 'Unknown' instead of throwing on data.state_name.
…zation

Sanitize remaining XSS gaps in customer view and alert modal/popover
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.

4 participants