Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ COPY /client/.npmrc /client/.npmrc
WORKDIR /client
RUN npm ci
COPY /client /client
COPY /docs /docs
RUN npm run build

FROM node:24-bookworm AS build_v3

RUN mkdir -p /server/static/ui-new
RUN mkdir -p /server/static
COPY /client-v3/package.json /client-v3/package.json
COPY /client-v3/package-lock.json /client-v3/package-lock.json
WORKDIR /client-v3
RUN npm ci
COPY /client-v3 /client-v3
COPY /docs /docs
RUN npm run build

FROM python:3.13-bookworm
Expand All @@ -30,7 +30,7 @@ RUN apt install -y nano

COPY /server /server
COPY --from=build_v2 /server/static /server/static
COPY --from=build_v3 /server/static/ui-new /server/static/ui-new
COPY --from=build_v3 /server/static /server/static
WORKDIR /server
RUN mkdir conf
EXPOSE 8080
Expand Down
2 changes: 1 addition & 1 deletion client-v3/e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test';

export const SERVER_PORT = 8888;
export const UI_BASE = `http://localhost:${SERVER_PORT}/ui-new`;
export const UI_BASE = `http://localhost:${SERVER_PORT}`;
export const ADMIN_USERNAME = 'admin';
export const ADMIN_PASSWORD = 'testpassword';

Expand Down
1 change: 1 addition & 0 deletions client-v3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"private": true,
"type": "module",
"scripts": {
"prebuild": "scripts/copy-docs.sh && node --strip-types scripts/generate-doc-manifest.ts",
"build": "vite build",
"build:analyze": "vite build --mode analyze",
"lint": "npm run format && npm run lint:eslint",
Expand Down
16 changes: 16 additions & 0 deletions client-v3/scripts/copy-docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
set -e

DOCS_SOURCE="../docs"
DOCS_DEST="./public/docs"

echo "Copying documentation assets..."

# Remove existing docs and ensure parent dir exists
rm -rf "$DOCS_DEST"
mkdir -p "$(dirname "$DOCS_DEST")"

# Copy docs directory
cp -r "$DOCS_SOURCE" "$DOCS_DEST"

echo "Documentation assets copied successfully"
135 changes: 135 additions & 0 deletions client-v3/scripts/generate-doc-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env node
import fs from 'fs';

Check warning on line 2 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:fs` over `fs`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFA&open=AZ6sZiB4t2R3LkcYbnFA&pullRequest=1178
import path from 'path';

Check warning on line 3 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:path` over `path`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFB&open=AZ6sZiB4t2R3LkcYbnFB&pullRequest=1178
import { fileURLToPath } from 'url';

Check warning on line 4 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:url` over `url`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFC&open=AZ6sZiB4t2R3LkcYbnFC&pullRequest=1178

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const DOCS_DIR = path.join(__dirname, '../../docs');
const OUTPUT_DIR = path.join(__dirname, '../public/docs');
const MANIFEST_PATH = path.join(OUTPUT_DIR, 'manifest.json');

interface ManifestEntry {
title: string;
slug: string;
path: string;
category: string;
order: number;
}

function generateSlug(filepath: string): string {
// pages/getting_started.md → getting-started
// pages/show_config/acts_and_scenes.md → show-config/acts-and-scenes
const withoutExt = filepath.replace(/\.md$/, '');
const withoutPages = withoutExt.replace(/^pages\//, '');
return withoutPages.replace(/_/g, '-');

Check warning on line 25 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFD&open=AZ6sZiB4t2R3LkcYbnFD&pullRequest=1178
}

function extractTitle(content: string, filepath: string): string {
const parts = filepath.split('/');

if (parts.length > 2) {
const h3Match = content.match(/^###\s+(.+)$/m);

Check warning on line 32 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "RegExp.exec()" method instead.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFE&open=AZ6sZiB4t2R3LkcYbnFE&pullRequest=1178
if (h3Match) return h3Match[1];
}

const match = content.match(/^##?\s+(.+)$/m);

Check warning on line 36 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "RegExp.exec()" method instead.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFG&open=AZ6sZiB4t2R3LkcYbnFG&pullRequest=1178
if (match) return match[1];

const filename = parts[parts.length - 1].replace(/\.md$/, '');

Check warning on line 39 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.at(…)` over `[….length - index]`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFI&open=AZ6sZiB4t2R3LkcYbnFI&pullRequest=1178
return filename.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());

