Skip to content

phase3: modernize frontend deps (ACE→CM6, showdown→markdown-it, vis split, Chart.js 4, Day.js) + security fixes - #15

Merged
gru3zi merged 112 commits into
bv-developfrom
feat/phase3-ace-removal
Jul 2, 2026
Merged

gru3zi merged 112 commits into
bv-developfrom
feat/phase3-ace-removal

Conversation

@gru3zi

@gru3zi gru3zi commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 3 frontend modernization + security hardening on top of the Milkdown split-editor work (Phase 1/2).

Dependency upgrades (slices 3.1–3.6)

  • ACE editor removed (3.1a–3.1d): all JSON/config and markdown editor surfaces migrated to CodeMirror 6 (IrisJsonEditor) and the Milkdown split-pane editor (IrisCollabSession.mountMarkdownSplitEditor). ACE, ace-builds, and the showdown helper function deleted from common.js.
  • showdown → markdown-it (3.2): replaced across all 13 call sites; two custom extensions ported (img sanitizer, Bootstrap table wrapper).
  • vis monolith → vis-network + vis-timeline (3.3): tree-shaken split bundles; XSS smoke test added for tooltip escaping.
  • Chart.js 2 → 4 (3.4): dashboard chart config migrated to v4 API (scales, plugins.legend/tooltip).
  • moment removed (3.5): never actually loaded in browser; Day.js + chartjs-adapter-dayjs-4 wired in as global (DataTables requires window.dayjs).
  • Backend bumps (3.6): graphene-sqlalchemy, docxcompose/docxtpl/python-docx, idna bumped.

Security fixes (CodeRabbit + Kimi findings)

  • SSRF (CRITICAL): picture_globals._process_remote now validates URLs via _is_safe_url() — blocks private/loopback/link-local/multicast/cloud metadata (169.254.0.0/16) IPs.
  • Path traversal (CRITICAL): picture_globals._process_local, document_globals.add_sub_document, and docx_generator._sanitize_path replaced regex \.\. pattern with os.path.realpath containment checks.
  • Stored XSS (CRITICAL): ret_obj_dt_description() in common.js now sanitizes data-content via do_md_filter_xss() before setting on Bootstrap popovers (html: true).
  • IndexError crash (CRITICAL): file_adapter.recover_file_path_from_uuid now raises RenderingError on empty UUID directory before accessing available_files[0].
  • milkdown_editor.js missing (CRITICAL): was only loaded on case.html; added to default_ext.html and alerts.html — case sub-pages and alerts were silently hanging.

Template bug fixes

  • milkdown_editor.css moved to default_ext.html base layout (was only in case.html) — split editor layout and toolbar positioning now works on all sub-pages.
  • _markdown_split_editor.html: |default('', true) — Jinja2 default('') without boolean=True doesn't handle Python None, causing literal "None" to appear in editors.

Additional fixes

  • waitForMilkdown() timeout: rejects after 10s instead of hanging forever.
  • downloadCaseTemplateDefinition(): null guard on ctpl_editor.
  • IrisJsonEditor.mountJsonEditor(): throws explicit Error (not TypeError) for missing DOM target.
  • report_db.py: URL typo (8000:/ → 8000/).

Test plan

  • Open Add IOC modal on /case/ioc?cid=X — split editor renders correctly (no floating toolbar, no "None" content)
  • Open Add Task, Add Event, Add Evidence modals — same
  • Case notes collab editor still works
  • Case summary Milkdown toggle still works
  • Dashboard chart renders
  • Case timeline / graph load
  • Datastore JSON editor works
  • Alerts page loads
  • DOCX export with images works
  • e2e suite: diag-cr-js-fixes, diag-cr-py-security, diag-split-subpage, diag-split-mount, diag-vis-xss — all pass

🤖 Generated with Claude Code

gru3zi and others added 30 commits June 11, 2026 05:49
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.
…Error

The base docx_generator add_picture wraps all expected per-image
failures (missing/invalid path, decode error) in RenderingError
before they reach this override, so catching bare Exception was also
silently swallowing unrelated bugs (e.g. a filesystem error creating
the temp image output directory) as "skip this image". Catch
RenderingError specifically so real bugs surface instead of producing
a report with a silently-missing image.
…t description

