Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/coding/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Metadata } from "next";
import CodingAgentSlackTagsPage from "@/components/CodingAgentSlackTags/CodingAgentSlackTagsPage";

export const metadata: Metadata = {
title: "Coding Agent Slack Tags — Recoup Admin",
};

export default function Page() {
return <CodingAgentSlackTagsPage />;
}
Comment on lines +8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate admin-guard patterns used by this route/component stack.
rg -n -C3 'useIsAdmin|isAdmin|admins/check|redirect\(|notFound\(' --type=ts --type=tsx
rg -n -C3 'CodingAgentSlackTagsPage' --type=ts --type=tsx

Repository: recoupable/admin

Length of output: 87


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Fix: remove unsupported file type, search all files with TS/TSX extensions
echo "=== Searching for admin/auth guard patterns ==="
rg -n -C3 'useIsAdmin|isAdmin|admins/check|redirect\(|notFound\(' \
  --glob='*.ts' --glob='*.tsx'

echo ""
echo "=== Searching for CodingAgentSlackTagsPage ==="
rg -n 'CodingAgentSlackTagsPage' --glob='*.ts' --glob='*.tsx'

echo ""
echo "=== Checking the target file ==="
if [ -f "app/coding-agent/page.tsx" ]; then
  cat -n "app/coding-agent/page.tsx"
else
  echo "File not found, searching for similar files..."
  find . -name "*coding-agent*" -type f 2>/dev/null | head -20
fi

Repository: recoupable/admin

Length of output: 3787


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the CodingAgentSlackTagsPage component
echo "=== CodingAgentSlackTagsPage component ==="
cat -n "components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx"

echo ""
echo "=== Check for middleware in app directory ==="
find app -name "middleware*" -type f 2>/dev/null || echo "No middleware files found in app"

echo ""
echo "=== Check for route.ts or layout.tsx in coding-agent ==="
ls -la app/coding-agent/ 2>/dev/null || echo "Directory listing failed"

Repository: recoupable/admin

Length of output: 3657


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check for middleware at root level
echo "=== Looking for middleware.ts at repository root ==="
find . -maxdepth 1 -name "middleware.ts" -o -name "middleware.js" 2>/dev/null || echo "No root middleware found"

echo ""
echo "=== Check for app directory layout.tsx ==="
if [ -f "app/layout.tsx" ]; then
  head -50 app/layout.tsx
else
  echo "No app/layout.tsx found"
fi

echo ""
echo "=== Compare with HomeContent pattern (which has proper admin check) ==="
grep -A 15 "const { data: isAdmin" components/Home/HomeContent.tsx

Repository: recoupable/admin

Length of output: 1853


Add admin authorization check to this component.

CodingAgentSlackTagsPage must verify admin status before rendering. Import useIsAdmin and conditionally render an access denied message if the user is not an admin, following the pattern used in HomeContent.tsx:

const { data: isAdmin, isLoading } = useIsAdmin();

if (isLoading) return <Skeleton />;
if (!isAdmin) return <AccessDenied />;

Currently the page renders without any authorization verification, allowing non-admin authenticated users to access admin-only data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/coding-agent/page.tsx` around lines 8 - 10, Page currently renders
<CodingAgentSlackTagsPage /> without checking admin rights; import and call
useIsAdmin inside the Page component, follow the pattern from HomeContent.tsx:
const { data: isAdmin, isLoading } = useIsAdmin(); if (isLoading) return
<Skeleton />; if (!isAdmin) return <AccessDenied />; then only render
<CodingAgentSlackTagsPage /> when isAdmin is true. Ensure you import useIsAdmin,
Skeleton, and AccessDenied at the top of the file and apply these checks inside
the exported Page function.

Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,26 @@ import {
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import type { PrivyUser } from "@/types/privy";
import { getLastSeenByDate } from "@/lib/privy/getLastSeenByDate";

const chartConfig = {
count: {
label: "Last Seen",
color: "#345A5D",
},
} satisfies ChartConfig;

interface PrivyLastSeenChartProps {
logins: PrivyUser[];
interface AdminLineChartProps {
title: string;
data: Array<{ date: string; count: number }>;
label?: string;
}

export default function PrivyLastSeenChart({ logins }: PrivyLastSeenChartProps) {
const data = getLastSeenByDate(logins);

export default function AdminLineChart({ title, data, label = "Count" }: AdminLineChartProps) {
if (data.length === 0) return null;

const chartConfig = {
count: {
label,
color: "#345A5D",
},
} satisfies ChartConfig;

return (
<div className="mb-6 rounded-lg border p-4">
<h2 className="mb-4 text-sm font-medium text-gray-700 dark:text-gray-300">
Last Seen Activity
</h2>
<h2 className="mb-4 text-sm font-medium text-gray-700 dark:text-gray-300">{title}</h2>
<ChartContainer config={chartConfig} className="h-[250px] w-full">
<LineChart data={data} accessibilityLayer>
<CartesianGrid vertical={false} />
Expand All @@ -50,7 +46,11 @@ export default function PrivyLastSeenChart({ logins }: PrivyLastSeenChartProps)
<ChartTooltipContent
labelFormatter={(value) => {
const d = new Date(String(value) + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
return d.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
}}
/>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { PrivyLoginsPeriod } from "@/types/privy";
import type { AdminPeriod } from "@/types/admin";

const PERIODS: { value: PrivyLoginsPeriod; label: string }[] = [
const PERIODS: { value: AdminPeriod; label: string }[] = [
{ value: "all", label: "All Time" },
{ value: "daily", label: "Daily" },
{ value: "weekly", label: "Weekly" },
{ value: "monthly", label: "Monthly" },
];

interface PrivyPeriodSelectorProps {
period: PrivyLoginsPeriod;
onPeriodChange: (period: PrivyLoginsPeriod) => void;
interface PeriodSelectorProps {
period: AdminPeriod;
onPeriodChange: (period: AdminPeriod) => void;
}

export default function PrivyPeriodSelector({ period, onPeriodChange }: PrivyPeriodSelectorProps) {
export default function PeriodSelector({ period, onPeriodChange }: PeriodSelectorProps) {
return (
<div className="flex rounded-lg border bg-white dark:bg-gray-900 overflow-hidden">
{PERIODS.map(({ value, label }) => (
Expand Down
71 changes: 71 additions & 0 deletions components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";

import { useState } from "react";
import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb";
import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink";
import { useSlackTags } from "@/hooks/useSlackTags";
import SlackTagsTable from "./SlackTagsTable";
import AdminLineChart from "@/components/Admin/AdminLineChart";
import { getTagsByDate } from "@/lib/coding-agent/getTagsByDate";
import PeriodSelector from "@/components/Admin/PeriodSelector";
import TableSkeleton from "@/components/Sandboxes/TableSkeleton";
import ChartSkeleton from "@/components/PrivyLogins/ChartSkeleton";
import type { AdminPeriod } from "@/types/admin";

export default function CodingAgentSlackTagsPage() {
const [period, setPeriod] = useState<AdminPeriod>("all");
const { data, isLoading, error } = useSlackTags(period);

return (
<main className="mx-auto max-w-6xl px-4 py-10">
<div className="mb-6 flex items-start justify-between">
<div>
<PageBreadcrumb current="Coding Agent Tags" />
<h1 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-gray-100">
Coding Agent Slack Tags
</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Slack mentions of the Recoup Coding Agent, pulled directly from the Slack API.
</p>
</div>
<ApiDocsLink path="admins/coding-agent-slack-tags" />
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) ApiDocsLink implementation:"
fd 'ApiDocsLink.tsx' --exec sed -n '1,220p' {}

echo
echo "2) All ApiDocsLink path usages:"
rg -nP --type=tsx -C1 'ApiDocsLink\s+path='

echo
echo "3) Coding Slack-related route/path strings:"
rg -n -C2 'coding-agent-slack-tags|admins/coding/slack|/api/admins/coding/slack|/api/admins/coding'

Repository: recoupable/admin

Length of output: 539


🏁 Script executed:

rg 'ApiDocsLink.*path=' --type-list

Repository: recoupable/admin

Length of output: 449


🏁 Script executed:

rg 'ApiDocsLink.*path=' -A1 -B1

Repository: recoupable/admin

Length of output: 1197


🏁 Script executed:

rg 'coding-agent-slack-tags|coding.*slack|admins/coding' -i

Repository: recoupable/admin

Length of output: 1101


🏁 Script executed:

fd -e tsx -e ts | head -20

Repository: recoupable/admin

Length of output: 817


Update ApiDocsLink path to match the actual API endpoint.

On Line 30, the path admins/coding-agent-slack-tags does not match the actual API endpoint being used. The code in lib/recoup/fetchSlackTags.ts constructs requests to /api/admins/coding/slack, so the ApiDocsLink should use admins/coding/slack instead to route users to the correct documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx` at line 30,
Update the ApiDocsLink path in CodingAgentSlackTagsPage.tsx: change the current
ApiDocsLink invocation (symbol: ApiDocsLink) to use the correct path
"admins/coding/slack" to match the actual endpoint used by
lib/recoup/fetchSlackTags.ts (which builds requests to
/api/admins/coding/slack); ensure the single ApiDocsLink component invocation in
the CodingAgentSlackTagsPage component is updated so documentation routing
matches the real API route.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable — ApiDocsLink path references the Mintlify docs page filename (admins/coding-agent-slack-tags.mdx), not the API endpoint path. The docs page is correctly named coding-agent-slack-tags.mdx.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sweetmantech, thanks for the clarification! That makes sense — the ApiDocsLink path corresponds to the Mintlify documentation page filename, not the API endpoint route. My analysis was incorrect. I'll note this for future reviews.


✏️ Learnings added
Learnt from: sweetmantech
Repo: recoupable/admin PR: 20
File: components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx:30-30
Timestamp: 2026-03-23T16:51:34.228Z
Learning: In the `recoupable/admin` repository, the `ApiDocsLink` component's `path` prop references the Mintlify docs page filename (e.g., `admins/coding-agent-slack-tags` maps to `admins/coding-agent-slack-tags.mdx`), NOT the actual API endpoint path. Do not flag mismatches between the `ApiDocsLink` path and the API endpoint URL used in fetch calls.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

</div>

<div className="mb-6 flex items-center gap-4">
<PeriodSelector period={period} onPeriodChange={setPeriod} />
{data && (
<div className="text-sm text-gray-600 dark:text-gray-400">
<span className="font-semibold text-gray-900 dark:text-gray-100">{data.total}</span>{" "}
{data.total === 1 ? "tag" : "tags"} found
</div>
)}
</div>

{isLoading && (
<>
<ChartSkeleton />
<TableSkeleton columns={["Tagged By", "Prompt", "Channel", "Timestamp"]} />
</>
)}

{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-400">
{error instanceof Error ? error.message : "Failed to load Slack tags"}
</div>
)}

{!isLoading && !error && data && data.tags.length === 0 && (
<div className="flex items-center justify-center py-12 text-sm text-gray-400">
No tags found for this period.
</div>
)}

{!isLoading && !error && data && data.tags.length > 0 && (
<>
<AdminLineChart title="Tags Over Time" data={getTagsByDate(data.tags)} label="Tags" />
<SlackTagsTable tags={data.tags} />
</>
)}
</main>
);
}
51 changes: 51 additions & 0 deletions components/CodingAgentSlackTags/SlackTagsColumns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { type ColumnDef } from "@tanstack/react-table";
import { SortableHeader } from "@/components/SandboxOrgs/SortableHeader";
import type { SlackTag } from "@/types/coding-agent";

export const slackTagsColumns: ColumnDef<SlackTag>[] = [
{
id: "user_name",
accessorKey: "user_name",
header: "Tagged By",
cell: ({ row }) => {
const tag = row.original;
return (
<div className="flex items-center gap-2">
{tag.user_avatar && (
<img

Check warning on line 15 in components/CodingAgentSlackTags/SlackTagsColumns.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={tag.user_avatar}
alt={tag.user_name}
className="h-6 w-6 rounded-full"
/>
Comment on lines +15 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify image configuration for external avatar hosts before switching to next/image.
for f in $(fd 'next.config.*'); do
  echo "== $f =="
  sed -n '1,240p' "$f"
done

Repository: recoupable/admin

Length of output: 184


🏁 Script executed:

cat -n components/CodingAgentSlackTags/SlackTagsColumns.tsx | head -50

Repository: recoupable/admin

Length of output: 1853


🏁 Script executed:

fd '.eslintrc*' -o 'eslint.config.*'

Repository: recoupable/admin

Length of output: 209


🏁 Script executed:

rg 'user_avatar' --type tsx --type ts -B 3 -A 3

Repository: recoupable/admin

Length of output: 87


🏁 Script executed:

fd -t f '\.(eslintrc|eslintrc\.js|eslintrc\.json|eslintrc\.yml|eslintrc\.yaml)$'

Repository: recoupable/admin

Length of output: 42


🏁 Script executed:

fd 'coding-agent' -t f

Repository: recoupable/admin

Length of output: 81


🏁 Script executed:

rg 'interface SlackTag|type SlackTag' -A 5

Repository: recoupable/admin

Length of output: 1618


🏁 Script executed:

cat package.json | head -80

Repository: recoupable/admin

Length of output: 879


🏁 Script executed:

rg '@next/next' -A 2 -B 2

Repository: recoupable/admin

Length of output: 42


🏁 Script executed:

rg 'no-img-element' -r

Repository: recoupable/admin

Length of output: 473


🏁 Script executed:

fd -H '\.eslintrc' -o 'eslint\.config\.'

Repository: recoupable/admin

Length of output: 211


🏁 Script executed:

rg 'no-img-element'

Repository: recoupable/admin

Length of output: 42


🏁 Script executed:

ls -la | grep eslint

Repository: recoupable/admin

Length of output: 126


🏁 Script executed:

cat .eslintrc.json

Repository: recoupable/admin

Length of output: 120


🏁 Script executed:

rg 'remotePatterns|domains' types/ components/ -A 3 -B 3

Repository: recoupable/admin

Length of output: 42


Using next/image requires additional configuration for external Slack avatar URLs.

While the no-img-element lint rule from next/core-web-vitals is active and the refactor direction is sound, the proposed change is incomplete. Switching to next/image without configuring remotePatterns in next.config.ts will cause runtime failures with "Invalid src prop" errors for external Slack avatar URLs.

The refactor must include:

Required next.config.ts changes
-const nextConfig: NextConfig = {};
+const nextConfig: NextConfig = {
+  images: {
+    remotePatterns: [
+      {
+        protocol: 'https',
+        hostname: '*.slack-edge.com',
+      },
+      {
+        protocol: 'https',
+        hostname: '*.slack.com',
+      },
+    ],
+  },
+};

Then apply the component refactor:

Component changes
+import Image from "next/image";
 ...
-          {tag.user_avatar && (
-            <img
-              src={tag.user_avatar}
-              alt={tag.user_name}
-              className="h-6 w-6 rounded-full"
-            />
-          )}
+          {tag.user_avatar && (
+            <Image
+              src={tag.user_avatar}
+              alt={tag.user_name}
+              width={24}
+              height={24}
+              className="h-6 w-6 rounded-full"
+            />
+          )}
🧰 Tools
🪛 GitHub Check: ESLint

[warning] 15-15:
Using <img> could result in slower LCP and higher bandwidth. Consider using <Image /> from next/image or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/CodingAgentSlackTags/SlackTagsColumns.tsx` around lines 15 - 19,
The current replacement of <img> with next/image will fail for external Slack
avatar URLs because next.config.ts lacks remotePatterns/domains for Slack;
update next.config.ts to add a remotePatterns entry (or domains) that matches
Slack avatar hosts (e.g., avatars.slack-edge.com or other Slack CDN patterns) so
next/image accepts those external URLs, then refactor the SlackTagsColumns
component to import Image from 'next/image' and replace the <img
src={tag.user_avatar} alt={tag.user_name} className="..."> with <Image ... />
using the same src/alt, appropriate width/height or layout/sizes, and include a
fallback for missing avatars (or use unoptimized if you prefer) so runtime
"Invalid src prop" errors are resolved.

)}
<span className="font-medium">{tag.user_name}</span>
</div>
);
},
},
{
id: "prompt",
accessorKey: "prompt",
header: "Prompt",
cell: ({ getValue }) => (
<span className="max-w-md truncate block text-sm text-gray-700 dark:text-gray-300">
{getValue<string>()}
</span>
),
},
{
id: "channel_name",
accessorKey: "channel_name",
header: "Channel",
cell: ({ getValue }) => (
<span className="text-sm text-gray-500 dark:text-gray-400">#{getValue<string>()}</span>
),
},
{
id: "timestamp",
accessorKey: "timestamp",
header: ({ column }) => <SortableHeader column={column} label="Timestamp" />,
cell: ({ getValue }) => new Date(getValue<string>()).toLocaleString(),
sortingFn: "basic",
},
];
78 changes: 78 additions & 0 deletions components/CodingAgentSlackTags/SlackTagsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client";

import {
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
type SortingState,
} from "@tanstack/react-table";
import { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { slackTagsColumns } from "./SlackTagsColumns";
import type { SlackTag } from "@/types/coding-agent";

interface SlackTagsTableProps {
tags: SlackTag[];
}

export default function SlackTagsTable({ tags }: SlackTagsTableProps) {
const [sorting, setSorting] = useState<SortingState>([
{ id: "timestamp", desc: true },
]);

const table = useReactTable({
data: tags,
columns: slackTagsColumns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});

return (
<div className="rounded-lg border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={slackTagsColumns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}
1 change: 1 addition & 0 deletions components/Home/AdminDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function AdminDashboard() {
<NavButton href="/sandboxes" label="View Sandboxes" />
<NavButton href="/sandboxes/orgs" label="View Org Commits" />
<NavButton href="/privy" label="View Privy Logins" />
<NavButton href="/coding" label="Coding Agent Tags" />
</nav>
</div>
);
Expand Down
13 changes: 7 additions & 6 deletions components/PrivyLogins/PrivyLoginsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb";
import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink";
import { usePrivyLogins } from "@/hooks/usePrivyLogins";
import PrivyLoginsTable from "@/components/PrivyLogins/PrivyLoginsTable";
import PrivyPeriodSelector from "@/components/PrivyLogins/PrivyPeriodSelector";
import PeriodSelector from "@/components/Admin/PeriodSelector";
import PrivyLoginsStats from "@/components/PrivyLogins/PrivyLoginsStats";
import TableSkeleton from "@/components/Sandboxes/TableSkeleton";
import ChartSkeleton from "@/components/PrivyLogins/ChartSkeleton";
import PrivyLastSeenChart from "@/components/PrivyLogins/PrivyLastSeenChart";
import type { PrivyLoginsPeriod } from "@/types/privy";
import AdminLineChart from "@/components/Admin/AdminLineChart";
import { getLastSeenByDate } from "@/lib/privy/getLastSeenByDate";
import type { AdminPeriod } from "@/types/admin";

export default function PrivyLoginsPage() {
const [period, setPeriod] = useState<PrivyLoginsPeriod>("all");
const [period, setPeriod] = useState<AdminPeriod>("all");
const { data, isLoading, error } = usePrivyLogins(period);

return (
Expand All @@ -32,7 +33,7 @@ export default function PrivyLoginsPage() {
</div>

<div className="mb-6 flex items-center gap-4">
<PrivyPeriodSelector period={period} onPeriodChange={setPeriod} />
<PeriodSelector period={period} onPeriodChange={setPeriod} />
{data && <PrivyLoginsStats data={data} />}
</div>

Expand All @@ -57,7 +58,7 @@ export default function PrivyLoginsPage() {

{!isLoading && !error && data && data.logins.length > 0 && (
<>
<PrivyLastSeenChart logins={data.logins} />
<AdminLineChart title="Last Seen Activity" data={getLastSeenByDate(data.logins)} label="Last Seen" />
<PrivyLoginsTable logins={data.logins} />
</>
)}
Expand Down
24 changes: 24 additions & 0 deletions hooks/useSlackTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { usePrivy } from "@privy-io/react-auth";
import { fetchSlackTags } from "@/lib/recoup/fetchSlackTags";
import type { SlackTagsPeriod } from "@/types/coding-agent";

/**
* Fetches Slack tagging analytics for the Recoup Coding Agent for the given period.
* Authenticates with the Privy access token (admin Bearer auth).
*/
export function useSlackTags(period: SlackTagsPeriod) {
const { ready, authenticated, getAccessToken } = usePrivy();

return useQuery({
queryKey: ["admin", "coding-agent", "slack-tags", period],
queryFn: async () => {
const token = await getAccessToken();
if (!token) throw new Error("Not authenticated");
return fetchSlackTags(token, period);
},
enabled: ready && authenticated,
});
Comment on lines +15 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether auth transitions clear/invalidate React Query cache.
rg -n -C3 'queryClient\.(clear|removeQueries|resetQueries|invalidateQueries)|logout|onLogout' --type=ts --type=tsx

# Inspect current query keys for admin datasets.
rg -n -C2 'queryKey:\s*\[' --type=ts --type=tsx

Repository: recoupable/admin

Length of output: 87


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Searching for cache invalidation on auth transitions ==="
rg -n -C3 'queryClient\.(clear|removeQueries|resetQueries|invalidateQueries)|logout|onLogout' --type=ts

echo ""
echo "=== Checking usePrivy usage in the codebase ==="
rg -n 'usePrivy' --type=ts -A2

echo ""
echo "=== Examining the specific file: hooks/useSlackTags.ts ==="
fd 'useSlackTags.ts' -type f -exec cat {} \;

echo ""
echo "=== Checking other query keys in admin context ==="
rg -n 'queryKey.*admin' --type=ts -B1 -A1

echo ""
echo "=== Looking for Privy logout handlers ==="
rg -n 'logout' --type=ts -C3

Repository: recoupable/admin

Length of output: 5722


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Reading useSlackTags.ts ==="
cat hooks/useSlackTags.ts

echo ""
echo "=== Searching for queryClient initialization and setup ==="
rg -n 'queryClient\s*=' --type=ts 

echo ""
echo "=== Searching for Privy provider setup ==="
rg -n 'PrivyProvider\|QueryClientProvider' --type=ts

echo ""
echo "=== Checking root app files for initialization ==="
fd -e ts -e js 'app\.|main\.|_app\.' | head -20

echo ""
echo "=== Checking _app.tsx or similar ==="
fd '_app' -type f | head -5

Repository: recoupable/admin

Length of output: 1083


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Reading providers/Providers.tsx ==="
cat providers/Providers.tsx

echo ""
echo "=== Checking for logout or auth change handlers ==="
rg -n 'onLogout\|logout\|onAuthStateChange' --type=ts -B2 -A3

echo ""
echo "=== Checking if other admin queries also lack user scope ==="
rg -n 'queryKey.*\["admin' --type=ts -A1

echo ""
echo "=== Looking for Privy docs or integration patterns ==="
rg -n 'PrivyProvider' --type=ts -B2 -A5

Repository: recoupable/admin

Length of output: 659


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Reading providers/PrivyProvider.tsx ==="
cat providers/PrivyProvider.tsx

echo ""
echo "=== All admin query keys in codebase ==="
rg -n 'queryKey.*\["admin' --type=ts -B1 -A2

echo ""
echo "=== Check if any hook includes user?.id in queryKey ==="
rg -n 'queryKey.*user' --type=ts -B2 -A1

echo ""
echo "=== Looking for useEffect with logout dependency ==="
rg -n 'useEffect.*logout\|logout.*useEffect' --type=ts -B2 -A5

Repository: recoupable/admin

Length of output: 2662


Cache leakage vulnerability across all admin queries—not just useSlackTags.

This is a systemic security issue. All admin hooks (useSlackTags, usePulseEmail, usePrivyLogins, useAdminSandboxes, useAdminSandboxOrgs, useAccountTaskRuns) omit user ID from their query keys. The shared queryClient singleton is never cleared on authentication changes, so cached admin data from one user can be served to another user in the same session.

Required fixes:

  1. Add user scoping to all admin query keys (e.g., ["admin", "coding-agent", "slack-tags", user?.id ?? "anon", period])
  2. OR implement cache invalidation on logout/auth changes via queryClient.invalidateQueries() or queryClient.clear()

The PrivyProvider has no logout callbacks configured, and no cache-clearing logic exists elsewhere in the application.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useSlackTags.ts` around lines 15 - 23, The admin hooks (e.g.,
useSlackTags) build query keys without user scoping, causing cached admin data
to leak between users; update each admin hook (useSlackTags, usePulseEmail,
usePrivyLogins, useAdminSandboxes, useAdminSandboxOrgs, useAccountTaskRuns) to
include the current user identifier in the queryKey (for example add user?.id ??
"anon" into the key used in the queryKey array inside useQuery) OR add
cache-clearing on auth transitions by wiring
queryClient.invalidateQueries()/queryClient.clear() to the authentication/logout
lifecycle (ensure PrivyProvider logout callbacks call this); ensure
getAccessToken/ready/authenticated checks still run, and that queryKey changes
or invalidation happen whenever user changes to prevent cross-user cache reuse.

}
25 changes: 25 additions & 0 deletions lib/coding-agent/getTagsByDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { SlackTag } from "@/types/coding-agent";

interface TagsByDateEntry {
date: string;
count: number;
}

/**
* Aggregates Slack tags by UTC date (YYYY-MM-DD) for charting.
*
* @param tags - Array of SlackTag objects
* @returns Array of { date, count } sorted ascending by date
*/
export function getTagsByDate(tags: SlackTag[]): TagsByDateEntry[] {
const counts: Record<string, number> = {};

for (const tag of tags) {
const date = tag.timestamp.slice(0, 10); // "YYYY-MM-DD"
counts[date] = (counts[date] ?? 0) + 1;
Comment on lines +18 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify what timestamp formats are documented/produced for slack-tags payloads.
rg -n -C2 'slack-tags|timestamp|coding-agent' --type=ts --type=tsx --type=md --type=json

Repository: recoupable/admin

Length of output: 87


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check file structure and find relevant files
find . -type f -name "*.ts" -o -name "*.js" | grep -E "(getTagsByDate|slack|tag)" | head -20

Repository: recoupable/admin

Length of output: 2133


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for timestamp/date-related patterns without problematic file types
rg -n 'slack|timestamp|getTagsByDate' --type=ts --max-count=50

Repository: recoupable/admin

Length of output: 1684


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look at the actual file in question
cat -n lib/coding-agent/getTagsByDate.ts

Repository: recoupable/admin

Length of output: 918


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for where this function is called or how timestamps are generated
rg -n 'getTagsByDate|tag\.timestamp' --type=js --type=ts --max-count=100

Repository: recoupable/admin

Length of output: 473


🏁 Script executed:

#!/bin/bash
cat -n types/coding-agent.ts

Repository: recoupable/admin

Length of output: 532


🏁 Script executed:

#!/bin/bash
cat -n lib/recoup/fetchSlackTags.ts

Repository: recoupable/admin

Length of output: 1309


🏁 Script executed:

#!/bin/bash
rg -n 'timestamp.*:' --type=ts --max-count=50 -A 2 -B 2 | head -100

Repository: recoupable/admin

Length of output: 740


🏁 Script executed:

#!/bin/bash
# Look for test files related to getTagsByDate or SlackTags
find . -path ./node_modules -prune -o -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "__tests__" \) -print | xargs grep -l "getTagsByDate\|SlackTag" 2>/dev/null || echo "No tests found"

Repository: recoupable/admin

Length of output: 74


🏁 Script executed:

#!/bin/bash
# Check for mock data or fixtures
rg -n 'SlackTag\|timestamp' --type=ts -B 2 -A 2 | grep -E "(2026|2025|timestamp.*[0-9]{4})" | head -20

Repository: recoupable/admin

Length of output: 42


UTC bucketing logic is broken for timezone-aware timestamps.

The function's JSDoc (line 9) explicitly declares it should aggregate by UTC date, but the implementation simply slices the timestamp string without normalizing to UTC. For a timestamp like 2026-03-23T23:30:00-05:00 (ISO 8601 with offset), slicing characters 0-9 yields 2026-03-23, when this should count toward 2026-03-24 UTC.

Fix by normalizing to UTC before slicing:

Diff
   for (const tag of tags) {
-    const date = tag.timestamp.slice(0, 10); // "YYYY-MM-DD"
+    const parsed = new Date(tag.timestamp);
+    if (Number.isNaN(parsed.getTime())) continue;
+    const date = parsed.toISOString().slice(0, 10); // UTC "YYYY-MM-DD"
     counts[date] = (counts[date] ?? 0) + 1;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const date = tag.timestamp.slice(0, 10); // "YYYY-MM-DD"
counts[date] = (counts[date] ?? 0) + 1;
for (const tag of tags) {
const parsed = new Date(tag.timestamp);
if (Number.isNaN(parsed.getTime())) continue;
const date = parsed.toISOString().slice(0, 10); // UTC "YYYY-MM-DD"
counts[date] = (counts[date] ?? 0) + 1;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/coding-agent/getTagsByDate.ts` around lines 18 - 19, getTagsByDate
currently slices tag.timestamp directly (see tag.timestamp and counts[date])
which fails for timezone-aware ISO strings; convert the timestamp to a UTC date
and then derive the YYYY-MM-DD bucket before incrementing counts. Update
getTagsByDate to parse tag.timestamp into a Date (or use a UTC-aware library),
compute the UTC year/month/day, format as "YYYY-MM-DD" in UTC, and use that
string as the key for counts[date] so offsets like "-05:00" are correctly
attributed to the UTC day.

}

return Object.entries(counts)
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
}
Loading
Loading