Skip to content

feat(chat): chat plan UX fixes + admin chat dashboard - #2595

Merged
smakosh merged 8 commits into
mainfrom
feat/chat-sidebar-improvements-admin-chat
Jun 8, 2026
Merged

smakosh merged 8 commits into
mainfrom
feat/chat-sidebar-improvements-admin-chat

Conversation

@smakosh

@smakosh smakosh commented Jun 8, 2026 •

Copy link
Copy Markdown
Member

Summary

Addresses a batch of chat (apps/playground) feedback for the Chat plan context, plus a new admin "LLM Gateway Chat" dashboard.

Chat sidebar / studio UX (apps/playground)

  • Search Chats moved to the top of the sidebar header (Claude-style), keeping our design.
  • Pinned chats stay sticky above the scrollable history list instead of scrolling away.
  • Canvas now defaults to the Chat plan context so "Chat plan" is actually selectable in its org dropdown (mirrors the image/video pages: resolves the dedicated Chat org for billing while the switcher shows "Chat plan").
  • Skeleton loaders in the image/video studio history sidebars while history is being fetched.
  • Chat plan branding fix: the dropdown trigger now shows the Sparkles "Chat plan" label for the dedicated Chat org instead of an org/building icon (Group Chat previously made it look like an organization).
  • Pay-as-you-go credits are hidden in the Chat plan context — to use credits you switch to an organization.