Extract the Milkdown split editor's shell markup into a reusable Jinja
partial (_markdown_split_editor.html, parameterized by split_prefix) and
add a mountMarkdownSplitEditor() helper to collab_editor_session.js.

Adopt both for the asset description field (add/edit asset modals),
replacing the legacy Ace+Showdown editor/preview wiring in case.asset.js.
Since the asset modal markup is reloaded via AJAX on every open, any
existing split-editor instance is destroyed before the modal content is
replaced.

Remaining markdown fields (ioc, tasks, timeline, rfiles, comments, alerts,
overview) follow the same pattern in future commits.

Not manually verified in-browser: modal height/scroll bounds for the new
split editor (#asset_desc_split_shell max-height: 400px) and the
Split/Source/Preview toggle inside the modal.
gru3zi and others added 27 commits June 23, 2026 06:55
- Add @tabler/core@1.0.0-beta21 and @popperjs/core@^2.11.8 to ui/package.json
- Swap vite copy target: popper.js → @popperjs/core (same dest path)
- Add vite copy targets for tabler.min.css and tabler.min.js
- Remove Flaticon and simple-line-icons from WebFont.load families in all
  4 layouts — prevents hang when font files are removed in 5.1; @font-face
  declarations in fonts.css still load via the urls injection

No visual changes. Tabler/Popper v2 assets now in dist/ ready for 5.1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSS/JS:
- Replace bootstrap.min.css + atlantis.css → tabler.min.css in all 4 layouts
- Replace bootstrap.min.js + atlantis.min.js → tabler.min.js in all 4 layouts
- Remove atlantis-draggable-shim.js, jquery.scrollbar.min.js,
  bootstrap-select.min.css/js, ajax-bootstrap-picker.js from all layouts

Layout structure:
- class="wrapper" → class="page" in default.html/ext/centered (Tabler container)
- Gate off dark-theme.css in 3 layouts with Jinja2 comment; add in-navbar
  dismissible warning for dark mode users (data-bs-dismiss, btn-close)
- static-default.html: no wrapper div, no dark mode conditional — left intact

Icons:
- flaticon-interface-1/3/7, flaticon-shapes-1, flaticon-round → FA equivalents
  in navigation.html, navigation_ext.html, sidenav.html
- Remaining flaticons deferred to 5.6 (font still loads via fonts.css/WebFont)

Fixes:
- sidenav.html: collapse in → collapse show (BS3 remnant)
- quick-sidebar.html: btn-xs → btn-sm (×2)

Vite build clean. No Jinja2 template logic removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…patches

ATOMIC: BS5 now loaded (5.1); all plugin call sites must migrate together.

5.2a — HTML data-attribute sweep (8 include/partial files):
- data-toggle → data-bs-toggle (modal/collapse/dropdown/tab/popover/tooltip)
- data-target → data-bs-target
- data-dismiss → data-bs-dismiss (modal + alert)
- data-backdrop/trigger/content/placement/html → data-bs-* equivalents
- .close + <span>&times;</span> → <button class="btn-close"> (×5 in footer.html)
- modal-dialog class ordering: modal-lg/xl before modal-dialog → correct BS5 order
- Zero old-style data-toggle/data-dismiss remain in any HTML template

5.2b — JavaScript bridge shim + data-attr sweep:
- Add jQuery BS5 bridge shim at top of common.js: $.fn.modal/popover/tooltip
  delegate to bootstrap.Modal/Popover/Tooltip.getOrCreateInstance — covers
  ~167 call sites across 25 JS files without mechanical rewrites
- .wrapper → .page in common.js sidebar toggle handler (3 lines)
- data-toggle/data-dismiss strings in JS template literals → data-bs-* (10 files)
- Selector queries [data-toggle="popover/tooltip"] → [data-bs-toggle=...] (7 files)
- Zero old-style data-toggle remain in any JS source file

5.2c — Vendor JS patches:
- dataTables.contextualActions.min.js: data-toggle → data-bs-toggle (tooltip attr)
- bootstrap-multiselect.min.js: data-toggle="dropdown" → data-bs-toggle="dropdown"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
30 files changed (10 HTML includes/partials + 20 JS files):

Directional spacing:
- ml-N → ms-N, mr-N → me-N (margin-start/end)
- pl-N → ps-N, pr-N → pe-N (padding-start/end)
- float-left/right → float-start/end, pull-left/right → float-start/end

Typography:
- text-left → text-start, text-right → text-end (~217 occurrences)
- font-weight-bold/light/normal → fw-bold/light/normal

Badges (static only — dynamic badge-${style} deferred, needs CSS compat shim):
- badge-{color} → text-bg-{color} across alerts.js, timeline, dashboard, etc.

Forms:
- custom-select → form-select
- form-group class removed from wrapper divs (divs kept)
- input-group-append/prepend wrappers unwrapped in navigation.html + alerts.js

Buttons:
- btn-xs → btn-sm in case.timeline.js (×10 template literals)

Atlantis negative margins:
- mt--N → mt-nN in sidenav.html (×1) and alerts.js (×4)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jQuery UI (jquery-ui-1.12.1.custom) was bundled inside atlantis.min.js.
When 5.1 removed Atlantis, draggable/resizable broke silently. The file
was already vendored; just missing the <script> tag.

Added after jquery.min.js in all 4 layouts — must load before Tabler/common.js.

Caught by new diag-phase5-smoke.cjs smoke test (login, Tabler layout check,
modal shim verification, JS error monitoring, manage page load).
Playwright config: baseURL now reads process.env.BASE_URL for local testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.4a — Tom Select replaces bootstrap-select + ajax-bootstrap-picker:
- Add tom-select@2.3.1 to package.json; copy CSS+JS via vite
- Add tom-select.bootstrap5.min.css + tom-select.min.js to all 4 layouts
- $.fn.selectpicker shim in common.js (window._irisSelects map) covers all
  26 non-AJAX call sites without mechanical rewrites
- Replace 3 ajaxSelectPicker AJAX call sites with TomSelect load callbacks:
  common.js #user_context, alerts.js #mergeAlertCaseSelect (×2)
- savedFilters: shown.bs.select → dropdown_open, hide.bs.select → dropdown_close
- Remove dead bootstrap-select.min.css from header_case.html

5.4b — iris_toast replaces bootstrap-notify:
- New ui/src/lib/iris-toast.js (BS5 Toast, position fixed top-right, 3s autohide)
- 3 call paths updated: scripts.html wrappers, common.js runtime definitions,
  case.pipelines.js raw $.notify → iris_toast()
- bootstrap-notify script removed from all 4 layouts
- flaticon-error icon → fas fa-exclamation-circle in iris-toast

5.4c — jquery-scrollbar CSS replacement:
- overflow-y: auto CSS added to layouts (scrollbar.min.js removed in 5.1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 10 datatables.net npm packages (core, bs5, buttons, responsive,
  select, searchbuilder — each with -bs5 variant)
- Copy BS5 CSS/JS shims to assets/js/plugin/datatables-bs5/ and css/
- Load dataTables.bootstrap5.min.js in all 4 layouts after the existing
  datatables.min.js bundle (overrides BS4 renderer)
- header_case.html: swap BS4 DT CSS for BS5 equivalents
- footer_case.html: add buttons/select/responsive BS5 shims (dataTables.bootstrap5
  already loaded by layout, deduped)
- overview.html: dataTables.searchBuilder.bs4.js → searchBuilder.bootstrap5.min.js
- 5 blueprint templates (activities, dashboard, dim_tasks, manage_cases, search):
  swap BS4 DT CSS for BS5 equivalents + add BS5 shim loads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New ui/src/lib/sidebar-toggle.js (78 lines, no jQuery):
  - .sidenav-toggler click → toggles nav_open on .page (mobile sidebar)
  - Outside-click on .page.nav_open → closes sidebar (with contains() guard)
  - .topbar-toggler click → toggles topbar_open on .page
  - .quick-sidebar-toggler → toggles quick_sidebar_open on html element
  - .close-quick-sidebar → removes quick_sidebar_open
  - Preserves existing common.js .toggle-sidebar (desktop minimize) untouched
- Added copy target in vite.config.js → assets/js/iris/sidebar-toggle.js
- Script loaded in all 4 layouts after tabler.min.js, before common.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.6A — selectgroup → BS5 form-check:
- alerts.js: 4-checkbox selectgroup-pills block → d-flex flex-wrap gap-2
  with form-check form-check-inline; all onclick/name/value preserved

5.6B — flaticon → FA5 (30 replacements, 6 files):
- navigation.html + navigation_ext.html: 3 each (refresh, database, plus)
- sidenav.html: 12 (home, network-wired, bell, search, list, user, cog, etc.)
- quick-sidebar.html: 10 (times, camera, inbox, laptop, file-alt, stopwatch...)
- datastore-sidebar.html: 1 (times)
- case.timeline.js: 1 (stopwatch), dashboard.js: 2 (bell, check-circle)
- Ambiguous icons marked with <!-- TODO: icon review --> comment

5.6C — badge-${style} CSS compat shim added to all 4 layouts:
- 8 rules (primary/secondary/success/danger/warning/info/light/dark)
- Covers DB-driven dynamic badge class names in common.js

5.6D — DataTables className cleanup (16 occurrences, 5 JS files):
- btn-border btn-round removed from activities.js, dashboard.js,
  dim_tasks.js, overview.js, search.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…icons

- BS5 jQuery bridge: add object-action branch — when action is {show:true},
  call instance.show() after getOrCreateInstance(); fixes silent fail across
  all 52 modal call sites that pass {show:true} as options object
- dashboard/index.html: flaticon-file-1 → fas fa-file-alt (cases stat card)
- dashboard/index.html: flaticon-suitcase → fas fa-briefcase (user cases card)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cher

SEC: iris-toast.js — replace innerHTML concatenation with DOM APIs
- message is now set via createTextNode() (XSS-safe, no markup injection)
- iconHtml uses insertAdjacentHTML (controlled FA icon strings only)
- close button built via createElement (no user data in attributes)

FIX: package.json — bootstrap ^4.0.0 → ^5.3.0
- Aligns declared dependency with actual BS5 usage (Tabler bundles BS5;
  the bootstrap package was orphaned after 5.1 removed bootstrap.min.js
  from layouts, but the declaration was incorrect)

FIX: common.js load_context_switcher() ordering
- context_data_parser(data, false) called BEFORE TomSelect init so DOM
  <option> elements exist when TomSelect scans the <select>
- TomSelect instance created, stored in window._irisSelects['user_context']
- sync() + setValue() + modal show called AFTER instance is ready
- Fixes: context switcher showed an empty dropdown on first open

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5: Bootstrap 4 → Bootstrap 5 + Tabler migration
Atlantis.min.css removal in 5.1 took the sidebar+main-panel positioning
rules with it. Added iris-layout.css extracted from vendored atlantis.css
to restore .sidebar (position:fixed, 250px), .main-panel (margin-left:250px),
header (z-index:1001), minimized and mobile states. Replaced float:right
(broken inside Tabler's flex .page) with margin-left. All 4 layouts load
the shim after tabler.min.css. Added diag-phase5-layout.cjs automated CSS
layout regression test (sidebar width, main-panel alignment, header height,
overflow, mobile hide). Layout: ALL PASS, QA: 33 pass 0 fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracted visual-only rules from vendored atlantis.css (colors, shadows,
typography) into iris-visual.css — loaded after iris-layout.css. Covers:
sidebar-style-2 active nav pill (#05316a + white text), hover pill
(rgba(199,199,199,0.2) + border-radius), logo-header dark blue background
(data-background-color=blue), btn-toggle/topbar-toggler transparent on
dark header, white hamburger SVG icon, card box-shadow + border:0,
card-header border, body/content background (#f9fbfd), main-header
text/link colors, navbar transparent inside main-header, profile
picture max-width:35px, user block separator + version menu padding.

All 4 layouts load: tabler.min.css → iris-layout.css → iris-visual.css.
Layout check: ALL PASS. QA: 33 pass 0 fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker-compose.ref.yml — spins up ghcr.io/dfir-iris/iriswebapp_app:v2.5.0
on port 8444 (isolated network/volumes) as a visual ground-truth reference.

e2e/style-audit.cjs — Playwright script that logs into both :8443 (fork)
and :8444 (ref), extracts computed CSS for 24 key element/property pairs
across dashboard/manage/case pages, diffs them as JSON, and writes
style-diff.json + style-audit-full.json. Eliminates manual screenshot
review — run after each CSS change to measure remaining delta.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…le headers, modals

Style-audit (style-audit.cjs vs ghcr.io/dfir-iris/iriswebapp_app:latest) flagged:
- body font-family: add Lato via Google Fonts import
- nav-tabs: restore #05316a blue text + 2px solid active indicator (case page tabs)
- .badge: pill shape (border-radius 50px, 5px 10px padding)
- table.dataTable thead th: restore 14px font-size (was 10px via Tabler override)
- .modal-content: 1rem border-radius + proper box-shadow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rol radius

Style-audit round 2:
- table.dataTable thead: transparent bg, #343a40 text, no border (removes Tabler
  light-gray header tint from DataTable column headers)
- .form-control: border-radius 8px, min-height 35px (rounds inputs to match
  Atlantis appearance; Bootstrap input-groups zero the right corners naturally)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…banner backgrounds

Diagnostics revealed .panel-header uses four gradient utility classes that
Atlantis defined but Tabler removed — leaving case/dashboard hero sections
transparent with dark text:

- .bg-close-gradient  → dark navy  (#1f283e, linear-gradient −45°)
- .bg-primary-gradient / .bg-case-gradient → IRIS brand blue (#05316a)
- .bg-shadow-gradient  → dark charcoal gradient

For each: restore background, force white text on all descendants, style
.btn-light/.btn-default as frosted-glass, keep .btn-white solid white.

Also: .btn / .btn-sm global border-radius restore (0.375rem / 0.25rem) to
fix Tabler's 2px square button appearance; .btn-rounded / .badge-pill
kept as pill shapes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dark-theme.css is gated off in all layouts post-BS5 migration. The
in_dark_mode flag still existed in the vis graph colour helpers, causing
white-on-white node/edge rendering for users with in_dark_mode=True.

Remove both remaining dark-mode branches:
- _get_font(): drop 'if in_dark_mode: return ...' branch → always ''
- _get_icon_ioc(): drop ternary → 'color' always ''

Function signatures preserved; navigation.html already shows a
dismissible dark-mode notice — no template change needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
case_rfile.html loaded bootstrap-datetimepicker.min.js but contained
no .datetimepicker() call sites — the tag was a dead Bootstrap 3/4
remnant. amsifySuggestags confirmed BS5-agnostic, no change needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ler/core dropzone peer conflict

npm 10 (Node 20 in CI) fails ERESOLVE because @tabler/core@1.0.0-beta21 declares
peerOptional dropzone@^6.0.0-beta.2 but we have dropzone@5.9.3 (used by flask_dropzone).
npm 11 (local, Node 24) silently installs without the flag. Adding --legacy-peer-deps
makes CI npm ci use the same lenient peer resolution as local.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…k handler

response_api_error(message, data=None) has no status= parameter in this branch;
the phase4 fix was written against a different version. Remove the invalid kwarg
so the OperationalError handler can return a clean 200-error JSON response and
suppress the Traceback that fails CI's log check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bootstrap 4→5 migration removed bootstrap-select (selectpicker), which
rendered <select> as <button> elements. The Playwright tests were written
against selectpicker's button/listbox UI; they now use native selectOption()
on the underlying <select> elements.

Also fixes:
- manage_customer_view.html: data-toggle → data-bs-toggle on three collapse
  triggers (BS4 attribute not recognized by BS5 JS)
- dim/tasks.spec.js: .form-group → .has-feedback wrapper (form-group removed
  in BS5; DataTable column filter helper now wraps inputs in .has-feedback)
- dashboard.spec.js: check for .invalid-feedback instead of specific error
  text (propagate_form_api_errors shows inline validation, not toast)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ate duplicate

- assets.spec.js: add { force: true } to selectOption on #asset_type_id
  (Tom Select wraps native <select> with ts-hidden-accessible; Playwright
  visibility check fails without force)
- templates.spec.js: add { force: true } to selectOption on #report_language
  (same Tom Select ts-hidden-accessible issue)
- modal_add_report_template.html: remove duplicate form.report_language render
  (template rendered the field twice causing Playwright strict-mode violation)
- dashboard.spec.js: replace .invalid-feedback DOM check with waitForResponse
  checking HTTP 400 (more reliable than asserting a specific DOM error pattern
  that varies based on which validation path fires)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… E2E dashboard test resilient assertion

Tom Select auto-selects first option sending case_customer_id as a string ("1"), which
fails SQLAlchemy 2.0 + psycopg3 BigInteger comparison.  Coerce to int after assert_type_mml
validates it is numeric.  Also relax E2E assertion from exact status 400 to response.ok()===false
so the test verifies user-visible error behaviour regardless of whether the server returns 400 or 500.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5+6: Bootstrap 5 / Tabler migration + tech debt resolution
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​tabler/​core@​1.0.0-beta21931001008370
Addedpypi/​psycopg@​3.3.410010010010070
Addedpypi/​docxtpl@​0.20.29910010010070
Addednpm/​@​milkdown/​plugin-collab@​7.21.2761007294100
Addednpm/​@​lezer/​highlight@​1.2.31001007681100
Addednpm/​@​codemirror/​language-data@​6.5.2991007781100
Addednpm/​codemirror@​6.0.21001007981100
Addednpm/​@​milkdown/​crepe@​7.21.2991007992100
Addednpm/​@​codemirror/​state@​6.6.01001007987100
Addednpm/​datatables.net-searchbuilder-bs5@​1.7.0801008681100
Addednpm/​@​codemirror/​lang-json@​6.0.21001008580100
Addednpm/​y-protocols@​1.0.71001009581100
Addednpm/​y-prosemirror@​1.3.710010010081100
Addednpm/​datatables.net-searchbuilder@​1.7.01001008981100
Updatednpm/​@​sveltejs/​vite-plugin-svelte@​3.1.2 ⏵ 5.1.1100 +110081 +193100
Addednpm/​@​popperjs/​core@​2.11.810010010082100
Addednpm/​@​codemirror/​lang-markdown@​6.5.010010010082100
Updatednpm/​vite@​5.4.21 ⏵ 6.4.394 +1100 +198398 +1100
Updatednpm/​bootstrap@​4.6.2 ⏵ 5.3.89910010084 +34100
Addednpm/​datatables.net-buttons-bs5@​2.4.2981008885100
Addednpm/​datatables.net-buttons@​2.4.2991009085100
Addednpm/​datatables.net-responsive-bs5@​2.5.0971008785100
Addednpm/​datatables.net-select-bs5@​1.7.0971008685100
Addednpm/​datatables.net-responsive@​2.5.01001008985100
Updatednpm/​chart.js@​2.9.4 ⏵ 4.5.110010086 +285100
Addednpm/​@​codemirror/​view@​6.43.11001008596100
Addednpm/​markdown-it@​14.2.08710010089100
Addednpm/​datatables.net@​1.13.81001009187100
Addednpm/​datatables.net-select@​1.7.01001008887100
Addednpm/​yjs@​13.6.3110010010087100
Updatednpm/​svelte@​4.2.20 ⏵ 5.56.388 -6100 +1688 +198100
Addednpm/​tom-select@​2.6.19910010088100
Addednpm/​datatables.net-bs5@​1.13.81001008888100
See 7 more rows in the dashboard

View full report

@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm js-yaml is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ui/package-lock.json → npm/eslint@9.39.4 → npm/js-yaml@4.2.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/js-yaml@4.2.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@gru3zi
gru3zi merged commit aa645a6 into bv-develop Jul 2, 2026
12 checks passed
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.

2 participants