Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
167a2f2
Cleanup server side code and comments
Tim020 Jan 26, 2026
32a5019
Add future annotations for TYPE_CHECKING compatibility (#881)
Tim020 Jan 27, 2026
b6be4b8
Add server-side version checker (#878) (#880)
Tim020 Jan 27, 2026
0dd0650
Standardize on `from __future__ import annotations` and unquote type …
Tim020 Jan 28, 2026
6314033
Stage & Crew features (#692)
Tim020 Jan 29, 2026
ea903fa
Update electron package dependencies
Tim020 Jan 29, 2026
f5c3327
Merge branch 'main' into dev
Tim020 Jan 29, 2026
108871b
Bump alembic from 1.18.1 to 1.18.2 in /server (#887)
dependabot[bot] Jan 29, 2026
3985876
Update electron package dependencies
Tim020 Jan 30, 2026
cb61840
Refactor frontend components to use Promise.all for parallel data fet…
Tim020 Jan 30, 2026
66bbf37
Add hover colour change to active/next scene cards
Tim020 Jan 30, 2026
7323053
Fix duplicate HTML IDs to pass SonarQube quality gate (#892)
Tim020 Jan 30, 2026
b611dcb
Reduce code duplication to pass SonarQube quality gate (#893)
Tim020 Jan 31, 2026
b8829f0
Bump alembic from 1.18.2 to 1.18.3 in /server (#891)
dependabot[bot] Jan 31, 2026
4ba439a
Improved Stage Manager features (#895)
Tim020 Feb 1, 2026
fca6d7b
[npm] Update packages
Tim020 Feb 1, 2026
530c839
Always show Plan button in stage manager panel
Tim020 Feb 1, 2026
c3ab648
Bump version to 0.25.0
Tim020 Feb 1, 2026
ba6adfd
Add documentation for stage management features from PR 885 (#896)
Tim020 Feb 2, 2026
3d9e56d
Bump pyjwt[crypto] from 2.10.1 to 2.11.0 in /server (#897)
dependabot[bot] Feb 2, 2026
fdb600d
Add stage crew assignments with timeline visualization (#902)
Tim020 Feb 11, 2026
0b04581
[npm] Update packages
Tim020 Feb 11, 2026
c8cefdf
Bump alembic from 1.18.3 to 1.18.4 in /server (#903)
dependabot[bot] Feb 11, 2026
697f892
Bump ruff from 0.14.14 to 0.15.0 in /server (#899)
dependabot[bot] Feb 11, 2026
5d8f717
[npm] Update electron packages
Tim020 Feb 13, 2026
e7bd116
[pypi] Bump ruff to 0.15.1
Tim020 Feb 13, 2026
e351e07
Allow user configurable log level (#911)
Tim020 Feb 13, 2026
2563803
[Fix] Require first user to be admin and auth user create endpoint
Tim020 Feb 15, 2026
a3a1d90
Auto login initial admin user upon creation
Tim020 Feb 15, 2026
894f29f
[Fix] Remove disable behaviour on Settings->Users tab (#914)
Tim020 Feb 15, 2026
a7ddebe
[Docs] Add link to DeepWiki site
Tim020 Feb 15, 2026
0c46096
[Fix] Users table empty when now show loaded
Tim020 Feb 15, 2026
b408bac
Allow multiple admin users to be created
Tim020 Feb 15, 2026
9750978
Move shows table into dedicated tab (#915)
Tim020 Feb 15, 2026
3de1161
Fix CI: update nodelint.yml actions and regenerate electron lock file…
Tim020 Feb 19, 2026
bfbb47d
Client and Server logging improvements (#923)
Tim020 Feb 21, 2026
14499ee
Disable logging from websocket sessions controller
Tim020 Feb 21, 2026
b82c9e9
Bump ruff from 0.15.1 to 0.15.2 in /server (#926)
dependabot[bot] Feb 22, 2026
d352e9b
Add log redaction setting and config to user routes (#933)
Tim020 Feb 23, 2026
30c0e50
Refactor log buffer into utils package
Tim020 Feb 23, 2026
7d15f90
Organise system settings into categories (#934)
Tim020 Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/nodelint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ jobs:
run:
working-directory: ./client
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
Expand All @@ -23,8 +23,8 @@ jobs:
run:
working-directory: ./electron
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
CLAUDE.md
.idea/
.playwright-mcp/
PLAN.md
plans/

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
Expand Down
199 changes: 103 additions & 96 deletions client/package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "client",
"version": "0.24.2",
"version": "0.25.0",
"description": "DigiScript front end",
"author": "DreamTeamProd",
"private": true,
Expand Down Expand Up @@ -41,6 +41,7 @@
"lodash": "4.17.23",
"loglevel": "1.9.2",
"marked": "11.2.0",
"splitpanes": "^2.4.1",
"vue": "2.7.14",
"vue-multiselect": "2.1.9",
"vue-native-websocket": "2.0.15",
Expand All @@ -51,9 +52,9 @@
"vuex-persistedstate": "3.2.1"
},
"devDependencies": {
"@babel/core": "7.28.6",
"@babel/core": "7.29.0",
"@babel/eslint-parser": "7.28.6",
"@babel/preset-env": "7.28.6",
"@babel/preset-env": "7.29.0",
"@eslint/js": "^9.39.2",
"@types/vuelidate": "^0.7.22",
"@vitejs/plugin-vue2": "2.3.4",
Expand Down
29 changes: 19 additions & 10 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@
>
Jump To Page
</b-dropdown-item>
<b-dropdown-item-btn
:disabled="
CURRENT_SHOW_SESSION == null ||
!WEBSOCKET_HEALTHY ||
stoppingSession ||
startingSession
"
@click.stop.prevent="SET_STAGE_MANAGER_MODE(!STAGE_MANAGER_MODE)"
>
{{ STAGE_MANAGER_MODE ? 'Disable' : 'Enable' }} Stage Manager
</b-dropdown-item-btn>
</b-nav-item-dropdown>
</template>
<b-nav-item
Expand Down Expand Up @@ -162,7 +173,7 @@
</template>

<script>
import { mapGetters, mapActions } from 'vuex';
import { mapGetters, mapActions, mapMutations } from 'vuex';
import log from 'loglevel';
import CreateUser from '@/vue_components/user/CreateUser.vue';
import { makeURL } from '@/js/utils';
Expand Down Expand Up @@ -227,6 +238,7 @@ export default {
'IS_SCRIPT_EDITOR',
'IS_CUE_READER',
'IS_CUE_EDITOR',
'STAGE_MANAGER_MODE',
]),
},
async created() {
Expand Down Expand Up @@ -285,6 +297,7 @@ export default {
'CHECK_WEBSOCKET_STATE',
'GET_USER_SETTINGS',
]),
...mapMutations(['SET_STAGE_MANAGER_MODE']),
isElectron,
async switchServer() {
// Clear the active connection and disconnect WebSocket
Expand All @@ -311,19 +324,15 @@ export default {
async awaitWSConnect() {
if (this.WEBSOCKET_HEALTHY) {
clearTimeout(this.loadTimer);
await this.GET_RBAC_ROLES();

// Check WebSocket state for any pending operations
if (this.WEBSOCKET_HAS_PENDING_OPERATIONS) {
await this.CHECK_WEBSOCKET_STATE();
}
await Promise.all([
this.GET_RBAC_ROLES(),
this.WEBSOCKET_HAS_PENDING_OPERATIONS ? this.CHECK_WEBSOCKET_STATE() : Promise.resolve(),
]);

// Check for authentication via token first
if (this.AUTH_TOKEN) {
// Then get user data
await this.GET_CURRENT_USER();
await this.GET_CURRENT_RBAC();
await this.GET_USER_SETTINGS();
await Promise.all([this.GET_CURRENT_RBAC(), this.GET_USER_SETTINGS()]);
}

if (this.SETTINGS.current_show != null) {
Expand Down
8 changes: 8 additions & 0 deletions client/src/assets/styles/dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@
:root {
--body-background: #{$body-bg};
}

.splitpanes {
background: inherit !important;
}

.splitpanes__pane {
background: inherit !important;
}
121 changes: 121 additions & 0 deletions client/src/assets/styles/timeline.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Shared styles for timeline visualization components (MicTimeline, StageTimeline).
*
* Usage: Import in Vue component <style> block:
* @use '@/assets/styles/timeline';
*
* Then apply the container class to your root element:
* <div class="timeline-container">
*/

.timeline-container {
position: relative;
background-color: var(--body-background);
border: 1px solid #dee2e6;
border-radius: 0.25rem;
overflow: hidden;
min-height: 400px;
height: calc(100vh - 200px);
}

.timeline-wrapper {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}

.timeline-controls-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: rgba(52, 58, 64, 0.95);
border-bottom: 1px solid #6c757d;
z-index: 10;
flex-shrink: 0;
}

.svg-container {
flex: 1;
overflow: auto;
position: relative;
min-height: 0;
}

.timeline-svg {
display: block;
background-color: var(--body-background);
}

// Scene dividers (vertical lines)
.scene-divider {
stroke: #6c757d;
stroke-width: 1;
opacity: 0.6;
shape-rendering: crispEdges;
}

// Act labels
.act-header {
fill: #343a40;
stroke: #6c757d;
stroke-width: 1px;
}

.act-label {
fill: #dee2e6;
font-size: 14px;
font-weight: 600;
pointer-events: none;
user-select: none;
}

// Scene labels
.scene-label {
fill: #adb5bd;
font-size: 11px;
pointer-events: none;
user-select: none;
}

// Row labels
.row-label {
fill: #dee2e6;
font-size: 12px;
font-weight: 500;
pointer-events: none;
user-select: none;
}

// Row separators (horizontal lines)
.row-separator {
stroke: #6c757d;
stroke-width: 1px;
opacity: 0.5;
}

// Allocation bars
.allocation-bar {
stroke: #212529;
stroke-width: 1px;
cursor: pointer;
transition:
opacity 0.2s ease,
stroke-width 0.2s ease;

&:hover {
opacity: 0.8;
stroke-width: 2px;
stroke: #fff;
}
}

.bar-label {
fill: #fff;
font-size: 12px;
font-weight: 600;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
user-select: none;
}
133 changes: 133 additions & 0 deletions client/src/js/blockOrphanUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Block computation and orphan detection utilities for stage crew assignments.
*
* A "block" is a consecutive sequence of scenes (within an act) where an item
* is allocated. The first scene is the SET boundary; the last is the STRIKE
* boundary. These pure functions mirror the backend logic in
* server/utils/show/block_computation.py.
*/

/**
* Compute allocation blocks from ordered scenes and a set of allocated scene IDs.
*
* Blocks break at act boundaries and allocation gaps.
*
* @param {Array<{id: number, act: number}>} orderedScenes - Scenes in display order
* @param {Set<number>} allocatedSceneIds - Scene IDs where the item is allocated
* @returns {Array<{actId: number, sceneIds: number[], setSceneId: number, strikeSceneId: number}>}
*/
export function computeBlocks(orderedScenes, allocatedSceneIds) {
if (
!orderedScenes ||
orderedScenes.length === 0 ||
!allocatedSceneIds ||
allocatedSceneIds.size === 0
) {
return [];
}

const blocks = [];
let currentBlockScenes = [];
let currentActId = null;

for (const scene of orderedScenes) {
// Act boundary breaks the current block
if (currentActId !== null && scene.act !== currentActId) {
if (currentBlockScenes.length > 0) {
blocks.push({
actId: currentActId,
sceneIds: [...currentBlockScenes],
setSceneId: currentBlockScenes[0],
strikeSceneId: currentBlockScenes[currentBlockScenes.length - 1],
});
currentBlockScenes = [];
}
}
currentActId = scene.act;

if (allocatedSceneIds.has(scene.id)) {
currentBlockScenes.push(scene.id);
} else if (currentBlockScenes.length > 0) {
blocks.push({
actId: currentActId,
sceneIds: [...currentBlockScenes],
setSceneId: currentBlockScenes[0],
strikeSceneId: currentBlockScenes[currentBlockScenes.length - 1],
});
currentBlockScenes = [];
}
}

// Flush last block
if (currentBlockScenes.length > 0) {
blocks.push({
actId: currentActId,
sceneIds: [...currentBlockScenes],
setSceneId: currentBlockScenes[0],
strikeSceneId: currentBlockScenes[currentBlockScenes.length - 1],
});
}

return blocks;
}

/**
* Find crew assignments that would become orphaned by adding or removing
* a scene allocation.
*
* @param {Object} params
* @param {Array<{id: number, act: number}>} params.orderedScenes - All scenes in order
* @param {Array<{scene_id: number}>} params.currentAllocations - Current allocations for the item
* @param {Array<{id: number, scene_id: number, assignment_type: string, crew_id: number}>} params.crewAssignments - Current crew assignments for the item
* @param {'add'|'remove'} params.changeType - Whether a scene allocation is being added or removed
* @param {number} params.changeSceneId - The scene ID being added/removed
* @returns {Array<{id: number, scene_id: number, assignment_type: string, crew_id: number}>} Orphaned assignments
*/
export function findOrphanedAssignments({
orderedScenes,
currentAllocations,
crewAssignments,
changeType,
changeSceneId,
}) {
if (!crewAssignments || crewAssignments.length === 0) {
return [];
}

// Build current allocated set
const currentSet = new Set(currentAllocations.map((a) => a.scene_id));

// Simulate the change
const newSet = new Set(currentSet);
if (changeType === 'add') {
newSet.add(changeSceneId);
} else if (changeType === 'remove') {
newSet.delete(changeSceneId);
}

// Compute blocks before and after
const oldBlocks = computeBlocks(orderedScenes, currentSet);
const newBlocks = computeBlocks(orderedScenes, newSet);

// Build valid boundary sets from new blocks
const validSetScenes = new Set(newBlocks.map((b) => b.setSceneId));
const validStrikeScenes = new Set(newBlocks.map((b) => b.strikeSceneId));

// Also check which assignments were valid before — only flag ones that
// become invalid (were on a valid boundary before, but aren't after)
const oldValidSetScenes = new Set(oldBlocks.map((b) => b.setSceneId));
const oldValidStrikeScenes = new Set(oldBlocks.map((b) => b.strikeSceneId));

return crewAssignments.filter((assignment) => {
if (assignment.assignment_type === 'set') {
const wasValid = oldValidSetScenes.has(assignment.scene_id);
const isValid = validSetScenes.has(assignment.scene_id);
return wasValid && !isValid;
} else if (assignment.assignment_type === 'strike') {
const wasValid = oldValidStrikeScenes.has(assignment.scene_id);
const isValid = validStrikeScenes.has(assignment.scene_id);
return wasValid && !isValid;
}
return false;
});
}
Loading
Loading