-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: admin dashboard #2988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: admin dashboard #2988
Conversation
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdded an npm dev script for the admin app, converted several absolute UI imports to relative paths, added an apps/admin Git submodule pinned to a commit, and added docs/pages for an optional Admin Dashboard. No runtime logic or public API signatures changed. Changes
Sequence Diagram(s)Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Nitpick comments (7)
apps/admin/README.md (2)
16-52: Add language specifier to fenced code block.The directory structure code block should have a language specifier for proper rendering and accessibility.
Apply this diff:
-``` +```text apps/admin/ ├── src/
123-123: Wrap bare URL in markdown link syntax.For better markdown rendering and accessibility.
Apply this diff:
-4. **Access**: http://localhost:3001 +4. **Access**: <http://localhost:3001>apps/admin/tsconfig.json (1)
5-9: Remove redundant path aliases.The configuration defines both
@/*and~/*mapping to./src/*, which is redundant. Additionally, the/*alias mapping to./*is overly broad and could cause path resolution confusion with node_modules or other root-level directories.Apply this diff to simplify the path aliases:
"paths": { - "@/*": ["./src/*"], - "/*": ["./*"], - "~/*": ["./src/*"] + "@/*": ["./src/*"] }apps/admin/src/components/users/edit-rate-limit.tsx (2)
31-41: Replace alert() with toast notifications.Using
alert()for success and error feedback (Lines 36, 39) provides a poor user experience in modern web applications. Consider using a toast notification library or inline UI feedback instead.Example using a hypothetical toast library:
- alert(`Successfully added ${data.creditsAdded} credits. New balance: ${data.newLeft}/${max}`); + toast.success(`Successfully added ${data.creditsAdded} credits. New balance: ${data.newLeft}/${max}`);- alert(`Error: ${error.message}`); + toast.error(`Error: ${error.message}`);
46-56: Replace alert() with inline validation feedback.The validation alert (Line 49) interrupts the user flow. Consider displaying the validation error inline within the dialog, near the input field.
You could add a state variable for the error message and display it conditionally:
const [validationError, setValidationError] = useState<string>(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (creditsToAdd < 1 || creditsToAdd > maxCreditsCanAdd) { setValidationError(`Please enter a value between 1 and ${maxCreditsCanAdd}`); return; } setValidationError(''); updateMutation.mutate({ rateLimitId, creditsToAdd, }); };Then render the error below the input:
{validationError && ( <p className="text-xs text-destructive">{validationError}</p> )}apps/admin/src/components/projects/project-detail.tsx (2)
208-223: Replace confirm() with a proper confirmation dialog.Using the browser's native
confirm()(Line 212) for destructive actions provides a poor user experience and lacks styling consistency. Consider using a proper dialog component from your UI library.Example using a dialog component:
const [userToRemove, setUserToRemove] = useState<string | null>(null); // In render: <Button variant="ghost" size="sm" onClick={() => setUserToRemove(user.id)} className="h-8 px-2 text-destructive hover:text-destructive" > <Trash2 className="size-4" /> </Button> {/* Add a confirmation dialog */} <AlertDialog open={!!userToRemove} onOpenChange={(open) => !open && setUserToRemove(null)}> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Remove User</AlertDialogTitle> <AlertDialogDescription> Are you sure you want to remove {user.name} from this project? </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction onClick={() => { removeUserMutation.mutate({ projectId, userId: userToRemove!, }); setUserToRemove(null); }}> Remove </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog>
76-94: Extract repeated date formatting logic.The date formatting logic is duplicated for createdAt (Lines 78-82) and updatedAt (Lines 87-91). Consider extracting it into a helper function to improve maintainability.
const formatDate = (date: Date | string) => { return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); }; // Then use: <span>Created: {formatDate(project.createdAt)}</span> <span>Updated: {formatDate(project.updatedAt)}</span>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (41)
apps/admin/.env.example(1 hunks)apps/admin/.gitignore(1 hunks)apps/admin/README.md(1 hunks)apps/admin/eslint.config.js(1 hunks)apps/admin/next.config.ts(1 hunks)apps/admin/package.json(1 hunks)apps/admin/postcss.config.js(1 hunks)apps/admin/src/app/api/trpc/[trpc]/route.ts(1 hunks)apps/admin/src/app/layout.tsx(1 hunks)apps/admin/src/app/page.tsx(1 hunks)apps/admin/src/app/projects/[id].backup/page.tsx(1 hunks)apps/admin/src/app/projects/[id]/page.tsx(1 hunks)apps/admin/src/app/projects/page.tsx(1 hunks)apps/admin/src/app/users/[id]/page.tsx(1 hunks)apps/admin/src/app/users/page.tsx(1 hunks)apps/admin/src/components/layout/breadcrumb.tsx(1 hunks)apps/admin/src/components/layout/sidebar.tsx(1 hunks)apps/admin/src/components/projects/add-user-to-project.tsx(1 hunks)apps/admin/src/components/projects/project-detail.tsx(1 hunks)apps/admin/src/components/projects/projects-list.tsx(1 hunks)apps/admin/src/components/users/edit-rate-limit.tsx(1 hunks)apps/admin/src/components/users/user-detail.tsx(1 hunks)apps/admin/src/components/users/users-list.tsx(1 hunks)apps/admin/src/env.ts(1 hunks)apps/admin/src/server/api/root.ts(1 hunks)apps/admin/src/server/api/routers/projects.ts(1 hunks)apps/admin/src/server/api/routers/users.ts(1 hunks)apps/admin/src/server/api/trpc.ts(1 hunks)apps/admin/src/styles/globals.css(1 hunks)apps/admin/src/trpc/helpers.ts(1 hunks)apps/admin/src/trpc/query-client.ts(1 hunks)apps/admin/src/trpc/react.tsx(1 hunks)apps/admin/src/utils/supabase/admin.ts(1 hunks)apps/admin/src/utils/supabase/client/index.ts(1 hunks)apps/admin/src/utils/supabase/server.ts(1 hunks)apps/admin/tailwind.config.ts(1 hunks)apps/admin/tsconfig.json(1 hunks)package.json(1 hunks)packages/ui/src/components/command.tsx(1 hunks)packages/ui/src/components/form.tsx(2 hunks)packages/ui/src/components/sidebar.tsx(3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Do not use the any type unless necessary
Files:
apps/admin/src/app/users/[id]/page.tsxapps/admin/src/components/layout/breadcrumb.tsxpackages/ui/src/components/command.tsxapps/admin/src/components/projects/add-user-to-project.tsxapps/admin/src/trpc/helpers.tsapps/admin/src/server/api/routers/users.tsapps/admin/src/app/users/page.tsxapps/admin/src/components/layout/sidebar.tsxapps/admin/src/utils/supabase/admin.tsapps/admin/src/trpc/query-client.tsapps/admin/src/app/projects/page.tsxapps/admin/src/trpc/react.tsxapps/admin/next.config.tsapps/admin/src/env.tsapps/admin/src/server/api/root.tsapps/admin/src/utils/supabase/client/index.tsapps/admin/src/app/projects/[id].backup/page.tsxapps/admin/tailwind.config.tsapps/admin/src/components/projects/projects-list.tsxpackages/ui/src/components/sidebar.tsxapps/admin/src/components/users/edit-rate-limit.tsxapps/admin/src/server/api/trpc.tsapps/admin/src/app/projects/[id]/page.tsxapps/admin/src/components/projects/project-detail.tsxapps/admin/src/app/layout.tsxapps/admin/src/components/users/user-detail.tsxapps/admin/src/app/page.tsxapps/admin/src/components/users/users-list.tsxapps/admin/src/utils/supabase/server.tsapps/admin/src/app/api/trpc/[trpc]/route.tspackages/ui/src/components/form.tsxapps/admin/src/server/api/routers/projects.ts
{apps,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using the any type unless absolutely necessary
Files:
apps/admin/src/app/users/[id]/page.tsxapps/admin/src/components/layout/breadcrumb.tsxpackages/ui/src/components/command.tsxapps/admin/src/components/projects/add-user-to-project.tsxapps/admin/src/trpc/helpers.tsapps/admin/src/server/api/routers/users.tsapps/admin/src/app/users/page.tsxapps/admin/src/components/layout/sidebar.tsxapps/admin/src/utils/supabase/admin.tsapps/admin/src/trpc/query-client.tsapps/admin/src/app/projects/page.tsxapps/admin/src/trpc/react.tsxapps/admin/next.config.tsapps/admin/src/env.tsapps/admin/src/server/api/root.tsapps/admin/src/utils/supabase/client/index.tsapps/admin/src/app/projects/[id].backup/page.tsxapps/admin/tailwind.config.tsapps/admin/src/components/projects/projects-list.tsxpackages/ui/src/components/sidebar.tsxapps/admin/src/components/users/edit-rate-limit.tsxapps/admin/src/server/api/trpc.tsapps/admin/src/app/projects/[id]/page.tsxapps/admin/src/components/projects/project-detail.tsxapps/admin/src/app/layout.tsxapps/admin/src/components/users/user-detail.tsxapps/admin/src/app/page.tsxapps/admin/src/components/users/users-list.tsxapps/admin/src/utils/supabase/server.tsapps/admin/src/app/api/trpc/[trpc]/route.tspackages/ui/src/components/form.tsxapps/admin/src/server/api/routers/projects.ts
🧠 Learnings (20)
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/trpc/helpers.ts : If reading process.env in server-only helpers, limit to deployment variables (e.g., VERCEL_URL, PORT)
Applied to files:
apps/admin/src/trpc/helpers.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from src/server/api/trpc.ts and validate inputs with Zod
Applied to files:
apps/admin/src/server/api/routers/users.tsapps/admin/src/server/api/trpc.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/trpc/react.tsx : Keep tRPC React client/provider behind a single client boundary in src/trpc/react.tsx
Applied to files:
apps/admin/src/trpc/react.tsx
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/trpc/react.tsx : Keep tRPC React client provider(s) behind a client boundary (ensure this provider file is a client component)
Applied to files:
apps/admin/src/trpc/react.tsx
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from apps/web/client/src/server/api/root.ts
Applied to files:
apps/admin/src/trpc/react.tsxapps/admin/src/server/api/root.tsapps/admin/src/server/api/trpc.tsapps/admin/src/app/api/trpc/[trpc]/route.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from src/server/api/root.ts
Applied to files:
apps/admin/src/trpc/react.tsxapps/admin/src/server/api/root.tsapps/admin/src/server/api/trpc.tsapps/admin/src/app/api/trpc/[trpc]/route.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/next.config.ts : Import ./src/env in Next.js config to enforce env validation at build time
Applied to files:
apps/admin/next.config.tsapps/admin/src/env.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/next.config.ts : Import ./src/env in next.config.ts to enforce env validation at build time
Applied to files:
apps/admin/next.config.tsapps/admin/src/env.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/env.ts : Define and validate environment variables via t3-oss/env-nextjs in apps/web/client/src/env.ts
Applied to files:
apps/admin/src/env.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/env.ts : Define and validate environment variables in src/env.ts via t3-oss/env-nextjs
Applied to files:
apps/admin/src/env.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/env.ts : Expose browser environment variables with NEXT_PUBLIC_* and declare them in the client schema
Applied to files:
apps/admin/src/env.tsapps/admin/src/utils/supabase/client/index.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/app/**/*.tsx : Do not use process.env in client code; import env from @/env instead
Applied to files:
apps/admin/src/env.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Place tRPC routers under apps/web/client/src/server/api/routers/**
Applied to files:
apps/admin/src/server/api/root.tsapps/admin/src/server/api/trpc.tsapps/admin/src/app/api/trpc/[trpc]/route.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Place tRPC routers under src/server/api/routers/**
Applied to files:
apps/admin/src/server/api/root.tsapps/admin/src/server/api/trpc.tsapps/admin/src/app/api/trpc/[trpc]/route.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/utils/supabase/client/index.ts : Use the browser Supabase client only in client components
Applied to files:
apps/admin/src/utils/supabase/client/index.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod
Applied to files:
apps/admin/src/server/api/trpc.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/app/{page,layout,route}.tsx : Follow App Router file conventions (page.tsx, layout.tsx, route.ts) within src/app
Applied to files:
apps/admin/src/app/page.tsx
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Prefer TailwindCSS-first styling and reuse existing UI components from onlook/ui and local patterns
Applied to files:
apps/admin/src/styles/globals.css
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/utils/supabase/server.ts : Use the server-side Supabase client (headers/cookies) only in server components, actions, and route handlers
Applied to files:
apps/admin/src/utils/supabase/server.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Use the Supabase server client in server components/actions/routes and the browser client in client components; never pass server‑only clients into client code
Applied to files:
apps/admin/src/utils/supabase/server.ts
🧬 Code graph analysis (20)
apps/admin/src/app/users/[id]/page.tsx (2)
apps/admin/src/components/layout/breadcrumb.tsx (1)
Breadcrumb(15-40)apps/admin/src/components/users/user-detail.tsx (1)
UserDetail(18-324)
apps/admin/src/components/projects/add-user-to-project.tsx (4)
packages/ui/src/components/popover.tsx (3)
Popover(44-44)PopoverTrigger(44-44)PopoverContent(44-44)packages/ui/src/components/button.tsx (1)
Button(57-57)packages/ui/src/components/command.tsx (6)
Command(148-148)CommandInput(152-152)CommandList(154-154)CommandEmpty(150-150)CommandGroup(151-151)CommandItem(153-153)packages/ui/src/components/select.tsx (5)
Select(158-158)SelectTrigger(166-166)SelectValue(167-167)SelectContent(159-159)SelectItem(161-161)
apps/admin/src/server/api/routers/users.ts (7)
packages/db/src/schema/user/user.ts (1)
users(12-25)packages/db/src/schema/user/user-project.ts (1)
userProjects(10-23)packages/db/src/schema/project/project.ts (1)
projects(13-32)packages/db/src/schema/subscription/subscription.ts (1)
subscriptions(11-38)packages/db/src/schema/subscription/product.ts (1)
products(6-13)packages/db/src/schema/subscription/price.ts (1)
prices(8-22)packages/db/src/schema/subscription/rate-limits.ts (1)
rateLimits(6-43)
apps/admin/src/app/users/page.tsx (1)
apps/admin/src/components/users/users-list.tsx (1)
UsersList(22-248)
apps/admin/src/utils/supabase/admin.ts (2)
apps/admin/src/utils/supabase/client/index.ts (1)
createClient(4-10)apps/admin/src/utils/supabase/server.ts (1)
createClient(5-32)
apps/admin/src/app/projects/page.tsx (1)
apps/admin/src/components/projects/projects-list.tsx (1)
ProjectsList(28-318)
apps/admin/src/trpc/react.tsx (2)
apps/admin/src/trpc/query-client.ts (1)
createQueryClient(4-21)apps/admin/src/server/api/root.ts (1)
AppRouter(16-16)
apps/admin/next.config.ts (1)
.vscode/.debug.script.mjs (1)
__dirname(8-8)
apps/admin/src/server/api/root.ts (3)
apps/admin/src/server/api/trpc.ts (2)
createTRPCRouter(89-89)createCallerFactory(75-75)apps/admin/src/server/api/routers/projects.ts (1)
projectsRouter(7-279)apps/admin/src/server/api/routers/users.ts (1)
usersRouter(6-282)
apps/admin/src/utils/supabase/client/index.ts (1)
apps/admin/src/utils/supabase/server.ts (1)
createClient(5-32)
apps/admin/src/app/projects/[id].backup/page.tsx (2)
apps/admin/src/app/projects/[id]/page.tsx (1)
ProjectDetailPage(4-18)apps/admin/src/components/layout/breadcrumb.tsx (1)
Breadcrumb(15-40)
apps/admin/src/components/projects/projects-list.tsx (1)
apps/admin/src/trpc/react.tsx (1)
api(23-23)
apps/admin/src/server/api/trpc.ts (3)
apps/admin/src/utils/supabase/client/index.ts (1)
createClient(4-10)apps/admin/src/utils/supabase/server.ts (1)
createClient(5-32)apps/admin/src/utils/supabase/admin.ts (1)
createAdminClient(9-20)
apps/admin/src/app/projects/[id]/page.tsx (2)
apps/admin/src/components/layout/breadcrumb.tsx (1)
Breadcrumb(15-40)apps/admin/src/components/projects/project-detail.tsx (1)
ProjectDetail(23-234)
apps/admin/src/components/projects/project-detail.tsx (1)
apps/admin/src/components/projects/add-user-to-project.tsx (1)
AddUserToProject(35-183)
apps/admin/src/app/layout.tsx (2)
apps/admin/src/trpc/react.tsx (1)
TRPCReactProvider(39-55)apps/admin/src/components/layout/sidebar.tsx (1)
Sidebar(13-46)
apps/admin/src/components/users/user-detail.tsx (1)
apps/admin/src/components/users/edit-rate-limit.tsx (1)
EditRateLimit(26-122)
apps/admin/src/utils/supabase/server.ts (1)
apps/admin/src/utils/supabase/client/index.ts (1)
createClient(4-10)
apps/admin/src/app/api/trpc/[trpc]/route.ts (2)
apps/admin/src/server/api/trpc.ts (1)
createTRPCContext(31-48)apps/admin/src/server/api/root.ts (1)
appRouter(10-13)
apps/admin/src/server/api/routers/projects.ts (4)
apps/admin/src/server/api/trpc.ts (2)
createTRPCRouter(89-89)adminProcedure(160-182)packages/db/src/schema/project/project.ts (1)
projects(13-32)packages/db/src/schema/user/user.ts (1)
users(12-25)packages/db/src/schema/user/user-project.ts (1)
userProjects(10-23)
🪛 dotenv-linter (3.3.0)
apps/admin/.env.example
[warning] 4-4: [UnorderedKey] The NEXT_PUBLIC_SUPABASE_ANON_KEY key should go before the NEXT_PUBLIC_SUPABASE_URL key
(UnorderedKey)
[warning] 6-6: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
🪛 markdownlint-cli2 (0.18.1)
apps/admin/README.md
16-16: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
123-123: Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (20)
packages/ui/src/components/command.tsx (1)
13-13: LGTM! Import path refactor to relative path.The change from an absolute alias to a relative import is appropriate for this internal UI component reorganization.
package.json (1)
26-26: LGTM! New admin dev script follows existing patterns.The new
dev:adminscript is consistent with the monorepo's workspace-filtered script pattern.apps/admin/.gitignore (1)
1-46: LGTM! Comprehensive gitignore for Next.js admin app.The ignore patterns appropriately cover dependencies, build artifacts, environment files, and IDE-specific files.
packages/ui/src/components/form.tsx (2)
15-15: LGTM! Import path refactor to relative path.Consistent with the internal UI module reorganization across the package.
151-151: LGTM! Export formatting cleanup.The trailing comma removal is a minor stylistic adjustment with no semantic impact.
packages/ui/src/components/sidebar.tsx (2)
8-15: LGTM! Import path consolidation to relative paths.The refactor from absolute/alias imports to relative paths is appropriate for this internal UI component and aligns with the broader package reorganization.
689-689: LGTM! Export formatting cleanup.Minor stylistic adjustment with no semantic impact.
apps/admin/src/components/layout/sidebar.tsx (1)
13-46: LGTM! Clean sidebar implementation with proper active state logic.The active route detection correctly handles both exact matches and nested routes, with appropriate guarding for the root path.
apps/admin/src/utils/supabase/server.ts (1)
5-32: LGTM! Proper server-side Supabase client setup.The cookie adapter implementation correctly handles the Server Component scenario with the try/catch block. The async/await pattern for cookies() is appropriate for Next.js 15.
Reminder: Based on learnings, ensure this createClient function is only imported and used in Server Components, Server Actions, and Route Handlers—never in Client Components.
apps/admin/README.md (1)
1-199: Excellent comprehensive documentation!The README provides clear architectural overview, setup instructions, security considerations, and development workflow. Well-structured for both immediate use and future reference.
apps/admin/src/app/projects/page.tsx (1)
1-19: LGTM!The projects page structure is clean and follows a consistent pattern with proper component composition.
apps/admin/src/utils/supabase/admin.ts (1)
9-20: LGTM!The admin client configuration is correct. Disabling
autoRefreshTokenandpersistSessionis appropriate for service-role clients that bypass RLS. The warning comment about cautious use is helpful.apps/admin/src/utils/supabase/client/index.ts (1)
4-10: LGTM! Ensure usage in client components only.The browser client factory correctly uses
NEXT_PUBLIC_*environment variables and follows the proper pattern for client-side Supabase usage. Ensure this client is only imported and used in client components (marked with'use client'directive).Based on learnings
apps/admin/src/app/projects/[id]/page.tsx (1)
4-4: Update to handle async params in Next.js 15.Same issue as in
apps/admin/src/app/users/[id]/page.tsx: Next.js 15 made route params async, requiring the component to be async and params to be awaited.Apply this diff:
-export default function ProjectDetailPage({ params }: { params: { id: string } }) { +export default async function ProjectDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; return ( <div className="bg-background"> <div className="container mx-auto px-4 py-8 space-y-6"> <Breadcrumb items={[ { label: 'Projects', href: '/projects' }, - { label: params.id }, + { label: id }, ]} /> - <ProjectDetail projectId={params.id} /> + <ProjectDetail projectId={id} /> </div> </div> ); }apps/admin/src/server/api/root.ts (1)
1-24: LGTM!The tRPC root router configuration follows best practices:
- Properly aggregates the projects and users routers
- Exports the AppRouter type for client-side usage
- Provides a server-side caller factory with helpful JSDoc
The implementation aligns with the learnings for tRPC router organization.
Based on learnings
apps/admin/src/components/layout/breadcrumb.tsx (1)
15-40: LGTM!The Breadcrumb component is well-structured with appropriate conditional rendering logic for links and static labels. The use of index as a key (Line 21) is acceptable in this context since breadcrumb items don't dynamically reorder.
apps/admin/src/app/layout.tsx (1)
1-27: LGTM!The root layout properly sets up the admin app with:
- Correct metadata configuration
- TRPCReactProvider wrapping for React Query integration
- Clean layout structure with Sidebar and scrollable main content area
The implementation follows Next.js 15 app directory conventions.
apps/admin/src/trpc/helpers.ts (1)
1-25: LGTM!The helper functions are well-structured:
getBaseUrl()correctly handles browser, production (Vercel), and local environments- The links array properly configures logging for development and error cases
- Environment variable usage follows best practices (deployment variables only)
The implementation aligns with tRPC best practices and environment configuration guidelines.
Based on learnings
apps/admin/src/components/users/users-list.tsx (1)
22-248: LGTM!The UsersList component is well-implemented with:
- Proper loading, error, and empty state handling
- Correct pagination logic that respects server-side page boundaries
- Appropriate sort handling that resets to page 1 on sort changes
- Good use of skeleton loaders for a better perceived performance
The component follows React and Next.js best practices.
apps/admin/src/env.ts (1)
1-69: LGTM!The environment configuration follows best practices:
- Proper use of
@t3-oss/env-nextjsfor type-safe environment validation- Server-side variables include required Supabase credentials with appropriate validation
- Client-side variables correctly use
NEXT_PUBLIC_*prefix with sensible defaults- Manual
runtimeEnvdestructuring for edge runtime compatibility- Skip validation flag for Docker builds
- Empty string as undefined behavior for stricter validation
The implementation aligns with the learnings for environment variable management in Next.js apps.
Based on learnings
apps/admin/package.json
Outdated
| "dev": "next dev --port 3001", | ||
| "build": "next build", | ||
| "start": "next start --port 3001", | ||
| "typecheck": "tsc --noEmit --project tsconfig.json 2>&1 | grep -v 'web/client' || exit 0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the typecheck script filter.
The typecheck script filters out errors containing 'web/client', which appears to be copied from another package and doesn't apply to the admin app. This could inadvertently hide legitimate TypeScript errors.
Apply this diff to remove the incorrect filter:
- "typecheck": "tsc --noEmit --project tsconfig.json 2>&1 | grep -v 'web/client' || exit 0",
+ "typecheck": "tsc --noEmit --project tsconfig.json",📝 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.
| "typecheck": "tsc --noEmit --project tsconfig.json 2>&1 | grep -v 'web/client' || exit 0", | |
| "typecheck": "tsc --noEmit --project tsconfig.json", |
🤖 Prompt for AI Agents
In apps/admin/package.json around line 10, the "typecheck" script currently
pipes tsc output through a grep that excludes 'web/client' (copied from another
package) which can hide legitimate errors; remove the entire pipe filter and
trailing "|| exit 0" so the script simply runs tsc --noEmit --project
tsconfig.json and fails on real type errors.
| { label: 'Projects', href: '/' }, | ||
| { label: params.id }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix breadcrumb link target.
The backup page breadcrumb points “Projects” to the admin root ('/'), but the primary page uses '/projects'. This regression breaks navigation back to the projects list. Align the backup route with the main page.
- { label: 'Projects', href: '/' },
+ { label: 'Projects', href: '/projects' },🤖 Prompt for AI Agents
In apps/admin/src/app/projects/[id].backup/page.tsx around lines 9 to 10, the
breadcrumb entry for "Projects" incorrectly links to '/' instead of the projects
list; update the breadcrumb object's href from '/' to '/projects' so it matches
the main projects page and restores correct navigation back to the projects
list.
| import { Breadcrumb } from '@/components/layout/breadcrumb'; | ||
| import { UserDetail } from '@/components/users/user-detail'; | ||
|
|
||
| export default function UserDetailPage({ params }: { params: { id: string } }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Update to handle async params in Next.js 15.
Next.js 15 made route params async. The current implementation doesn't await params, which may cause TypeScript errors or runtime issues.
Apply this diff to properly handle async params:
-export default function UserDetailPage({ params }: { params: { id: string } }) {
+export default async function UserDetailPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
return (
<div className="bg-background">
<div className="container mx-auto px-4 py-8 space-y-6">
<Breadcrumb
items={[
{ label: 'Users', href: '/users' },
- { label: params.id },
+ { label: id },
]}
/>
- <UserDetail userId={params.id} />
+ <UserDetail userId={id} />
</div>
</div>
);
}Run the following script to verify if other dynamic route pages in the admin app have the same issue:
🏁 Script executed:
#!/bin/bash
# Description: Find all dynamic route pages that may need async params handling
# Search for page.tsx files in dynamic route directories
fd -e tsx 'page\.tsx$' apps/admin/src/app --exec grep -l "params:" {} \;Length of output: 129
Handle async route params in Next.js 15 dynamic pages
- Convert apps/admin/src/app/users/[id]/page.tsx:
-export default function UserDetailPage({ params }: { params: { id: string } }) {
+export default async function UserDetailPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;- Apply the same change to apps/admin/src/app/projects/[id]/page.tsx
Ensures route params are correctly awaited per Next.js 15.
📝 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.
| export default function UserDetailPage({ params }: { params: { id: string } }) { | |
| export default async function UserDetailPage({ params }: { params: Promise<{ id: string }> }) { | |
| const { id } = await params; |
🤖 Prompt for AI Agents
In apps/admin/src/app/users/[id]/page.tsx around line 4, the page component must
handle Next.js 15 async route params: change the component to an async function
and await the incoming params before using them (e.g., declare the function
async and do const { id } = await params). Also apply the same change to
apps/admin/src/app/projects/[id]/page.tsx so both dynamic pages properly await
route params per Next.js 15.
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| window.open(editorUrl, '_blank'); | ||
| }} | ||
| className="h-6 px-2" | ||
| > | ||
| <Code2 className="size-3.5" /> | ||
| </Button> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| <p className="text-xs">{editorUrl}</p> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| window.open(previewUrl, '_blank'); | ||
| }} | ||
| className="h-6 px-2" | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden external sandbox links against tabnabbing
Both sandbox buttons call window.open(url, '_blank') without noopener. That leaves window.opener writable, so a compromised sandbox can redirect the admin dashboard (reverse tabnabbing). Add the noopener,noreferrer feature string (and clear opener defensively) before shipping.
- window.open(editorUrl, '_blank');
+ const editorWindow = window.open(editorUrl, '_blank', 'noopener,noreferrer');
+ editorWindow?.opener && (editorWindow.opener = null);
...
- window.open(previewUrl, '_blank');
+ const previewWindow = window.open(previewUrl, '_blank', 'noopener,noreferrer');
+ previewWindow?.opener && (previewWindow.opener = null);📝 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.
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| window.open(editorUrl, '_blank'); | |
| }} | |
| className="h-6 px-2" | |
| > | |
| <Code2 className="size-3.5" /> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p className="text-xs">{editorUrl}</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| window.open(previewUrl, '_blank'); | |
| }} | |
| className="h-6 px-2" | |
| > | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| const editorWindow = window.open( | |
| editorUrl, | |
| '_blank', | |
| 'noopener,noreferrer' | |
| ); | |
| if (editorWindow?.opener) { | |
| editorWindow.opener = null; | |
| } | |
| }} | |
| className="h-6 px-2" | |
| > | |
| <Code2 className="size-3.5" /> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p className="text-xs">{editorUrl}</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| const previewWindow = window.open( | |
| previewUrl, | |
| '_blank', | |
| 'noopener,noreferrer' | |
| ); | |
| if (previewWindow?.opener) { | |
| previewWindow.opener = null; | |
| } | |
| }} | |
| className="h-6 px-2" | |
| > |
🤖 Prompt for AI Agents
In apps/admin/src/components/projects/projects-list.tsx around lines 195 to 223,
the two sandbox buttons open external pages using window.open(url, '_blank')
which leaves window.opener writable and allows reverse tabnabbing; change the
calls to include the feature string 'noopener,noreferrer' (e.g. window.open(url,
'_blank', 'noopener,noreferrer')) and defensively clear the opener on the
returned window object (const newWin = window.open(...); if (newWin)
newWin.opener = null;) so that external pages cannot access or redirect the
admin dashboard.
| const isActive = new Date() >= new Date(rateLimit.startedAt) && | ||
| new Date() <= new Date(rateLimit.endedAt); | ||
| const usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100; | ||
|
|
||
| return ( | ||
| <div | ||
| key={rateLimit.id} | ||
| className="p-4 border rounded-lg space-y-3" | ||
| > | ||
| <div className="flex items-center justify-between"> | ||
| <div className="space-y-1"> | ||
| <div className="flex items-center gap-2"> | ||
| <p className="font-medium"> | ||
| {rateLimit.max - rateLimit.left} / {rateLimit.max} used | ||
| </p> | ||
| {isActive && <Badge variant="default">Active</Badge>} | ||
| </div> | ||
| <p className="text-xs text-muted-foreground"> | ||
| {rateLimit.left} requests remaining | ||
| </p> | ||
| </div> | ||
| <div className="flex items-center gap-2"> | ||
| {rateLimit.carryOverTotal > 0 && ( | ||
| <Badge variant="outline"> | ||
| Carried over {rateLimit.carryOverTotal}x | ||
| </Badge> | ||
| )} | ||
| {isActive && ( | ||
| <EditRateLimit | ||
| rateLimitId={rateLimit.id} | ||
| currentLeft={rateLimit.left} | ||
| max={rateLimit.max} | ||
| userId={userId} | ||
| /> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Progress bar */} | ||
| <div className="space-y-1"> | ||
| <div className="h-2 bg-muted rounded-full overflow-hidden"> | ||
| <div | ||
| className="h-full bg-primary transition-all" | ||
| style={{ width: `${usagePercent}%` }} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard rate-limit progress against zero max.
If a rate limit row has max = 0 (which happens for exhausted or misconfigured limits), the current calculation yields Infinity/NaN, so the inline width style becomes invalid and the bar disappears. Clamp the math to handle zero/negative max values.
- const usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100;
+ const usagePercent =
+ rateLimit.max > 0
+ ? Math.min(
+ 100,
+ Math.max(0, ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100),
+ )
+ : 0;📝 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.
| const isActive = new Date() >= new Date(rateLimit.startedAt) && | |
| new Date() <= new Date(rateLimit.endedAt); | |
| const usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100; | |
| return ( | |
| <div | |
| key={rateLimit.id} | |
| className="p-4 border rounded-lg space-y-3" | |
| > | |
| <div className="flex items-center justify-between"> | |
| <div className="space-y-1"> | |
| <div className="flex items-center gap-2"> | |
| <p className="font-medium"> | |
| {rateLimit.max - rateLimit.left} / {rateLimit.max} used | |
| </p> | |
| {isActive && <Badge variant="default">Active</Badge>} | |
| </div> | |
| <p className="text-xs text-muted-foreground"> | |
| {rateLimit.left} requests remaining | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| {rateLimit.carryOverTotal > 0 && ( | |
| <Badge variant="outline"> | |
| Carried over {rateLimit.carryOverTotal}x | |
| </Badge> | |
| )} | |
| {isActive && ( | |
| <EditRateLimit | |
| rateLimitId={rateLimit.id} | |
| currentLeft={rateLimit.left} | |
| max={rateLimit.max} | |
| userId={userId} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| {/* Progress bar */} | |
| <div className="space-y-1"> | |
| <div className="h-2 bg-muted rounded-full overflow-hidden"> | |
| <div | |
| className="h-full bg-primary transition-all" | |
| style={{ width: `${usagePercent}%` }} | |
| /> | |
| const isActive = new Date() >= new Date(rateLimit.startedAt) && | |
| new Date() <= new Date(rateLimit.endedAt); | |
| const usagePercent = | |
| rateLimit.max > 0 | |
| ? Math.min( | |
| 100, | |
| Math.max( | |
| 0, | |
| ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100, | |
| ), | |
| ) | |
| : 0; | |
| return ( | |
| <div | |
| key={rateLimit.id} | |
| className="p-4 border rounded-lg space-y-3" | |
| > | |
| <div className="flex items-center justify-between"> | |
| <div className="space-y-1"> | |
| <div className="flex items-center gap-2"> | |
| <p className="font-medium"> | |
| {rateLimit.max - rateLimit.left} / {rateLimit.max} used | |
| </p> | |
| {isActive && <Badge variant="default">Active</Badge>} | |
| </div> | |
| <p className="text-xs text-muted-foreground"> | |
| {rateLimit.left} requests remaining | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| {rateLimit.carryOverTotal > 0 && ( | |
| <Badge variant="outline"> | |
| Carried over {rateLimit.carryOverTotal}x | |
| </Badge> | |
| )} | |
| {isActive && ( | |
| <EditRateLimit | |
| rateLimitId={rateLimit.id} | |
| currentLeft={rateLimit.left} | |
| max={rateLimit.max} | |
| userId={userId} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| {/* Progress bar */} | |
| <div className="space-y-1"> | |
| <div className="h-2 bg-muted rounded-full overflow-hidden"> | |
| <div | |
| className="h-full bg-primary transition-all" | |
| style={{ width: `${usagePercent}%` }} | |
| /> |
🤖 Prompt for AI Agents
In apps/admin/src/components/users/user-detail.tsx around lines 243 to 287, the
usagePercent calculation can produce Infinity/NaN when rateLimit.max is 0 or
negative; change the calculation to first check rateLimit.max > 0, compute
usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100 only
when max > 0, otherwise set usagePercent = 0, then clamp usagePercent between 0
and 100 before using it in the inline width style so the progress bar always
receives a valid percentage.
apps/admin/src/server/api/trpc.ts
Outdated
| export const adminProcedure = t.procedure.use(timingMiddleware).use(({ ctx, next }) => { | ||
| if (!ctx.user) { | ||
| throw new TRPCError({ code: 'UNAUTHORIZED' }); | ||
| } | ||
|
|
||
| if (!ctx.user.email) { | ||
| throw new TRPCError({ | ||
| code: 'UNAUTHORIZED', | ||
| message: 'User must have an email address to access this resource', | ||
| }); | ||
| } | ||
|
|
||
| const adminSupabase = createAdminClient(); | ||
|
|
||
| return next({ | ||
| ctx: { | ||
| // infers the `session` as non-nullable | ||
| user: ctx.user as SetRequiredDeep<User, 'email'>, | ||
| db: ctx.db, | ||
| supabase: adminSupabase, // Override with admin client | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restrict admin procedures to actual admins
adminProcedure only verifies that a user is logged in with an email, yet it hands out a service-role Supabase client (bypasses all RLS). Today any authenticated user can invoke admin routers and operate with full DB privileges. Add an explicit authorization guard (e.g., check app_metadata.roles or a dedicated flag) before instantiating the admin client, and return FORBIDDEN when the caller isn’t an admin. Until that check exists, this endpoint is effectively public.
🤖 Prompt for AI Agents
In apps/admin/src/server/api/trpc.ts around lines 160 to 181, the adminProcedure
currently only ensures a logged-in user with an email and then creates a
service-role Supabase client; you must enforce an explicit admin authorization
check before instantiating the admin client. Inspect ctx.user.app_metadata.roles
(or a dedicated admin flag on the user) and if the user does not include the
required admin role, throw new TRPCError({ code: 'FORBIDDEN', message: 'Admin
access required' }); only after that check create the adminSupabase and return
next with the elevated supabase client and the narrowed user type. Ensure the
authorization check runs before any admin client creation to avoid leaking
service-role privileges.
apps/admin/src/trpc/query-client.ts
Outdated
| export const createQueryClient = () => | ||
| new QueryClient({ | ||
| defaultOptions: { | ||
| queries: { | ||
| // With SSR, we usually want to set some default staleTime | ||
| // above 0 to avoid refetching immediately on the client | ||
| staleTime: 30 * 1000, | ||
| }, | ||
| dehydrate: { | ||
| serializeData: SuperJSON.serialize, | ||
| shouldDehydrateQuery: (query) => | ||
| defaultShouldDehydrateQuery(query) || query.state.status === 'pending', | ||
| }, | ||
| hydrate: { | ||
| deserializeData: SuperJSON.deserialize, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move hydrate/dehydrate options to the correct level.
defaultOptions doesn’t include dehydrate/hydrate, so these settings are ignored. As written, we fall back to vanilla JSON serialization, losing SuperJSON round‑tripping (e.g., Dates). Configure them on the QueryClient instead.
export const createQueryClient = () =>
- new QueryClient({
- defaultOptions: {
- queries: {
- // With SSR, we usually want to set some default staleTime
- // above 0 to avoid refetching immediately on the client
- staleTime: 30 * 1000,
- },
- dehydrate: {
- serializeData: SuperJSON.serialize,
- shouldDehydrateQuery: (query) =>
- defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
- },
- hydrate: {
- deserializeData: SuperJSON.deserialize,
- },
- },
- });
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ // With SSR, we usually want to set some default staleTime
+ // above 0 to avoid refetching immediately on the client
+ staleTime: 30 * 1000,
+ },
+ },
+ dehydrateOptions: {
+ serializeData: SuperJSON.serialize,
+ shouldDehydrateQuery: (query) =>
+ defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
+ },
+ hydrateOptions: {
+ deserializeData: SuperJSON.deserialize,
+ },
+ });📝 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.
| export const createQueryClient = () => | |
| new QueryClient({ | |
| defaultOptions: { | |
| queries: { | |
| // With SSR, we usually want to set some default staleTime | |
| // above 0 to avoid refetching immediately on the client | |
| staleTime: 30 * 1000, | |
| }, | |
| dehydrate: { | |
| serializeData: SuperJSON.serialize, | |
| shouldDehydrateQuery: (query) => | |
| defaultShouldDehydrateQuery(query) || query.state.status === 'pending', | |
| }, | |
| hydrate: { | |
| deserializeData: SuperJSON.deserialize, | |
| }, | |
| }, | |
| export const createQueryClient = () => | |
| new QueryClient({ | |
| defaultOptions: { | |
| queries: { | |
| // With SSR, we usually want to set some default staleTime | |
| // above 0 to avoid refetching immediately on the client | |
| staleTime: 30 * 1000, | |
| }, | |
| }, | |
| dehydrateOptions: { | |
| serializeData: SuperJSON.serialize, | |
| shouldDehydrateQuery: (query) => | |
| defaultShouldDehydrateQuery(query) || query.state.status === 'pending', | |
| }, | |
| hydrateOptions: { | |
| deserializeData: SuperJSON.deserialize, | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In apps/admin/src/trpc/query-client.ts around lines 4 to 20, the
dehydrate/hydrate options are mistakenly placed inside defaultOptions (so
SuperJSON serialize/deserialize and the custom shouldDehydrateQuery are
ignored); move dehydrate and hydrate up to the top-level QueryClient config
(alongside defaultOptions), leaving defaultOptions.queries.staleTime as-is, and
keep shouldDehydrateQuery inside the dehydrate option so SuperJSON is actually
used for server->client round‑tripping.
| listProducts: adminProcedure | ||
| .query(async ({ ctx }) => { | ||
| // Fetch all products with their prices | ||
| const productsData = await ctx.db |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the listProducts query, products and prices are fetched in separate queries and then merged in-memory. For scalability, consider performing a JOIN or grouping operation in the database query to reduce post-processing in JavaScript.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (3)
apps/admin/src/components/layout/sidebar.tsx (2)
29-42: Addaria-currentfor improved accessibility.Active navigation links should include the
aria-current="page"attribute to help screen reader users understand the current page context.Apply this diff to add the attribute:
<Link key={item.name} href={item.href} + aria-current={isActive ? 'page' : undefined} className={cn( 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors', isActive ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted hover:text-foreground' )} >
24-44: Consider adding anaria-labelto the navigation landmark.Adding a descriptive label to the
<nav>element helps screen reader users identify the purpose of this navigation region.Apply this diff:
- <nav className="flex-1 space-y-1 px-3 py-4"> + <nav aria-label="Main navigation" className="flex-1 space-y-1 px-3 py-4">apps/admin/src/server/api/routers/subscriptions.ts (1)
19-33: Group prices by product to avoid O(n²) filtersEach product currently re-filters the entire
pricesDataarray, turning this into quadratic work as your catalog grows. Pre-grouping prices byproductIdbefore the map keeps the procedure linear and avoids unnecessary allocations—especially helpful once the admin dashboard is dealing with hundreds of SKUs.- const productsWithPrices = productsData.map(product => ({ - ...product, - prices: pricesData.filter(price => price.productId === product.id), - })); + const pricesByProduct = pricesData.reduce((acc, price) => { + const list = acc.get(price.productId); + if (list) { + list.push(price); + } else { + acc.set(price.productId, [price]); + } + return acc; + }, new Map<string, typeof pricesData[number][]>()); + + const productsWithPrices = productsData.map(product => ({ + ...product, + prices: pricesByProduct.get(product.id) ?? [], + }));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/admin/src/app/subscriptions/page.tsx(1 hunks)apps/admin/src/components/layout/sidebar.tsx(1 hunks)apps/admin/src/components/subscriptions/products-list.tsx(1 hunks)apps/admin/src/components/subscriptions/subscriptions-list.tsx(1 hunks)apps/admin/src/server/api/root.ts(1 hunks)apps/admin/src/server/api/routers/subscriptions.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/admin/src/server/api/root.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Do not use the any type unless necessary
Files:
apps/admin/src/app/subscriptions/page.tsxapps/admin/src/components/subscriptions/subscriptions-list.tsxapps/admin/src/components/subscriptions/products-list.tsxapps/admin/src/server/api/routers/subscriptions.tsapps/admin/src/components/layout/sidebar.tsx
{apps,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using the any type unless absolutely necessary
Files:
apps/admin/src/app/subscriptions/page.tsxapps/admin/src/components/subscriptions/subscriptions-list.tsxapps/admin/src/components/subscriptions/products-list.tsxapps/admin/src/server/api/routers/subscriptions.tsapps/admin/src/components/layout/sidebar.tsx
🧠 Learnings (4)
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from apps/web/client/src/server/api/root.ts
Applied to files:
apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from src/server/api/trpc.ts and validate inputs with Zod
Applied to files:
apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from src/server/api/root.ts
Applied to files:
apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod
Applied to files:
apps/admin/src/server/api/routers/subscriptions.ts
🧬 Code graph analysis (3)
apps/admin/src/app/subscriptions/page.tsx (2)
apps/admin/src/components/subscriptions/products-list.tsx (1)
ProductsList(8-91)apps/admin/src/components/subscriptions/subscriptions-list.tsx (1)
SubscriptionsList(20-165)
apps/admin/src/components/subscriptions/products-list.tsx (1)
packages/db/src/schema/subscription/product.ts (1)
products(6-13)
apps/admin/src/server/api/routers/subscriptions.ts (4)
packages/db/src/schema/subscription/product.ts (1)
products(6-13)packages/db/src/schema/subscription/price.ts (1)
prices(8-22)packages/db/src/schema/subscription/subscription.ts (1)
subscriptions(11-38)packages/db/src/schema/user/user.ts (1)
users(12-25)
🔇 Additional comments (1)
apps/admin/src/components/layout/sidebar.tsx (1)
8-12: LGTM!The navigation array is correctly defined at module scope, avoiding unnecessary recreation on each render.
| const isActive = pathname === item.href || | ||
| (item.href !== '/' && pathname.startsWith(item.href)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential false positive in active route matching.
The startsWith check could incorrectly mark routes as active if you add routes like /projects-archive or /users-admin, since '/projects-archive'.startsWith('/projects') returns true.
Apply this diff to fix the edge case:
- const isActive = pathname === item.href ||
- (item.href !== '/' && pathname.startsWith(item.href));
+ const isActive =
+ pathname === item.href ||
+ (item.href !== '/' && pathname.startsWith(item.href + '/'));This ensures that /projects matches /projects/123 but not /projects-archive.
📝 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.
| const isActive = pathname === item.href || | |
| (item.href !== '/' && pathname.startsWith(item.href)); | |
| const isActive = | |
| pathname === item.href || | |
| (item.href !== '/' && pathname.startsWith(item.href + '/')); |
🤖 Prompt for AI Agents
In apps/admin/src/components/layout/sidebar.tsx around lines 26-27, the
active-route logic using pathname.startsWith(item.href) can produce false
positives for routes like '/projects-archive'; change the check so that a route
is considered active only if pathname equals item.href or it starts with
item.href followed by a '/' (i.e., pathname.startsWith(item.href + '/') ),
keeping the existing special-case for '/' intact so '/projects' matches
'/projects/123' but not '/projects-archive'.
| <Button | ||
| variant="link" | ||
| className="h-auto p-0 text-xs font-mono mt-1" | ||
| onClick={() => window.open(`https://dashboard.stripe.com/products/${data.price.stripeProductId}`, '_blank')} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For security best practices, include 'noopener,noreferrer' in the window.open calls. For example: window.open(url, '_blank', 'noopener,noreferrer').
| onClick={() => window.open(`https://dashboard.stripe.com/products/${data.price.stripeProductId}`, '_blank')} | |
| onClick={() => window.open(`https://dashboard.stripe.com/products/${data.price.stripeProductId}`, '_blank', 'noopener,noreferrer')} |
| className="h-auto p-0 text-xs font-mono" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| window.open(`https://dashboard.stripe.com/products/${product.stripeProductId}`, '_blank'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add 'noopener,noreferrer' to window.open calls here for security. This applies to both the Stripe Product and Stripe Price links.
| window.open(`https://dashboard.stripe.com/products/${product.stripeProductId}`, '_blank'); | |
| window.open(`https://dashboard.stripe.com/products/${product.stripeProductId}`, '_blank', 'noopener,noreferrer'); |
| <Button | ||
| variant="link" | ||
| className="h-auto p-0 text-sm font-mono mt-1" | ||
| onClick={() => window.open(`https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, '_blank')} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding 'noopener,noreferrer' to the window.open call for the Stripe Customer ID link for improved security.
| onClick={() => window.open(`https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, '_blank')} | |
| onClick={() => window.open(`https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, '_blank', 'noopener,noreferrer')} |
| .limit(1); | ||
|
|
||
| if (!priceData[0]) { | ||
| throw new Error('Price not found'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For more consistent API error handling, consider using TRPCError (e.g. new TRPCError({ code: 'NOT_FOUND', message: 'Price not found' })) instead of throwing a generic Error.
| throw new Error('Price not found'); | |
| throw new TRPCError({ code: 'NOT_FOUND', message: 'Price not found' }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/admin/src/app/subscriptions/prices/[id]/page.tsx(1 hunks)apps/admin/src/components/subscriptions/price-detail.tsx(1 hunks)apps/admin/src/components/subscriptions/products-list.tsx(1 hunks)apps/admin/src/components/subscriptions/subscriptions-list.tsx(1 hunks)apps/admin/src/components/users/user-detail.tsx(1 hunks)apps/admin/src/server/api/routers/subscriptions.ts(1 hunks)apps/admin/src/server/api/trpc.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Do not use the any type unless necessary
Files:
apps/admin/src/app/subscriptions/prices/[id]/page.tsxapps/admin/src/components/users/user-detail.tsxapps/admin/src/server/api/trpc.tsapps/admin/src/components/subscriptions/price-detail.tsxapps/admin/src/components/subscriptions/products-list.tsxapps/admin/src/server/api/routers/subscriptions.tsapps/admin/src/components/subscriptions/subscriptions-list.tsx
{apps,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using the any type unless absolutely necessary
Files:
apps/admin/src/app/subscriptions/prices/[id]/page.tsxapps/admin/src/components/users/user-detail.tsxapps/admin/src/server/api/trpc.tsapps/admin/src/components/subscriptions/price-detail.tsxapps/admin/src/components/subscriptions/products-list.tsxapps/admin/src/server/api/routers/subscriptions.tsapps/admin/src/components/subscriptions/subscriptions-list.tsx
🧠 Learnings (6)
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from src/server/api/trpc.ts and validate inputs with Zod
Applied to files:
apps/admin/src/server/api/trpc.tsapps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod
Applied to files:
apps/admin/src/server/api/trpc.tsapps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from apps/web/client/src/server/api/root.ts
Applied to files:
apps/admin/src/server/api/trpc.tsapps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from src/server/api/root.ts
Applied to files:
apps/admin/src/server/api/trpc.tsapps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Place tRPC routers under src/server/api/routers/**
Applied to files:
apps/admin/src/server/api/trpc.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Place tRPC routers under apps/web/client/src/server/api/routers/**
Applied to files:
apps/admin/src/server/api/trpc.ts
🧬 Code graph analysis (5)
apps/admin/src/app/subscriptions/prices/[id]/page.tsx (2)
apps/admin/src/components/layout/breadcrumb.tsx (1)
Breadcrumb(15-40)apps/admin/src/components/subscriptions/price-detail.tsx (1)
PriceDetail(23-181)
apps/admin/src/components/users/user-detail.tsx (1)
apps/admin/src/components/users/edit-rate-limit.tsx (1)
EditRateLimit(26-122)
apps/admin/src/server/api/trpc.ts (3)
apps/admin/src/utils/supabase/client/index.ts (1)
createClient(4-10)apps/admin/src/utils/supabase/server.ts (1)
createClient(5-32)apps/admin/src/utils/supabase/admin.ts (1)
createAdminClient(9-20)
apps/admin/src/components/subscriptions/products-list.tsx (1)
packages/db/src/schema/subscription/product.ts (1)
products(6-13)
apps/admin/src/server/api/routers/subscriptions.ts (4)
packages/db/src/schema/subscription/product.ts (1)
products(6-13)packages/db/src/schema/subscription/price.ts (1)
prices(8-22)packages/db/src/schema/subscription/subscription.ts (1)
subscriptions(11-38)packages/db/src/schema/user/user.ts (1)
users(12-25)
| import { Breadcrumb } from '@/components/layout/breadcrumb'; | ||
| import { PriceDetail } from '@/components/subscriptions/price-detail'; | ||
|
|
||
| export default function PriceDetailPage({ params }: { params: { id: string } }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Await params in Next.js 15 page components.
Next.js 15 changed route params to be asynchronous. The current synchronous access will cause runtime errors. Make the component async and await the params prop.
Based on learnings
Apply this diff:
-export default function PriceDetailPage({ params }: { params: { id: string } }) {
+export default async function PriceDetailPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
return (
<div className="bg-background">
<div className="container mx-auto px-4 py-8 space-y-6">
<Breadcrumb
items={[
{ label: 'Subscriptions', href: '/subscriptions' },
- { label: 'Price Details', href: `/subscriptions/prices/${params.id}` },
+ { label: 'Price Details', href: `/subscriptions/prices/${id}` },
]}
/>
- <PriceDetail priceId={params.id} />
+ <PriceDetail priceId={id} />
</div>
</div>
);
}📝 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.
| export default function PriceDetailPage({ params }: { params: { id: string } }) { | |
| export default async function PriceDetailPage({ params }: { params: Promise<{ id: string }> }) { | |
| const { id } = await params; | |
| return ( | |
| <div className="bg-background"> | |
| <div className="container mx-auto px-4 py-8 space-y-6"> | |
| <Breadcrumb | |
| items={[ | |
| { label: 'Subscriptions', href: '/subscriptions' }, | |
| { label: 'Price Details', href: `/subscriptions/prices/${id}` }, | |
| ]} | |
| /> | |
| <PriceDetail priceId={id} /> | |
| </div> | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In apps/admin/src/app/subscriptions/prices/[id]/page.tsx around line 4, the page
component accesses params synchronously but Next.js 15 provides params
asynchronously; make the component async and await the params prop (e.g., change
the function to "export default async function PriceDetailPage(...)" and use
"const { id } = await params" or similar), then replace all uses of params.id
with the awaited id.
| if (!priceData[0]) { | ||
| throw new Error('Price not found'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use TRPCError for API error responses.
When a price is not found, throwing a generic Error results in a 500 Internal Server Error response. Use TRPCError with code 'NOT_FOUND' to return a proper 404 status and improve API semantics.
Apply this diff:
+import { TRPCError } from '@trpc/server'; if (!priceData[0]) {
- throw new Error('Price not found');
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Price not found' });
}📝 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.
| if (!priceData[0]) { | |
| throw new Error('Price not found'); | |
| } | |
| // Add at the top of apps/admin/src/server/api/routers/subscriptions.ts | |
| import { TRPCError } from '@trpc/server'; | |
| // …existing code… | |
| if (!priceData[0]) { | |
| throw new TRPCError({ code: 'NOT_FOUND', message: 'Price not found' }); | |
| } |
🤖 Prompt for AI Agents
In apps/admin/src/server/api/routers/subscriptions.ts around lines 77 to 79, the
code throws a generic Error when no price is found; replace this with a
TRPCError to return a proper 404. Import TRPCError from '@trpc/server' if not
already imported, then throw new TRPCError({ code: 'NOT_FOUND', message: 'Price
not found' }) instead of throw new Error('Price not found'); ensure the import
and throw follow existing project style and types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/admin/src/app/products/prices/[id]/page.tsx (1)
4-18: Await params in Next.js 15 page components.Next.js 15 changed route
paramsto be asynchronous. The current synchronous access will cause runtime errors. Make the component async and await the params prop.Based on learnings
Apply this diff:
-export default function PriceDetailPage({ params }: { params: { id: string } }) { +export default async function PriceDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; return ( <div className="bg-background"> <div className="container mx-auto px-4 py-8 space-y-6"> <Breadcrumb items={[ { label: 'Products', href: '/products' }, - { label: 'Price Details', href: `/products/prices/${params.id}` }, + { label: 'Price Details', href: `/products/prices/${id}` }, ]} /> - <PriceDetail priceId={params.id} /> + <PriceDetail priceId={id} /> </div> </div> ); }
🧹 Nitpick comments (6)
apps/admin/src/app/products/page.tsx (1)
3-19: LGTM!The page component is well-structured and correctly implements a Next.js App Router page. The composition with ProductsList is clean and follows good separation of concerns.
Optional enhancement: Add metadata export.
Consider adding metadata for better browser tab identification:
export const metadata = { title: 'Products & Pricing', description: 'Manage products and pricing tiers', };apps/admin/src/components/deployments/deployments-list.tsx (2)
189-241: Extract hardcoded CodeSandbox URLs to constants.The CodeSandbox URLs at lines 204 and 224 are hardcoded, which reduces flexibility if URL patterns change or different environments require different base URLs.
Consider extracting these to module-level constants:
+'use client'; + +const CODESANDBOX_EDITOR_URL = 'https://codesandbox.io/s'; +const CODESANDBOX_PREVIEW_URL_TEMPLATE = (id: string) => `https://${id}-3000.csb.app`; + import { api } from '@/trpc/react';Then use them in the handlers:
-window.open(`https://codesandbox.io/s/${deployment.sandboxId}`, '_blank'); +window.open(`${CODESANDBOX_EDITOR_URL}/${deployment.sandboxId}`, '_blank');-window.open(`https://${deployment.sandboxId}-3000.csb.app`, '_blank'); +window.open(CODESANDBOX_PREVIEW_URL_TEMPLATE(deployment.sandboxId), '_blank');
299-313: Consider removing hardcoded locale for date formatting.The date and time formatting uses hardcoded
'en-US'locale, which doesn't respect user locale preferences. For better internationalization, consider either using the browser's default locale (by passingundefined) or making the locale configurable.Apply this diff to use the browser's locale:
<p className="text-sm"> - {new Date(deployment.createdAt).toLocaleDateString('en-US', { + {new Date(deployment.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', })} </p> <p className="text-xs text-muted-foreground"> - {new Date(deployment.createdAt).toLocaleTimeString('en-US', { + {new Date(deployment.createdAt).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', })} </p>apps/admin/src/server/api/routers/domains.ts (1)
107-114: Consider using optional chaining.While the non-null assertion on line 113 is safe because line 110 checks for key existence, using optional chaining would be more idiomatic and eliminate the need for the assertion.
Apply this diff:
- projectsByDomain.get(pd.customDomainId)!.push(pd); + projectsByDomain.get(pd.customDomainId)?.push(pd);apps/admin/src/components/domains/domains-list.tsx (2)
176-185: Remove redundant non-null assertions.The type guard on line 176 (
domain.projects.length > 0 && domain.projects[0]) ensuresprojects[0]exists, making the non-null assertions on lines 179, 181, and 183 redundant.Apply this diff:
{domain.projects.length > 0 && domain.projects[0] ? ( <div className="cursor-pointer hover:underline" - onClick={() => router.push(`/users/${domain.projects[0]!.ownerId}`)} + onClick={() => router.push(`/users/${domain.projects[0].ownerId}`)} > - <p className="font-medium text-sm">{domain.projects[0]!.ownerName}</p> + <p className="font-medium text-sm">{domain.projects[0].ownerName}</p> <p className="text-xs text-muted-foreground"> - {domain.projects[0]!.ownerEmail} + {domain.projects[0].ownerEmail} </p> </div>
207-210: Use enum constant instead of string literal.The status comparison uses the string literal
'active'instead of theProjectCustomDomainStatusenum. Consider importing and using the enum constant for type safety and consistency with the backend.Add the import at the top of the file:
+'use client'; + +import { ProjectCustomDomainStatus } from '@onlook/db/src/schema/domain/custom/project-custom-domain'; import { api } from '@/trpc/react';Then update the comparison:
<Badge - variant={project.status === 'active' ? 'default' : 'secondary'} + variant={project.status === ProjectCustomDomainStatus.ACTIVE ? 'default' : 'secondary'} className="text-xs" >
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
apps/admin/src/app/domains/page.tsx(1 hunks)apps/admin/src/app/products/page.tsx(1 hunks)apps/admin/src/app/products/prices/[id]/page.tsx(1 hunks)apps/admin/src/app/subscriptions/page.tsx(1 hunks)apps/admin/src/components/deployments/deployments-list.tsx(1 hunks)apps/admin/src/components/domains/domains-list.tsx(1 hunks)apps/admin/src/components/layout/sidebar.tsx(1 hunks)apps/admin/src/components/subscriptions/products-list.tsx(1 hunks)apps/admin/src/server/api/root.ts(1 hunks)apps/admin/src/server/api/routers/domains.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/admin/src/components/layout/sidebar.tsx
- apps/admin/src/app/subscriptions/page.tsx
- apps/admin/src/server/api/root.ts
- apps/admin/src/components/subscriptions/products-list.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Do not use the any type unless necessary
Files:
apps/admin/src/server/api/routers/domains.tsapps/admin/src/app/products/prices/[id]/page.tsxapps/admin/src/app/domains/page.tsxapps/admin/src/app/products/page.tsxapps/admin/src/components/domains/domains-list.tsxapps/admin/src/components/deployments/deployments-list.tsx
{apps,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using the any type unless absolutely necessary
Files:
apps/admin/src/server/api/routers/domains.tsapps/admin/src/app/products/prices/[id]/page.tsxapps/admin/src/app/domains/page.tsxapps/admin/src/app/products/page.tsxapps/admin/src/components/domains/domains-list.tsxapps/admin/src/components/deployments/deployments-list.tsx
🧬 Code graph analysis (4)
apps/admin/src/server/api/routers/domains.ts (5)
packages/db/src/schema/domain/custom/domain.ts (1)
customDomains(6-12)packages/db/src/schema/domain/custom/project-custom-domain.ts (1)
projectCustomDomains(15-23)packages/db/src/schema/project/project.ts (1)
projects(13-32)packages/db/src/schema/user/user-project.ts (1)
userProjects(10-23)packages/db/src/schema/user/user.ts (1)
users(12-25)
apps/admin/src/app/products/prices/[id]/page.tsx (2)
apps/admin/src/components/layout/breadcrumb.tsx (1)
Breadcrumb(15-40)apps/admin/src/components/subscriptions/price-detail.tsx (1)
PriceDetail(23-181)
apps/admin/src/app/domains/page.tsx (1)
apps/admin/src/components/domains/domains-list.tsx (1)
DomainsList(24-287)
apps/admin/src/app/products/page.tsx (1)
apps/admin/src/components/subscriptions/products-list.tsx (1)
ProductsList(11-119)
🔇 Additional comments (13)
apps/admin/src/app/products/page.tsx (1)
1-1: LGTM!Clean import using the path alias.
apps/admin/src/components/deployments/deployments-list.tsx (7)
1-31: LGTM!The component setup follows Next.js 13+ client component patterns correctly, with appropriate state management for pagination, sorting, and search functionality.
33-39: LGTM!The tRPC query setup correctly passes pagination, sorting, and search parameters.
41-49: LGTM!The sorting logic correctly toggles order for the same column and resets to page 1, preventing out-of-bounds page numbers after sort changes.
51-79: LGTM!The helper functions provide sensible mappings with fallback defaults for badge variants and type labels.
81-90: LGTM!The error handling provides clear visual feedback with an appropriate destructive style.
320-346: LGTM!The pagination controls correctly use the API's
hasPrev/hasNextflags and only display when there are multiple pages.
25-351: Well-structured component with comprehensive state handling.The component effectively manages loading, error, empty, and data states with appropriate UI feedback. The search, sort, and pagination features are correctly implemented with proper state synchronization.
apps/admin/src/app/domains/page.tsx (1)
3-19: LGTM! Clean page structure.The page component properly delegates data fetching and rendering to the
DomainsListcomponent while maintaining a clean layout structure.apps/admin/src/server/api/routers/domains.ts (3)
123-125: Owner name fallback logic is well-structured.The cascading fallback pattern (displayName → first+last → email → empty) provides robust handling of missing data and ensures a displayable name is always available.
8-138: Overall implementation looks solid.The query structure properly handles pagination, sorting, and search functionality. The join logic correctly fetches owner information, and the response mapping is clear and type-safe.
23-30: No SQL injection risk—thesqltemplate literal parameterizes thesearchvariable Drizzle-ORM automatically binds${…}values, so the current implementation is safe.apps/admin/src/components/domains/domains-list.tsx (1)
24-287: Well-structured component with good UX.The component provides a comprehensive admin interface with proper:
- Loading and error states
- Search and sorting functionality
- Pagination controls
- Navigation to related entities
- Empty state handling
| const [open, setOpen] = useState(false); | ||
| const utils = api.useUtils(); | ||
|
|
||
| const removeMutation = api.users.removeSubscription.useMutation({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using a non-blocking notification (e.g. toast) instead of alert() for success and error messages for better UX.
| periodEnd.setMonth(periodEnd.getMonth() + 1); | ||
|
|
||
| // Use existing Stripe customer ID or generate a stub one | ||
| const stripeCustomerId = user[0].stripeCustomerId || `stub_stripe_customer_id_${Date.now()}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a stub Stripe customer ID when absent is pragmatic; however, ensure this fallback is acceptable for production and consider wrapping the subscription and rate limit inserts in a transaction to avoid partial updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
apps/admin/src/components/users/user-detail.tsx (2)
94-105: Harden external link opener.Opening Stripe in a new tab without
'noopener,noreferrer'leaves the admin window vulnerable to reverse tabnabbing. Pass the appropriate features string towindow.open.- onClick={() => window.open(`https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, '_blank')} + onClick={() => + window.open( + `https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, + '_blank', + 'noopener,noreferrer', + ) + }
266-311: Guard rate-limit progress whenmaxis zero.We still divide by
rateLimit.maxwithout checking for zero/negative values, so the progress bar can receiveInfinity/NaNwidths and disappear. Clamp the calculation around a non-zeromax.- const usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100; + const usagePercent = + rateLimit.max > 0 + ? Math.min( + 100, + Math.max(0, ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100), + ) + : 0;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/admin/src/components/users/add-subscription.tsx(1 hunks)apps/admin/src/components/users/remove-subscription.tsx(1 hunks)apps/admin/src/components/users/user-detail.tsx(1 hunks)apps/admin/src/server/api/routers/users.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Do not use the any type unless necessary
Files:
apps/admin/src/components/users/add-subscription.tsxapps/admin/src/components/users/user-detail.tsxapps/admin/src/server/api/routers/users.tsapps/admin/src/components/users/remove-subscription.tsx
{apps,packages}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Avoid using the any type unless absolutely necessary
Files:
apps/admin/src/components/users/add-subscription.tsxapps/admin/src/components/users/user-detail.tsxapps/admin/src/server/api/routers/users.tsapps/admin/src/components/users/remove-subscription.tsx
🧬 Code graph analysis (3)
apps/admin/src/components/users/add-subscription.tsx (1)
packages/db/src/schema/subscription/product.ts (1)
products(6-13)
apps/admin/src/components/users/user-detail.tsx (3)
apps/admin/src/components/users/add-subscription.tsx (1)
AddSubscription(29-174)apps/admin/src/components/users/remove-subscription.tsx (1)
RemoveSubscription(24-97)apps/admin/src/components/users/edit-rate-limit.tsx (1)
EditRateLimit(26-122)
apps/admin/src/server/api/routers/users.ts (7)
packages/db/src/schema/user/user.ts (1)
users(12-25)packages/db/src/schema/user/user-project.ts (1)
userProjects(10-23)packages/db/src/schema/project/project.ts (1)
projects(13-32)packages/db/src/schema/subscription/subscription.ts (1)
subscriptions(11-38)packages/db/src/schema/subscription/product.ts (1)
products(6-13)packages/db/src/schema/subscription/price.ts (1)
prices(8-22)packages/db/src/schema/subscription/rate-limits.ts (1)
rateLimits(6-43)
| // Fetch price to get monthly message limit | ||
| const price = await ctx.db | ||
| .select() | ||
| .from(prices) | ||
| .where(eq(prices.id, input.priceId)) | ||
| .limit(1); | ||
|
|
||
| if (price.length === 0 || !price[0]) { | ||
| throw new Error('Price not found'); | ||
| } | ||
|
|
||
| // Create subscription with admin/mock Stripe data | ||
| const now = new Date(); | ||
| const periodEnd = new Date(now); | ||
| periodEnd.setMonth(periodEnd.getMonth() + 1); | ||
|
|
||
| // Use existing Stripe customer ID or generate a stub one | ||
| const stripeCustomerId = user[0].stripeCustomerId || `stub_stripe_customer_id_${Date.now()}`; | ||
| const stripeSubscriptionItemId = `si_admin_${Date.now()}`; | ||
|
|
||
| const newSubscription = await ctx.db | ||
| .insert(subscriptions) | ||
| .values({ | ||
| userId: input.userId, | ||
| productId: input.productId, | ||
| priceId: input.priceId, | ||
| stripeCustomerId, | ||
| stripeSubscriptionId: `sub_admin_${Date.now()}`, | ||
| stripeSubscriptionItemId, | ||
| status: SubscriptionStatus.ACTIVE, | ||
| stripeCurrentPeriodStart: now, | ||
| stripeCurrentPeriodEnd: periodEnd, | ||
| }) | ||
| .returning(); | ||
|
|
||
| if (!newSubscription[0]) { | ||
| throw new Error('Failed to create subscription'); | ||
| } | ||
|
|
||
| // Create rate limit for the subscription | ||
| const carryOverKey = crypto.randomUUID(); | ||
| await ctx.db | ||
| .insert(rateLimits) | ||
| .values({ | ||
| userId: input.userId, | ||
| subscriptionId: newSubscription[0].id, | ||
| startedAt: now, | ||
| endedAt: periodEnd, | ||
| max: price[0].monthlyMessageLimit, | ||
| left: price[0].monthlyMessageLimit, | ||
| carryOverKey, | ||
| carryOverTotal: 0, | ||
| stripeSubscriptionItemId, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate price/product relationship before inserting.
Right now we only check that the priceId exists. Because we never assert that the chosen price belongs to the supplied productId, callers can create subscriptions whose productId and priceId point to unrelated rows. That breaks billing assumptions and conflicts with how we model prices per product. Add an explicit guard (and ideally read the product to verify existence) before persisting the subscription.
- if (price.length === 0 || !price[0]) {
- throw new Error('Price not found');
- }
+ if (price.length === 0 || !price[0]) {
+ throw new Error('Price not found');
+ }
+
+ if (price[0].productId !== input.productId) {
+ throw new Error('Selected price does not belong to the specified product');
+ }📝 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.
| // Fetch price to get monthly message limit | |
| const price = await ctx.db | |
| .select() | |
| .from(prices) | |
| .where(eq(prices.id, input.priceId)) | |
| .limit(1); | |
| if (price.length === 0 || !price[0]) { | |
| throw new Error('Price not found'); | |
| } | |
| // Create subscription with admin/mock Stripe data | |
| const now = new Date(); | |
| const periodEnd = new Date(now); | |
| periodEnd.setMonth(periodEnd.getMonth() + 1); | |
| // Use existing Stripe customer ID or generate a stub one | |
| const stripeCustomerId = user[0].stripeCustomerId || `stub_stripe_customer_id_${Date.now()}`; | |
| const stripeSubscriptionItemId = `si_admin_${Date.now()}`; | |
| const newSubscription = await ctx.db | |
| .insert(subscriptions) | |
| .values({ | |
| userId: input.userId, | |
| productId: input.productId, | |
| priceId: input.priceId, | |
| stripeCustomerId, | |
| stripeSubscriptionId: `sub_admin_${Date.now()}`, | |
| stripeSubscriptionItemId, | |
| status: SubscriptionStatus.ACTIVE, | |
| stripeCurrentPeriodStart: now, | |
| stripeCurrentPeriodEnd: periodEnd, | |
| }) | |
| .returning(); | |
| if (!newSubscription[0]) { | |
| throw new Error('Failed to create subscription'); | |
| } | |
| // Create rate limit for the subscription | |
| const carryOverKey = crypto.randomUUID(); | |
| await ctx.db | |
| .insert(rateLimits) | |
| .values({ | |
| userId: input.userId, | |
| subscriptionId: newSubscription[0].id, | |
| startedAt: now, | |
| endedAt: periodEnd, | |
| max: price[0].monthlyMessageLimit, | |
| left: price[0].monthlyMessageLimit, | |
| carryOverKey, | |
| carryOverTotal: 0, | |
| stripeSubscriptionItemId, | |
| }); | |
| // Fetch price to get monthly message limit | |
| const price = await ctx.db | |
| .select() | |
| .from(prices) | |
| .where(eq(prices.id, input.priceId)) | |
| .limit(1); | |
| if (price.length === 0 || !price[0]) { | |
| throw new Error('Price not found'); | |
| } | |
| // Ensure the price belongs to the specified product | |
| if (price[0].productId !== input.productId) { | |
| throw new Error('Selected price does not belong to the specified product'); | |
| } | |
| // Create subscription with admin/mock Stripe data | |
| const now = new Date(); | |
| const periodEnd = new Date(now); | |
| periodEnd.setMonth(periodEnd.getMonth() + 1); |
🤖 Prompt for AI Agents
In apps/admin/src/server/api/routers/users.ts around lines 303 to 356, the code
fetches a price by priceId but does not verify that the price belongs to the
provided productId (and does not check product existence), allowing
subscriptions to be created with mismatched product/price; fix by querying the
prices table for a row where id = input.priceId AND productId = input.productId
(or fetch the product separately and assert it exists), throw a clear error if
no matching row is returned, and only proceed to insert the subscription and
rate limit when the price/product relationship is validated.
| const newSubscription = await ctx.db | ||
| .insert(subscriptions) | ||
| .values({ | ||
| userId: input.userId, | ||
| productId: input.productId, | ||
| priceId: input.priceId, | ||
| stripeCustomerId, | ||
| stripeSubscriptionId: `sub_admin_${Date.now()}`, | ||
| stripeSubscriptionItemId, | ||
| status: SubscriptionStatus.ACTIVE, | ||
| stripeCurrentPeriodStart: now, | ||
| stripeCurrentPeriodEnd: periodEnd, | ||
| }) | ||
| .returning(); | ||
|
|
||
| if (!newSubscription[0]) { | ||
| throw new Error('Failed to create subscription'); | ||
| } | ||
|
|
||
| // Create rate limit for the subscription | ||
| const carryOverKey = crypto.randomUUID(); | ||
| await ctx.db | ||
| .insert(rateLimits) | ||
| .values({ | ||
| userId: input.userId, | ||
| subscriptionId: newSubscription[0].id, | ||
| startedAt: now, | ||
| endedAt: periodEnd, | ||
| max: price[0].monthlyMessageLimit, | ||
| left: price[0].monthlyMessageLimit, | ||
| carryOverKey, | ||
| carryOverTotal: 0, | ||
| stripeSubscriptionItemId, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap subscription + rate-limit writes in a transaction.
The subscription insert and the follow-up rate-limit insert need to succeed or fail together. If the second insert throws (constraint violations, transient DB failure, etc.), we are left with a subscription row without its corresponding rate limit. Use a DB transaction so both operations commit atomically.
- const newSubscription = await ctx.db
- .insert(subscriptions)
- .values({ ... })
- .returning();
- ...
- await ctx.db
- .insert(rateLimits)
- .values({ ... });
+ const newSubscription = await ctx.db.transaction(async (tx) => {
+ const [createdSubscription] = await tx
+ .insert(subscriptions)
+ .values({ ... })
+ .returning();
+
+ if (!createdSubscription) {
+ throw new Error('Failed to create subscription');
+ }
+
+ await tx.insert(rateLimits).values({ ...createdSubscription dependent values... });
+
+ return createdSubscription;
+ });Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/admin/src/server/api/routers/users.ts around lines 323 to 356, the
subscription insert and the subsequent rate-limit insert must be performed
inside a single DB transaction so they commit or roll back together; update the
code to open a transaction with ctx.db (e.g., ctx.db.transaction or the
project's transaction helper), perform the subscription insert and await its
result, then perform the rate-limit insert using the returned subscription id,
and finally commit the transaction; ensure any thrown error causes the
transaction to roll back and propagate, and adjust return/usage to use the
inserted subscription/rate-limit from the transaction result (avoid leaving
partial data on failure).
- Created private repository at onlook-dev/admin - Removed admin code from main repository - Added admin as git submodule at apps/admin - Supports open-core model with private admin dashboard 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Explain admin module is optional for self-hosting - Provide contact information for admin access - Clarify main app works without admin module 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Create comprehensive admin dashboard guide - Document features: user, subscription, rate limit management - Add deployment details and contact information - Link from self-hosting index page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Description
Related Issues
Type of Change
Testing
Screenshots (if applicable)
Additional Notes
Important
Introduces an optional admin dashboard, updates documentation, and refactors UI component imports to relative paths.
.gitmodulesandapps/admin.dev:adminscript inpackage.jsonto run the admin app locally.admin-dashboard.mdxfor admin dashboard features and access.index.mdxandmeta.jsonto include admin dashboard in self-hosting docs.command.tsx,form.tsx, andsidebar.tsxfor consistency.This description was created by
for 4e7a416. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit