diff --git a/.gitignore b/.gitignore index ded5a79..33304cd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ dist/ .DS_Store coverage/ .vitest/ + +# Playwright +frontend/test-results/ +frontend/playwright-tests/screenshots/ +frontend/playwright-tests/*.json diff --git a/backend/src/database/SqliteAdapter.ts b/backend/src/database/SqliteAdapter.ts index dd6f13e..4c3c9cf 100644 --- a/backend/src/database/SqliteAdapter.ts +++ b/backend/src/database/SqliteAdapter.ts @@ -207,6 +207,10 @@ export class SqliteAdapter implements DatabaseInterface { createLayer(data: Partial): DBLayer { 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( '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, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b8bcef6..79fb334 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "yjs": "^13.6.0" }, "devDependencies": { + "@playwright/test": "^1.62.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -660,6 +661,21 @@ "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": { "version": "1.1.4", "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" } }, + "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": { "version": "8.5.16", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", diff --git a/frontend/package.json b/frontend/package.json index b9bf462..2aee8cc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "yjs": "^13.6.0" }, "devDependencies": { + "@playwright/test": "^1.62.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", diff --git a/frontend/playwright-tests/browser-exhaustive.test.ts b/frontend/playwright-tests/browser-exhaustive.test.ts new file mode 100644 index 0000000..2e82993 --- /dev/null +++ b/frontend/playwright-tests/browser-exhaustive.test.ts @@ -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 = { '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(); + }); +}); diff --git a/frontend/playwright-tests/diagnose-editor.test.ts b/frontend/playwright-tests/diagnose-editor.test.ts new file mode 100644 index 0000000..50c1a9a --- /dev/null +++ b/frontend/playwright-tests/diagnose-editor.test.ts @@ -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 = { '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(); +}); diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..42382c3 --- /dev/null +++ b/frontend/playwright.config.ts @@ -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 } }, + }, + ], +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 805bb22..625f8be 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1778,7 +1778,10 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack // ─── App Wrapper (Auth Gate) ─────────────────────────── const App: React.FC = () => { 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(null); // Not authenticated → show Login or Register diff --git a/frontend/src/components/LeftSidebar.tsx b/frontend/src/components/LeftSidebar.tsx index 84963eb..c0cdd3f 100644 --- a/frontend/src/components/LeftSidebar.tsx +++ b/frontend/src/components/LeftSidebar.tsx @@ -22,6 +22,8 @@ const sectionZeichnen: ToolDef[] = [ { tool: 'rect', title: 'Rechteck (REC)', label: 'Rechteck', kbd: 'REC', svg: }, { tool: 'circle', title: 'Kreis (C)', label: 'Kreis', kbd: 'C', svg: }, { tool: 'arc', title: 'Bogen (A)', label: 'Bogen', kbd: 'A', svg: }, + { tool: 'polygon', title: 'Polygon (POL)', label: 'Polygon', kbd: 'POL', svg: }, + { tool: 'chair', title: 'Stuhl (CH)', label: 'Stuhl', kbd: 'CH', svg: }, { tool: 'text', title: 'Text (T)', label: 'Text', kbd: 'T', svg: }, { tool: 'dimension', title: 'Bemaßung (DIM)', label: 'Bemaßung', kbd: 'DIM', svg: }, { tool: 'hatch', title: 'Schraffur (H)', label: 'Schraffur', kbd: 'H', svg: }, @@ -36,6 +38,8 @@ const sectionBearbeiten: ToolDef[] = [ { tool: 'scale', title: 'Skalieren (SC)', label: 'Scale', kbd: 'SC', svg: }, { tool: 'mirror', title: 'Spiegeln (MI)', label: 'Mirror', kbd: 'MI', svg: }, { tool: 'trim', title: 'Trimmen (TR)', label: 'Trim', kbd: 'TR', svg: }, + { tool: 'extend', title: 'Verlängern (EX)', label: 'Extend', kbd: 'EX', svg: }, + { tool: 'fillet', title: 'Verrunden (F)', label: 'Fillet', kbd: 'F', svg: }, { tool: 'offset', title: 'Versatz (O)', label: 'Offset', kbd: 'O', svg: }, { tool: 'delete', title: 'Löschen (E)', label: 'Löschen', kbd: 'E', svg: }, ]; diff --git a/frontend/src/crdt/useYjsBinding.ts b/frontend/src/crdt/useYjsBinding.ts index 38155a0..215a169 100644 --- a/frontend/src/crdt/useYjsBinding.ts +++ b/frontend/src/crdt/useYjsBinding.ts @@ -57,7 +57,7 @@ export interface UseYjsBindingResult { 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 { const { docName, wsUrl = DEFAULT_WS_URL, userId, userName, userColor, enabled = true, token } = opts;