Skip to content

Release 0.30.0#1078

Merged
Tim020 merged 10 commits into
mainfrom
dev
May 24, 2026
Merged

Release 0.30.0#1078
Tim020 merged 10 commits into
mainfrom
dev

Conversation

@Tim020
Copy link
Copy Markdown
Contributor

@Tim020 Tim020 commented May 24, 2026

No description provided.

dependabot Bot and others added 6 commits May 23, 2026 13:24
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.12.1 to 2.13.0.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](jpadilla/pyjwt@2.12.1...2.13.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.14.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](astral-sh/ruff@0.15.12...0.15.14)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [zeroconf](https://github.com/python-zeroconf/python-zeroconf) from 0.148.0 to 0.149.16.
- [Release notes](https://github.com/python-zeroconf/python-zeroconf/releases)
- [Changelog](https://github.com/python-zeroconf/python-zeroconf/blob/master/CHANGELOG.md)
- [Commits](python-zeroconf/python-zeroconf@0.148.0...0.149.16)

---
updated-dependencies:
- dependency-name: zeroconf
  dependency-version: 0.149.16
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Vue 3 migration: Phase 0 — skeleton & dual-serving infrastructure (#1035)

* Add Vue 3 migration skeleton (Phase 0) — issue #1033

Creates client-v3/ alongside the existing Vue 2 client/, serving a
placeholder Vue 3 app at /ui-new/ via a new RootControllerV3. Both
frontends build and serve independently.

Backend: RootControllerV3 + /ui-new/assets/ and /ui-new/ handlers in
app_server.py (registered before the Vue 2 catch-all route).

client-v3 stack: Vue 3.5 / Pinia 3 / Vue Router 5 / Bootstrap-Vue-Next
0.45 / Vite 8 / ESLint 10 / TypeScript strict mode.

CI: lint, typecheck, and test jobs added for client-v3 in nodelint.yml
and client-test.yml.

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

* Fix formatting

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 migration: Phase 1 — core infrastructure (#1033) (#1037)

* Add Vue 3 core infrastructure (Phase 1) — issue #1033

Pinia stores (user/auth, system/RBAC, websocket), useWebSocket() composable,
HTTP interceptor, full router with beforeEach guard, platform/utils/logger
ports, API types, constants, full BVN navbar App.vue, and stub views.

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

* Fix BVN component rendering and visual parity with Vue 2

- Switch to BApp wrapper + unplugin-vue-components/BootstrapVueNextResolver
  for automatic per-component tree-shaken imports (no global plugin needed)
- Add bootstrap-vue-next/dist/bootstrap-vue-next.css import in main.ts
- Add data-bs-theme="dark" to BNavbar so text renders white on info background
  (Bootstrap 5 equivalent of Bootstrap 4's type="dark")
- Add components.d.ts to tsconfig includes for GlobalComponents type augmentation
- Gitignore components.d.ts (auto-generated by unplugin-vue-components on build)
- Fix NotFoundView copy and centering to match Vue 2 404View exactly

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

* Fix npm ci lockfile sync for @emnapi/core and @emnapi/runtime

These are transitive deps of @rolldown/binding-wasm32-wasi (cpu: wasm32).
npm doesn't install the wasm32 binding on macOS arm64, so their lockfile
entries were missing. Added as optionalDependencies to force inclusion.

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

* Fix lockfile: add @emnapi/core and @emnapi/runtime as explicit devDeps

These are transitive deps of @rolldown/binding-wasm32-wasi (cpu: wasm32).
npm skips that binding on macOS arm64 so its deps never get lockfile
entries, causing npm ci to fail on Linux CI.

Adding them explicitly to devDependencies forces npm to resolve and
record their entries in the lockfile on all platforms.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Migrate Phase 2 read-only pages to Vue 3 (Home, About, Help) (#1038)

- stores/help.ts: Pinia port of Vuex help module with manifest loading, document
  cache, and Fuse.js full-text search
- components/MarkdownRenderer.vue: async marked v18 API via watch+ref; :deep() CSS
  selectors; import.meta.env.BASE_URL for portable help links across base paths
- views/HomeView.vue: reads systemStore.currentShow/settings + userStore.currentUser;
  currentShowSession stubbed null until Phase 6
- views/AboutView.vue: static content port
- views/HelpView.vue: BVN port with sticky sidebar, debounced search, dynamic navbar
  height offset
- views/help/HelpDocView.vue: cache-first doc loading, watch route.params.slug
- router: wire /about and /help routes; fix /help child redirect to absolute path;
  fix catch-all to render NotFoundView in-place (no URL redirect) matching Vue 2
  behaviour

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 migration: Phase 3 — authentication pages (Login, ForcePasswordChange) (#1039)

- Port LoginView and ForcePasswordChangeView to BVN + @vuelidate/core v2
- Add useFormValidation and usePasswordValidation composables
- Add changePassword() action to user store
- Wire /force-password-change route to real component

Bug fixes discovered during verification:
- stores/user: authToken was a non-reactive getter reading localStorage, causing
  Pinia to cache null on first access and never re-evaluate; moved to reactive
  state field with _setToken/_clearToken actions keeping localStorage in sync
- http-interceptor: exclude login endpoint from 401 handling to avoid logout
  cascade on bad credentials
- stores/websocket: websocketHealthy getter incorrectly required authenticated=true;
  corrected to match Vue 2 behaviour (connection only)
- views/LoginView: missing @submit.prevent on BForm caused native form submission
- main.ts: import theme-sugar.css and create toast.ts singleton with position
  top-right; replace scattered useToast() calls with the shared instance

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 migration: Phase 4 — user settings page (/me) (#1041)

* Vue 3 migration: Phase 4 — user settings page (/me)

- views/user/SettingsView.vue: 6-tab pill-vertical shell (About, Settings,
  Stage Direction Styles, Cue Colour Preferences, Change Password, API Token)
- components/user/settings/AboutUser.vue: titleCase + sorted BTableSimple
- components/user/settings/UserSettingsConfig.vue: 7-field settings form,
  PATCH /api/v1/user/settings, vuelidate with notNull/notNullAndGreaterThanZero
- components/user/settings/ChangePassword.vue: 3-field form using
  usePasswordValidation, sends old_password for settings context
- components/user/settings/ApiToken.vue: generate/regenerate/revoke with
  BModal confirmations, #append slot for copy button
- components/user/settings/CueColourPreferences.vue: cue types fetched
  locally (show store not yet available), BModal ref pattern, contrastColor
- components/user/settings/StageDirectionStyles.vue: stage direction styles
  fetched locally, v-model:pressed for toggle buttons, no Vue 3 filters
- js/customValidators.ts: notNull, notNullAndGreaterThanZero validators
- stores/user.ts: 8 CRUD actions for overrides, changePassword accepts
  oldPassword, logout clears cueColourOverrides
- router/index.ts: /me wired to SettingsView (was PlaceholderView)

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

* Fix BTabs content pane width in user settings page

Add content-class="flex-fill" to BTabs so the tab pane fills the
available horizontal space (BVN doesn't auto-fill unlike BV2). Also
add w-100 to BTableSimple in AboutUser for full-width rendering.

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

* Update migration plan: Phase 4 complete + BVN layout gotchas

Mark Phase 4 as done. Add two BVN-specific notes to the reference
section that will apply to all future phases:
- BTabs vertical requires content-class="flex-fill" to fill width
- BTableSimple requires class="w-100" to be full-width

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

* Remove plans/VUE3_MIGRATION_PLAN.md from git tracking

File is covered by .gitignore and should not be committed.

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

* Fix V3 user settings visual parity with V2

- dark.scss: override body font to Avenir (matching V2's index.html inline
  style) and add .b-form-group margin-bottom (BVN dropped Bootstrap 4's
  .form-group built-in 1rem margin)
- AboutUser.vue: wrap rows in <BTbody> so Bootstrap 5's deep child selector
  (.table > :not(caption) > * > *) matches and row borders render correctly
- UserSettingsConfig.vue: label-cols="auto" → label-cols="4" for consistent
  33% label column across all form rows
- SettingsView.vue: content-class="flex-fill text-start" to fill horizontal
  space and reset text-align inherited from #app { text-align: center }

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 migration: Phase 5 — system configuration page (/config) (#1042)

* Migrate system configuration page to Vue 3 (Phase 5) (#1035)

Ports the admin /config section — Shows, System, Settings, Users, Logs,
Backups — from Vue 2 Vuex to Vue 3 Pinia with bootstrap-vue-next.

Key patterns introduced:
- BModal via ref<InstanceType<typeof BModal>> (replaces v-b-modal directive)
- #footer slot (replaces #modal-footer in BVN)
- window.confirm replaces $bvModal.msgBoxConfirm
- SSE log streaming via Web Streams API (response.body.getReader())
- setTimeout-based polling with onBeforeUnmount cleanup
- Dynamic Vuelidate rules computed from server-returned setting types
- Cross-field date validators using helpers.withMessage

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

* Add reusable ConfirmDialog composable to replace window.confirm

Creates a singleton useConfirm() composable backed by a BModal-based
ConfirmDialog component, replacing all window.confirm() calls with a
styled, accessible modal dialog that matches the rest of the UI.

ConfirmDialog is mounted once in App.vue; all callers share the same
instance via module-level reactive state. Supports title, okVariant,
okTitle, and cancelTitle options mirroring BV2's msgBoxConfirm API.

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

* Fix ConfigView tab layout to match V2 (horizontal, lazy)

V2 used plain horizontal tabs with lazy mounting, not the vertical pill
layout carried over from the user settings page. Also restores lazy prop
so tab content only mounts on first activation, avoiding all polling
timers (sessions, users) starting simultaneously on page load.

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

* Fix Settings accordion animation: use v-model instead of :visible on BCollapse

BVN's BCollapse :visible prop sets localNoAnimation=true in useShowHide,
bypassing the Bootstrap 5 collapsing transition entirely. Switching to
:model-value/@update:model-value keeps animation enabled. Expanded state
changed from string[] to Record<string, boolean> to support v-model binding
per category key.

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

* Fix Settings form control spacing by removing inline margin-bottom override

Bootstrap 5 has no built-in .form-group margin; the global dark.scss adds
margin-bottom: 1rem to .b-form-group, but the inline style="margin-bottom: 0"
copied from V2 was overriding it, causing rows to appear compacted.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 migration: Phase 6 — show configuration foundation (#1043)

- Add stores/show.ts: full Pinia port of V2 show Vuex module (cast, characters,
  character groups, acts, scenes, cue types, sessions, microphones, mic allocations,
  script modes, session tags, stageManagerMode persisted via pinia-plugin-persistedstate)
- Add js/micConflictUtils.ts: mic conflict detection utilities (ported from V2)
- Add views/show/ShowConfigView.vue: sticky vertical nav sidebar + RouterView shell
- Wire /show-config parent route to ShowConfigView.vue (was PlaceholderView)
- Move scriptModes state/action from system.ts to show.ts; update ConfigShows.vue
- Fill in GET_CAST_LIST, ELECTED_LEADER, NO_LEADER, SCRIPT_SCROLL WS action handlers

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 migration: Phase 7 — show configuration basic tabs (#1044)

Ports the four core show config tabs (Show, Acts & Scenes, Cast,
Characters) to Vue 3 + BVN, using Pinia, Vuelidate, and
vue-multiselect.

- Add getShowDetails() + updateShow() actions to systemStore
- Add useStatsTable() composable (port of statsTableMixin)
- ConfigShow: detail table with titleCase keys + edit modal
- ConfigActsAndScenes: Acts CRUD (linked-list order, loop validator)
  and Scenes CRUD (2-column layout, per-act previous/first scene)
- ConfigCast: cast list CRUD + CastLineStats (dynamic act/scene cols)
- ConfigCharacters: character CRUD + CharacterGroups (vue-multiselect)
  + CharacterLineStats
- Wire four child routes in router; remaining placeholders unchanged

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 migration: Phase 8 — Cues & Sessions config tabs (#1045)

* Vue 3 migration: Phase 8 — cue types and session management (#phase8)

Port ConfigCues (cue types CRUD + import + counts stats) and ConfigSessions
(session list with start/stop + session tag CRUD + import) to Vue 3. Adds
useCueDisplay composable, scriptRevisions state + getter + action to show store,
and contrast-color type declarations. Cue Configuration tab deferred to Phase 11.

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

* Fix getScriptRevisions API unwrapping and contrastColor strict-ESM crash

getScriptRevisions was storing the raw API response object instead of
response.revisions. The contrast-color library's standalone function uses
`this.namedColors` internally which is undefined in strict ESM — replaced
with an inline YIQ formula in utils.ts used by all session + cue components.

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

* Replace explicit WS actionMap with convention-based Pinia store dispatch

WS ACTION names (SCREAMING_SNAKE_CASE) are automatically routed to the
matching camelCase action on any instantiated Pinia store, so adding a
new store action is all that's needed to handle the corresponding WS
event — no registration or map entries required.

Only four special cases remain for actions that can't follow the naming
convention: TOKEN_REFRESH, SHOW_CHANGED, USER_LOGOUT, WS_SETTINGS_CHANGED.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 Migration Phase 9: Microphone Configuration (#1048)

* Add Vue 3 migration Phase 9: Microphone configuration

Ports the microphone configuration tab including CRUD, scene×character
allocation grid with delta tracking and conflict display, SVG timeline
with three view modes (mic/character/cast) and PNG export, scene density
heatmap, and resource availability grid.

Key fix: add `lazy` to BTabs to prevent BVN's eager tab rendering from
causing MicAllocations to mount before parent data is loaded, which would
leave internalState empty and break allocation toggling.

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

* Fix ConfigMics tab loading: use v-if="loaded" instead of BTabs lazy

BVN renders all tab panels simultaneously; wrapping the BTabs in a
v-if="loaded" guard (with a spinner in v-else) prevents MicAllocations
from mounting before the parent has fetched microphone data, matching
the V2 pattern exactly.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 migration: Phase 10 — Stage configuration (#1049)

Ports the Stage config tab from V2 to V3, including:
- Crew CRUD (CrewList.vue) with first/last name fields
- Scenery types + items CRUD (SceneryList.vue) with cascade delete warning
- Prop types + items CRUD (PropsList.vue)
- Stage Manager (StageManager.vue): scene navigation, allocation cards,
  SET/STRIKE boundary detection, crew assignments, orphan detection via
  blockOrphanUtils
- SVG props/scenery timeline (StageTimeline.vue) with view mode toggle
  and PNG export
- SVG crew timeline (CrewTimeline.vue) with hard/soft conflict detection
- Timeline side panel (TimelineSidePanel.vue) for crew assignment editing
- Pinia store (stores/stage.ts) with 8 state arrays, parameterised getters,
  and 28 actions; POST/PATCH requests use camelCase keys matching the API
- blockOrphanUtils.ts + tests copied from V2 (pure TS, no Vue deps)
- ConfigStage.vue shell with 5 tabs; router updated to replace PlaceholderView

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Vue 3 migration: Phase 11 — Script & Revisions configuration (#1057)

* Vue 3 migration: Phase 11 — Script & Revisions configuration

Ports the most complex config section of the Vue 2 UI to Vue 3/Pinia:

- Stores: script.ts, scriptConfig.ts (with exported computePageStatus)
- Composables: useScriptNavigation, useScriptDisplay
- Utilities: scriptUtils.ts, mruSortUtils.ts (ported from V2)
- Components: RevisionDetailModal, RevisionGraph (D3 hierarchy + zoom),
  ScriptRevisions (table + branch/load/delete modals),
  CompiledScripts, StageDirectionStyles (CRUD + import),
  ScriptLinePart, ScriptLineViewer, ScriptLineEditor,
  BulkActSceneModal, ScriptEditor (full non-collaborative editor)
- Views: ConfigScript (Script + Stage Direction Styles tabs),
  ConfigScriptRevisions (Revisions + Compiled Scripts tabs)
- Router: both show-config-script routes now point to real views

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

* Fix bugs found during Phase 11 browser testing

- Replace structuredClone with JSON round-trip in scriptConfig store and
  ScriptLineEditor: Vue reactive Proxy objects (Pinia state) cannot be
  structuredCloned in some environments
- Fix canRequestEdit/currentEditor field name mapping (backend returns
  camelCase, store was reading snake_case)
- Reload current page after stopEditing() clears tmpScript so lines
  remain visible in view mode
- Complete StyleForm render function in StageDirectionStyles with all
  form fields (description, bold/italic/underline toggles, text format
  select, text colour picker, background colour toggle + picker)

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

* Fix sticky navbar and transparent sticky header in V3 script editor

BVN's :sticky="true" generates class sticky-true (not Bootstrap's
sticky-top), so the top navbar scrolled away instead of staying fixed.
Replace with class="sticky-top" on BNavbar in App.vue.

Also define --body-background in dark.scss as an alias for
--bs-body-bg, so the script editor sticky header (and timeline
components) have a solid background instead of transparent.

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

* Remove redundant hr below script sticky header

The sticky header already has border-bottom via CSS, so the separate
<hr /> created a double divider with extra gap below the header.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Phase 12: Live Show View (Vue 3 migration) (#1058)

* Add Phase 12: Live Show View (Vue 3 migration)

Ports the real-time live show execution view to Vue 3 + Pinia:

- ShowLiveView.vue: session header, elapsed time, interval overlay with countdown, Splitpanes layout
- ScriptViewPane.vue: compiled/page-by-page script loading, keyboard/wheel navigation, script leader mode, lazy page loading via MutationObserver, add-cue and start-interval modals
- ScriptLineViewer.vue: normal mode with cue buttons, act/scene labels, interval banners, stage direction styling, cue position left/right support
- ScriptLineViewerCompact.vue: compact 2-column mode with cues as rows
- StageManagerPane.vue: scene list with setting/striking props/scenery/crew, auto-expand on session follow

Adds cues state + loadCues/addNewCue actions to script store.
Updates /live route from PlaceholderView to ShowLiveView.

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

* Fix three live view bugs: session guard, splitpanes background, v-once collision

- router: add proper /live session guard matching V2 — redirect home if no
  active session or WebSocket is unhealthy (was placeholder comment)
- ShowLiveView: override splitpanes v4 pane background with correct
  specificity (:deep(.default-theme.splitpanes .splitpanes__pane)) so the
  dark body colour shows instead of the library's default light grey
- ScriptViewPane: remove v-once from ScriptLineViewer/Compact in the
  double-nested v-for — Vue 3 shares _cache[] across all outer iterations
  so every page rendered page 1's cached VNodes, causing all 1275 line IDs
  to appear as page_1_* and making cross-page navigation impossible

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

* Wire show session into App.vue: fix navbar state and live redirect

currentShowSession was hardcoded to null (Phase 6 placeholder), causing
three bugs: Live nav item always disabled, System/Show Config items never
hidden during sessions, and no auto-redirect to /live on initial load.

- Import useShowStore and useRouter in App.vue
- currentShowSession now reads from showStore.currentSession (reactive)
- awaitWSConnect fetches session data after WS connects, then redirects
  to /live if a session is already active and the user isn't there yet
  (matches V2 App.vue awaitWSConnect behaviour)

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

* Fix live redirect to use full path matching WS composable convention

router.push('/live') with createWebHistory('/ui-new/') navigates to the
V2 app at /live instead of the V3 app at /ui-new/live. The WS composable
already uses the full path explicitly (router.push('/ui-new/live')) and
checks router.currentRoute.value.path against '/ui-new/live'. Apply the
same pattern in awaitWSConnect.

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

* Revert live redirect path: useRouter() prepends base automatically

router.push('/ui-new/live') via useRouter() causes double-base
/ui-new/ui-new/live. The direct module import in useWebSocket.ts bypasses
base handling and needs the full path; useRouter() in components does not.
Revert to router.push('/live') with currentRoute path check against '/live'.

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

* Fix cues not rendering: contrastColor called with object instead of string

contrastColor(utils.ts) expects a plain string but the template passed
{ bgColor: '...' }. This threw TypeError inside the v-once BContainer
block; Vue caught the error and cached the failed (empty) render for the
cue column. Fix: call contrastColor(cueBackgroundColour(cue)) directly.

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

* Add variant="success" to add-cue buttons in ScriptLineViewer

Matches V2 styling — compact viewer already had this correctly.

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

* Fix V2 parity issues in live show view

- Add Stage Manager toggle to Live Config navbar dropdown
- Fix needsIntervalBanner to show at all act boundaries (not just interval_after=true)
- Fix cue_position_right default to false (left) matching backend default
- Block keyboard/wheel navigation when interval or cue modal is open
- Move add-cue "+" button outside BButtonGroup for independent rounded corners
- Add variant="success" to "+" button for correct green styling

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

* Fix START_SHOW/STOP_SHOW router push paths

Vue Router 4 applies the base path (/ui-new/) internally regardless of
whether the router is accessed via useRouter() or direct import. Using
full paths like /ui-new/live caused double-prepending to /ui-new/ui-new/live.

Also fix the currentRoute.value.path guard comparisons — the path property
returns the route without the base prefix.

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

* Fix V2 parity gaps found during pre-release audit

- HomeView: wire currentShowSession to showStore.currentSession (was null placeholder)
- App.vue: add CreateUser component to the no-admin-user setup screen
- App.vue: call getRbacRoles() in awaitWSConnect so navbar RBAC is ready before first navigation
- Router: /live route now requiresAuth: false (unauthenticated clients can join live view)
- Router: already-logged-in redirect returns from.fullPath instead of / unconditionally
- Router: /force-password-change gains requiresPasswordChange: true meta
- Router: remove unused PlaceholderView import
- stores/system: settingsChanged skips re-fetch when show ID unchanged; redirects away
  from /show-config and /live when no show is loaded (matching V2 behaviour)
- stores/show: noLeader toast is now persistent (duration: 0) and dismissed when a
  leader is elected or getShowSessionData finds an active leader
- useWebSocket: re-fetch show session data on reconnect after errors

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

* Add cue assignment editor to Phase 12 (Cue Configuration tab)

Ports the V2 CueEditor/ScriptLineCueEditor/JumpToCueModal trio to V3,
completing the Cue Configuration tab previously showing a placeholder.

- ScriptLineCueEditor: per-line cue display with add/edit/delete modals,
  RBAC-filtered cue type options, and duplicate-ident validation
- JumpToCueModal: fuzzy cue search with exact/suggestion/no-match states
- CueEditor: page navigator with localStorage persistence, Go To Page modal,
  and adjacent-page pre-fetching for smooth scrolling
- script store: adds editCue, deleteCue, and searchCues actions
- ConfigCues: replaces PlaceholderView with CueEditor in Cue Configuration tab

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

* Fix cue column visual parity: remove border-right, square add button

- Remove border-right from .cue-column (V2 had no vertical divider line)
- Move "+" button inside BButtonGroup (matches V2 behaviour)
- Add .add-cue-button CSS to make the "+" button a square icon-sized
  element, matching V2's plus-square-fill icon appearance

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

* Use inline plus-square-fill SVG for add-cue button

Replaces the variant="success" text "+" button with an inline SVG that
faithfully reproduces V2's b-icon-plus-square-fill icon: default button
variant (dark background in dark mode) with a 1em × 1em green filled-
square icon, exactly matching the original cue column appearance.

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

* Fix add-cue icon colour to match V2 (#06BC8C)

SVG fill was using Bootstrap 5's default success green (#198754).
V2's b-icon-plus-square-fill variant="success" resolved to the Bootswatch
darkly success colour (#06BC8C), which is the correct value.

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

* Restore 15px base font size to match V2 Bootswatch darkly

Bootstrap 4 Bootswatch darkly sets $font-size-base: 0.9375rem (15px).
Bootstrap 5 Bootswatch darkly drops this override, defaulting to 1rem
(16px). Every rem-based size — line heights, paddings, margins, button
heights — was 6.7% larger, reducing visible content per screen.

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

* Fix navbar padding and font weight to match V2

Bootstrap 4 defaulted $navbar-padding-x to $spacer (1rem = 16px);
Bootstrap 5 changed it to null → 0px, making navbar content flush to
the viewport edge. Restore with $navbar-padding-x: 1rem.

V2 App.vue had a global `nav a { font-weight: bold }` rule making the
navbar brand and nav-links bold (fontWeight 700). V3 never carried this
over. Add equivalent scoped to .navbar so tab nav-links are unaffected.

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

* Fix visual parity between V3 and V2 UI across all pages

Resolves ~15 visual differences identified by page-by-page Playwright
comparison to make the V3 migration look identical to V2.

Global CSS (dark.scss SCSS variable overrides before Bootstrap imports):
- border-radius: .375rem → .25rem (matching Bootstrap 4 default)
- table-cell-padding: .5rem → .75rem (matching Bootstrap 4 default)
- dropdown-item-padding-x: 1rem → 1.5rem (matching Bootstrap 4 default)
- link-decoration: none (Bootstrap 5 Reboot adds underline by default)
- Active navbar link: add #nav-collapse a.router-link-active teal colour
- Vertical nav-pills: add full-width + centered rule for sidebar pills

Per-component fixes:
- ConfigActs: interval_after badge → check-square/x-square SVG icons
- ScriptRevisions: ✓ span → check-square SVG; add Edit button; remove
  size="sm" from table buttons; replace text ▼/▲ with chevron SVG icons
- CrewList: "Add Crew Member" → "New Crew Member" (match V2 label)
- MicList: "Add Microphone" → "New Microphone" (match V2 label)
- SessionTagDropdown: ✏️ emoji → inline pencil-fill SVG icon
- ConfigCast: add explicit 'First Name'/'Last Name' labels (BVN
  auto-label only capitalises first word unlike BV2)
- AboutUser: add text-center to match V2's centred table alignment

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Phase 12: Integrate unplugin-icons with Material Design Icons (#1059)

* Phase 12: Integrate unplugin-icons with Material Design Icons

Replaces all inline SVGs and Unicode text-character icon substitutes across
the V3 UI with auto-imported MDI components (unplugin-icons + unplugin-vue-
components resolver). Removes the bi and ph iconify packages after user chose
MDI following a side-by-side preview page comparison.

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

* Replace remaining text-character icon substitutes with MDI components

- StageManagerPane: ▼/▶ expand indicators → IMdiChevronDown/IMdiChevronRight;
  📌 pin emoji → IMdiPin
- TimelineSidePanel: ⚠ Conflicts heading → IMdiAlert
- RevisionGraph: +/-/↺ zoom buttons → IMdiMagnifyPlus/IMdiMagnifyMinus/IMdiRefresh
  (V2 used b-icon-zoom-in, b-icon-zoom-out, b-icon-arrow-clockwise)

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Phase 12c: Dual-UI integration (#1061)

* Phase 12c: Dual-UI integration (default UI setting, user preference, navbar toggle)

- Dockerfile: split into parallel build_v2/build_v3 stages (BuildKit builds them
  concurrently); Python stage copies assets from both via --from directives
- CI: add V3 install + build steps to build-server.yml frontend job
- Backend: add default_ui system setting (choice: old/new, default old); when set
  to "new" the root "/" redirects to "/ui-new/"
- Backend: add preferred_ui field to UserSettings model + CheckConstraint;
  Alembic migration 11311df29aa4
- V2 App.vue: "Switch to New UI" navbar link; post-login redirect to /ui-new/ when
  preferred_ui == "new"
- V3 App.vue: "Switch to Classic UI" navbar link; post-login redirect to / when
  preferred_ui == "old"
- V2 + V3 user settings pages: preferred_ui dropdown (system default / Classic / New)

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

* Fix Phase 12c dual-UI integration bugs

- RootController.get(): make async and await digi_settings.get() so
  the default_ui redirect actually evaluates; add _switch bypass
- Both navbar toggle links now include ?_switch=1 so clicking them
  bypasses server-side and client-side redirects, preventing loops
- V2 awaitWSConnect: also check system default_ui when preferred_ui
  is null, matching the full redirect logic from the plan
- V2 Settings: add real isValidUi validator to preferred_ui so
  Vuelidate tracks it as a dirty-able field and enables Submit
- V3 UserSettingsConfig: replace v$.$anyDirty with computed formDirty
  (JSON.stringify comparison) to avoid Vue 3 watcher async race
- V2 vite.config: add cleanV2StaticPlugin to preserve server/static/ui-new/
  when rebuilding V2; set emptyOutDir: false

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

* Fix V2 settings dirty tracking and cross-UI redirect loop

- V2 Settings: replace $anyDirty (unreliable for nested Vuelidate v0
  groups) with a computed formDirty using JSON.stringify comparison,
  matching the approach used in V3; also track savedSettings so Reset
  and Submit buttons correctly reflect unsaved changes
- V3 App: redirect to /?_switch=1 (not /) when preferred_ui='old',
  preventing an infinite loop when default_ui='new' causes the server
  to immediately redirect / back to /ui-new/

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

* Remove ?_switch=1 query param from URL after cross-UI navigation

After the redirect decision has been made, use router.replace() with
the current route path (base-stripped) to clean up the _switch param
from the URL bar without adding a history entry.

Uses $route.path (V2) and router.currentRoute.value.path (V3) rather
than window.location.pathname to avoid Vue Router's base-path doubling
in the V3 app (which is mounted at /ui-new/).

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Phase 13: Electron support for Vue 3 frontend (#1070)

- client-v3 router: detect file:// protocol and use createWebHashHistory()
  so deep-linking and refresh work correctly when loaded via Electron
- client-v3 App.vue: fix switchServer() to use router.push() instead of
  window.location.href (works in both hash and history modes); hide
  "Switch to Classic UI" link in Electron (no web server to switch to)
- client-v3 platform/electron.ts: extend Window augmentation with full
  IPC surface (getAllConnections, addConnection, deleteConnection,
  setActiveConnection, checkVersion, discoverServersWithVersionCheck);
  export ElectronConnection, ElectronVersionCheckResult,
  ElectronDiscoveredServer interfaces
- client-v3 ServerSelector.vue: full Vue 3 port of the V2 Options API
  component (saved connections, mDNS discovery, manual add, status
  polling) using script setup, Vuelidate 4, useConfirm, and MDI icons
- client App.vue: hide "Switch to New UI" link in Electron (no /ui-new/
  path exists in Electron build)
- electron/main.ts: fix dev-mode static path (../client/dist-electron was
  resolving relative to electron/dist/, needed ../../client/dist-electron)

Electron continues to serve the V2 frontend; V3 Electron support is
fully scaffolded for the Phase 14 cutover.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix CodeQL alert 1154 and add client-v3 to CI tooling (#1071)

Guard convention-based Pinia dispatch with Object.hasOwn() to prevent
accidental dispatch to prototype-inherited method names (CodeQL js/unvalidated-dynamic-method-call #1154).

Add dependabot npm entry for /client-v3 and a client-v3 labeler rule so
automated dependency updates and PR labels work once client-v3 lands in dev.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix SonarCloud reliability issues (PR #1036) (#1072)

* Fix SonarCloud reliability issues for PR #1036

HIGH: Use run_in_executor for file read in async RootController.get() to
avoid blocking the Tornado event loop (python:S7493).

MEDIUM: Remove dead ternary in blankLine() — both branches returned []
(typescript:S3923). Associate form labels with their BFormInput controls
via for/id attributes in ScriptViewPane.vue (Web:S6853, 3 instances).

LOW/quality-gate: Replace bare parseInt() with Number.parseInt() across
12 client-v3 files (36 occurrences) to satisfy typescript:S7773 and
improve the SonarCloud reliability rating from C toward A.

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

* Fix negated ternary condition in ScriptEditor.vue (typescript:S7735)

Flip storedPage != null ternary to non-negated form so the null/default
case comes first: storedPage == null ? 1 : Number.parseInt(storedPage, 10).

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve SonarCloud HIGH and MEDIUM maintainability issues (#1073)

* fix: resolve SonarCloud HIGH and MEDIUM maintainability issues

Addresses 9 HIGH cognitive complexity (S3776), 8 deep nesting (S2004),
10 inner-function scope (S7721), 1 duplicate import (S3863), 8 JSON
clone (S7784), and 3 CSS contrast (S7924) issues flagged on PR #1036.

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

* fix: revert structuredClone in ScriptLineEditor — Vue reactive Proxy incompatible

structuredClone cannot clone Vue reactive Proxy objects. ScriptLineEditor
receives props.modelValue as a reactive proxy from the store, causing a
DataCloneError at runtime. Revert to JSON round-trip (same pattern as
scriptConfig.ts:74) with an explanatory comment.

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

* fix: revert structuredClone in computePageStatus — Pinia reactive arrays cannot be cloned

actualScriptPage, insertedLines, and tmpScriptPage all originate from Pinia reactive
state (Proxy objects). structuredClone throws DataCloneError on these at runtime.
Revert to JSON round-trip, matching the existing pattern at line 74.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address critical error handling issues from PR #1036 review (#1074)

- TOKEN_REFRESH WS handler: fix data shape bug — handler received msg.DATA
  but was reading payload.DATA.access_token (i.e. msg.DATA.DATA), always
  undefined; corrected to (data as { access_token: string }).access_token
- WS_AUTH_ERROR: add toast notification and logout call so the user is
  informed and redirected rather than left in a silently broken state
- refreshToken(): wrap fetch in try/catch so network errors return false
  rather than propagating as unhandled rejections
- login(): wrap post-login data fetches (getRbacRoles, getCurrentUser,
  getCurrentRbac, getUserSettings, setupTokenRefresh) in try/catch with
  token rollback to prevent half-logged-in ghost state
- App.vue startup: wrap startup sequence in try/catch; add startupError
  state with a visible error message and Retry button instead of a
  permanent spinner on failure
- getMaxPage(): return boolean success flag; saveScript() in ScriptEditor
  aborts with a toast if getMaxPage() fails rather than proceeding with
  potentially stale page count

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Post-merge review fixes: 401 queuing, revision toasts, ws.onopen, logout await, dropdown fix (#1075)

* Post-merge review fixes: 401 queuing, revision toasts, onopen guard, logout await

- Queue concurrent 401s during token refresh rather than immediately
  logging out; replay with the new token on success or resolve with a
  synthetic 401 on failure (http-interceptor.ts)
- Parse server response body in addScriptRevision, deleteScriptRevision,
  loadScriptRevision error branches so 409 conflict messages reach the user
  (stores/show.ts)
- Wrap ws.onopen async body in try/catch so getShowSessionData() rejection
  doesn't become an unhandled promise rejection (useWebSocket.ts)
- Move Sign Out click handler to an awaited handleLogout() so async logout
  steps complete before the UI updates (App.vue)

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

* Fix script line editor dropdown button and positioning

The arrow toggle on each script line's Edit button had two bugs:

1. @click on BDropdown fell through to the root DOM element (BVN does
   not declare 'click' in its emits), so both the split button and the
   toggle arrow triggered editLine. Fixed by splitting into a BButtonGroup
   containing a plain BButton for 'Edit' and a standalone BDropdown for
   the context menu toggle.

2. boundary="window" applied BVN's position-static class to the BDropdown
   root, which changed the dropdown menu's offsetParent and caused
   Floating UI to compute a (0,0) transform — placing the menu at the
   top-left of the script editor container. Removed boundary="window";
   without it the BDropdown root stays positioned and Floating UI anchors
   the menu correctly next to the toggle button.

Same fix applied to the 'Add Dialogue' split dropdown in ScriptEditor.
Verified via Playwright: Edit enters edit mode, toggle opens menu in the
correct right-aligned position within the viewport.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix 88 SonarQube maintainability issues on Vue 3 migration PR (#1076)

Mechanical fixes across 30 files in client-v3/:

- S7765: indexOf() !== -1 → .includes() (ScriptLineViewer)
- S7770: arrow fn equivalent to Boolean → Boolean (ScriptLineViewer)
- S7755: [length - 1] → .at(-1) (blockOrphanUtils)
- S7754: .find() existence check → .some() (ConfigActs, ConfigScenes, system)
- S7784: JSON.parse/stringify → // NOSONAR where structuredClone can't be
  used on Pinia Proxy objects (scriptConfig, ScriptLineEditor)
- S7746: return Promise.resolve() → return (CueEditor, ScriptViewPane)
- S7723: Array() → new Array() (ScriptViewPane)
- S3863: duplicate imports from same module merged (ScriptLinePart,
  CueColourPreferences)
- S1128: unused imports removed — log (ScriptEditor), computed (StageDirectionStyles)
- S7735: negated conditions flipped to positive form in 20+ locations across
  show, script, scriptConfig stores and multiple components
- S6582: manual null guards replaced with optional chaining (logger,
  useScriptNavigation, useStatsTable, micConflictUtils, router)
- S7721: _handleOk/_handleHidden moved to module scope (useConfirm)
- S6571: redundant EntityType | string union → string (useTimeline)
- S4325: unnecessary type assertion removed (user store)
- S3358: nested ternary extracted to if/else (ScriptEditor, CrewTimeline)
- S7735: show.ts byId getter pattern and orderedScenes traversal
- S6582: router optional chain + negated condition (router/index)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix remaining SonarQube maintainability issues (round 2) (#1077)

- Remove redundant return statements in CueEditor and ScriptViewPane
- Replace .filter().length > 0 with .some() in system.ts (two more instances)
- Fix WCAG AA contrast failures in App.vue (#e74c3c → #a93226, #00bc8c → #007a5e)
- Use node: protocol for built-in imports in vite/vitest configs
- Replace rgba backgrounds with solid equivalents in ResourceAvailability.vue
- Extract "index.html" string literal to constant in controllers.py
- Remove empty <style scoped> block from LoginView.vue
- Mark ScriptCut type alias with NOSONAR

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@Tim020 Tim020 added the release Pull requests for creating a new release label May 24, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 24, 2026

Client Test Results

128 tests   128 ✅  0s ⏱️
  6 suites    0 💤
  1 files      0 ❌

Results for commit c39e427.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 24, 2026

Client V3 Test Results

23 tests   23 ✅  0s ⏱️
 2 suites   0 💤
 1 files     0 ❌

Results for commit c39e427.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 24, 2026

Python Test Results

  1 files    1 suites   1m 33s ⏱️
603 tests 603 ✅ 0 💤 0 ❌
608 runs  608 ✅ 0 💤 0 ❌

Results for commit c39e427.

♻️ This comment has been updated with latest results.

@Tim020 Tim020 enabled auto-merge May 24, 2026 00:52
…nnotation

bonjour-service uses `export = Bonjour` (CommonJS namespace), making the named
`Service` import resolve to the class constructor (a value), not an instance type.
Removing the redundant annotation lets TypeScript infer the correct type from the
typed EventEmitter<BrowserEvents> signature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
5.7% Duplication on New Code (required ≤ 3%)
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@Tim020 Tim020 merged commit 4de13a8 into main May 24, 2026
35 of 36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Pull requests for creating a new release xlarge-diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant