fix: browser testing issues and missing UI tools

- Fix WebSocket URL to use ws:// for http development environments
- Make createLayer idempotent to prevent race-condition 500 under React StrictMode
- Add URL-based routing for /register so register page actually shows register form
- Add missing tool buttons to LeftSidebar: polygon, chair, extend, fillet
- Add Playwright browser test suite for exhaustive UI testing
- Update .gitignore for Playwright artifacts

Unit tests: backend 254/254, frontend 380/380
This commit is contained in:
2026-07-27 01:17:18 +02:00
parent 8f4ca1581c
commit c86b5e7031
10 changed files with 551 additions and 2 deletions
+5
View File
@@ -11,3 +11,8 @@ dist/
.DS_Store .DS_Store
coverage/ coverage/
.vitest/ .vitest/
# Playwright
frontend/test-results/
frontend/playwright-tests/screenshots/
frontend/playwright-tests/*.json
+4
View File
@@ -207,6 +207,10 @@ export class SqliteAdapter implements DatabaseInterface {
createLayer(data: Partial<DBLayer>): DBLayer { createLayer(data: Partial<DBLayer>): DBLayer {
const id = data.id ?? `layer-${Date.now()}`; const id = data.id ?? `layer-${Date.now()}`;
// Idempotent: if a layer with the same id already exists, return it.
// This prevents race conditions under React StrictMode double-render.
const existing = this.db.prepare('SELECT * FROM layers WHERE id = ?').get(id) as DBLayer | undefined;
if (existing) return existing;
this.db.prepare( this.db.prepare(
'INSERT INTO layers (id, drawing_id, name, visible, locked, color, line_type, transparency, sort_order, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', 'INSERT INTO layers (id, drawing_id, name, visible, locked, color, line_type, transparency, sort_order, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
).run(id, data.drawing_id!, data.name ?? 'Layer', data.visible !== undefined ? (data.visible ? 1 : 0) : 1, data.locked !== undefined ? (data.locked ? 1 : 0) : 0, ).run(id, data.drawing_id!, data.name ?? 'Layer', data.visible !== undefined ? (data.visible ? 1 : 0) : 1, data.locked !== undefined ? (data.locked ? 1 : 0) : 0,
+60
View File
@@ -18,6 +18,7 @@
"yjs": "^13.6.0" "yjs": "^13.6.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.62.0",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
@@ -660,6 +661,21 @@
"pako": "^1.0.10" "pako": "^1.0.10"
} }
}, },
"node_modules/@playwright/test": {
"version": "1.62.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz",
"integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==",
"dev": true,
"dependencies": {
"playwright": "1.62.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
@@ -2408,6 +2424,50 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.62.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz",
"integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==",
"dev": true,
"dependencies": {
"playwright-core": "1.62.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz",
"integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.16", "version": "8.5.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+1
View File
@@ -20,6 +20,7 @@
"yjs": "^13.6.0" "yjs": "^13.6.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.62.0",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
@@ -0,0 +1,355 @@
/**
* Exhaustive Browser Test Suite for Web CAD
* Tests every tool, menu, dialog and workflow reachable in the UI.
* Run with: npx playwright test playwright-tests/browser-exhaustive.test.ts --project=chromium
*/
import { test, expect, chromium, type Browser, type Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
const BASE_URL = process.env.FRONTEND_URL || 'http://localhost:5173';
const API_URL = process.env.API_URL || 'http://localhost:3001';
const TEST_EMAIL = `browser-test-${Date.now()}@example.com`;
const TEST_PASSWORD = 'Test1234!';
interface Issue {
category: string;
severity: 'critical' | 'major' | 'minor' | 'info';
title: string;
detail: string;
screenshot?: string;
}
const issues: Issue[] = [];
async function api(method: string, endpoint: string, body?: unknown, token?: string) {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${API_URL}${endpoint}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
return { status: res.status, body: text ? JSON.parse(text) : null };
}
function logIssue(category: string, severity: Issue['severity'], title: string, detail: string, screenshot?: string) {
issues.push({ category, severity, title, detail, screenshot });
console.log(`[${severity.toUpperCase()}] ${category}: ${title}`);
}
async function screenshot(page: Page, name: string) {
const file = path.resolve(`/tmp/web-cad-clone/frontend/playwright-tests/screenshots/${name}.png`);
await page.screenshot({ path: file, fullPage: false });
return file;
}
async function captureConsoleErrors(page: Page, label: string) {
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') {
const text = msg.text();
// Ignore benign 404s for settings that don't exist yet
if (text.includes('/api/settings/')) return;
errors.push(text);
logIssue('Console', 'major', `Console error during ${label}`, text);
}
});
page.on('pageerror', err => {
const text = err.message;
errors.push(text);
logIssue('PageError', 'critical', `Page error during ${label}`, text);
});
return () => errors;
}
async function waitForStable(page: Page, ms = 300) {
await page.waitForTimeout(ms);
}
async function clickIfExists(page: Page, selector: string, label: string) {
const el = await page.locator(selector).first();
if (await el.count() > 0 && await el.isVisible().catch(() => false)) {
await el.click();
return true;
}
logIssue('UI', 'minor', `${label} not found`, `Selector ${selector} not visible`);
return false;
}
test.describe('Web CAD Exhaustive Browser Tests', () => {
test.setTimeout(300000);
let browser: Browser;
let page: Page;
let token: string;
let userId: string;
let projectId: string;
test.beforeAll(async () => {
// Ensure screenshot dir exists
fs.mkdirSync('/tmp/web-cad-clone/frontend/playwright-tests/screenshots', { recursive: true });
browser = await chromium.launch({ headless: true });
page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
// Register test user
const reg = await api('POST', '/api/auth/register', { email: TEST_EMAIL, password: TEST_PASSWORD, name: 'Browser Bot' });
if (reg.status !== 200 && reg.status !== 201) {
// Maybe user already exists? Try login
const login = await api('POST', '/api/auth/login', { email: TEST_EMAIL, password: TEST_PASSWORD });
if (login.status !== 200) throw new Error(`Auth failed: ${login.status} ${JSON.stringify(login.body)}`);
token = login.body.session.token;
userId = login.body.user.id;
} else {
token = reg.body.session.token;
userId = reg.body.user.id;
}
// Create project
const proj = await api('POST', '/api/projects', { name: 'Exhaustive Browser Test', description: 'Automated test' }, token);
if (proj.status !== 200 && proj.status !== 201) throw new Error(`Project create failed: ${proj.status} ${JSON.stringify(proj.body)}`);
projectId = proj.body.id;
// Inject session and open app
await page.goto(`${BASE_URL}/`);
await page.evaluate((t) => { localStorage.setItem('auth_token', t); }, token);
await page.reload();
await page.waitForSelector('.dashboard-project-card', { timeout: 10000 });
});
test.afterAll(async () => {
await browser.close();
// Write JSON report
const report = {
totalIssues: issues.length,
critical: issues.filter(i => i.severity === 'critical').length,
major: issues.filter(i => i.severity === 'major').length,
minor: issues.filter(i => i.severity === 'minor').length,
info: issues.filter(i => i.severity === 'info').length,
issues,
};
fs.writeFileSync('/tmp/web-cad-clone/frontend/playwright-tests/browser-test-report.json', JSON.stringify(report, null, 2));
console.log('\n=== REPORT ===');
console.log(JSON.stringify(report, null, 2));
});
test('01: open project and load CAD editor', async () => {
const stopCapture = captureConsoleErrors(page, 'open project');
await page.locator('.dashboard-project-card').first().click();
await waitForStable(page, 500);
// Dashboard opens detail panel on the right with 'Projekt öffnen'
const openBtn = await page.locator('button:has-text("Projekt öffnen")').first();
if (await openBtn.count() > 0 && await openBtn.isVisible().catch(() => false)) {
await openBtn.click();
} else {
logIssue('Dashboard', 'major', 'Projekt öffnen button not visible', '');
}
// Wait for editor to render: any tool button from the left sidebar is visible
try {
await page.waitForSelector('[data-tool="line"]', { timeout: 10000 });
await waitForStable(page, 1000);
} catch (e) {
logIssue('Editor', 'critical', 'CAD editor did not render tool buttons', String(e), await screenshot(page, '01_editor_not_loaded'));
}
stopCapture();
});
test('02: draw tools - line, rect, circle', async () => {
const stopCapture = captureConsoleErrors(page, 'draw tools');
for (const tool of ['line', 'rect', 'circle']) {
const clicked = await clickIfExists(page, `[data-tool="${tool}"]`, `${tool} tool button`);
if (!clicked) {
// Try alternative selectors
const alt = await page.locator('.tool-button, .ribbon-button, button').filter({ hasText: new RegExp(tool, 'i') }).first();
if (await alt.count() > 0 && await alt.isVisible().catch(() => false)) {
await alt.click();
} else {
logIssue('DrawTool', 'major', `Cannot select ${tool} tool`, 'Tool button not found');
continue;
}
}
await waitForStable(page, 300);
// Draw on canvas center (two clicks for 2-point tools)
const canvas = await page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) {
logIssue('DrawTool', 'critical', `Canvas not found for ${tool}`, '');
continue;
}
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
await page.mouse.click(cx - 50, cy - 50);
await waitForStable(page, 200);
await page.mouse.click(cx + 50, cy + 50);
await waitForStable(page, 500);
await page.keyboard.press('Escape');
await waitForStable(page, 200);
}
stopCapture();
});
test('03: modification tools - select, move, copy, delete', async () => {
const stopCapture = captureConsoleErrors(page, 'modification tools');
await clickIfExists(page, '[data-tool="select"], button:has-text("select")', 'select tool');
const canvas = await page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) {
logIssue('ModifyTool', 'critical', 'Canvas not found', '');
return;
}
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
// Select element at center
await page.mouse.click(cx, cy);
await waitForStable(page, 300);
// Try move tool and drag
await clickIfExists(page, '[data-tool="move"], button:has-text("move")', 'move tool');
await page.mouse.move(cx, cy);
await page.mouse.down();
await page.mouse.move(cx + 100, cy + 100);
await page.mouse.up();
await waitForStable(page, 500);
// Copy tool
await clickIfExists(page, '[data-tool="copy"], button:has-text("copy")', 'copy tool');
await page.mouse.move(cx + 100, cy + 100);
await page.mouse.down();
await page.mouse.move(cx + 200, cy + 200);
await page.mouse.up();
await waitForStable(page, 500);
// Delete tool
await clickIfExists(page, '[data-tool="delete"], button:has-text("delete")', 'delete tool');
await page.mouse.click(cx, cy);
await waitForStable(page, 300);
stopCapture();
});
test('04: seating tools - row, block, table, stage', async () => {
const stopCapture = captureConsoleErrors(page, 'seating tools');
for (const tool of ['seating-row', 'seating-block', 'table', 'stage']) {
const clicked = await clickIfExists(page, `[data-tool="${tool}"]`, `${tool} tool`);
if (!clicked) {
logIssue('SeatingTool', 'major', `Cannot select ${tool} tool`, 'Tool button not found');
continue;
}
await waitForStable(page, 300);
const canvas = await page.locator('canvas').first();
const box = await canvas.boundingBox();
if (!box) continue;
const cx = box.x + box.width / 2 + (Math.random() * 200 - 100);
const cy = box.y + box.height / 2 + (Math.random() * 200 - 100);
await page.mouse.click(cx, cy);
await waitForStable(page, 500);
await page.keyboard.press('Escape');
await waitForStable(page, 200);
}
stopCapture();
});
test('05: layer panel operations', async () => {
const stopCapture = captureConsoleErrors(page, 'layer panel');
const opened = await clickIfExists(page, '[data-testid="layer-panel-toggle"], button:has-text("Layers")', 'layer panel toggle');
if (opened) {
await clickIfExists(page, 'button:has-text("+ Layer")', 'add layer button');
await waitForStable(page, 500);
await clickIfExists(page, 'button:has-text("+ Layer")', 'add layer button second');
}
stopCapture();
});
test('06: block library', async () => {
const stopCapture = captureConsoleErrors(page, 'block library');
await clickIfExists(page, '[data-testid="block-library-toggle"], button:has-text("Blocks")', 'block library toggle');
await waitForStable(page, 500);
stopCapture();
});
test('07: topbar menus and ribbon', async () => {
const stopCapture = captureConsoleErrors(page, 'topbar ribbon');
// Try clicking each ribbon tab
const tabs = ['Start', 'Zeichnen', 'Bearbeiten', 'Ansicht', 'Bestuhlung', 'Einfügen'];
for (const tab of tabs) {
await clickIfExists(page, `.ribbon-tab:has-text("${tab}"), button:has-text("${tab}")`, `${tab} ribbon tab`);
await waitForStable(page, 300);
}
stopCapture();
});
test('08: settings modal', async () => {
const stopCapture = captureConsoleErrors(page, 'settings');
await clickIfExists(page, 'button[aria-label="Einstellungen"], .settings-button, button:has-text("⚙")', 'settings button');
await waitForStable(page, 500);
await clickIfExists(page, 'button:has-text("Schließen"), button:has-text("Close"), .modal-close', 'close settings');
stopCapture();
});
test('09: notifications', async () => {
const stopCapture = captureConsoleErrors(page, 'notifications');
await clickIfExists(page, 'button[aria-label="Benachrichtigungen"], .notifications-button', 'notifications button');
await waitForStable(page, 500);
await page.keyboard.press('Escape');
stopCapture();
});
test('10: AI copilot panel', async () => {
const stopCapture = captureConsoleErrors(page, 'ai copilot');
await clickIfExists(page, '[data-testid="ki-copilot-toggle"], button:has-text("KI"), button:has-text("Copilot")', 'KI copilot toggle');
await waitForStable(page, 500);
const input = await page.locator('.copilot-input, input[placeholder*="KI"]').first();
if (await input.count() > 0 && await input.isVisible().catch(() => false)) {
await input.fill('Erstelle einen Tisch mit 8 Stühlen');
await input.press('Enter');
await waitForStable(page, 1000);
} else {
logIssue('AICopilot', 'minor', 'KI Copilot input not found', '');
}
stopCapture();
});
test('11: zoom and pan controls', async () => {
const stopCapture = captureConsoleErrors(page, 'zoom pan');
await clickIfExists(page, 'button:has-text("+")', 'zoom in');
await clickIfExists(page, 'button:has-text("-")', 'zoom out');
await clickIfExists(page, 'button:has-text("Fit")', 'fit view');
stopCapture();
});
test('12: command line', async () => {
const stopCapture = captureConsoleErrors(page, 'command line');
const cmd = await page.locator('.command-line-input, input[placeholder*="Befehl"]').first();
if (await cmd.count() > 0 && await cmd.isVisible().catch(() => false)) {
await cmd.fill('line');
await cmd.press('Enter');
await waitForStable(page, 500);
}
stopCapture();
});
test('13: undo/redo shortcuts', async () => {
const stopCapture = captureConsoleErrors(page, 'undo redo');
await page.keyboard.press('Control+z');
await waitForStable(page, 300);
await page.keyboard.press('Control+y');
await waitForStable(page, 300);
stopCapture();
});
test('14: export buttons', async () => {
const stopCapture = captureConsoleErrors(page, 'export');
await clickIfExists(page, 'button:has-text("Export"), button:has-text("DXF"), button:has-text("PDF")', 'export buttons');
await waitForStable(page, 500);
stopCapture();
});
test('15: logout returns to login', async () => {
const stopCapture = captureConsoleErrors(page, 'logout');
await clickIfExists(page, 'button:has-text("Abmelden"), button:has-text("Logout")', 'logout button');
await waitForStable(page, 1000);
const loginVisible = await page.locator('text=Anmelden').first().isVisible().catch(() => false);
if (!loginVisible) {
logIssue('Auth', 'major', 'Logout did not return to login screen', '', await screenshot(page, '15_logout_failed'));
}
stopCapture();
});
});
@@ -0,0 +1,82 @@
import { test, expect, chromium } from '@playwright/test';
import * as fs from 'fs';
const API_URL = process.env.API_URL || 'http://localhost:3001';
const BASE_URL = process.env.FRONTEND_URL || 'http://localhost:5173';
async function api(method: string, endpoint: string, body?: unknown, token?: string) {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${API_URL}${endpoint}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
return { status: res.status, body: await res.json() };
}
test('diagnose editor DOM', async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
// Register user
const email = `diag-${Date.now()}@example.com`;
const reg = await api('POST', '/api/auth/register', { email, password: 'Test1234!', name: 'Diag' });
const token = reg.body.session.token;
// Create project
const proj = await api('POST', '/api/projects', { name: 'Diag', description: 'x' }, token);
// Open app with token
await page.goto(`${BASE_URL}/`);
await page.evaluate((t) => localStorage.setItem('auth_token', t), token);
await page.reload();
const failedRequests: string[] = [];
const badResponses: { url: string; status: number }[] = [];
page.on('requestfailed', req => failedRequests.push(`${req.method()} ${req.url()}: ${req.failure()?.errorText}`));
page.on('response', res => {
const status = res.status();
if (status >= 400) badResponses.push({ url: res.url(), status });
});
page.on('console', msg => { if (msg.type() === 'error') console.log('CONSOLE ERROR:', msg.text()); });
page.on('pageerror', err => console.log('PAGE ERROR:', err.message));
await page.waitForSelector('.dashboard-project-card', { timeout: 10000 });
await page.locator('.dashboard-project-card').first().click();
await page.waitForTimeout(500);
// Dashboard shows a detail panel on the right with 'Projekt öffnen'
await page.locator('button:has-text("Projekt öffnen")').first().click();
await page.waitForTimeout(3000);
// Take screenshot
const shot = '/tmp/web-cad-clone/frontend/playwright-tests/screenshots/diagnose-editor.png';
fs.mkdirSync('/tmp/web-cad-clone/frontend/playwright-tests/screenshots', { recursive: true });
await page.screenshot({ path: shot, fullPage: false });
// Dump DOM structure of key elements
const info = await page.evaluate(() => {
const dataTools = Array.from(document.querySelectorAll('[data-tool]')).map(el => ({
tool: el.getAttribute('data-tool'),
tag: el.tagName,
className: el.className,
text: el.textContent?.slice(0, 40) || '',
visible: !!(el as HTMLElement).offsetParent,
}));
const toolBtns = Array.from(document.querySelectorAll('.tool-btn')).map(el => ({
tag: el.tagName,
className: el.className,
dataTool: el.getAttribute('data-tool'),
text: el.textContent?.slice(0, 40) || '',
visible: !!(el as HTMLElement).offsetParent,
}));
const leftbar = !!document.querySelector('.leftbar');
const ribbon = !!document.querySelector('.ribbon-bar, .ribbon');
const canvas = !!document.querySelector('canvas');
const canvasCount = document.querySelectorAll('canvas').length;
return { leftbar, ribbon, canvas, canvasCount, dataTools, toolBtns };
});
const output = { ...info, failedRequests, badResponses };
fs.writeFileSync('/tmp/web-cad-clone/frontend/playwright-tests/editor-dom.json', JSON.stringify(output, null, 2));
console.log('DOM INFO:', JSON.stringify(output, null, 2));
console.log('Screenshot:', shot);
await browser.close();
});
+35
View File
@@ -0,0 +1,35 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './playwright-tests',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: 0,
workers: 1,
reporter: [['list'], ['json', { outputFile: './playwright-tests/playwright-report.json' }]],
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
webServer: [
{
command: 'cd /tmp/web-cad-clone/backend && npm run dev',
url: 'http://localhost:3001/api/health',
reuseExistingServer: true,
timeout: 120000,
},
{
command: 'cd /tmp/web-cad-clone/frontend && npm run dev -- --port 5173',
url: 'http://localhost:5173',
reuseExistingServer: true,
timeout: 120000,
},
],
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], viewport: { width: 1600, height: 1000 } },
},
],
});
+4 -1
View File
@@ -1778,7 +1778,10 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
// ─── App Wrapper (Auth Gate) ─────────────────────────── // ─── App Wrapper (Auth Gate) ───────────────────────────
const App: React.FC = () => { const App: React.FC = () => {
const { user, token } = useAuth(); const { user, token } = useAuth();
const [authView, setAuthView] = useState<'login' | 'register'>('login'); const [authView, setAuthView] = useState<'login' | 'register'>(() => {
// URL-based routing: /register shows register form, otherwise login
return window.location.pathname === '/register' ? 'register' : 'login';
});
const [openedProjectId, setOpenedProjectId] = useState<string | null>(null); const [openedProjectId, setOpenedProjectId] = useState<string | null>(null);
// Not authenticated → show Login or Register // Not authenticated → show Login or Register
+4
View File
@@ -22,6 +22,8 @@ const sectionZeichnen: ToolDef[] = [
{ tool: 'rect', title: 'Rechteck (REC)', label: 'Rechteck', kbd: 'REC', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="14" rx="1"/></svg> }, { tool: 'rect', title: 'Rechteck (REC)', label: 'Rechteck', kbd: 'REC', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="14" rx="1"/></svg> },
{ tool: 'circle', title: 'Kreis (C)', label: 'Kreis', kbd: 'C', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9"/></svg> }, { tool: 'circle', title: 'Kreis (C)', label: 'Kreis', kbd: 'C', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9"/></svg> },
{ tool: 'arc', title: 'Bogen (A)', label: 'Bogen', kbd: 'A', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/><path d="M3 12a9 9 0 0 1 9-9"/></svg> }, { tool: 'arc', title: 'Bogen (A)', label: 'Bogen', kbd: 'A', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/><path d="M3 12a9 9 0 0 1 9-9"/></svg> },
{ tool: 'polygon', title: 'Polygon (POL)', label: 'Polygon', kbd: 'POL', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 22 8.5 18 22 6 22 2 8.5"/></svg> },
{ tool: 'chair', title: 'Stuhl (CH)', label: 'Stuhl', kbd: 'CH', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="6" y="10" width="12" height="8" rx="1"/><path d="M6 10V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4"/></svg> },
{ tool: 'text', title: 'Text (T)', label: 'Text', kbd: 'T', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg> }, { tool: 'text', title: 'Text (T)', label: 'Text', kbd: 'T', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg> },
{ tool: 'dimension', title: 'Bemaßung (DIM)', label: 'Bemaßung', kbd: 'DIM', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 3H3l3 18 3-9 6 9 3-9 3 9 3-18z"/><line x1="3" y1="3" x2="21" y2="21"/></svg> }, { tool: 'dimension', title: 'Bemaßung (DIM)', label: 'Bemaßung', kbd: 'DIM', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 3H3l3 18 3-9 6 9 3-9 3 9 3-18z"/><line x1="3" y1="3" x2="21" y2="21"/></svg> },
{ tool: 'hatch', title: 'Schraffur (H)', label: 'Schraffur', kbd: 'H', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="0"/><line x1="3" y1="9" x2="9" y2="3"/><line x1="9" y1="21" x2="21" y2="9"/><line x1="15" y1="21" x2="21" y2="15"/></svg> }, { tool: 'hatch', title: 'Schraffur (H)', label: 'Schraffur', kbd: 'H', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="0"/><line x1="3" y1="9" x2="9" y2="3"/><line x1="9" y1="21" x2="21" y2="9"/><line x1="15" y1="21" x2="21" y2="15"/></svg> },
@@ -36,6 +38,8 @@ const sectionBearbeiten: ToolDef[] = [
{ tool: 'scale', title: 'Skalieren (SC)', label: 'Scale', kbd: 'SC', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg> }, { tool: 'scale', title: 'Skalieren (SC)', label: 'Scale', kbd: 'SC', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg> },
{ tool: 'mirror', title: 'Spiegeln (MI)', label: 'Mirror', kbd: 'MI', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3v18"/><path d="M5 8l5-5 5 5"/><path d="M5 16l5 5 5-5"/></svg> }, { tool: 'mirror', title: 'Spiegeln (MI)', label: 'Mirror', kbd: 'MI', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3v18"/><path d="M5 8l5-5 5 5"/><path d="M5 16l5 5 5-5"/></svg> },
{ tool: 'trim', title: 'Trimmen (TR)', label: 'Trim', kbd: 'TR', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg> }, { tool: 'trim', title: 'Trimmen (TR)', label: 'Trim', kbd: 'TR', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg> },
{ tool: 'extend', title: 'Verlängern (EX)', label: 'Extend', kbd: 'EX', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 12 20 12"/><polyline points="14 6 20 12 14 18"/></svg> },
{ tool: 'fillet', title: 'Verrunden (F)', label: 'Fillet', kbd: 'F', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 22V12a10 10 0 0 1 10-10h10"/></svg> },
{ tool: 'offset', title: 'Versatz (O)', label: 'Offset', kbd: 'O', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h12"/><path d="M3 12h18"/><path d="M3 18h12"/><path d="M21 6v12"/></svg> }, { tool: 'offset', title: 'Versatz (O)', label: 'Offset', kbd: 'O', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h12"/><path d="M3 12h18"/><path d="M3 18h12"/><path d="M21 6v12"/></svg> },
{ tool: 'delete', title: 'Löschen (E)', label: 'Löschen', kbd: 'E', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> }, { tool: 'delete', title: 'Löschen (E)', label: 'Löschen', kbd: 'E', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> },
]; ];
+1 -1
View File
@@ -57,7 +57,7 @@ export interface UseYjsBindingResult {
clearSelection: () => void; clearSelection: () => void;
} }
const DEFAULT_WS_URL = `wss://${window.location.host}`; const DEFAULT_WS_URL = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}`;
export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult {
const { docName, wsUrl = DEFAULT_WS_URL, userId, userName, userColor, enabled = true, token } = opts; const { docName, wsUrl = DEFAULT_WS_URL, userId, userName, userColor, enabled = true, token } = opts;