Check warning on line 40 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFJ&open=AZ6sZiB4t2R3LkcYbnFJ&pullRequest=1178
}

function extractCategory(relativePath: string): string {
const parts = relativePath.split('/');
return parts.length > 2 ? parts[1] : 'root';
}

function extractMarkdownLinks(content: string): string[] {
// Extract all markdown links: [text](./path.md) or [text](path.md)
return [...content.matchAll(/\[([^\]]+)\]\(([^)]+\.md)\)/g)].map((m) => m[2]);
}

function normalizeLink(currentFile: string, link: string): string {
const currentDir = path.dirname(currentFile);
return path.normalize(path.join(currentDir, link)).replace(/\\/g, '/');

Check warning on line 55 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFL&open=AZ6sZiB4t2R3LkcYbnFL&pullRequest=1178
}

function buildOrderFromLinks(docsDir: string): string[] {
const visited = new Set<string>();
const order: string[] = [];

function traverse(relativePath: string): void {
if (visited.has(relativePath)) return;

const fullPath = path.join(docsDir, relativePath);
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) return;

visited.add(relativePath);
order.push(relativePath);

const content = fs.readFileSync(fullPath, 'utf8');
for (const link of extractMarkdownLinks(content)) {
traverse(normalizeLink(relativePath, link));
}
}

traverse('index.md');
return order;
}

function walkDocs(dir: string, basePath = ''): Omit<ManifestEntry, 'order'>[] {
const manifest: Omit<ManifestEntry, 'order'>[] = [];

if (!fs.existsSync(dir)) {
console.warn(`Warning: Directory ${dir} does not exist`);
return manifest;
}

for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
const relativePath = path.join(basePath, entry.name);

if (entry.isDirectory() && entry.name !== 'images') {
manifest.push(...walkDocs(fullPath, relativePath));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
const content = fs.readFileSync(fullPath, 'utf8');
manifest.push({
title: extractTitle(content, relativePath),
slug: generateSlug(relativePath),
path: relativePath.replace(/\\/g, '/'),

Check warning on line 100 in client-v3/scripts/generate-doc-manifest.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6sZiB4t2R3LkcYbnFM&open=AZ6sZiB4t2R3LkcYbnFM&pullRequest=1178
category: extractCategory(relativePath),
});
}
}

return manifest;
}

if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}

console.log('Generating documentation manifest...');

const linkOrder = buildOrderFromLinks(DOCS_DIR);
console.log(`📊 Discovered ${linkOrder.length} documents via link traversal`);

const orderMap = new Map<string, number>(linkOrder.map((filePath, i) => [filePath, i]));

const manifest: ManifestEntry[] = walkDocs(DOCS_DIR).map((entry) => ({
...entry,
order: orderMap.get(entry.path) ?? 999,
}));

const filteredManifest = manifest
.filter((entry) => entry.path !== 'index.md')
.sort((a, b) => {
if (a.order !== b.order) return a.order - b.order;
if (a.category !== b.category) return a.category.localeCompare(b.category);
return a.title.localeCompare(b.title);
});