Separate history by organization

  • Added a nullable organizationId to chat, playground_image_history, and playground_video_history.
  • New chats/images/videos are tagged with the selected organization context; the history lists are now scoped to it.
  • The dedicated Chat org (the "Chat plan" context) also surfaces legacy rows with no org, so existing history keeps showing in the default context (no backfill needed, backward compatible — when no organizationId is sent the API returns all of the user's rows as before).

Admin: LLM Gateway Chat plans dashboard (ee/admin)

  • New /chat-plans list (subscribers + KPIs), /chat-plans/[orgId] detail (transactions + payment failures), plus revenue/cost/margin timeseries and a model/provider/source usage breakdown — mirroring the existing DevPass admin feature but for Chat Plans (chat_plan_* transaction types, CHAT_PLAN_PRICES, isChat orgs, Starter/Plus/Pro tiers, monthly-only).
  • Adds 4 /admin/chat-plans* API endpoints and a "Chat Plans" sidebar entry.

Migration

  • packages/db/migrations/1780949651_narrow_skullbuster.sql adds organization_id (FK, ON DELETE SET NULL) to the three tables.

Testing

  • tsc --noEmit clean for api, admin, and playground on all changed files.
  • pnpm --filter api build and pnpm --filter admin build pass.
  • Note: pnpm --filter playground build fails only on pre-existing ai-elements/{reasoning,response}.tsx shiki version-mismatch errors (untouched files; the documented pnpm.overrides issue), unrelated to this change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Chat Plans admin dashboard for monitoring subscribers, revenue, and KPI metrics with filtering and analytics
    • Organization-scoped chat history and generated content (images/videos) with dedicated chat organization context
    • Organization-aware navigation preserving user context across playground routes
    • Skeleton loading states for history sections

smakosh and others added 3 commits June 8, 2026 21:10
- Move Search Chats to top of the sidebar header
- Keep pinned chats sticky above the scrollable history list
- Default Canvas to the Chat plan context so it is selectable
- Show skeleton loaders in image/video studio history while fetching
- Show Sparkles 'Chat plan' branding (not an org icon) for the chat org
- Hide pay-as-you-go credits in the Chat plan context

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a nullable organizationId to chat, playground_image_history and
playground_video_history so history lists are scoped to the selected
organization context. The dedicated Chat org (the 'Chat plan' context)
also surfaces legacy rows with no org so existing history keeps showing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the DevPass admin feature for Chat Plans: subscribers list with
KPIs, subscriber detail with transaction history and payment failures,
revenue/cost/margin timeseries, and a model/provider/source usage
breakdown. Adds 4 /admin/chat-plans* endpoints and a Chat Plans nav item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds organization-scoped history for chats and playground content (images/videos), implements comprehensive chat-plan admin analytics with KPI reporting, and refactors playground navigation to preserve selected organization context across route changes.

Changes

Organization-scoped chat history and admin chat-plans

Layer / File(s) Summary
Database schema foundation
packages/db/migrations/1780958022_easy_wolfsbane.sql, packages/db/migrations/meta/_journal.json, packages/db/src/schema.ts
Adds nullable organizationId foreign key columns to chat, playground_image_history, and playground_video_history tables with ON DELETE SET NULL, allowing history rows to default to the chat-plan org context when organizationId is NULL.
Org filter utility and API route scoping
apps/api/src/utils/org-history-filter.ts, apps/api/src/routes/chats.ts, apps/api/src/routes/playground.ts
Introduces buildOrgHistoryFilter helper that returns SQL predicates for org-scoped filtering; updates chat listing/creation and playground history routes to accept optional organizationId, apply org filtering to queries, and persist org context on inserted rows.
Admin chat-plans reporting endpoints
apps/api/src/routes/admin.ts
Implements four new chat-plans admin endpoints: /chat-plans (filtered subscriber list with KPIs), /chat-plans/timeseries (daily revenue/cost/margin series with invoice dedup and refund netting), /chat-plans/usage (top-N usage breakdowns by model/provider/source), and /chat-plans/{orgId} (single subscriber detail with status and recent activity).
Admin chat-plans pages, charts, and navigation
ee/admin/src/app/chat-plans/page.tsx, ee/admin/src/app/chat-plans/[orgId]/page.tsx, ee/admin/src/components/chat-plans-timeseries-chart.tsx, ee/admin/src/components/chat-plans-usage.tsx, ee/admin/src/components/admin-shell.tsx
Adds paginated chat-plans list dashboard with filtering/sorting, per-subscriber KPI cards and detail page, timeseries and usage visualization components, and sidebar navigation entry.
Frontend hooks and history item organization contract
apps/playground/src/hooks/useChats.ts, apps/playground/src/hooks/usePlaygroundHistory.ts, apps/playground/src/lib/image-gen.ts, apps/playground/src/lib/video-gen.ts
Updates useChats, useImageHistory, and useVideoHistory hooks to accept optional organizationId and pass it to API queries; documents organization attribution in GalleryItem and VideoGalleryItem types.
Canvas/org selection and chat-plan context behavior
apps/playground/src/app/canvas/page.tsx, apps/playground/src/components/playground/organization-switcher.tsx, apps/playground/src/components/credits/credits-display.tsx, apps/playground/src/components/playground/chat-page-client.tsx
Ensures chat org exists on page load, updates org fallback selection order to include chat org, treats chat context as a special UI state in org switcher, hides pay-as-you-go top-up for chat-plan orgs, and passes organizationId to primary chat creation.
Org-preserving sidebar navigation and chat sidebar refactor
apps/playground/src/components/playground/canvas-sidebar.tsx, apps/playground/src/components/playground/chat-sidebar.tsx
Defines withOrg() helper to append orgId query param to sidebar navigation links across canvas/chat/image/video routes; refactors chat sidebar to extract ChatHistoryItem component, scope history loading by resolved org via useChats(orgId), and render pinned chats separately from virtualized history.
Image/video history persistence and loading states
apps/playground/src/components/playground/history-skeleton.tsx, apps/playground/src/components/playground/image-page-client.tsx, apps/playground/src/components/playground/image-sidebar.tsx, apps/playground/src/components/playground/video-page-client.tsx, apps/playground/src/components/playground/video-sidebar.tsx
Adds HistorySkeleton loading component; threads organizationId through image/video history hooks, save payloads, and placeholder generation flows; adds isHistoryLoading prop to sidebars for conditional loading-state rendering.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • theopenco/llmgateway#2436: Introduces the chat-plan subscription and pricing data model that the new admin endpoints use to compute MRR/cost/margin KPIs via CHAT_PLAN_PRICES.
  • theopenco/llmgateway#2578: Modifies chat/credits organization-context plumbing in CreditsDisplay, chat-page-client.tsx, and organization-switcher.tsx to handle isChatPlanOrg flag similarly.
  • theopenco/llmgateway#2215: Related to chat-sidebar refactoring around virtualized row rendering and react-window integration for chat history.

Suggested reviewers

  • steebchen
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(chat): chat plan UX fixes + admin chat dashboard' accurately summarizes the main changes: chat plan UX improvements, org-scoped history, and a new admin dashboard.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/chat-sidebar-improvements-admin-chat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/playground/src/components/playground/chat-page-client.tsx (1)

1090-1097: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require an organization context before creating a chat.

Line 1096 can send organizationId as undefined. That creates new chats without org linkage, which undermines org-scoped history and reintroduces legacy null-org rows.

Suggested fix
 		try {
+			const organizationId = selectedOrganization?.id ?? chatOrg?.id;
+			if (!organizationId) {
+				throw new Error("Organization context is not ready yet. Please retry.");
+			}
+
 			const title = userMessage
 				? userMessage.slice(0, 50) + (userMessage.length > 50 ? "..." : "")
 				: "New Chat";

 			const chatData = await createChat.mutateAsync({
 				body: {
 					title,
 					model: selectedModel,
 					webSearch: webSearchEnabled,
 					comparisonEnabled,
-					organizationId: selectedOrganization?.id ?? chatOrg?.id,
+					organizationId,
 				},
 			});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/playground/chat-page-client.tsx` around lines
1090 - 1097, The current createChat call can pass organizationId as undefined
(via createChat.mutateAsync with selectedOrganization?.id ?? chatOrg?.id), which
allows chats to be created without org linkage; before calling
createChat.mutateAsync in the component handling chat creation, explicitly
require an organization id by checking selectedOrganization and chatOrg and if
neither provides an id, abort the creation (return early), surface a user-facing
error/validation message, or redirect the user to select/claim an organization;
update the call site to only invoke createChat.mutateAsync when a non-empty
organizationId is available.
apps/playground/src/components/playground/video-page-client.tsx (1)

200-205: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Capture org context per generated item before async completion.

organizationId is taken from current UI selection when autosave runs. If the user changes org while jobs are in flight, saved video history can land in the wrong org.

Suggested fix
+const itemOrgIdRef = useRef<Map<string, string | undefined>>(new Map());

 // in generateVideos(), when creating itemId
+const orgIdAtGeneration = selectedOrganization?.id;
+itemOrgIdRef.current.set(itemId, orgIdAtGeneration);

 // in save payload
-organizationId: selectedOrganization?.id,
+organizationId: itemOrgIdRef.current.get(item.id),

 // when item is finalized/removed
+itemOrgIdRef.current.delete(item.id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/playground/video-page-client.tsx` around lines
200 - 205, The saveVideoHistory call is using selectedOrganization at async
completion, which can change before the request is sent; capture the org context
early by reading selectedOrganization?.id into a local const (e.g. const orgId =
selectedOrganization?.id) at the start of the per-item processing (where item is
created/queued) and then pass orgId into saveVideoHistory's body (instead of
selectedOrganization?.id) so each generated item is saved to the organization
that was active when it was created.
apps/playground/src/components/playground/image-page-client.tsx (1)

220-225: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist history with the org captured at generation start, not current selection.

Using selectedOrganization?.id here can mis-attribute records if the user switches orgs while generation is still running; the item is then saved into the wrong org history.

Suggested fix
+const itemOrgIdRef = useRef<Map<string, string | undefined>>(new Map());

 // in generateImages(), when creating itemId
+const orgIdAtGeneration = selectedOrganization?.id;
+itemOrgIdRef.current.set(itemId, orgIdAtGeneration);

 // in save payload
-organizationId: selectedOrganization?.id,
+organizationId: itemOrgIdRef.current.get(item.id),

 // when item is finalized/removed
+itemOrgIdRef.current.delete(item.id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/playground/image-page-client.tsx` around lines
220 - 225, The call to saveImageHistory is using the mutable
selectedOrganization?.id which can change mid-generation; instead persist and
use the organization id captured when the generation started (e.g., a property
on the image item such as item.organizationId or item.generatedOrganizationId)
and pass that into saveImageHistory.body.organizationId; if the item lacks that
field, set it at creation time when the generation is enqueued (populate
item.generatedOrganizationId = selectedOrganization?.id) and update
saveImageHistory to read that stable value rather than selectedOrganization?.id.
🧹 Nitpick comments (1)
apps/api/src/routes/playground.ts (1)

12-29: 💤 Low value

Consider extracting the shared org-filter logic to reduce duplication.

buildHistoryOrgFilter here and buildChatOrgFilter in chats.ts implement nearly identical logic for scoping queries by organization with special handling for the Chat org. The only difference is that this version accepts a Column parameter while the chat version uses a hardcoded column reference.

Consider extracting a shared helper (e.g., in a utils file) that both routes can use. The buildHistoryOrgFilter signature is already generic enough to serve both use cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/playground.ts` around lines 12 - 29, Extract the shared
org-filter logic into a single helper (e.g., buildOrgFilter) that accepts a
Column (or column reference) and organizationId and performs the same steps
currently in buildHistoryOrgFilter: lookup org via
db.query.organization.findFirst, check org?.isChat, and return or(eq(column,
organizationId), isNull(column)) for chat orgs or eq(column, organizationId)
otherwise (and undefined when no org id). Replace buildHistoryOrgFilter and
buildChatOrgFilter to call this new helper (pass the Column or the hardcoded
column reference used in chats.ts) so the lookup and isChat handling are
centralized and duplicated logic removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/routes/admin.ts`:
- Around line 11805-11811: The route for /chat-plans/{orgId} currently only
verifies the organization exists but does not enforce that it is a chat
organization; update the org lookup or subsequent check so only organizations
with isChat === true are allowed: when calling
db.query.organization.findFirst(...) include isChat: { eq: true } in the where
clause (or, after loading org, check org.isChat and throw new HTTPException(404,
{ message: "Subscriber not found" }) if false) for the handler that uses the org
variable and do the same change for the second lookup referenced around the same
area (the other db.query.organization.findFirst call noted in the comment).
- Around line 11312-11327: universeRow is using a looser predicate (only
chatPlan != "none") so cycle totals include expired subscribers; update the
universeRow .where(...) to use the same “active subscriber” predicate as
activeRows (i.e. include the same isChat, chatPlan !== "none" plus the
expiry/active-date condition used in activeRows) so the SUMs
(realCostSub.realCost and tierPriceExpr) are computed over the exact same active
set; locate universeRow and mirror the exact predicate expression used by
activeRows (including any expiry checks) when building the leftJoin/where for
tables.organization and realCostSub.

In `@apps/playground/src/app/canvas/page.tsx`:
- Around line 40-45: The call to fetchServerData("GET", "/playground/chat-org")
can throw and currently will abort rendering; wrap that bootstrap call in a
try/catch (or otherwise handle Promise rejection) so failures are ignored or
logged but do not stop execution, then continue to call fetchServerData for
"/orgs" to populate initialOrganizationsData; reference the fetchServerData call
and the initialOrganizationsData variable and ensure any error handling is
non-throwing (e.g., process/log the error and proceed).

In `@ee/admin/src/app/chat-plans/`[orgId]/page.tsx:
- Around line 148-158: The code treats any falsy API result as either
unauthenticated or a 404; instead, wrap the
$api.GET("/admin/chat-plans/{orgId}", ...) call in a try/catch and differentiate
outcomes: if the call succeeds but returns data === null -> return <SignInPrompt
/>; if the call succeeds but returns no data and indicates a 404 (inspect the
response status or explicit flag from $api.GET) -> call notFound(); if the call
throws or returns an unexpected transport/backend error -> surface or rethrow
the error (or render an error UI) rather than mapping it to notFound. Update the
logic around the data variable and the $api.GET call so only real 404 responses
trigger notFound(), authentication triggers SignInPrompt, and other errors are
handled/logged explicitly.

In `@ee/admin/src/app/chat-plans/page.tsx`:
- Around line 355-374: The current check after the $api.GET call treats any
missing data as an auth issue and returns SignInPrompt; instead, inspect the
response/error from $api.GET (status/code or thrown error) and only render
SignInPrompt on explicit authentication errors (e.g., 401/403). For
non-auth/network/backend failures, render an error state (or ErrorBanner/Retry
UI) or rethrow so higher-level error boundaries handle it; update the
conditional around data (and any caught exceptions from $api.GET) to distinguish
auth vs other errors and use SignInPrompt only for auth-specific cases.

In `@ee/admin/src/components/chat-plans-usage.tsx`:
- Around line 122-129: The usage query currently only reads data and isLoading
from $api.useQuery, so failures fall through to the empty-state UI; update the
call to also destructure the error (e.g., const { data, isLoading, error } =
$api.useQuery(...)) and add an explicit error branch in the ChatPlansUsage
component that renders a clear error message/UI (an Alert or similar) when error
is truthy, returning early so failed requests are visually distinct from an
empty usage result.

---

Outside diff comments:
In `@apps/playground/src/components/playground/chat-page-client.tsx`:
- Around line 1090-1097: The current createChat call can pass organizationId as
undefined (via createChat.mutateAsync with selectedOrganization?.id ??
chatOrg?.id), which allows chats to be created without org linkage; before
calling createChat.mutateAsync in the component handling chat creation,
explicitly require an organization id by checking selectedOrganization and
chatOrg and if neither provides an id, abort the creation (return early),
surface a user-facing error/validation message, or redirect the user to
select/claim an organization; update the call site to only invoke
createChat.mutateAsync when a non-empty organizationId is available.

In `@apps/playground/src/components/playground/image-page-client.tsx`:
- Around line 220-225: The call to saveImageHistory is using the mutable
selectedOrganization?.id which can change mid-generation; instead persist and
use the organization id captured when the generation started (e.g., a property
on the image item such as item.organizationId or item.generatedOrganizationId)
and pass that into saveImageHistory.body.organizationId; if the item lacks that
field, set it at creation time when the generation is enqueued (populate
item.generatedOrganizationId = selectedOrganization?.id) and update
saveImageHistory to read that stable value rather than selectedOrganization?.id.

In `@apps/playground/src/components/playground/video-page-client.tsx`:
- Around line 200-205: The saveVideoHistory call is using selectedOrganization
at async completion, which can change before the request is sent; capture the
org context early by reading selectedOrganization?.id into a local const (e.g.
const orgId = selectedOrganization?.id) at the start of the per-item processing
(where item is created/queued) and then pass orgId into saveVideoHistory's body
(instead of selectedOrganization?.id) so each generated item is saved to the
organization that was active when it was created.

---

Nitpick comments:
In `@apps/api/src/routes/playground.ts`:
- Around line 12-29: Extract the shared org-filter logic into a single helper
(e.g., buildOrgFilter) that accepts a Column (or column reference) and
organizationId and performs the same steps currently in buildHistoryOrgFilter:
lookup org via db.query.organization.findFirst, check org?.isChat, and return
or(eq(column, organizationId), isNull(column)) for chat orgs or eq(column,
organizationId) otherwise (and undefined when no org id). Replace
buildHistoryOrgFilter and buildChatOrgFilter to call this new helper (pass the
Column or the hardcoded column reference used in chats.ts) so the lookup and
isChat handling are centralized and duplicated logic removed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e609a72e-334d-4c70-acba-61526ac7f642

📥 Commits

Reviewing files that changed from the base of the PR and between e7533de and 901b6e6.

📒 Files selected for processing (24)
  • apps/api/src/routes/admin.ts
  • apps/api/src/routes/chats.ts
  • apps/api/src/routes/playground.ts
  • apps/playground/src/app/canvas/page.tsx
  • apps/playground/src/components/credits/credits-display.tsx
  • apps/playground/src/components/playground/chat-page-client.tsx
  • apps/playground/src/components/playground/chat-sidebar.tsx
  • apps/playground/src/components/playground/history-skeleton.tsx
  • apps/playground/src/components/playground/image-page-client.tsx
  • apps/playground/src/components/playground/image-sidebar.tsx
  • apps/playground/src/components/playground/organization-switcher.tsx
  • apps/playground/src/components/playground/video-page-client.tsx
  • apps/playground/src/components/playground/video-sidebar.tsx
  • apps/playground/src/hooks/useChats.ts
  • apps/playground/src/hooks/usePlaygroundHistory.ts
  • ee/admin/src/app/chat-plans/[orgId]/page.tsx
  • ee/admin/src/app/chat-plans/page.tsx
  • ee/admin/src/components/admin-shell.tsx
  • ee/admin/src/components/chat-plans-timeseries-chart.tsx
  • ee/admin/src/components/chat-plans-usage.tsx
  • packages/db/migrations/1780949651_narrow_skullbuster.sql
  • packages/db/migrations/meta/1780949651_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts

Comment thread apps/api/src/routes/admin.ts
Comment thread apps/api/src/routes/admin.ts
Comment thread apps/playground/src/app/canvas/page.tsx
Comment thread ee/admin/src/app/chat-plans/[orgId]/page.tsx
Comment thread ee/admin/src/app/chat-plans/page.tsx
Comment thread ee/admin/src/components/chat-plans-usage.tsx
- admin chat-plans detail: enforce isChat on the org lookup (404 otherwise)
- admin chat-plans KPIs: scope cycle cost/MRR universe to active (non-expired)
  subscribers to match the active-subscriber predicate
- capture the org context on image/video items at generation start so saves
  are attributed to the org active when the generation began, not at save time
- extract the shared org-history filter into a single helper reused by the
  chats and playground routes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/api/src/routes/chats.ts (2)

572-579: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate organizationId ownership before chat insert.

Line 578 stores caller-provided organizationId without verifying the user can access that organization. This allows mis-attributed org data and corrupts org-scoped history/analytics boundaries. Enforce membership/access check before insert.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/chats.ts` around lines 572 - 579, Before calling
db.insert(tables.chat) to create newChat, validate that the provided
body.organizationId actually belongs to or is accessible by the current user
(user.id): query the organizations/memberships table (or OrganizationMember
model) to confirm membership/access for body.organizationId, and if the check
fails either set organizationId to null or return a 403/validation error; then
use the verified organizationId (e.g., verifiedOrgId) in the .values call
instead of directly using body.organizationId. Ensure this membership check
happens before the insert and that any failure path stops the insert.

332-336: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

GET /search is not organization-scoped.

/search still queries all active chats for the user and ignores organization context, so org-scoped UI can return cross-context results. Add optional organizationId to query schema and apply buildOrgHistoryFilter in where(...).

Also applies to: 374-385

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/chats.ts` around lines 332 - 336, The GET /search handler
is not scoped to an organization; update the request query schema (the z.object
used for query in the chats route) to include an optional organizationId (e.g.,
organizationId: z.string().optional()), and then apply
buildOrgHistoryFilter(...) when constructing the Prisma/DB where(...) clause in
the search handler (and also in the other similar search/query block around the
other occurrence). Locate the query schema and the where(...) usage in the chats
route (references: the query z.object, the GET /search handler, and the
buildOrgHistoryFilter function) and combine the existing filters with
buildOrgHistoryFilter({ organizationId, userId }) so results are filtered by
organization when organizationId is provided.
apps/playground/src/components/playground/video-page-client.tsx (1)

200-227: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Completed items can remain stuck after org switch.

Line 204 correctly saves with item.organizationId, but cleanup depends on current historyData (scoped by selected org). If the user switches org mid-flight, pending.dbId may never appear in current scope, leaving stale local items (and possible duplicates later). Remove/replace the local item on save success independent of current history scope.

Also applies to: 239-249

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/playground/video-page-client.tsx` around lines
200 - 227, The save callback currently assumes the saved DB id will appear in
the currently-scoped historyData, so pendingSaveRef.current may never be
resolved if the user switches orgs; instead, in the onSuccess handler of
saveVideoHistory (the block that calls setSelectedItemId and updates router),
immediately replace or remove the local item identified by
pendingSaveRef.current.localId regardless of historyData scope: locate the
saveVideoHistory onSuccess callback and after obtaining newId call the
local-items updater (the same logic used to remove/replace local items around
the other save path) to either remove the local temp item or replace it with the
DB-backed item using the localId and newId, and then clear
pendingSaveRef.current; do the same fix for the analogous block around lines
239-249 so local state is updated independent of selected organization.
apps/api/src/routes/playground.ts (1)

314-323: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce org access checks before saving history rows.

Both image and video save handlers persist body.organizationId directly without confirming the authenticated user can use that organization. This can mis-attribute rows to unrelated orgs. Validate access (and org status/type constraints) before insert.

Also applies to: 548-557

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/utils/org-history-filter.ts`:
- Around line 19-21: The current guard in org-history-filter.ts uses "if (!org
|| org.isChat)" which treats missing orgs as Chat-plan and allows NULL rows;
change the logic so NULL fallback is only used when org?.isChat === true.
Replace the conditional so that when org?.isChat is true you return
or(eq(column, organizationId), isNull(column)), otherwise return eq(column,
organizationId); reference symbols: org, org.isChat, column, organizationId,
or(...), eq(...), isNull(...).

---

Outside diff comments:
In `@apps/api/src/routes/chats.ts`:
- Around line 572-579: Before calling db.insert(tables.chat) to create newChat,
validate that the provided body.organizationId actually belongs to or is
accessible by the current user (user.id): query the organizations/memberships
table (or OrganizationMember model) to confirm membership/access for
body.organizationId, and if the check fails either set organizationId to null or
return a 403/validation error; then use the verified organizationId (e.g.,
verifiedOrgId) in the .values call instead of directly using
body.organizationId. Ensure this membership check happens before the insert and
that any failure path stops the insert.
- Around line 332-336: The GET /search handler is not scoped to an organization;
update the request query schema (the z.object used for query in the chats route)
to include an optional organizationId (e.g., organizationId:
z.string().optional()), and then apply buildOrgHistoryFilter(...) when
constructing the Prisma/DB where(...) clause in the search handler (and also in
the other similar search/query block around the other occurrence). Locate the
query schema and the where(...) usage in the chats route (references: the query
z.object, the GET /search handler, and the buildOrgHistoryFilter function) and
combine the existing filters with buildOrgHistoryFilter({ organizationId, userId
}) so results are filtered by organization when organizationId is provided.

In `@apps/playground/src/components/playground/video-page-client.tsx`:
- Around line 200-227: The save callback currently assumes the saved DB id will
appear in the currently-scoped historyData, so pendingSaveRef.current may never
be resolved if the user switches orgs; instead, in the onSuccess handler of
saveVideoHistory (the block that calls setSelectedItemId and updates router),
immediately replace or remove the local item identified by
pendingSaveRef.current.localId regardless of historyData scope: locate the
saveVideoHistory onSuccess callback and after obtaining newId call the
local-items updater (the same logic used to remove/replace local items around
the other save path) to either remove the local temp item or replace it with the
DB-backed item using the localId and newId, and then clear
pendingSaveRef.current; do the same fix for the analogous block around lines
239-249 so local state is updated independent of selected organization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4e6fc4a1-74db-4c1b-98ee-8a6cea96dce7

📥 Commits

Reviewing files that changed from the base of the PR and between 901b6e6 and 01ca96e.

📒 Files selected for processing (8)
  • apps/api/src/routes/admin.ts
  • apps/api/src/routes/chats.ts
  • apps/api/src/routes/playground.ts
  • apps/api/src/utils/org-history-filter.ts
  • apps/playground/src/components/playground/image-page-client.tsx
  • apps/playground/src/components/playground/video-page-client.tsx
  • apps/playground/src/lib/image-gen.ts
  • apps/playground/src/lib/video-gen.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/playground/src/lib/video-gen.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/playground/src/components/playground/image-page-client.tsx
  • apps/api/src/routes/admin.ts

Comment thread apps/api/src/utils/org-history-filter.ts
smakosh and others added 4 commits June 8, 2026 23:23
Sidebar nav links (Chat, Group Chat, Image/Video Studio, Canvas, logo)
now carry the current orgId query param so the selected organization
persists when navigating between playground pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/db/migrations/meta/_journal.json`:
- Around line 1111-1117: The new migration entry object (idx: 158, version: "8",
tag: "1780958022_easy_wolfsbane") in packages/db/migrations/meta/_journal.json
uses spaces for indentation; update this JSON entry so all indentation
characters are tabs (replace leading spaces with tabs for the entire object
block including keys "idx", "version", "when", "tag", and "breakpoints") to
comply with the repository coding guideline that .json files use tabs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d9ea9eba-311e-4e34-869d-afefd35b15d5

📥 Commits

Reviewing files that changed from the base of the PR and between cd00af9 and b2342a0.

📒 Files selected for processing (4)
  • packages/db/migrations/1780958022_easy_wolfsbane.sql
  • packages/db/migrations/meta/1780958022_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts
💤 Files with no reviewable changes (1)
  • packages/db/migrations/1780958022_easy_wolfsbane.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/db/src/schema.ts

Comment thread packages/db/migrations/meta/_journal.json
@smakosh
smakosh enabled auto-merge June 8, 2026 22:42
@smakosh
smakosh added this pull request to the merge queue Jun 8, 2026
Merged via the queue into main with commit 5909022 Jun 8, 2026
12 checks passed
@smakosh
smakosh deleted the feat/chat-sidebar-improvements-admin-chat branch June 8, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant