diff --git a/.changelog/next/fixed-issue-4152.md b/.changelog/next/fixed-issue-4152.md
new file mode 100644
index 0000000000..d6b2eff91a
--- /dev/null
+++ b/.changelog/next/fixed-issue-4152.md
@@ -0,0 +1 @@
+- Fullscreen process-log view no longer gets trapped inside a card on glass themes — its overlay now portals to the document body so it covers the full viewport
diff --git a/client/src/components/apps/tabs/ProcessesTab.jsx b/client/src/components/apps/tabs/ProcessesTab.jsx
index c75a3036bf..aa4d47b5bc 100644
--- a/client/src/components/apps/tabs/ProcessesTab.jsx
+++ b/client/src/components/apps/tabs/ProcessesTab.jsx
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, Fragment } from 'react';
+import { createPortal } from 'react-dom';
import { Maximize2, X } from 'lucide-react';
import * as api from '../../../services/api';
import { executeCommand } from '../../../services/api';
@@ -227,8 +228,19 @@ export default function ProcessesTab({ appId, pm2ProcessNames, filterFn }) {
- {/* Fullscreen Log Modal */}
- {fullscreen && expandedProcess && (
+ {/*
+ Fullscreen Log Modal — portaled to
.
+
+ This tab renders inside an app-detail / processes-page card tree, and on
+ "glass" themes (`--port-backdrop-filter` non-none) `index.css` gives every
+ bordered/rounded `.bg-port-card` a backdrop-filter, which makes that card
+ the containing block for `position:fixed` descendants. Rendered inline the
+ overlay would be sized to the card instead of the viewport. Same escape
+ GalleryImagePicker / FolderPicker get via 's `usePortal`, and the
+ same fix MediaLightbox took in #4151 — reached through createPortal
+ directly because this overlay isn't a .
+ */}
+ {fullscreen && expandedProcess && createPortal(
@@ -273,7 +285,8 @@ export default function ProcessesTab({ appId, pm2ProcessNames, filterFn }) {
>
-
+
,
+ document.body
)}
>
);
diff --git a/client/src/components/apps/tabs/ProcessesTab.test.jsx b/client/src/components/apps/tabs/ProcessesTab.test.jsx
index e6d1ce7365..d05f2a2740 100644
--- a/client/src/components/apps/tabs/ProcessesTab.test.jsx
+++ b/client/src/components/apps/tabs/ProcessesTab.test.jsx
@@ -38,4 +38,26 @@ describe('ProcessesTab', () => {
expect(useProcessLogs).toHaveBeenLastCalledWith('example-api', { lines: 500, appId: 'app-1' });
});
+
+ // On "glass" themes a bordered/rounded `.bg-port-card` gets a backdrop-filter,
+ // which makes it the containing block for `position:fixed` descendants — an
+ // inline overlay would be sized to the card, not the viewport. Portaling to
+ // is the escape, so assert the overlay leaves the component's subtree.
+ it('portals the fullscreen log overlay to so glass-theme cards cannot trap it', () => {
+ const { container } = render();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Expand details for example-api' }));
+ fireEvent.click(screen.getByTitle('Fullscreen'));
+
+ const overlay = document.body.querySelector('.fixed.inset-0');
+ expect(overlay).toBeTruthy();
+ // Rendered out of the tab tree entirely, directly under .
+ expect(container.contains(overlay)).toBe(false);
+ expect(overlay.parentElement).toBe(document.body);
+ expect(screen.getByText('Logs: example-api')).toBeTruthy();
+
+ // Exiting fullscreen tears the portaled overlay back down.
+ fireEvent.click(screen.getByRole('button', { name: 'Exit fullscreen' }));
+ expect(document.body.querySelector('.fixed.inset-0')).toBeNull();
+ });
});