diff --git a/.claude/skills/ui-browser/SKILL.md b/.claude/skills/ui-browser/SKILL.md new file mode 100644 index 0000000000..95d9f99695 --- /dev/null +++ b/.claude/skills/ui-browser/SKILL.md @@ -0,0 +1,119 @@ +--- +name: UI Browser +description: > + This skill should be used when the user asks to "verify UI changes", + "check the UI", "take a screenshot", "browse the app", "inspect a page", + "test how it looks", "visually verify", "screenshot for PR", or + "update documentation screenshots". Also use after editing any Vue component, + Vuetify template, CSS, or layout code to confirm the result visually. +version: 0.1.0 +--- + +# UI Browser + +Browse, inspect, and screenshot the running zwave-js-ui dev server using Playwright MCP tools. Verify UI changes visually after editing components, debug layout issues interactively, and capture screenshots for PRs and documentation. + +## Prerequisites + +Ensure the dev servers are running before browsing: +- Frontend: `npm run dev` (port 8092) +- Backend: `npm run dev:server` (port 8091) +- Fake stick: `npm run fake-stick` (port 5555) + +The Playwright MCP server is configured in `.mcp.json` at the project root. + +## App Navigation + +The app uses Vue Router with hash history. All URLs follow this pattern: +``` +http://localhost:8092/#/ +``` + +### Route Quick Reference + +| Route | Page | +|-------|------| +| `/control-panel` | Control Panel (main page, node list + details) | +| `/settings` | Settings (config, gateway values, Home Assistant) | +| `/scenes` | Scenes | +| `/debug` | Debug (log viewer) | +| `/store` | Store (MQTT/gateway explorer) | +| `/mesh` | Network Graph (mesh visualization) | +| `/smart-start` | Smart Start | +| `/controller-chart` | Controller Chart | +| `/zniffer` | Zniffer (traffic sniffer) | +| `/configuration-templates` | Configuration Templates | + +### Layout Structure + +- **Navigation drawer** (left sidebar): `v-navigation-drawer` — toggle via hamburger icon +- **App bar** (top): `v-app-bar` — nav toggle, page title, action buttons +- **Main content**: below app bar, fills remaining viewport + +### Auth Handling + +Auth is controlled by `store/settings.json` → `gateway.authEnabled`: +- **When `false` or unset**: no login needed, `/` redirects to `/control-panel` +- **When `true`**: login required. Fill `input[name="username"]` with `admin`, `input[name="password"]` with `zwave`, click `button[type="submit"]` + +## Interactive Browsing with Playwright MCP + +### Navigate to a Page + +Use the browser navigate tool with `http://localhost:8092/#/`. Wait 2-3 seconds after navigation for Vuetify components to fully render. + +### Inspect Page Content + +Take a **snapshot** (accessibility tree) to see page structure, text, and interactive elements without a visual screenshot. Use this to find selectors, verify text content, or debug missing elements. + +### Capture Screenshots + +Take a **screenshot** to capture the visual state. Save to `/tmp/screenshots/` for ad-hoc use or `docs/_images/` for documentation updates. + +### Interact with Elements + +- **Click** buttons, tabs, list items, navigation links +- **Fill** text inputs and form fields +- **Select** dropdown options +- **Hover** to trigger tooltips or dropdown menus + +## Verification Workflows + +### After Editing a Vue Component + +1. Navigate to the page containing the component +2. Take a screenshot to verify the visual result +3. If something looks off, take a snapshot to inspect the DOM structure + +### After Changing Styles or Layout + +1. Navigate to the affected page +2. Screenshot at default viewport (1280x720) +3. Optionally resize the viewport and screenshot again to test responsive behavior + +### After Modifying a Dialog or Modal + +1. Navigate to the page +2. Click the button or action that opens the dialog +3. Screenshot the dialog in its open state + +### Verifying Dark Mode + +The app reads `localStorage.dark`. Set it via the browser console or toggle the theme switch in the UI. The batch screenshot script sets dark mode by default. + +## Batch Screenshots + +For capturing multiple pages at once, run the template script: + +```bash +node .claude/skills/ui-browser/screenshot-template.js [width] [height] +``` + +Default viewport is 1280x720. The script auto-handles auth and sets dark mode. + +```bash +node .claude/skills/ui-browser/screenshot-template.js "http://localhost:8092/#/settings" /tmp/screenshots/settings.png +node .claude/skills/ui-browser/screenshot-template.js "http://localhost:8092/#/mesh" /tmp/screenshots/mesh.png 1920 1080 +``` + +For the full route-to-documentation image mapping and troubleshooting tips, see `references/screenshots.md`. diff --git a/.claude/skills/ui-browser/references/screenshots.md b/.claude/skills/ui-browser/references/screenshots.md new file mode 100644 index 0000000000..cedaa46b84 --- /dev/null +++ b/.claude/skills/ui-browser/references/screenshots.md @@ -0,0 +1,38 @@ +# Screenshot Reference + +## Output Locations + +- **Ad-hoc screenshots:** `/tmp/screenshots/` +- **Documentation updates:** `docs/_images/` in the project root + +## Route-to-Documentation Mapping + +These files in `docs/_images/` correspond to specific routes and states: + +| File | Route | Notes | +|------|-------|-------| +| `control_panel_dark.png` | `/#/control-panel` | Dark mode | +| `settings.png` | `/#/settings` | Light mode override | +| `settings_dark.png` | `/#/settings` | Dark mode | +| `debug.png` | `/#/debug` | Needs backend | +| `mesh_diagram.png` | `/#/mesh` | Needs backend + fake-stick | +| `mesh-selected.png` | `/#/mesh` | With a node selected | +| `scenes.png` | `/#/scenes` | | +| `nodes_manager.png` | `/#/control-panel` | Nodes table view | +| `unknown-device.png` | `/#/control-panel` | Specific unknown node | +| `hass_devices.png` | `/#/settings` | Home Assistant tab | +| `gateway_values_table.png` | `/#/settings` | Gateway values section | + +## Documentation Update Workflow + +1. Take screenshots for each route in the mapping table above +2. Save directly to `docs/_images/` overwriting existing files +3. Stage and commit: `git add docs/_images/ && git commit -m "docs: update UI screenshots"` + +## Troubleshooting + +- **Login screen keeps appearing**: Check `store/settings.json` for `gateway.authEnabled`. If true, handle login first via Playwright MCP or let the batch script handle it automatically. +- **Blank page**: Ensure all three dev servers are running (frontend, backend, fake-stick). Take a snapshot to check for error messages in the DOM. +- **Components not rendered**: Vuetify needs 2-3s after `networkidle` to mount. Increase wait time before screenshotting. +- **Stale content**: The app uses Socket.IO for real-time updates. If data looks stale, the backend may not be connected to the fake stick. +- **Navigation drawer missing**: It may be collapsed. Click the hamburger icon (`v-app-bar-nav-icon`) in the top-left to toggle it open. diff --git a/.claude/skills/ui-browser/screenshot-template.js b/.claude/skills/ui-browser/screenshot-template.js new file mode 100644 index 0000000000..4e9f418497 --- /dev/null +++ b/.claude/skills/ui-browser/screenshot-template.js @@ -0,0 +1,109 @@ +#!/usr/bin/env node + +/** + * Screenshot template for zwave-js-ui + * + * Usage: node screenshot-template.js [width] [height] + * + * Examples: + * node screenshot-template.js "http://localhost:8092/#/settings" /tmp/screenshots/settings.png + * node screenshot-template.js "http://localhost:8092/#/mesh" /tmp/screenshots/mesh.png 1920 1080 + */ + +const { chromium } = require('playwright') +const path = require('path') +const fs = require('fs') + +const url = process.argv[2] +const outputPath = process.argv[3] +const viewportWidth = parseInt(process.argv[4]) || 1280 +const viewportHeight = parseInt(process.argv[5]) || 720 + +if (!url || !outputPath) { + console.error( + 'Usage: node screenshot-template.js [width] [height]', + ) + process.exit(1) +} + +// Check if auth is enabled by reading store/settings.json +function isAuthEnabled() { + const cwdSettingsPath = path.resolve(process.cwd(), 'store/settings.json') + const scriptSettingsPath = path.resolve( + __dirname, + '../../../store/settings.json', + ) + + for (const p of [cwdSettingsPath, scriptSettingsPath]) { + try { + const settings = JSON.parse(fs.readFileSync(p, 'utf8')) + return settings?.gateway?.authEnabled === true + } catch { + // continue to next path + } + } + return false +} + +async function takeScreenshot() { + const authEnabled = isAuthEnabled() + const outputDir = path.dirname(outputPath) + fs.mkdirSync(outputDir, { recursive: true }) + + const browser = await chromium.launch({ headless: true }) + const context = await browser.newContext({ + viewport: { width: viewportWidth, height: viewportHeight }, + }) + + // Set dark mode in localStorage before navigation + await context.addInitScript(() => { + localStorage.setItem('dark', 'true') + }) + + const page = await context.newPage() + + if (authEnabled) { + // Navigate to the app root first — router will redirect to login + await page.goto('http://localhost:8092/', { + waitUntil: 'networkidle', + }) + await page.waitForTimeout(1000) + + // Fill login form + try { + await page.fill( + 'input[type="text"], input[name="username"]', + 'admin', + { timeout: 5000 }, + ) + await page.fill('input[type="password"]', 'zwave', { + timeout: 5000, + }) + await page.click('button[type="submit"]', { timeout: 5000 }) + await page.waitForTimeout(2000) + } catch (e) { + console.warn( + 'Login form not found or login failed, continuing...', + e.message, + ) + } + } + + // Navigate to the target URL + await page.goto(url, { waitUntil: 'networkidle' }) + + // Wait for Vuetify rendering + await page.waitForTimeout(2000) + + await page.screenshot({ path: outputPath, fullPage: false }) + console.log( + `Screenshot saved to ${outputPath} (${viewportWidth}x${viewportHeight})`, + ) + + await browser.close() +} + +takeScreenshot().catch((err) => { + console.error('Screenshot failed:', err) + process.exit(1) +}) diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000000..899cfa04e6 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--headless", + "--viewport-size", + "1280,720" + ] + } + } +}