From 63ec69fb204e553a796b2db2d4ef353fd60e20ab Mon Sep 17 00:00:00 2001 From: Sev7eNup <79143581+Sev7eNup@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:16:54 +0200 Subject: [PATCH 1/2] Polish five rough edges across designer, settings and custom nodes Five small, independent usability fixes reported from daily use: - Designer node size now defaults to L instead of S. The persisted store bumps to version 2 and only lifts profiles still sitting on the old default, so a size picked from the stepper survives the change. - Settings > System moves the read-only System info tab to the far right, after every tab that actually edits configuration. Integrations stays first and remains the default section, so the deep links are untouched. - Settings > System > Security: every hardening switch now explains what it does, what happens when it is off, and where the boundary really is. Wording checked against PathGuard, SqlActivity, StartProgramActivity, WebhooksController and SecurityHardeningWarnings rather than the docs. The Remote pointer note was inline German and rendered German inside the English UI; it now goes through the i18n catalogue. - The support-log window starts ~30% taller in both view modes (table 540 -> 702, plain text 640 -> 832). Their localStorage keys move to .v2, otherwise a browser that already stored a height never sees the new default. The custom-node dialog gets its own note: it was the only page dialog using the shared .input-field class. That style is recessed and needs a raised surface to sink into, which ModalShell only provides in the dark skins. On light-grey it painted a flat beige box on a white panel and went lighter than the panel on focus, losing its outline exactly while in use. It now uses the outline chain every other admin dialog uses. Two real bugs fell out of that switch. .input-field is unlayered CSS and therefore beats every Tailwind utility from @layer utilities, so its width:100% silently killed the w-28/w-32/text-xs on the parameter rows - each row wrapped onto a line of its own - and squashed the icon-picker button in its auto grid column. nodepilot-ui/CLAUDE.md now describes the two field tiers instead of mandating .input-field everywhere, including why width must never be baked into a shared field constant. --- docs/workflow-designer-features.md | 4 +- src/nodepilot-ui/CLAUDE.md | 6 +- src/nodepilot-ui/e2e/admin-settings.spec.ts | 17 ++++- .../admin-settings/SecuritySection.test.tsx | 30 ++++++++ .../SupportLogViewerSection.test.tsx | 73 +++++++++++++++++++ .../pages/CustomActivitiesPage.test.tsx | 27 +++++++ .../src/__tests__/stores/designStore.test.ts | 36 ++++++++- .../admin-settings/SecuritySection.tsx | 14 +++- .../admin-settings/SupportEventsTable.tsx | 8 +- .../SupportLogViewerSection.tsx | 6 +- .../src/i18n/locales/de/adminSettings.json | 10 +++ .../src/i18n/locales/en/adminSettings.json | 10 +++ .../src/pages/CustomActivitiesPage.tsx | 65 +++++++++++++---- .../src/pages/MobileWorkflowView.tsx | 6 +- .../src/pages/SystemSettingsPage.tsx | 6 +- src/nodepilot-ui/src/stores/designStore.ts | 12 ++- 16 files changed, 293 insertions(+), 37 deletions(-) create mode 100644 src/nodepilot-ui/src/__tests__/components/admin-settings/SupportLogViewerSection.test.tsx diff --git a/docs/workflow-designer-features.md b/docs/workflow-designer-features.md index 845c2c22..da9b7f67 100644 --- a/docs/workflow-designer-features.md +++ b/docs/workflow-designer-features.md @@ -43,7 +43,7 @@ Vollständige Auflistung sämtlicher Features des NodePilot-Workflow-Designers ( - **Controls-Panel:** Standard React-Flow-Controls (Zoom in/out, Fit, Lock). - **Hintergrund:** Ein **Punkt-Raster** (Gap 24 px, Dot-Größe 1.6) — identisch in Premium **und** Classic, der frühere zweistufige Crosshatch ist ersatzlos entfallen. Die Deckkraft ist skin-abhängig: helle Skins tragen einen deutlich stärkeren Alpha, weil das Raster auf hellem Grund mehr Kontrast braucht als auf dunklem. Bei aktivem Snap-to-Grid ersetzt ein **Linien**-Raster auf der Snap-Schrittweite das Punkt-Raster. - **Snap-to-Grid (Expert):** `G` togglet Raster-Snapping (Standard 20 px, konfigurierbar). -- **Node-Skalierung:** 8 Größen-Presets (XS … 4XL, Default SM); `Ctrl+Shift+>` / `Ctrl+Shift+<` (Expert) ändern Node-Größe, `Ctrl+Alt+.` / `Ctrl+Alt+,` die Label-Schriftgröße (nur Classic-Node-Stil). +- **Node-Skalierung:** 8 Größen-Presets (XS … 4XL, Default L); `Ctrl+Shift+>` / `Ctrl+Shift+<` (Expert) ändern Node-Größe, `Ctrl+Alt+.` / `Ctrl+Alt+,` die Label-Schriftgröße (nur Classic-Node-Stil). - **Fullscreen / Distraction-Free:** `F11` blendet Sidebar, Panels und Banner aus, behält Header + Canvas + Exit-Pill. - **Viewport-Virtualisierung:** Nur sichtbare Elemente werden gerendert (`onlyRenderVisibleElements`) — spürbar ab ~50 Nodes. @@ -337,7 +337,7 @@ Alle Darstellungs-Einstellungen liegen im **`designStore`** (Zustand + persist, | `designerTheme` | `atelier` | atelier / classic | Designsprache des Designers: **Atelier** (eigener Werkbank-Look — Papier/Graphit-Grund mit dem gewohnten Karo-Raster, schwebendes Karten-Chrome, ein Akzent; `styles/designer-atelier.css` mappt die `--color-*`-Tokens über eine `--wd-*`-Palettenschicht) vs. **Classic** (bisheriger Look, byte-identisch). **Farb-Skins adaptieren beide Looks:** im Atelier re-pointet jeder Skin Akzentfamilie + Grundton (`--wd-accent*`, `--wd-canvas`/`--wd-panel`), die Atelier-Geometrie bleibt universell; Status-Farben bleiben in beiden Looks skin-stabil. Umschalter (`Brush`-Icon, `role="switch"`, `data-testid="toggle-atelier-theme"`) in beiden Kopfleisten-Layouts | | `toolbarLayout` | `compact` | compact / classic | Kopfleisten-Layout: kompakt (gruppierte Popover-Menüs, grüner „Ausführen") vs. klassische Inline-Reihe (jeder Toggle/Tool als einzelner Button, icon-only Play). Umschalter-Button in beiden Layouts | | `nodeStyle` | `classic` | classic / card | Node-Darstellung (`Ctrl+Shift+N`) | -| `nodeScaleIndex` | 1 | 0–7 (XS … 4XL) | Node-Größe | +| `nodeScaleIndex` | 3 | 0–7 (XS … 4XL) | Node-Größe | | `labelFontOffsetIndex` | 2 | 0–6 (−4 … +8 px) | Label-Schriftgröße | | `edgesAnimated` | true | bool | Fluss-Animation der Kanten (`A`) | | `edgeWidthIndex` | 2 | 1.5/2/2.5/3.5/5/7 px | Kantenbreite (`Ctrl+]`/`[`) | diff --git a/src/nodepilot-ui/CLAUDE.md b/src/nodepilot-ui/CLAUDE.md index b384e6a2..3c6833ce 100644 --- a/src/nodepilot-ui/CLAUDE.md +++ b/src/nodepilot-ui/CLAUDE.md @@ -14,7 +14,11 @@ Gilt für `src/nodepilot-ui/`. Projektweite Regeln stehen in der Root-`CLAUDE.md - Client-State via Zustand-Stores (`src/stores/`), Server-State via TanStack React Query (`refetchOnWindowFocus:false`, SignalR invalidiert Caches). - **Typografie:** `IBM Plex Sans Variable` (`--font-headline`/`--font-body`/`--font-label`, alle drei identisch) + `IBM Plex Mono` (`--font-mono`), deklariert im `@theme` von `index.css`. Beide self-hosted über fontsource — **keine externen Font-Requests**: die Prod-CSP kennt kein `font-src` und fällt auf `default-src 'self'`, ein CDN-Font wäre in Produktion geblockt. Body trägt ein `font-size-adjust` als einzigen Dichte-Ausgleich (Plex hat eine kleinere x-Höhe als das früher genutzte Inter); Monospace ist davon ausgenommen. Monaco kann keine CSS-Variable verwerten und hält den Stack als `MONO_FONT_STACK` in `lib/monacoSetup.ts` — `fontTokens.test.ts` hält beide Seiten deckungsgleich. Die Doku-Website fährt bewusst ein eigenes Type-System (Geist + JetBrains Mono). -- **Formularfelder:** `.input-field` (index.css) statt handgebauter `border …`-Klassenketten. Der Stil ist *versenkt* (dunkler als der Container + Inset-Schatten) und setzt damit eine **angehobene** Fläche voraus — `.np-card` bzw. das `ModalShell`-Panel (`.np-modal-panel`, dark = `surface-container`). Wer einen eigenen Dialog-Container baut, muss diese Anhebung mitnehmen, sonst malt sich das Feld in der Farbe seines Containers und verschwindet. +- **Formularfelder — zwei Ebenen, nicht eine.** Welche gilt, entscheidet die Fläche darunter: + - **`.input-field`** (index.css) für Felder auf einer angehobenen Fläche: Designer-Property-Panels, `.np-card`. Der Stil ist *versenkt* (dunkler als der Container + Inset-Schatten) und **setzt diese Anhebung voraus**. Handgebaute `border …`-Ketten sind dort falsch. + - **Umriss-Kette** `px-3 py-2 border border-outline-variant rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500` (+ `w-full`, bei ` setForm({ ...form, name: e.target.value })} className="input-field" /> + setForm({ ...form, name: e.target.value })} className={`${FIELD} w-full`} /> setForm({ ...form, key: e.target.value })} - placeholder="disk-check" pattern="[A-Za-z0-9_\-]+" className="input-field font-mono disabled:opacity-60" /> + placeholder="disk-check" pattern="[A-Za-z0-9_\-]+" className={`${FIELD} w-full font-mono disabled:opacity-60`} />
- - setForm({ ...form, description: e.target.value })} className="input-field" /> + setForm({ ...form, description: e.target.value })} className={`${FIELD} w-full`} /> setForm({ ...form, color: e.target.value })} className="h-9 w-12 rounded border border-outline-variant" /> @@ -378,26 +411,28 @@ export function CustomActivitiesPage() {
- setForm({ ...form, engine: e.target.value })} className={`${FIELD} w-full bg-surface-lowest`}> - setForm({ ...form, defaultTimeoutSeconds: e.target.value })} className="input-field" /> + setForm({ ...form, defaultTimeoutSeconds: e.target.value })} className={`${FIELD} w-full`} />
- setForm({ ...form, successExitCodes: e.target.value })} placeholder="0,1" className="input-field w-28 font-mono" /> + setForm({ ...form, successExitCodes: e.target.value })} placeholder="0,1" className={`${FIELD} w-28 font-mono`} />
@@ -443,7 +478,7 @@ export function CustomActivitiesPage() {
setIconSearch(e.target.value)} placeholder={t('customActivities:iconPicker.search')} - className="input-field mb-3 font-mono" /> + className={`${FIELD} w-full mb-3 font-mono`} />
{CUSTOM_ACTIVITY_ICON_CHOICES.filter((ic) => ic.includes(iconSearch.trim().toLowerCase())).map((ic) => (
diff --git a/src/nodepilot-ui/src/pages/MobileWorkflowView.tsx b/src/nodepilot-ui/src/pages/MobileWorkflowView.tsx index bb922669..d72a1f84 100644 --- a/src/nodepilot-ui/src/pages/MobileWorkflowView.tsx +++ b/src/nodepilot-ui/src/pages/MobileWorkflowView.tsx @@ -20,8 +20,10 @@ import { withDefaultEdgePorts } from '../lib/edgePorts'; import { NodeScaleOverrideContext } from '../components/designer/nodeScaleContext'; import { useWorkflowSignalR } from '../hooks/useSignalR'; -// Render the read-only phone graph at the `lg` node scale (NODE_SCALES index 3) — much bigger -// icons/labels than the desktop `xs` default, so a workflow stays legible on a small screen. +// Render the read-only phone graph at the `lg` node scale (NODE_SCALES index 3) — big enough +// icons/labels to keep a workflow legible on a small screen. This matches the desktop default +// today, but stays an explicit override: the phone view must not shrink when someone steps the +// desktop size down, since there is no size stepper on this screen to undo it. const MOBILE_SCALE_INDEX = 3; // Reuse the editor's node/edge renderers verbatim. ActivityNode reads everything it needs diff --git a/src/nodepilot-ui/src/pages/SystemSettingsPage.tsx b/src/nodepilot-ui/src/pages/SystemSettingsPage.tsx index fdbc637a..f946ac5d 100644 --- a/src/nodepilot-ui/src/pages/SystemSettingsPage.tsx +++ b/src/nodepilot-ui/src/pages/SystemSettingsPage.tsx @@ -27,16 +27,20 @@ type SubTab = 'integrations' | 'ai-knowledge' | 'retention' | 'system-info' // Tabs are progressively activated as their section is implemented. Disabled tabs // keep the operator informed about what's on the roadmap. +// `system-info` sits LAST on purpose: it is the only read-only tab here, so it belongs +// after everything that actually edits configuration rather than splitting that run. +// Order is presentation only — nothing indexes into this array, and `integrations` +// stays first because it is also the default/fallback section for a bare `?tab=system`. const TABS: { id: SubTab; ready: boolean }[] = [ { id: 'integrations', ready: true }, { id: 'ai-knowledge', ready: true }, { id: 'retention', ready: true }, - { id: 'system-info', ready: true }, { id: 'authentication', ready: true }, { id: 'logging-telemetry', ready: true }, { id: 'security', ready: true }, { id: 'performance', ready: true }, { id: 'db-admin', ready: true }, + { id: 'system-info', ready: true }, ]; const ICONS: Record> = { diff --git a/src/nodepilot-ui/src/stores/designStore.ts b/src/nodepilot-ui/src/stores/designStore.ts index 7cf46a24..d7568607 100644 --- a/src/nodepilot-ui/src/stores/designStore.ts +++ b/src/nodepilot-ui/src/stores/designStore.ts @@ -137,7 +137,7 @@ export const useDesignStore = create()( setToolbarLayout: (toolbarLayout: ToolbarLayout) => set({ toolbarLayout }), nodeStyle: 'classic', nodeIconStyle: 'shape', - nodeScaleIndex: 1, // sm default — xs rendered 33-node graphs as an unreadable dot cloud + nodeScaleIndex: 3, // lg default — sm was legible but too small to actually author in labelFontOffsetIndex: 2, // 0-offset — use base labelFont from NODE_SCALES edgesAnimated: true, edgeWidthIndex: 2, // 2px default — matches previous hardcoded width @@ -190,12 +190,16 @@ export const useDesignStore = create()( }), { name: 'nodepilot-design', - version: 1, + version: 2, // Profiles that already used the full designer retain the previous surface. Fresh // profiles use the standard-mode default above. migrate: (persisted, version) => { - const state = persisted as Partial; - if (version < 1 && !state.designerMode) return { ...state, designerMode: 'expert' }; + let state = persisted as Partial; + if (version < 1 && !state.designerMode) state = { ...state, designerMode: 'expert' }; + // v2 raised the node-size default from sm (1) to lg (3). Only lift profiles that are + // still sitting on the OLD default — a stored index of anything else is a deliberate + // choice from the size stepper and must survive the bump. + if (version < 2 && state.nodeScaleIndex === 1) state = { ...state, nodeScaleIndex: 3 }; return state; }, }, From e0847f791f6f50b0a5bd0752bfe53f1b6231ff5e Mon Sep 17 00:00:00 2001 From: Sev7eNup <79143581+Sev7eNup@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:45:26 +0200 Subject: [PATCH 2/2] Pin the node scale in the E2E fixture alongside the classic look Raising the node-size default to L moved the canvas geometry the hermetic suite was written against: fitView turns a larger node footprint into a different pan/zoom for the same seeded positions, and ai-assistant's step-b slid under the bottom-right minimap, which then swallowed its click - the exact hazard e2e/README.md already warns about. installDefaultMocks now pins nodeScaleIndex: 1 next to the existing designerTheme: 'classic' pin, for the same reason: both are geometry knobs, and letting either float means a presentation tweak reshuffles the canvas coordinates of unrelated specs. The scale itself stays covered by the unit tests (designStore, CanvasSettings). The seeds carry version: 2 to match the store's persist version. At version 1 the new migration would read the pinned sm as "still on the old default" and lift it straight back to lg, silently undoing the pin. seedExpertMode and seedAtelier replace the whole nodepilot-design key, so both re-assert the pin. toolbar-layout's two seeds are left alone: they deliberately exercise v1-profile hydration and assert toolbar state, not canvas geometry. --- src/nodepilot-ui/e2e/README.md | 15 ++++++-- src/nodepilot-ui/e2e/designer-atelier.spec.ts | 7 +++- src/nodepilot-ui/e2e/fixtures/mockApi.ts | 37 +++++++++++++------ 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/nodepilot-ui/e2e/README.md b/src/nodepilot-ui/e2e/README.md index 9e630c5a..dc80407e 100644 --- a/src/nodepilot-ui/e2e/README.md +++ b/src/nodepilot-ui/e2e/README.md @@ -29,11 +29,18 @@ alongside `dotnet test` and `npm run test:run`. combination keeps the pre-existing DOM of every spec (buttons visible, nav entry hidden) — do NOT change it to all-true, or the AI-Chat nav appears suite-wide. Override per test with `mockCaps(page, capsJson({...}))` (both exported from `fixtures/mockApi.ts`). -- `installDefaultMocks` also **pins the designer to the CLASSIC look** (localStorage seed - `designerTheme: 'classic'`) — the Atelier design (default for fresh real profiles) re-tokenises - colors/geometry the existing visual assertions were written against. The seed is skipped once - the app itself persisted a full designStore state (so mid-test toggles survive `page.reload`). +- `installDefaultMocks` also **pins the designer to the CLASSIC look and the SMALL node scale** + (localStorage seed `designerTheme: 'classic'`, `nodeScaleIndex: 1`, `version: 2`) — both are + geometry knobs the existing visual assertions were written against. The Atelier design (default + for fresh real profiles) re-tokenises colors/geometry; the node scale (default `3` = `lg`) changes + how much room a node takes, and `fitView` turns that into a different pan/zoom for the same seeded + positions — at `lg` a node can slide under the bottom-right minimap and lose its clicks. The + seeded `version` must match the store's persist version, or the store's own migration reads the + pinned `sm` as "still on the old default" and lifts it back to `lg`. The seed is skipped once the + app itself persisted a full designStore state (so mid-test toggles survive `page.reload`). Atelier-path specs live in `designer-atelier.spec.ts` and seed `'atelier'` explicitly. + **Any seed that replaces the whole `nodepilot-design` key must re-assert both pins** — that is why + `seedExpertMode` and `seedAtelier` repeat them. - **The preview build renders the UI in English** (i18n falls back to EN). All activity- and trigger-config panels are translated for both DE and EN. Prefer language-agnostic selectors anyway — `getByRole` with bilingual regex diff --git a/src/nodepilot-ui/e2e/designer-atelier.spec.ts b/src/nodepilot-ui/e2e/designer-atelier.spec.ts index 2f5f3098..660d7ea7 100644 --- a/src/nodepilot-ui/e2e/designer-atelier.spec.ts +++ b/src/nodepilot-ui/e2e/designer-atelier.spec.ts @@ -45,7 +45,12 @@ async function seedAtelier(page: Page) { let appWritten = false; try { appWritten = !!raw && JSON.parse(raw).state?.nodeStyle !== undefined; } catch { /* reseed */ } if (!appWritten) { - localStorage.setItem('nodepilot-design', JSON.stringify({ state: { designerTheme: 'atelier' }, version: 1 })); + // Carries the node-scale pin from installDefaultMocks too: this seed replaces the whole + // key, so without it the Atelier specs would be the only ones running at the `lg` default + // and would drift away from the rest of the suite's canvas geometry. + localStorage.setItem('nodepilot-design', JSON.stringify({ + state: { designerTheme: 'atelier', nodeScaleIndex: 1 }, version: 2, + })); } }); } diff --git a/src/nodepilot-ui/e2e/fixtures/mockApi.ts b/src/nodepilot-ui/e2e/fixtures/mockApi.ts index 78dc57ca..3594b9a5 100644 --- a/src/nodepilot-ui/e2e/fixtures/mockApi.ts +++ b/src/nodepilot-ui/e2e/fixtures/mockApi.ts @@ -52,23 +52,34 @@ export async function mockCaps(page: Page, caps: KnowledgeCapabilities) { } export async function installDefaultMocks(page: Page) { - // Pin the designer to the CLASSIC look for the whole hermetic suite. The Atelier design - // (designStore.designerTheme, default 'atelier') re-tokenises colors/geometry the visual - // assertions in these specs were written against; the classic look must stay byte-identical, - // so the entire existing suite keeps running against it. Atelier gets its own dedicated - // specs (designer-atelier.spec.ts) that seed 'atelier' explicitly. + // Pin the designer to the CLASSIC look AND the small node scale for the whole hermetic suite. + // Both are geometry knobs the canvas assertions in these specs were written against: + // - designerTheme (default 'atelier') re-tokenises colors/geometry; the classic look must stay + // byte-identical, so the entire existing suite keeps running against it. Atelier gets its own + // dedicated specs (designer-atelier.spec.ts) that seed 'atelier' explicitly. + // - nodeScaleIndex (default 3 = `lg` since the v2 store bump) changes how much room a node + // occupies, and `fitView` turns that into a different pan/zoom for the same seeded positions. + // At `lg`, ai-assistant's step-b slid under the bottom-right minimap, which then swallowed the + // click (the hazard e2e/README.md already warns about). Pin `sm` so a future size tweak can + // never reshuffle unrelated specs' canvas coordinates; the scale itself is covered by unit + // tests (designStore.test.ts, CanvasSettings.test.tsx). + // `version: 2` matches the store's current persist version so the seed is taken as-is — at + // version 1 the store's own migration would read this pinned `sm` as "still on the old default" + // and lift it straight back to `lg`. // // Init scripts re-run on EVERY navigation (including page.reload) — an unconditional // setItem would stomp state the app itself persisted mid-test (e.g. after clicking the // Atelier toggle) and make persistence untestable. An app write always contains the full - // designStore state (nodeStyle & friends); seeds only carry 1-2 keys — use that to only + // designStore state (nodeStyle & friends); seeds only carry a few keys — use that to only // seed fresh contexts. await page.addInitScript(() => { const raw = localStorage.getItem('nodepilot-design'); let appWritten = false; try { appWritten = !!raw && JSON.parse(raw).state?.nodeStyle !== undefined; } catch { /* reseed */ } if (!appWritten) { - localStorage.setItem('nodepilot-design', JSON.stringify({ state: { designerTheme: 'classic' }, version: 1 })); + localStorage.setItem('nodepilot-design', JSON.stringify({ + state: { designerTheme: 'classic', nodeScaleIndex: 1 }, version: 2, + })); } }); // Hermetic catch-all for any REST endpoint a test doesn't explicitly mock: return an @@ -180,17 +191,21 @@ function emptyArray(route: Route) { * Seed the designer into "expert" mode before the SPA boots. The default ("standard") mode hides * power-user affordances — node-context-menu breakpoints, the Debug-run toolbar button, and most * view-toggles — behind `designerMode === 'expert'` (designStore, persisted under the key - * 'nodepilot-design', schema version 1). Specs that exercise those features must run in expert + * 'nodepilot-design', schema version 2). Specs that exercise those features must run in expert * mode. Call this BEFORE `page.goto(...)` so the init script wins over the store's default. */ export async function seedExpertMode(page: Page) { // Init scripts run in addition order and the LAST setItem wins — this seed replaces the - // whole 'nodepilot-design' key, so it must re-assert the classic pin from - // installDefaultMocks or expert-mode specs would silently flip to the Atelier design. + // whole 'nodepilot-design' key, so it must re-assert BOTH pins from installDefaultMocks + // (classic look + small node scale) or expert-mode specs would silently flip to the Atelier + // design and the large node geometry. await page.addInitScript(() => localStorage.setItem( 'nodepilot-design', - JSON.stringify({ state: { designerMode: 'expert', designerTheme: 'classic' }, version: 1 }), + JSON.stringify({ + state: { designerMode: 'expert', designerTheme: 'classic', nodeScaleIndex: 1 }, + version: 2, + }), ), ); }