From f69854e1a1b8aeeaa40fc258c24d1ca41512f2a0 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Wed, 29 Apr 2026 13:27:42 -0400 Subject: [PATCH 1/5] fix(pentest): mirror split-view shell in loading.tsx to prevent layout shift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pentest route had no `loading.tsx`, so a hard refresh fell through to the parent `[orgId]/loading.tsx` which renders `` — a centered, padded card. The actual `SplitView` is full-bleed (negative margins escape the app-shell's `p-4 md:p-6`), 340px sidebar + main pane. The mismatch produced a visible CLS jump on hard refresh. This adds a route-level `loading.tsx` whose skeleton has the same outer geometry: same height (`h-[calc(100vh-4rem)]`), same negative margins, same 340px sidebar with header/list/footer rails, and a main-pane placeholder sized to match the overview's header + hero card + stat band. The skeleton shape is intentionally generic so it works whether the resolving page is overview, detail, or create. --- .../security/penetration-tests/loading.tsx | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 apps/app/src/app/(app)/[orgId]/security/penetration-tests/loading.tsx diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/loading.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/loading.tsx new file mode 100644 index 0000000000..b547fa0efb --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/loading.tsx @@ -0,0 +1,78 @@ +/** + * Loading state for the pentests route. Mirrors the `SplitView` shell — + * full-bleed (negative margins escape the app-shell's `p-4 md:p-6`), a + * 340px sidebar, and a main pane — so a hard refresh on any pentest URL + * (overview, detail, or create) doesn't briefly render the inherited + * padded `[orgId]/loading.tsx` and visibly snap into the IDE-style + * layout. The default `` placeholder is a + * centered card; pentest is edge-to-edge, so they don't overlap and the + * jump shows up as a large CLS event on slower networks. + */ +export default function Loading() { + return ( +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
0 + ? 'space-y-3 md:border-l md:border-border md:pl-6' + : 'space-y-3' + } + > +
+
+
+
+ ))} +
+
+
+
+
+ ); +} From 84ae91a6030e96d9f435172f4c6ebe7f5b36aee2 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Wed, 29 Apr 2026 13:38:32 -0400 Subject: [PATCH 2/5] feat(pentest): mobile-friendly split-view + per-route loading skeletons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SplitView is an IDE-style master-detail layout — fine on desktop, broken on phones: at <450px viewport the 340px sidebar squeezes the main pane down to ~100px, so the running-scan detail wraps text letter-by-letter and the live severity tally collapses into an unreadable stack. Standard fix is master-detail responsive routing: on mobile show ONE pane, picked from the URL. - `/pentests` → list only (full-width sidebar) - `/pentests/:id` → detail only (back-bar at top → list) - `/pentests/new` → create form only (back-bar at top → list) Desktop is unchanged — both panes render side by side as before. Specifics: - SplitView: hide whichever pane isn't active on mobile. New mobile-only "← Scans" bar at the top of `
` so users have a persistent path back to the list (the sidebar is the desktop equivalent). - RunList: `w-full md:w-[340px]` so the sidebar takes the full viewport on phones and the fixed 340px column on tablet+. - Detail / overview / failed / finding / empty state: padding drops from `px-8 py-8` to `px-4 py-6 md:px-8 md:py-8` so content gets a full-width feel without crowding the edges on small screens. - LoadingShell helper plus per-route `loading.tsx` files for list / detail / new — each variant's mobile skeleton matches its resolving page, so a hard refresh on any pentest URL transitions in without a CLS jump regardless of viewport. --- .../penetration-tests/[reportId]/loading.tsx | 9 + .../_components/CompletedDetail.tsx | 2 +- .../_components/EmptyState.tsx | 2 +- .../_components/FailedDetail.tsx | 2 +- .../_components/FindingDetail.tsx | 2 +- .../_components/LoadingShell.tsx | 196 ++++++++++++++++++ .../_components/OverviewPane.tsx | 4 +- .../penetration-tests/_components/RunList.tsx | 2 +- .../_components/RunningDetail.tsx | 2 +- .../_components/SplitView.tsx | 39 +++- .../security/penetration-tests/loading.tsx | 85 +------- .../penetration-tests/new/loading.tsx | 9 + 12 files changed, 270 insertions(+), 84 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/loading.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/LoadingShell.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/security/penetration-tests/new/loading.tsx diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/loading.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/loading.tsx new file mode 100644 index 0000000000..24845a8d1d --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/[reportId]/loading.tsx @@ -0,0 +1,9 @@ +import { + DetailMainSkeleton, + LoadingShell, +} from '../_components/LoadingShell'; + +/** Detail-route loading state. Mobile shows ONLY the detail-shape main pane. */ +export default function Loading() { + return } />; +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/CompletedDetail.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/CompletedDetail.tsx index 4abd99ed18..b2b69b136a 100644 --- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/CompletedDetail.tsx +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/CompletedDetail.tsx @@ -43,7 +43,7 @@ export function CompletedDetail({ return (
-
+
diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/EmptyState.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/EmptyState.tsx index 3a83908c54..ab3e054742 100644 --- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/EmptyState.tsx +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/EmptyState.tsx @@ -46,7 +46,7 @@ export function EmptyState({ ? "You've used your trial run. Paid plans are coming soon — contact support if you need access today." : 'Automated black-box pen testing. Start a scan to see findings here.'; return ( -
+

diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FailedDetail.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FailedDetail.tsx index 046dd6a384..8664ed61ad 100644 --- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FailedDetail.tsx +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FailedDetail.tsx @@ -16,7 +16,7 @@ export function FailedDetail({ run, onRetry }: FailedDetailProps) { return (
-
+
diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FindingDetail.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FindingDetail.tsx index 785f0df71a..8d262b69e8 100644 --- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FindingDetail.tsx +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/FindingDetail.tsx @@ -34,7 +34,7 @@ export function FindingDetail({ issue, onBack }: FindingDetailProps) { return (
-
+
+ ); +} + +/** Overview-shape main-pane skeleton (header + hero card + 4-stat band). */ +export function OverviewMainSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
0 + ? 'space-y-3 md:border-l md:border-border md:pl-6' + : 'space-y-3' + } + > +
+
+
+
+ ))} +
+
+
+ ); +} + +/** Detail-shape main-pane skeleton (header + sev tally + agent grid + table). */ +export function DetailMainSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 22 }).map((_, i) => ( +
+ ))} +
+
+
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ ))} +
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+
+
+
+ ); +} + +/** Create-form-shape main-pane skeleton. */ +export function CreateMainSkeleton() { + return ( +
+
+
+
+
+
+
+
+ {Array.from({ length: 2 }).map((_, i) => ( +
+
+
+
+
+ ))} +
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+
+
+
+
+
+
+
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/OverviewPane.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/OverviewPane.tsx index 5aa3d849ec..dd0cd6d659 100644 --- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/OverviewPane.tsx +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/OverviewPane.tsx @@ -74,7 +74,7 @@ function OnboardingState({ canCreate: boolean; }) { return ( -
+
Penetration tests · Overview @@ -142,7 +142,7 @@ function PostureOverview({ return (
-
+

diff --git a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/RunList.tsx b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/RunList.tsx index 3cb7023000..2f0d84f96e 100644 --- a/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/RunList.tsx +++ b/apps/app/src/app/(app)/[orgId]/security/penetration-tests/_components/RunList.tsx @@ -36,7 +36,7 @@ export function RunList({ : 'No pentest runs remaining.' : 'Start a new scan'; return ( -