diff --git a/.cursor/rules/relay-connection-item-components.mdc b/.cursor/rules/relay-connection-item-components.mdc
new file mode 100644
index 000000000..2058c99d7
--- /dev/null
+++ b/.cursor/rules/relay-connection-item-components.mdc
@@ -0,0 +1,72 @@
+---
+description: Extract connection list items (table rows, list entries) into their own fragment component
+globs: "**/*.tsx"
+alwaysApply: false
+---
+
+# Extract connection items into fragment components
+
+When rendering items from a Relay connection (e.g. `edges.map(…)`), each item
+MUST be rendered by a dedicated component that owns its own fragment — never
+inline the rendering of node fields directly in the parent's `.map()` body.
+
+This ensures:
+- Data requirements are colocated with the rendering component
+- Adding/removing fields in a row doesn't bloat the parent's fragment
+- The row component is independently testable and reusable
+
+## Pattern
+
+```tsx
+// _components/ThingRow.tsx — owns its fragment
+const thingRowFragment = graphql`
+ fragment ThingRow_thing on Thing {
+ id
+ name
+ status
+ }
+`;
+
+interface ThingRowProps {
+ thingKey: ThingRow_thing$key;
+}
+
+export function ThingRow({ thingKey }: ThingRowProps) {
+ const thing = useFragment(thingRowFragment, thingKey);
+ return (
+
+
{thing.name}
+
{thing.status}
+
+ );
+}
+```
+
+```tsx
+// Parent — spreads the row fragment in its connection and renders the component
+const parentFragment = graphql`
+ fragment ParentPage_things on Query
+ @refetchable(queryName: "ParentPageRefetchQuery") {
+ things(first: $first, after: $after) @connection(key: "ParentPage_things") {
+ edges {
+ node {
+ id
+ ...ThingRow_thing
+ }
+ }
+ }
+ }
+`;
+
+// In JSX:
+{things.map(thing => (
+
+))}
+```
+
+## Naming
+
+- File: `_components/Row.tsx` (for table rows) or
+ `_components/Card.tsx` (for card lists)
+- Fragment: `_` (e.g. `ThingRow_thing`)
+- Prop: `Key` (e.g. `thingKey`)
diff --git a/.cursor/rules/relay-fragments-not-data-props.mdc b/.cursor/rules/relay-fragments-not-data-props.mdc
index 43fa5566f..a6883b5a9 100644
--- a/.cursor/rules/relay-fragments-not-data-props.mdc
+++ b/.cursor/rules/relay-fragments-not-data-props.mdc
@@ -39,3 +39,6 @@ interface EditCookieRowProps {
Callback props (`onSave`, `onCancel`) and configuration props (`isUpdating`,
`variant`) are fine — only **domain data** must come from fragments.
+
+For connection items (table rows, list entries rendered in `.map()`), see the
+companion rule `relay-connection-item-components.mdc`.
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx
index ca292bab3..f1d8afcac 100644
--- a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx
+++ b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx
@@ -307,7 +307,7 @@ export function TrackerResourceRow({ resourceKey, connectionId }: TrackerResourc
- {resource.path}
+ {resource.path}
{resource.lastDetectedAt
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPage.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPage.tsx
new file mode 100644
index 000000000..e0915f9ee
--- /dev/null
+++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPage.tsx
@@ -0,0 +1,99 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+import { useTranslate } from "@probo/i18n";
+import { Breadcrumb, PageHeader } from "@probo/ui";
+import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
+
+import type { TrackerPatternDetailPageQuery } from "#/__generated__/core/TrackerPatternDetailPageQuery.graphql";
+import { useOrganizationId } from "#/hooks/useOrganizationId";
+
+import { TrackerPatternDetectedTrackersSection } from "./_components/TrackerPatternDetectedTrackersSection";
+import { TrackerPatternPropertiesSection } from "./_components/TrackerPatternPropertiesSection";
+
+export const trackerPatternDetailPageQuery = graphql`
+ query TrackerPatternDetailPageQuery(
+ $cookieBannerId: ID!
+ $trackerPatternId: ID!
+ ) {
+ cookieBanner: node(id: $cookieBannerId) @required(action: THROW) {
+ __typename
+ ... on CookieBanner {
+ id
+ name
+ }
+ }
+ node(id: $trackerPatternId) @required(action: THROW) {
+ __typename
+ ... on TrackerPattern {
+ id
+ displayName
+ ...TrackerPatternPropertiesSection_trackerPattern
+ ...TrackerPatternDetectedTrackersSection_trackerPattern
+ }
+ }
+ }
+`;
+
+interface TrackerPatternDetailPageProps {
+ queryRef: PreloadedQuery;
+}
+
+export default function TrackerPatternDetailPage({
+ queryRef,
+}: TrackerPatternDetailPageProps) {
+ const { __ } = useTranslate();
+ const organizationId = useOrganizationId();
+ const data = usePreloadedQuery(trackerPatternDetailPageQuery, queryRef);
+
+ if (data.cookieBanner.__typename !== "CookieBanner") {
+ throw new Error("invalid type for cookieBanner node");
+ }
+ if (data.node.__typename !== "TrackerPattern") {
+ throw new Error("invalid type for node");
+ }
+
+ const cookieBanner = data.cookieBanner;
+ const pattern = data.node;
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPageLoader.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPageLoader.tsx
new file mode 100644
index 000000000..40d689733
--- /dev/null
+++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPageLoader.tsx
@@ -0,0 +1,53 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+import { Suspense, useEffect } from "react";
+import { useQueryLoader } from "react-relay";
+import { useParams } from "react-router";
+
+import type { TrackerPatternDetailPageQuery } from "#/__generated__/core/TrackerPatternDetailPageQuery.graphql";
+
+import TrackerPatternDetailPage, { trackerPatternDetailPageQuery } from "./TrackerPatternDetailPage";
+import { TrackerPatternDetailPageSkeleton } from "./TrackerPatternDetailPageSkeleton";
+
+export default function TrackerPatternDetailPageLoader() {
+ const { cookieBannerId, trackerPatternId } = useParams<{
+ cookieBannerId: string;
+ trackerPatternId: string;
+ }>();
+ if (typeof cookieBannerId !== "string") {
+ throw new Error("Missing cookieBannerId parameter");
+ }
+ if (typeof trackerPatternId !== "string") {
+ throw new Error("Missing trackerPatternId parameter");
+ }
+
+ const [queryRef, loadQuery] = useQueryLoader(
+ trackerPatternDetailPageQuery,
+ );
+
+ useEffect(() => {
+ loadQuery({ cookieBannerId, trackerPatternId });
+ }, [loadQuery, cookieBannerId, trackerPatternId]);
+
+ if (!queryRef) {
+ return ;
+ }
+
+ return (
+ }>
+
+
+ );
+}
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPageSkeleton.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPageSkeleton.tsx
new file mode 100644
index 000000000..811698856
--- /dev/null
+++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/TrackerPatternDetailPageSkeleton.tsx
@@ -0,0 +1,40 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+export function TrackerPatternDetailPageSkeleton() {
+ return (
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+
+
+
+ ))}
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/DetectedTrackerRow.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/DetectedTrackerRow.tsx
new file mode 100644
index 000000000..f07355ef0
--- /dev/null
+++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/DetectedTrackerRow.tsx
@@ -0,0 +1,79 @@
+// Copyright (c) 2026 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+import { useTranslate } from "@probo/i18n";
+import { Badge, Td, Tr } from "@probo/ui";
+import { graphql, useFragment } from "react-relay";
+
+import type { DetectedTrackerRow_detectedTracker$key } from "#/__generated__/core/DetectedTrackerRow_detectedTracker.graphql";
+
+const detectedTrackerFragment = graphql`
+ fragment DetectedTrackerRow_detectedTracker on DetectedTracker {
+ id
+ identifier
+ initiatorUrl
+ maxAgeSeconds
+ source
+ lastDetectedAt
+ }
+`;
+
+function sourceBadge(source: string, __: (s: string) => string) {
+ switch (source) {
+ case "SCRIPT": return { label: __("Script"), variant: "info" as const };
+ case "PRE_EXISTING": return { label: __("Pre-existing"), variant: "outline" as const };
+ default: return { label: source, variant: "neutral" as const };
+ }
+}
+
+interface DetectedTrackerRowProps {
+ detectedTrackerKey: DetectedTrackerRow_detectedTracker$key;
+}
+
+export function DetectedTrackerRow({ detectedTrackerKey }: DetectedTrackerRowProps) {
+ const { __ } = useTranslate();
+ const tracker = useFragment(detectedTrackerFragment, detectedTrackerKey);
+
+ return (
+