From cbbed83addc84478655da8789df29f24f764b996 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:28:18 +0100 Subject: [PATCH 1/5] Make reverse-proxy help match the forwarded-header trust boundary Explain immediate-peer trust before proxy setup, constrain copyable HTTPS examples and preserve authentication during diagnostics. Keep both shipped guides in sync and cover the missing guidance in the documentation regression checks. Contract-Neutral: Documentation and regression checks only; proxy trust, authentication, routing and runtime behaviour are unchanged. Change-source: pulse-maintainer --- docs/CONFIGURATION.md | 2 +- docs/REVERSE_PROXY.md | 123 +++++++++++++----- frontend-modern/public/docs/CONFIGURATION.md | 2 +- frontend-modern/public/docs/REVERSE_PROXY.md | 123 +++++++++++++----- .../src/utils/__tests__/docsLinks.test.ts | 28 ++++ 5 files changed, 218 insertions(+), 60 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f95747ce41..190e1d6b93 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -307,7 +307,7 @@ Environment variables take precedence over `system.json`. | `DISCOVERY_HTTP_TIMEOUT_MS` | HTTP probe timeout (ms) | `2000` | | `PULSE_AUTH_HIDE_LOCAL_LOGIN` | Hide username/password form | `false` | | `DEMO_MODE` | Enable read-only demo mode | `false` | -| `PULSE_TRUSTED_PROXY_CIDRS` | Comma-separated IPs/CIDRs trusted to supply `X-Forwarded-For`/`X-Real-IP` | *(unset)* | +| `PULSE_TRUSTED_PROXY_CIDRS` | Comma-separated immediate proxy IPs/CIDRs trusted for forwarded client IP, scheme, host and port. Use the peer seen by Pulse; wildcard ranges are rejected. See [Reverse Proxy](REVERSE_PROXY.md#before-configuring-the-proxy). | *(unset)* | | `PULSE_TRUSTED_NETWORKS` | Comma-separated CIDRs treated as trusted local networks (does not bypass auth) | *(unset)* | | `ALLOW_UNPROTECTED_EXPORT` | Allow unauthenticated config export on public networks when no auth is configured (use with caution) | `false` | diff --git a/docs/REVERSE_PROXY.md b/docs/REVERSE_PROXY.md index d66fbe8f79..f6b19278d1 100644 --- a/docs/REVERSE_PROXY.md +++ b/docs/REVERSE_PROXY.md @@ -2,49 +2,118 @@ Pulse uses WebSockets for real-time updates. Your proxy **MUST** support WebSockets. +## Before configuring the proxy + +Use HTTPS at the public proxy and keep Pulse's backend private. The examples +below are configuration fragments, not complete TLS or authentication setups. +Keep Pulse authentication enabled; TLS termination does not authenticate users. +For identity supplied by an IdP/proxy, follow the separate +[proxy authentication guide](PROXY_AUTH.md#-header-trust-boundary). + +**Forwarded HTTPS headers are trusted only from a configured immediate peer.** +Set `PULSE_TRUSTED_PROXY_CIDRS` in Pulse's deployment configuration to the proxy +address **as seen by Pulse**, not the browser's address or the public hostname. +For a proxy on the same host connecting to `127.0.0.1:7655`, use: + +```dotenv +PULSE_TRUSTED_PROXY_CIDRS=127.0.0.1/32 +``` + +Use `::1/128` instead if that connection actually uses IPv6 loopback. A Docker +proxy normally reaches Pulse from its private container/network address, not +loopback; trust only that proxy's fixed address or a narrowly isolated proxy +network. Do not trust a whole LAN or all Docker networks to make a warning go +away. Wildcard ranges `0.0.0.0/0` and `::/0` are rejected at startup. + +At the public edge, replace client-supplied forwarded scheme, host and client-IP +headers with values established by the proxy; do not append an untrusted value. +With multiple proxy hops, configure the upstream trust boundary on each hop +and trust only the final peer in Pulse. `PULSE_TRUSTED_NETWORKS` is not a +substitute for proxy trust, and proxy trust is not an authentication bypass. +See [configuration overrides](CONFIGURATION.md#common-overrides-environment-variables). + ## ⚡ Quick Configs ### Nginx + +Inside your HTTPS `server` block, for a single public-edge proxy: + ```nginx location / { - proxy_pass http://localhost:7655; + proxy_pass http://127.0.0.1:7655; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - + proxy_set_header X-Forwarded-Scheme ""; + proxy_set_header Forwarded ""; + # Critical for WebSockets proxy_read_timeout 86400; # 24h } ``` ### Caddy + +Caddy handles the WebSocket upgrade and sets forwarded client-IP, host and +scheme headers automatically. The removals below discard other client-supplied +forwarding metadata. Pulse still needs the matching trusted-peer setting above: + ```caddy pulse.example.com { - reverse_proxy localhost:7655 + reverse_proxy 127.0.0.1:7655 { + header_up -Forwarded + header_up -X-Forwarded-Scheme + header_up -X-Forwarded-Port + } } ``` ### Traefik (Docker Compose) + +These labels assume an existing HTTPS entrypoint named `websecure` and a +configured certificate. Keep Pulse on the private proxy network; do not publish +its backend port publicly. Apply trusted-peer configuration to Pulse separately. +Keep Traefik's `forwardedHeaders.insecure` disabled; upstream forwarded headers +need their own narrow trusted-IP configuration, not blanket client trust. + ```yaml labels: - "traefik.enable=true" - "traefik.http.routers.pulse.rule=Host(`pulse.example.com`)" + - "traefik.http.routers.pulse.entrypoints=websecure" + - "traefik.http.routers.pulse.tls=true" + - "traefik.http.routers.pulse.service=pulse" - "traefik.http.services.pulse.loadbalancer.server.port=7655" ``` ### Apache + +Inside a TLS-enabled virtual host, with the required proxy, rewrite and headers +modules enabled: + ```apache +ProxyPreserveHost On +ProxyAddHeaders On +RequestHeader unset X-Forwarded-For +RequestHeader unset X-Forwarded-Host +RequestHeader unset X-Forwarded-Port +RequestHeader unset X-Real-IP +RequestHeader set X-Forwarded-Proto "https" +RequestHeader unset X-Forwarded-Scheme +RequestHeader unset Forwarded RewriteEngine On RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteCond %{HTTP:Connection} upgrade [NC] -RewriteRule ^/?(.*) "ws://localhost:7655/$1" [P,L] +RewriteRule ^/?(.*) "ws://127.0.0.1:7655/$1" [P,L] -ProxyPass / http://localhost:7655/ -ProxyPassReverse / http://localhost:7655/ +ProxyPass / http://127.0.0.1:7655/ +ProxyPassReverse / http://127.0.0.1:7655/ ``` --- @@ -55,33 +124,29 @@ ProxyPassReverse / http://localhost:7655/ If your reverse proxy terminates SSL but Pulse shows "HTTPS: HTTP only" in Settings → Security: -**Cause**: Pulse detects HTTPS in two ways: -1. Direct TLS connection (`req.TLS != nil`) -2. The `X-Forwarded-Proto: https` header +Pulse recognises direct TLS, or `X-Forwarded-Proto: https` from an immediate +peer in `PULSE_TRUSTED_PROXY_CIDRS`. An untrusted peer's header is ignored even +when the browser used HTTPS. Adding the header alone is not sufficient. -If your proxy terminates SSL but doesn't forward this header, Pulse sees plain HTTP. +Check the proxy's actual backend connection address and the effective Pulse +setting first, then check that the TLS proxy sends exactly `https`. For Nginx, +keep `proxy_set_header X-Forwarded-Proto $scheme` inside the HTTPS server's +location; do not label a plain-HTTP request as HTTPS. Caddy normally needs no +header override. Any Caddy `header_up` override belongs **inside** its +`reverse_proxy` block, not directly in the site block. -**Fix**: Add the `X-Forwarded-Proto` header in your proxy config: - -```nginx -# Nginx -proxy_set_header X-Forwarded-Proto $scheme; -``` - -```caddy -# Caddy (automatic, but explicit override if needed) -header_up X-Forwarded-Proto {scheme} -``` - -```apache -# Apache -RequestHeader set X-Forwarded-Proto "https" -``` +After applying configuration through your normal deployment path, sign in +through the real HTTPS URL and recheck Security Posture and live dashboard +updates. If using SSO, check its redirect/callback on that same URL. A successful +`/api/health` request alone does not verify authentication, WebSockets or proxy +trust. Do not disable authentication, certificate verification or origin checks, +or expose the backend, to test this. Share only redacted configuration and +HTTP/error status, not cookies, proxy secrets or a full network export. ### Other Issues - **"Connection Lost"**: WebSocket upgrade failed. Check `Upgrade` and `Connection` headers. -- **502 Bad Gateway**: Pulse is not running on port 7655. +- **502 Bad Gateway**: Check Pulse's actual listener and the proxy's upstream address, port and network reachability; this does not by itself prove Pulse is stopped. Container `localhost` refers to that container, not another service. - **CORS Errors**: Do not add CORS headers in the proxy; Pulse handles them. Set **Settings → System → Network → Allowed Origins** or use `ALLOWED_ORIGINS` if needed. - **OIDC redirects fail**: Ensure `X-Forwarded-Proto` is set (see above). -- **Wrong client IPs**: Set `PULSE_TRUSTED_PROXY_CIDRS` to your proxy IP/CIDR so `X-Forwarded-For` is trusted. +- **Wrong client IPs**: Check the immediate peer and header replacement above. Do not broaden proxy trust or change authentication to repair attribution. diff --git a/frontend-modern/public/docs/CONFIGURATION.md b/frontend-modern/public/docs/CONFIGURATION.md index f95747ce41..190e1d6b93 100644 --- a/frontend-modern/public/docs/CONFIGURATION.md +++ b/frontend-modern/public/docs/CONFIGURATION.md @@ -307,7 +307,7 @@ Environment variables take precedence over `system.json`. | `DISCOVERY_HTTP_TIMEOUT_MS` | HTTP probe timeout (ms) | `2000` | | `PULSE_AUTH_HIDE_LOCAL_LOGIN` | Hide username/password form | `false` | | `DEMO_MODE` | Enable read-only demo mode | `false` | -| `PULSE_TRUSTED_PROXY_CIDRS` | Comma-separated IPs/CIDRs trusted to supply `X-Forwarded-For`/`X-Real-IP` | *(unset)* | +| `PULSE_TRUSTED_PROXY_CIDRS` | Comma-separated immediate proxy IPs/CIDRs trusted for forwarded client IP, scheme, host and port. Use the peer seen by Pulse; wildcard ranges are rejected. See [Reverse Proxy](REVERSE_PROXY.md#before-configuring-the-proxy). | *(unset)* | | `PULSE_TRUSTED_NETWORKS` | Comma-separated CIDRs treated as trusted local networks (does not bypass auth) | *(unset)* | | `ALLOW_UNPROTECTED_EXPORT` | Allow unauthenticated config export on public networks when no auth is configured (use with caution) | `false` | diff --git a/frontend-modern/public/docs/REVERSE_PROXY.md b/frontend-modern/public/docs/REVERSE_PROXY.md index d66fbe8f79..f6b19278d1 100644 --- a/frontend-modern/public/docs/REVERSE_PROXY.md +++ b/frontend-modern/public/docs/REVERSE_PROXY.md @@ -2,49 +2,118 @@ Pulse uses WebSockets for real-time updates. Your proxy **MUST** support WebSockets. +## Before configuring the proxy + +Use HTTPS at the public proxy and keep Pulse's backend private. The examples +below are configuration fragments, not complete TLS or authentication setups. +Keep Pulse authentication enabled; TLS termination does not authenticate users. +For identity supplied by an IdP/proxy, follow the separate +[proxy authentication guide](PROXY_AUTH.md#-header-trust-boundary). + +**Forwarded HTTPS headers are trusted only from a configured immediate peer.** +Set `PULSE_TRUSTED_PROXY_CIDRS` in Pulse's deployment configuration to the proxy +address **as seen by Pulse**, not the browser's address or the public hostname. +For a proxy on the same host connecting to `127.0.0.1:7655`, use: + +```dotenv +PULSE_TRUSTED_PROXY_CIDRS=127.0.0.1/32 +``` + +Use `::1/128` instead if that connection actually uses IPv6 loopback. A Docker +proxy normally reaches Pulse from its private container/network address, not +loopback; trust only that proxy's fixed address or a narrowly isolated proxy +network. Do not trust a whole LAN or all Docker networks to make a warning go +away. Wildcard ranges `0.0.0.0/0` and `::/0` are rejected at startup. + +At the public edge, replace client-supplied forwarded scheme, host and client-IP +headers with values established by the proxy; do not append an untrusted value. +With multiple proxy hops, configure the upstream trust boundary on each hop +and trust only the final peer in Pulse. `PULSE_TRUSTED_NETWORKS` is not a +substitute for proxy trust, and proxy trust is not an authentication bypass. +See [configuration overrides](CONFIGURATION.md#common-overrides-environment-variables). + ## ⚡ Quick Configs ### Nginx + +Inside your HTTPS `server` block, for a single public-edge proxy: + ```nginx location / { - proxy_pass http://localhost:7655; + proxy_pass http://127.0.0.1:7655; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - + proxy_set_header X-Forwarded-Scheme ""; + proxy_set_header Forwarded ""; + # Critical for WebSockets proxy_read_timeout 86400; # 24h } ``` ### Caddy + +Caddy handles the WebSocket upgrade and sets forwarded client-IP, host and +scheme headers automatically. The removals below discard other client-supplied +forwarding metadata. Pulse still needs the matching trusted-peer setting above: + ```caddy pulse.example.com { - reverse_proxy localhost:7655 + reverse_proxy 127.0.0.1:7655 { + header_up -Forwarded + header_up -X-Forwarded-Scheme + header_up -X-Forwarded-Port + } } ``` ### Traefik (Docker Compose) + +These labels assume an existing HTTPS entrypoint named `websecure` and a +configured certificate. Keep Pulse on the private proxy network; do not publish +its backend port publicly. Apply trusted-peer configuration to Pulse separately. +Keep Traefik's `forwardedHeaders.insecure` disabled; upstream forwarded headers +need their own narrow trusted-IP configuration, not blanket client trust. + ```yaml labels: - "traefik.enable=true" - "traefik.http.routers.pulse.rule=Host(`pulse.example.com`)" + - "traefik.http.routers.pulse.entrypoints=websecure" + - "traefik.http.routers.pulse.tls=true" + - "traefik.http.routers.pulse.service=pulse" - "traefik.http.services.pulse.loadbalancer.server.port=7655" ``` ### Apache + +Inside a TLS-enabled virtual host, with the required proxy, rewrite and headers +modules enabled: + ```apache +ProxyPreserveHost On +ProxyAddHeaders On +RequestHeader unset X-Forwarded-For +RequestHeader unset X-Forwarded-Host +RequestHeader unset X-Forwarded-Port +RequestHeader unset X-Real-IP +RequestHeader set X-Forwarded-Proto "https" +RequestHeader unset X-Forwarded-Scheme +RequestHeader unset Forwarded RewriteEngine On RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteCond %{HTTP:Connection} upgrade [NC] -RewriteRule ^/?(.*) "ws://localhost:7655/$1" [P,L] +RewriteRule ^/?(.*) "ws://127.0.0.1:7655/$1" [P,L] -ProxyPass / http://localhost:7655/ -ProxyPassReverse / http://localhost:7655/ +ProxyPass / http://127.0.0.1:7655/ +ProxyPassReverse / http://127.0.0.1:7655/ ``` --- @@ -55,33 +124,29 @@ ProxyPassReverse / http://localhost:7655/ If your reverse proxy terminates SSL but Pulse shows "HTTPS: HTTP only" in Settings → Security: -**Cause**: Pulse detects HTTPS in two ways: -1. Direct TLS connection (`req.TLS != nil`) -2. The `X-Forwarded-Proto: https` header +Pulse recognises direct TLS, or `X-Forwarded-Proto: https` from an immediate +peer in `PULSE_TRUSTED_PROXY_CIDRS`. An untrusted peer's header is ignored even +when the browser used HTTPS. Adding the header alone is not sufficient. -If your proxy terminates SSL but doesn't forward this header, Pulse sees plain HTTP. +Check the proxy's actual backend connection address and the effective Pulse +setting first, then check that the TLS proxy sends exactly `https`. For Nginx, +keep `proxy_set_header X-Forwarded-Proto $scheme` inside the HTTPS server's +location; do not label a plain-HTTP request as HTTPS. Caddy normally needs no +header override. Any Caddy `header_up` override belongs **inside** its +`reverse_proxy` block, not directly in the site block. -**Fix**: Add the `X-Forwarded-Proto` header in your proxy config: - -```nginx -# Nginx -proxy_set_header X-Forwarded-Proto $scheme; -``` - -```caddy -# Caddy (automatic, but explicit override if needed) -header_up X-Forwarded-Proto {scheme} -``` - -```apache -# Apache -RequestHeader set X-Forwarded-Proto "https" -``` +After applying configuration through your normal deployment path, sign in +through the real HTTPS URL and recheck Security Posture and live dashboard +updates. If using SSO, check its redirect/callback on that same URL. A successful +`/api/health` request alone does not verify authentication, WebSockets or proxy +trust. Do not disable authentication, certificate verification or origin checks, +or expose the backend, to test this. Share only redacted configuration and +HTTP/error status, not cookies, proxy secrets or a full network export. ### Other Issues - **"Connection Lost"**: WebSocket upgrade failed. Check `Upgrade` and `Connection` headers. -- **502 Bad Gateway**: Pulse is not running on port 7655. +- **502 Bad Gateway**: Check Pulse's actual listener and the proxy's upstream address, port and network reachability; this does not by itself prove Pulse is stopped. Container `localhost` refers to that container, not another service. - **CORS Errors**: Do not add CORS headers in the proxy; Pulse handles them. Set **Settings → System → Network → Allowed Origins** or use `ALLOWED_ORIGINS` if needed. - **OIDC redirects fail**: Ensure `X-Forwarded-Proto` is set (see above). -- **Wrong client IPs**: Set `PULSE_TRUSTED_PROXY_CIDRS` to your proxy IP/CIDR so `X-Forwarded-For` is trusted. +- **Wrong client IPs**: Check the immediate peer and header replacement above. Do not broaden proxy trust or change authentication to repair attribution. diff --git a/frontend-modern/src/utils/__tests__/docsLinks.test.ts b/frontend-modern/src/utils/__tests__/docsLinks.test.ts index 0d980ec443..2988806241 100644 --- a/frontend-modern/src/utils/__tests__/docsLinks.test.ts +++ b/frontend-modern/src/utils/__tests__/docsLinks.test.ts @@ -131,6 +131,34 @@ describe('docsLinks', () => { } }); + it('keeps reverse-proxy help explicit about forwarded HTTPS and peer trust', () => { + const proxy = readFileSync(path.join(repoRoot, 'docs', 'REVERSE_PROXY.md'), 'utf8'); + const configuration = readFileSync(path.join(repoRoot, 'docs', 'CONFIGURATION.md'), 'utf8'); + const setup = proxy + .split('## Before configuring the proxy')[1] + ?.split('## ⚡ Quick Configs')[0]; + expect(setup).toBeDefined(); + expect(setup).toContain('configured immediate peer'); + expect(setup).toContain('PULSE_TRUSTED_PROXY_CIDRS=127.0.0.1/32'); + expect(setup).toContain('::1/128'); + expect(setup).toContain('Wildcard ranges `0.0.0.0/0` and `::/0` are rejected'); + expect(setup).toContain('not an authentication bypass'); + expect(setup).toContain('PROXY_AUTH.md#-header-trust-boundary'); + const nginx = proxy.split('### Nginx')[1].split('### Caddy')[0]; + expect(nginx).toContain('HTTPS `server` block'); + expect(nginx).toContain('proxy_set_header X-Forwarded-Proto $scheme;'); + expect(nginx).toContain('proxy_set_header X-Forwarded-For $remote_addr;'); + expect(nginx).toContain('proxy_set_header X-Forwarded-Host $host;'); + expect(nginx).toContain('proxy_set_header Forwarded "";'); + expect(proxy).not.toContain('$proxy_add_x_forwarded_for'); + const troubleshooting = proxy.split('### "HTTPS: HTTP only"')[1].split('### Other Issues')[0]; + expect(troubleshooting).toContain('Adding the header alone is not sufficient'); + expect(troubleshooting).toContain('inside** its\n`reverse_proxy` block'); + expect(troubleshooting).toContain('does not verify authentication, WebSockets or proxy'); + expect(configuration).toContain('REVERSE_PROXY.md#before-configuring-the-proxy'); + expect(configuration).toContain('forwarded client IP, scheme, host and port'); + }); + it('keeps configuration transfer separate from full-state recovery', () => { const migration = readFileSync(path.join(repoRoot, 'docs', 'MIGRATION.md'), 'utf8'); const agent = readFileSync(path.join(repoRoot, 'docs', 'UNIFIED_AGENT.md'), 'utf8'); From 95a231bca6ad679aae20990a23c1c1e890616585 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:30:49 +0100 Subject: [PATCH 2/5] Restore intended opacity classes Tailwind 3 never rendered The Tailwind 4 move kept the v3 rendering by deleting opacity modifiers on var() colors, which v3 silently dropped. Some of those deletions preserved real defects: - The Settings phone header is sticky with backdrop-blur but had no background, so page text showed through it while scrolling. - The neutral Resource Privacy meter (Internal sensitivity) had no fill color, so a 59% bar rendered empty. The Unknown coverage filter chip had an invisible status dot. - Assistant chips, the session picker divider, the activity dock and the Patrol model readiness card lost their border color, falling back to gray-200, which draws a bright line in dark mode. - Actions rows had no hover feedback, and disk segments lost the hover ring their transition was written for. Faint surface-alt tints, text dims and the remaining overlays stay removed: they are near-invisible in light mode, no-ops in dark mode, or would push muted text below AA contrast. Roles Edit and Delete now render their authored 44px phone targets; -my-2 keeps custom-role rows at the 53px height of built-in rows, as on the Machines table. Contract-Neutral: Presentation-only class restorations after the Tailwind CSS 4 migration, no subsystem contract change --- .../a60c4eedb1d29238.json | 63 +++++++++++++++++++ .../src/components/AI/Chat/index.tsx | 16 ++--- .../Settings/AIModelSelectionSection.tsx | 2 +- .../components/Settings/DataHandlingPanel.tsx | 2 +- .../src/components/Settings/RolesPanel.tsx | 4 +- .../components/Settings/SettingsPageShell.tsx | 2 +- .../__tests__/RBACPaywallPanels.test.tsx | 13 +++- .../components/Workloads/StackedDiskBar.tsx | 2 +- .../proxmox/proxmoxBackupsTableShared.tsx | 2 +- frontend-modern/src/pages/Actions.tsx | 2 +- 10 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 frontend-modern/browser-verification/a60c4eedb1d29238.json diff --git a/frontend-modern/browser-verification/a60c4eedb1d29238.json b/frontend-modern/browser-verification/a60c4eedb1d29238.json new file mode 100644 index 0000000000..5406525ac2 --- /dev/null +++ b/frontend-modern/browser-verification/a60c4eedb1d29238.json @@ -0,0 +1,63 @@ +{ + "version": 1, + "base_sha": "beb6c3f6a6f3fad74a1b9f76745bacdadcab7834", + "verified_at": "2026-10-03T18:30:37Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/AI/Chat/index.tsx", + "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx", + "frontend-modern/src/components/Settings/DataHandlingPanel.tsx", + "frontend-modern/src/components/Settings/RolesPanel.tsx", + "frontend-modern/src/components/Settings/SettingsPageShell.tsx", + "frontend-modern/src/components/Workloads/StackedDiskBar.tsx", + "frontend-modern/src/features/proxmox/proxmoxBackupsTableShared.tsx", + "frontend-modern/src/pages/Actions.tsx" + ], + "content_sha256": { + "frontend-modern/src/components/AI/Chat/index.tsx": "27d3bd9fbdb86c7f6048b29a65d6bac41308f4650ebb7da1b7072a0fec5522e5", + "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx": "c94ce3d2b8c4c47bf8b07699bdbd98e9f009feb77bebc7bb81dc24f784248f87", + "frontend-modern/src/components/Settings/DataHandlingPanel.tsx": "c0eff8e2c786a3baa45c22d0c7118ed55ad1d52b33f8e06692b7c3222431db96", + "frontend-modern/src/components/Settings/RolesPanel.tsx": "f8b1cb3a8e47e2815c3978ef07239699cb60989a8ecacd93024178896f6211de", + "frontend-modern/src/components/Settings/SettingsPageShell.tsx": "18a195c4c71a939ae17bba3136854f6419c5cbc6c8acb6e62d003d016b0dec74", + "frontend-modern/src/components/Workloads/StackedDiskBar.tsx": "be53a9d0c349a54516b29de58bf8e6fb28af6ea9f8028965591aa3cb960a851d", + "frontend-modern/src/features/proxmox/proxmoxBackupsTableShared.tsx": "3c4883dcd79af1ecea3c7e967de8a05797c5ffd360e873dbb8550f0632708438", + "frontend-modern/src/pages/Actions.tsx": "9ce58a091378c5edc1469f3ad496194668d6d743a217aa43bed6d7ab9c508dcb" + }, + "routes": [ + "/settings/security-data-handling", + "/settings/security-roles", + "/settings/system-ai", + "/proxmox", + "/proxmox/backups/coverage", + "/actions", + "/alerts/thresholds", + "/patrol" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "light and dark color schemes on a private mock stack with a dev Pro licence; each restored class toggled off and on in one page load for before/after", + "Resource Privacy at 390px scrolled under the sticky Settings header: body text no longer shows through (bg-surface/95)", + "Resource Privacy Internal meter at 59% renders a slate-400 fill instead of an empty track", + "Proxmox backups coverage filter chips: Unknown shows a grey status dot like its siblings", + "Assistant session picker in dark mode: section header divider is border-border/60 instead of a bright gray-200 line", + "Assistant model-route chips and Patrol model readiness card markup in dark mode: border-current borders replace bright gray-200", + "Roles table at 390px with rbac capability and roles intercepted: custom-role rows 53px like built-in rows with 44px Edit and Delete targets; 28px buttons on desktop", + "Alerts thresholds mobile card: Cancel threshold edits renders 44x44 like Save, Edit and Delay", + "Patrol decision detail at 390px: Copy ID 44px tall in a row already 44px from Open resource" + ], + "interactions": [ + "scrolled the app scroll shell under the Settings phone header", + "hovered an Actions row and a node disk segment", + "opened the Assistant, sent a message and opened the sessions picker", + "opened a Patrol decision and a threshold edit on the phone layout" + ] +} diff --git a/frontend-modern/src/components/AI/Chat/index.tsx b/frontend-modern/src/components/AI/Chat/index.tsx index 18e44ec7cc..d802df385e 100644 --- a/frontend-modern/src/components/AI/Chat/index.tsx +++ b/frontend-modern/src/components/AI/Chat/index.tsx @@ -4567,7 +4567,7 @@ export const AIChat: Component = (props) => { {(section) => ( <> -
+
{section.title}
@@ -4879,7 +4879,7 @@ export const AIChat: Component = (props) => { {AI_CHAT_PROVIDER_READINESS_SETTINGS_LABEL} @@ -4910,7 +4910,7 @@ export const AIChat: Component = (props) => {