Release v2.5.0-bv2#14
Open
Che4ter wants to merge 177 commits into
Open
Conversation
[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.
…expose source-incident on cases
…om case, business helpers
…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.
Sync/upstream 2026 07
…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
…ot automatic on push
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
bv-developintobv-mainfor the v2.5.0-bv2 releasePost-merge steps
bv-mainwithv2.5.0-bv2to trigger the AKS prod deploy pipeline (manual approval gate)release/2.5.0-bv2branch