fs.writeFileSync(MANIFEST_PATH, JSON.stringify(filteredManifest, null, 2));
console.log(`✅ Generated manifest with ${filteredManifest.length} documents (excluded index.md)`);
console.log(`📄 Manifest saved to: ${MANIFEST_PATH}`);
6 changes: 4 additions & 2 deletions client-v3/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@
</BNavItem>
</BNavbarNav>
<BNavbarNav class="ms-auto">
<BNavItem v-if="!isElectronEnv" href="/?_switch=1"> Switch to Classic UI </BNavItem>
<BNavItem v-if="!isElectronEnv" href="/ui-old/?_switch=1">
Switch to Classic UI
</BNavItem>
<BNavItem to="/help"> Help </BNavItem>
<BNavItem to="/about"> About </BNavItem>
<BNavItemDropdown v-if="isElectronEnv" text="Server">
Expand Down Expand Up @@ -273,7 +275,7 @@
await Promise.all([userStore.getCurrentRbac(), userStore.getUserSettings()]);
const switching = new URLSearchParams(window.location.search).has('_switch');
if (!switching && (userStore.userSettings as UserSettings).preferred_ui === 'old') {
window.location.href = '/?_switch=1';
window.location.href = '/ui-old/?_switch=1';

Check warning on line 278 in client-v3/src/App.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ6x35D2V7mXwHTv82UM&open=AZ6x35D2V7mXwHTv82UM&pullRequest=1178
return false;
}
if (switching) {
Expand Down
2 changes: 1 addition & 1 deletion client-v3/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import NotFoundView from '@/views/NotFoundView.vue';
const isFileProtocol = typeof window !== 'undefined' && window.location.protocol === 'file:';

const router = createRouter({
history: isFileProtocol ? createWebHashHistory() : createWebHistory('/ui-new/'),
history: isFileProtocol ? createWebHashHistory() : createWebHistory('/'),
routes: [
{
path: '/electron/server-selector',
Expand Down
27 changes: 23 additions & 4 deletions client-v3/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import path from 'node:path';
import { defineConfig } from 'vite';
import fs from 'node:fs';
import { defineConfig, type Plugin } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { BootstrapVueNextResolver } from 'bootstrap-vue-next';
import Icons from 'unplugin-icons/vite';
import IconsResolve from 'unplugin-icons/resolver';

function cleanV3StaticPlugin(): Plugin {
return {
name: 'clean-v3-static',
buildStart() {
if (process.env.BUILD_TARGET === 'electron') return;
const outDir = path.resolve(__dirname, '../server/static');
if (fs.existsSync(outDir)) {
for (const entry of fs.readdirSync(outDir)) {
if (entry !== 'ui-old') {
fs.rmSync(path.join(outDir, entry), { recursive: true, force: true });
}
}
}
},
};
}

export default defineConfig({
plugins: [
vue(),
Expand All @@ -17,13 +35,14 @@ export default defineConfig({
compiler: 'vue3',
autoInstall: false,
}),
cleanV3StaticPlugin(),
],
base: process.env.BUILD_TARGET === 'electron' ? './' : '/ui-new/',
base: process.env.BUILD_TARGET === 'electron' ? './' : '/',
build: {
outDir:
process.env.BUILD_TARGET === 'electron' ? './dist-electron' : '../server/static/ui-new/',
process.env.BUILD_TARGET === 'electron' ? './dist-electron' : '../server/static/',
assetsDir: './assets',
emptyOutDir: true,
emptyOutDir: false,
rollupOptions: {
output: {
manualChunks(id) {
Expand Down
1 change: 0 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"private": true,
"type": "module",
"scripts": {
"prebuild": "scripts/copy-docs.sh && node --strip-types scripts/generate-doc-manifest.ts",
"build": "vite build",
"build:analyze": "vite build --mode analyze",
"lint": "npm run format && npm run lint:eslint",
Expand Down
4 changes: 2 additions & 2 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
</b-nav-item>
</b-navbar-nav>
<b-navbar-nav class="ml-auto">
<b-nav-item v-if="!isElectron()" href="/ui-new/?_switch=1"> Switch to New UI </b-nav-item>
<b-nav-item v-if="!isElectron()" href="/?_switch=1"> Switch to New UI </b-nav-item>
<b-nav-item to="/help"> Help </b-nav-item>
<b-nav-item to="/about"> About </b-nav-item>
<b-nav-item-dropdown v-if="isElectron()" text="Server">
Expand Down Expand Up @@ -329,7 +329,7 @@ export default defineComponent({
const userPref = (this as any).USER_SETTINGS?.preferred_ui as string | null | undefined;
const systemDefault = (this as any).SETTINGS?.default_ui as string | undefined;
if (userPref === 'new' || (userPref == null && systemDefault === 'new')) {
window.location.href = '/ui-new/';
window.location.href = '/?_switch=1';
return;
}
}
Expand Down
27 changes: 4 additions & 23 deletions client/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,21 @@
import path from 'node:path';
import fs from 'node:fs';
import { defineConfig, type Plugin } from 'vite';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue2';

function cleanV2StaticPlugin(): Plugin {
return {
name: 'clean-v2-static',
buildStart() {
if (process.env.BUILD_TARGET === 'electron') return;
const outDir = path.resolve(__dirname, '../server/static');
if (fs.existsSync(outDir)) {
for (const entry of fs.readdirSync(outDir)) {
if (entry !== 'ui-new') {
fs.rmSync(path.join(outDir, entry), { recursive: true, force: true });
}
}
}
},
};
}

export default defineConfig({
plugins: [
vue(),
cleanV2StaticPlugin(),
],
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
},
// Use relative paths ONLY for Electron builds (file:// protocol)
// Use absolute paths for web server (prevents issues with nested routes)
base: process.env.BUILD_TARGET === 'electron' ? './' : '/',
base: process.env.BUILD_TARGET === 'electron' ? './' : '/ui-old/',
build: {
outDir: process.env.BUILD_TARGET === 'electron' ? './dist-electron' : '../server/static/',
outDir: process.env.BUILD_TARGET === 'electron' ? './dist-electron' : '../server/static/ui-old/',
assetsDir: './assets',
emptyOutDir: false,
emptyOutDir: true,
minify: 'esbuild',
cssMinify: 'esbuild',
rollupOptions: {
Expand Down
34 changes: 17 additions & 17 deletions server/controllers/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,26 @@ def import_all_controllers():


class RootController(BaseController):
def get(self, _path):
if is_frozen():
full_path = get_resource_path(os.path.join("static", "ui-old", INDEX_HTML))
else:
file_path = os.path.join(
os.path.abspath(os.path.dirname(__file__)), "..", "static", "ui-old"
)
full_path = os.path.join(file_path, INDEX_HTML)
if not os.path.isfile(full_path):
raise HTTPError(404)
with open(full_path, "r", encoding="utf-8") as file:
self.write(file.read())


class RootControllerV3(BaseController):
async def get(self, path):
if not path and not self.get_argument("_switch", None):
default_ui = await self.application.digi_settings.get("default_ui")
if default_ui == "new":
self.redirect("/ui-new/")
if default_ui == "old":
self.redirect("/ui-old/")
return
if is_frozen():
# In PyInstaller mode, use resource path
Expand All @@ -56,21 +71,6 @@ async def get(self, path):
raise HTTPError(500) from e


class RootControllerV3(BaseController):
def get(self, _path):
if is_frozen():
full_path = get_resource_path(os.path.join("static", "ui-new", INDEX_HTML))
else:
file_path = os.path.join(
os.path.abspath(os.path.dirname(__file__)), "..", "static", "ui-new"
)
full_path = os.path.join(file_path, INDEX_HTML)
if not os.path.isfile(full_path):
raise HTTPError(404)
with open(full_path, "r", encoding="utf-8") as file:
self.write(file.read())


class StaticController(BaseController):
def get(self):
self.set_header("Content-Type", "")
Expand Down
Loading
Loading