Skip to content

feat: implement workspace analytics and usage tracking#249

Merged
taqh merged 14 commits into
mainfrom
workspace-analytics-and-usage-tracking
Nov 18, 2025
Merged

feat: implement workspace analytics and usage tracking#249
taqh merged 14 commits into
mainfrom
workspace-analytics-and-usage-tracking

Conversation

@taqh
Copy link
Copy Markdown
Member

@taqh taqh commented Nov 18, 2025

Description

Makes the dashboard sexier

Motivation and Context

well i wasn't showing users their usage before so this fixes that

How to Test

Screenshots (if applicable)

Video Demo (if applicable)

Types of Changes

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that alters existing functionality)
  • 🎨 UI/UX Improvements
  • ⚡ Performance Enhancement
  • 📖 Documentation (updates to README, docs, or comments)

Summary by CodeRabbit

  • New Features

    • Usage dashboard: API, webhook, and media metrics with charts and recent items.
    • Webhook dispatcher and delivery tracking; optional external analytics integration.
    • New usage tracking (API requests, media uploads, webhook deliveries) stored in DB.
  • Improvements

    • Compact layout option and refreshed dashboard cards (API, Webhook, Media).
    • Updated loading spinners and sidebar/home navigation visuals.
    • Responsive container tokens and layout utilities added.
  • Chores

    • Removed legacy analytics routes/hooks and consolidated webhook logic.

@vercel
Copy link
Copy Markdown

vercel Bot commented Nov 18, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
marble-app Ready Ready Preview Comment Nov 18, 2025 11:14pm
marble-web Ready Ready Preview Comment Nov 18, 2025 11:14pm

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Nov 18, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
🔵 In progress
View logs
marble-api caebea0 Nov 18 2025, 10:30 PM

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 18, 2025

Walkthrough

Refactors analytics from Redis to DB-backed UsageEvent logging with optional Polar ingestion, centralizes webhook delivery via a dispatcher, introduces a unified /api/metrics/usage endpoint and dashboard cards, adds usage schema/migrations, and applies UI/layout and spinner refactors across the CMS.

Changes

Cohort / File(s) Change Summary
Polar SDK & env
apps/api/.dev.vars.example, apps/api/package.json, apps/api/src/lib/polar.ts, apps/api/src/types/env.ts, apps/cms/src/lib/polar/client.ts
Add @polar-sh/sdk, create Polar client factories, add POLAR_ACCESS_TOKEN and ENVIRONMENT env entries and Env type updates.
Analytics middleware
apps/api/src/middleware/analytics.ts, packages/db/prisma/*migration.sql, packages/db/prisma/schema.prisma
Replace Redis tracking with DB usage_event writes, optional Polar ingestion, add UsageEvent model and migration, indexes, and schema updates.
Webhook dispatcher & utils
apps/cms/src/lib/webhooks/dispatcher.ts, apps/cms/src/lib/webhooks/utils.ts, apps/cms/src/lib/webhooks/webhook-client.ts, apps/cms/src/lib/webhooks/usage.ts
Add centralized dispatchWebhooks, DB-backed getWebhooks, trackWebhookUsage; refactor WebhookClient internals and event shapes; per-delivery error isolation and usage tracking.
Metrics API routes
apps/cms/src/app/api/metrics/usage/route.ts, (removed) apps/cms/src/app/api/metrics/api/route.tsx, apps/cms/src/app/api/metrics/publishing/route.tsx, apps/cms/src/app/api/metrics/workspace/route.tsx
Add unified GET /api/metrics/usage (api, webhooks, media aggregates); remove legacy Redis-based metrics routes.
API webhook callers
apps/cms/src/app/api/categories*.ts, apps/cms/src/app/api/posts*.ts, apps/cms/src/app/api/media/route.ts, apps/cms/src/app/api/tags*.ts, apps/cms/src/app/api/posts/import/route.ts, apps/cms/src/app/api/posts/route.ts, apps/cms/src/app/api/categories/route.ts
Replace per-webhook loops with dispatchWebhooks calls, use workspaceId scoping, fire-and-forget dispatch with error logging.
Media upload tracking
apps/cms/src/app/api/upload/complete/route.ts, apps/cms/src/utils/usage/media.ts
Wire trackMediaUpload (DB + optional Polar), add workspaceId reuse, dispatch media_uploaded webhooks.
Dashboard UI: removed
apps/cms/src/components/home/AnalyticsChart.tsx, apps/cms/src/components/home/ApiUsageStats.tsx, apps/cms/src/components/home/PublishingActivityGraph.tsx, apps/cms/src/components/home/QuickStats.tsx, apps/cms/src/hooks/use-analytics.ts
Remove legacy analytics components and hooks tied to Redis/API-specific endpoints and query keys.
Dashboard UI: added
apps/cms/src/components/home/api-usage-card.tsx, apps/cms/src/components/home/media-usage-card.tsx, apps/cms/src/components/home/webhook-usage-card.tsx, apps/cms/src/types/usage-dashboard.ts, apps/cms/src/lib/queries/keys.ts
New usage cards and UsageDashboardData type; add USAGE_DASHBOARD query key and adapt dashboard queries.
Dashboard page & layout
apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx, apps/cms/src/components/layout/wrapper.tsx, apps/cms/src/styles/globals.css, apps/cms/src/app/(main)/[workspace]/(dashboard)/*/page-client.tsx
Replace multi-component dashboard with single React-Query fetch, render new cards; add WorkspacePageWrapper size="compact" prop and responsive workspace-container CSS.
Loading spinner & AsyncButton
apps/cms/src/components/ui/loading-spinner.tsx, apps/cms/src/components/ui/async-button.tsx, apps/cms/src/components/auth/*-form.tsx
Introduce LoadingSpinner and ButtonLoadingSpinner, refactor AsyncButton API to use ButtonLoadingSpinner and new isLoading/loadingText props; update auth forms.
Misc UI & styling tweaks
many files (editor imports, image props, nav, sidebar, media controls, posts grid, webhooks create, billing modal, etc.)
Import path adjustments, image prop changes, UI class/tailwind tweaks, Home icon/text rename, enable media_uploaded webhook event, various minor style/prop edits.
Rate limiter
apps/api/src/middleware/ratelimit.ts
Simplify exported function form; conditional sliding-window limits: authenticated/workspace (200/10s) vs unauthenticated (10/10s).
Prisma schema changes
packages/db/prisma/schema.prisma, migrations
Add UsageEvent model/enum, add usageEvents relation on Organization, add Media.alt, extend WebhookEvent enum.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Middleware as Analytics Middleware
    participant ExecCtx as Execution Context
    participant DB as Database
    participant Polar as Polar Service

    Client->>Middleware: HTTP request (with workspaceId)
    Middleware->>Middleware: extract endpoint, workspaceId, method, status
    Middleware->>Middleware: await next()
    Middleware-->>Client: respond
    Middleware->>ExecCtx: waitUntil(async task)
    activate ExecCtx
    ExecCtx->>DB: insert usage_event(type: api_request, workspaceId, endpoint)
    ExecCtx->>DB: load organization, owner
    alt POLAR configured
        ExecCtx->>Polar: ingest api_request (externalCustomerId, endpoint, method, status)
        Polar-->>ExecCtx: ack
    end
    ExecCtx->>ExecCtx: try/catch errors -> console.error
    deactivate ExecCtx
Loading
sequenceDiagram
    participant API
    participant Dispatcher as dispatchWebhooks
    participant DB as Database
    participant ClientWebhook as WebhookClient
    participant Usage as trackWebhookUsage
    participant Polar as Polar Service

    API->>Dispatcher: dispatchWebhooks(workspaceId, event, payload)
    Dispatcher->>DB: getWebhooks(workspaceId, event)
    DB-->>Dispatcher: [webhook1, webhook2, ...]
    loop per webhook
        Dispatcher->>ClientWebhook: new WebhookClient(config)
        ClientWebhook->>ClientWebhook: POST payload to endpoint
        ClientWebhook-->>Dispatcher: success/failure
        Dispatcher->>Usage: trackWebhookUsage(workspaceId, endpoint)
        Usage->>DB: insert usage_event(type: webhook_delivery, workspaceId, endpoint)
        alt POLAR configured
            Usage->>Polar: ingest webhook_delivery (externalCustomerId, endpoint)
        end
    end
    Dispatcher-->>API: return (fire-and-forget / Promise.allSettled)
Loading
sequenceDiagram
    participant UI as Dashboard Page
    participant Query as React Query
    participant API as /api/metrics/usage
    participant DB as Database

    UI->>Query: useQuery(USAGE_DASHBOARD(workspaceId))
    Query->>API: GET /api/metrics/usage
    API->>DB: aggregate API, webhook, media metrics (30-day windows)
    DB-->>API: aggregated UsageDashboardData
    API-->>Query: UsageDashboardData
    Query->>UI: render ApiUsageCard, WebhookUsageCard, MediaUsageCard
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • apps/api/src/middleware/analytics.ts — fire-and-forget semantics, error isolation, and Polar ingestion conditions.
  • apps/cms/src/lib/webhooks/dispatcher.ts & getWebhooks — concurrency, per-delivery try/catch, and secrets/format handling.
  • apps/cms/src/app/api/metrics/usage/route.ts — correctness of aggregation windows and performance of parallel queries.
  • Prisma migrations and schema changes — indexes, FK cascades, and deployment ordering.
  • AsyncButton API changes — verify consumer updates where isLoading/loadingText are now used.

Possibly related PRs

Suggested reviewers

  • mezotv
  • prateekbisht23

"I nibble logs and trace each hook,
I hop through metrics — take a look!
Redis sleeps, events now play,
Polar sings and webhooks relay,
Compact cards bloom across the pane,
A rabbit cheers — data's gained!" 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete. While it indicates the feature type (UI/UX improvements and new feature), it lacks detailed explanation of changes, lacks testing instructions, and uses informal language ('sexier'). Critical sections are empty or minimal. Provide a detailed description of what was added/changed, complete 'How to Test' with step-by-step instructions, and add screenshots or demo videos showing the new usage dashboard functionality.
Docstring Coverage ⚠️ Warning Docstring coverage is 7.41% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature: implementing workspace analytics and usage tracking. It accurately captures the primary change across all modified files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch workspace-analytics-and-usage-tracking

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

❤️ Share

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

@vercel vercel Bot temporarily deployed to Preview – marble-app November 18, 2025 22:31 Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (2)
apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx (1)

178-178: Hardcoded API usage contradicts new usage tracking.

This PR introduces apps/cms/src/app/api/metrics/usage/route.ts with real API request metrics, but the billing page still displays a hardcoded 0. Consider integrating the new usage endpoint to show actual API request counts.

Would you like me to generate the integration code to fetch and display real usage data?

apps/cms/src/components/ui/async-button.tsx (1)

7-31: Ref handling on AsyncButton is broken; use forwardRef instead

Because AsyncButton is a plain function component, React will not pass ref through props, so ref here will always be undefined and consumers can’t actually get a ref to the underlying button. You should convert this to a forwardRef and drop ref from the props surface.

-import type { ComponentProps, ReactNode, RefObject } from "react";
+import type { ComponentProps, ReactNode } from "react";
+import { forwardRef } from "react";
@@
-interface AsyncButtonProps extends ComponentProps<typeof Button> {
+interface AsyncButtonProps extends ComponentProps<typeof Button> {
@@
-  /**
-   * Optional ref for the underlying button element
-   */
-  ref?: RefObject<HTMLButtonElement | null>;
 }
@@
-const AsyncButton = ({
-  children,
-  isLoading = false,
-  loadingText,
-  keepTextWhileLoading = false,
-  disabled,
-  variant = "default",
-  className,
-  ref,
-  ...props
-}: AsyncButtonProps) => {
-  const renderLoadingContent = () => {
+const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>(
+  (
+    {
+      children,
+      isLoading = false,
+      loadingText,
+      keepTextWhileLoading = false,
+      disabled,
+      variant = "default",
+      className,
+      ...props
+    },
+    ref
+  ) => {
+    const renderLoadingContent = () => {
@@
-    // Default: spinner only
-    return <ButtonLoadingSpinner variant={variant} />;
-  };
-
-  return (
-    <Button
-      className={className}
-      disabled={disabled || isLoading}
-      ref={ref}
-      variant={variant}
-      {...props}
-    >
-      {isLoading ? renderLoadingContent() : children}
-    </Button>
-  );
-};
+    // Default: spinner only
+    return <ButtonLoadingSpinner variant={variant} />;
+  };
+
+    return (
+      <Button
+        className={className}
+        disabled={disabled || isLoading}
+        ref={ref}
+        variant={variant}
+        {...props}
+      >
+        {isLoading ? renderLoadingContent() : children}
+      </Button>
+    );
+  }
+);

Also applies to: 33-78

🧹 Nitpick comments (16)
apps/api/.dev.vars.example (1)

4-5: Consider adding comments for new environment variables.

While the additions are correct, adding brief comments would help developers understand the purpose and expected values (e.g., "production" or "sandbox" for ENVIRONMENT, and that POLAR_ACCESS_TOKEN is optional).

Apply this diff to add helpful comments:

 REDIS_URL=
 REDIS_TOKEN=
+# Optional: Access token for Polar integration (analytics/usage tracking)
 POLAR_ACCESS_TOKEN=
+# Optional: Environment for Polar (e.g., "production" or leave empty for sandbox)
 ENVIRONMENT=
apps/cms/src/components/media/media-card.tsx (1)

86-94: Conflicting Image sizing approach.

The Image component has absolute inset-0 size-full classes (line 89) combined with explicit height={160} width={250} props. This creates a conflict:

  • The classes suggest the image should fill its container
  • The explicit dimensions override the responsive behavior
  • The parent has aspect-video which won't work as intended with fixed dimensions

Apply this diff to use the fill prop instead for proper responsive behavior:

           <Image
             alt={media.name}
             className="absolute inset-0 size-full object-cover"
-            height={160}
+            fill
             src={media.url}
             unoptimized
-            width={250}
           />
apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx (1)

47-47: Remove debug statement.

This console.log appears to be a debugging leftover.

-  console.log("invitations", activeWorkspace.invitations);
apps/cms/src/components/nav/nav-main.tsx (1)

24-24: Remove unused LayoutIcon import.

LayoutIcon is imported but no longer used after switching to HouseIcon for the Home navigation entry.

Apply this diff to remove the unused import:

 import {
   FadersIcon,
   HouseIcon,
   ImagesIcon,
-  LayoutIcon,
   NoteIcon,
   PackageIcon,
   TagIcon,
   UsersThreeIcon,
 } from "@phosphor-icons/react";
apps/cms/src/components/home/api-usage-card.tsx (1)

30-36: Minor cleanup: redundant mapping and identity formatter

chartData is just copying label, value, and date from rawChartData, and formatXAxisLabel is an identity function. You could drop the map and inline tickFormatter={value => value} (or omit it if default is fine) to simplify a bit, unless you expect to transform these later.

Also applies to: 44-44

packages/db/prisma/migrations/20251114225009_add_usage_event_table/migration.sql (1)

16-20: Consider dropping the second overlapping index

usage_event_workspaceId_type_createdAt_idx can already support queries filtered by just workspaceId (and ordered by createdAt), so usage_event_workspaceId_createdAt_idx may be redundant and add write/storage overhead unless you have a specific query pattern that benefits from it. Worth double‑checking your query plans before keeping both.

apps/cms/src/lib/webhooks/utils.ts (1)

1-1: Centralized getWebhooks helper looks solid; types could be tightened later.

Query filters (enabled, workspaceId, events: { has: event }) and the workspaceId guard align well with dispatcher usage. If you want a bit more safety in the future, consider replacing DatabaseFields = Record<string, any> with a narrower type (or a generic) so where/select are checked at compile time, but this is non-blocking.

Also applies to: 3-3, 75-96

apps/cms/src/lib/webhooks/usage.ts (1)

1-71: trackWebhookUsage flow looks correct; you can skip the org lookup when Polar is disabled.

DB write + Polar ingestion and error handling all look good. One small optimization: db.organization.findFirst runs even when createPolarClient() returns null, so you could move the customerId resolution block inside if (polarClient) to avoid an extra query when Polar isn’t configured.

-  let customerId = workspaceId;
-
-  try {
-    const organization = await db.organization.findFirst({
-      where: { id: workspaceId },
-      select: { members: { where: { role: "owner" }, select: { userId: true } } },
-    });
-    if (organization?.members[0]?.userId) {
-      customerId = organization.members[0].userId;
-    }
-  } catch (error) {
-    console.error("[WebhookUsage] Failed to get customer ID:", error);
-  }
-
-  if (polarClient) {
+  if (polarClient) {
+    let customerId = workspaceId;
+    try {
+      const organization = await db.organization.findFirst({
+        where: { id: workspaceId },
+        select: {
+          members: { where: { role: "owner" }, select: { userId: true } },
+        },
+      });
+      if (organization?.members[0]?.userId) {
+        customerId = organization.members[0].userId;
+      }
+    } catch (error) {
+      console.error("[WebhookUsage] Failed to get customer ID:", error);
+    }
     try {
       await polarClient.events.ingest({

</blockquote></details>
<details>
<summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary><blockquote>

`1-66`: **Spinner components look good; consider extracting the SVG to avoid duplication.**

Both `LoadingSpinner` and `ButtonLoadingSpinner` are correct and nicely typed. If this grows, you could pull the SVG into a shared `SpinnerIcon` and have each wrapper just supply the appropriate `className` to reduce duplication, but it’s fine as-is.

</blockquote></details>
<details>
<summary>apps/cms/src/components/home/webhook-usage-card.tsx (1)</summary><blockquote>

`1-139`: **WebhookUsageCard implementation is sound; only micro-nits.**

Loading/empty/data states, workspace totals, and date range logic all look good. The `chartData` transform is effectively an identity and `?? []` after `.map` is redundant since `rawChartData` already defaults to `[]`, so you could simplify that later if you touch this file, but there’s no functional issue.

</blockquote></details>
<details>
<summary>apps/api/src/middleware/analytics.ts (2)</summary><blockquote>

`12-31`: **Double‑check analytics gating, endpoint derivation, and runtime assumptions**

A few things worth confirming here:
- Line 20: `status >= 400` means failed requests are not logged at all; if you want to see error traffic in usage analytics, you may want to record these too.
- Lines 24‑26: `pathParts.slice(2)` will keep workspaceId in the `endpoint` for paths like `/api/workspaces/:workspaceId/...`; confirm that including the workspaceId in the stored endpoint is what you want.
- Line 98: `c.executionCtx?.waitUntil(task())` will no‑op on runtimes that don’t provide `executionCtx` (e.g. non‑Workers), so analytics won’t run there; consider a fallback `await task()` in those environments if you need coverage.
- Lines 30‑31: If `createClient(DATABASE_URL)` is an expensive construction, you may want to reuse a cached client instead of creating it on every request inside the task.




Also applies to: 20-27, 98-99

---

`62-92`: **Tighten Polar error handling and logging noise**

- Lines 80‑86: The `errorDetails` object is built but never used, so the Polar error is effectively swallowed without any signal; either log/selectively log it or remove the dead code.
- Lines 88‑91: Logging “Skipping Polar: POLAR_ACCESS_TOKEN not configured” on every request can get noisy in environments without Polar; consider guarding this behind a debug flag or logging it once at startup instead.

</blockquote></details>
<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (2)</summary><blockquote>

`6-24`: **Keep event typings in sync with validation and client event types**

You’re effectively duplicating the event → payload map here; to avoid drift with `WebhookValidationEvent` / the `WebhookEvent` used in `WebhookClient`, consider deriving `WebhookEventMap` from a single shared type (or zod schema) instead of hand-maintaining this object shape.

---

`33-90`: **Clarify async semantics of `dispatchWebhooks` and background work**

`dispatchWebhooks` is `async` and awaits `getWebhooks`, but the actual deliveries and `trackWebhookUsage` calls run in a fire‑and‑forget `Promise.allSettled` that is not awaited, so `await dispatchWebhooks(...)` does *not* wait for sends or usage tracking to finish. If this is intentional, consider either returning `Promise.allSettled(deliveries)` or making the function sync and documenting it as best‑effort background dispatch to avoid surprising callers; also worth noting that `trackWebhookUsage` runs once per payload per webhook, which could be heavy at high volume.

</blockquote></details>
<details>
<summary>apps/cms/src/styles/globals.css (1)</summary><blockquote>

`65-90`: **Avoid drift between tokens, Tailwind defaults, and media queries**

Defining `--breakpoint-*` here duplicates Tailwind v4’s defaults and hard‑codes the same rem values again in the `@media (width >= …)` rules; if these ever change, your tokens and media queries can get out of sync. Consider either relying on Tailwind’s built‑in `--breakpoint-*` vars, or basing the media queries and workspace container max‑widths directly on your custom vars (e.g. `var(--breakpoint-sm)`, `var(--workspace-container-sm)`) so there’s a single source of truth.



Also applies to: 295-342

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx (1)</summary><blockquote>

`14-29`: **Confirm workspace scoping and disabled query behavior**

Using `enabled: Boolean(workspaceId)` with a fallback `["usage-dashboard", "disabled"]` key is solid, but note that when `workspaceId` is ever `null`, `isPending` will stay true and you render a loader indefinitely. If a missing workspace is possible, you might want an explicit guard (e.g. render nothing or an error) instead. Also, ensure `/api/metrics/usage` is truly workspace‑scoped server‑side (via auth/context) since you’re not passing `workspaceId` in the request.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4e96e56cf8f896b7deb318a4b8cc14442d111e5d and 96a2d984fbf7620e5484b6a996315d943f6256ae.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (71)</summary>

* `apps/api/.dev.vars.example` (1 hunks)
* `apps/api/package.json` (1 hunks)
* `apps/api/src/lib/polar.ts` (1 hunks)
* `apps/api/src/middleware/analytics.ts` (1 hunks)
* `apps/api/src/middleware/ratelimit.ts` (1 hunks)
* `apps/api/src/types/env.ts` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/keys/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/layout.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx` (3 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/editor-preferences/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/schemas/page.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx` (1 hunks)
* `apps/cms/src/app/(main)/[workspace]/(dashboard)/webhooks/page-client.tsx` (2 hunks)
* `apps/cms/src/app/api/categories/[id]/route.ts` (4 hunks)
* `apps/cms/src/app/api/categories/route.ts` (2 hunks)
* `apps/cms/src/app/api/media/route.ts` (6 hunks)
* `apps/cms/src/app/api/metrics/api/route.tsx` (0 hunks)
* `apps/cms/src/app/api/metrics/publishing/route.tsx` (0 hunks)
* `apps/cms/src/app/api/metrics/usage/route.ts` (1 hunks)
* `apps/cms/src/app/api/metrics/workspace/route.tsx` (0 hunks)
* `apps/cms/src/app/api/posts/[id]/route.ts` (4 hunks)
* `apps/cms/src/app/api/posts/import/route.ts` (2 hunks)
* `apps/cms/src/app/api/posts/route.ts` (2 hunks)
* `apps/cms/src/app/api/tags/[id]/route.ts` (4 hunks)
* `apps/cms/src/app/api/tags/route.ts` (2 hunks)
* `apps/cms/src/app/api/upload/complete/route.ts` (2 hunks)
* `apps/cms/src/components/auth/login-form.tsx` (2 hunks)
* `apps/cms/src/components/auth/register-form.tsx` (2 hunks)
* `apps/cms/src/components/billing/upgrade-modal.tsx` (0 hunks)
* `apps/cms/src/components/editor/editor-page.tsx` (1 hunks)
* `apps/cms/src/components/editor/image-upload-modal.tsx` (1 hunks)
* `apps/cms/src/components/editor/tabs/analysis-tab.tsx` (1 hunks)
* `apps/cms/src/components/editor/tabs/metadata-tab.tsx` (1 hunks)
* `apps/cms/src/components/home/AnalyticsChart.tsx` (0 hunks)
* `apps/cms/src/components/home/ApiUsageStats.tsx` (0 hunks)
* `apps/cms/src/components/home/PublishingActivityGraph.tsx` (0 hunks)
* `apps/cms/src/components/home/QuickStats.tsx` (0 hunks)
* `apps/cms/src/components/home/api-usage-card.tsx` (1 hunks)
* `apps/cms/src/components/home/media-usage-card.tsx` (1 hunks)
* `apps/cms/src/components/home/webhook-usage-card.tsx` (1 hunks)
* `apps/cms/src/components/layout/wrapper.tsx` (1 hunks)
* `apps/cms/src/components/media/media-card.tsx` (1 hunks)
* `apps/cms/src/components/media/media-controls.tsx` (1 hunks)
* `apps/cms/src/components/nav/app-sidebar.tsx` (1 hunks)
* `apps/cms/src/components/nav/nav-main.tsx` (3 hunks)
* `apps/cms/src/components/nav/page-header.tsx` (1 hunks)
* `apps/cms/src/components/nav/workspace-switcher.tsx` (1 hunks)
* `apps/cms/src/components/posts/data-grid.tsx` (2 hunks)
* `apps/cms/src/components/ui/async-button.tsx` (4 hunks)
* `apps/cms/src/components/ui/loading-spinner.tsx` (1 hunks)
* `apps/cms/src/components/webhooks/create-webhook.tsx` (1 hunks)
* `apps/cms/src/hooks/use-analytics.ts` (0 hunks)
* `apps/cms/src/lib/polar/client.ts` (1 hunks)
* `apps/cms/src/lib/queries/keys.ts` (1 hunks)
* `apps/cms/src/lib/validations/webhook.ts` (2 hunks)
* `apps/cms/src/lib/webhooks/dispatcher.ts` (1 hunks)
* `apps/cms/src/lib/webhooks/usage.ts` (1 hunks)
* `apps/cms/src/lib/webhooks/utils.ts` (2 hunks)
* `apps/cms/src/lib/webhooks/webhook-client.ts` (3 hunks)
* `apps/cms/src/styles/globals.css` (2 hunks)
* `apps/cms/src/types/usage-dashboard.ts` (1 hunks)
* `packages/db/prisma/migrations/20251114225009_add_usage_event_table/migration.sql` (1 hunks)
* `packages/db/prisma/migrations/20251116173412_new_media_enum_and_alt_text_column/migration.sql` (1 hunks)
* `packages/db/prisma/schema.prisma` (5 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (9)</summary>

* apps/cms/src/components/billing/upgrade-modal.tsx
* apps/cms/src/app/api/metrics/workspace/route.tsx
* apps/cms/src/components/home/QuickStats.tsx
* apps/cms/src/app/api/metrics/publishing/route.tsx
* apps/cms/src/components/home/PublishingActivityGraph.tsx
* apps/cms/src/components/home/ApiUsageStats.tsx
* apps/cms/src/components/home/AnalyticsChart.tsx
* apps/cms/src/app/api/metrics/api/route.tsx
* apps/cms/src/hooks/use-analytics.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (6)</summary>

<details>
<summary>📓 Common learnings</summary>

Learnt from: taqh
Repo: usemarble/marble PR: 137
File: packages/db/prisma/migrations/20250826194942_make_category_required/migration.sql:11-11
Timestamp: 2025-08-26T20:20:46.367Z
Learning: User "taqh" prefers concise code review comments rather than verbose explanations. Keep reviews short and to the point.


</details>
<details>
<summary>📚 Learning: 2025-10-26T20:32:09.573Z</summary>

Learnt from: dpaulos6
Repo: usemarble/marble PR: 233
File: apps/cms/src/app/(main)/[workspace]/set-workspace-cookie.tsx:1-16
Timestamp: 2025-10-26T20:32:09.573Z
Learning: In Next.js, Server Actions defined in modules with a module-level "use server" directive can be called from client components, including within useEffect hooks. This is a valid and documented pattern. The server function will be executed on the server even when invoked from client-side code.


**Applied to files:**
- `apps/cms/src/app/api/categories/route.ts`
- `apps/cms/src/app/api/tags/route.ts`

</details>
<details>
<summary>📚 Learning: 2025-09-28T22:18:20.726Z</summary>

Learnt from: mezotv
Repo: usemarble/marble PR: 198
File: apps/cms/src/app/(main)/[workspace]/(workspace)/keys/page-client.tsx:23-32
Timestamp: 2025-09-28T22:18:20.726Z
Learning: For Radix UI tooltips with disabled buttons: using asChild with disabled buttons breaks tooltip functionality because disabled elements don't receive the pointer events needed to trigger tooltips. Solutions include: 1) wrapper span with asChild and pointer-events: none on the disabled button, or 2) using aria-disabled instead of disabled to preserve interaction while preventing clicks.


**Applied to files:**
- `apps/cms/src/components/nav/workspace-switcher.tsx`

</details>
<details>
<summary>📚 Learning: 2025-08-20T06:18:53.734Z</summary>

Learnt from: brandonmcconnell
Repo: usemarble/marble PR: 113
File: apps/cms/src/components/shared/container.tsx:0-0
Timestamp: 2025-08-20T06:18:53.734Z
Learning: Tailwind CSS v4 provides default CSS variables for breakpoints including --breakpoint-2xl (1536px), --breakpoint-xl, --breakpoint-lg, --breakpoint-md, and --breakpoint-sm. These are available without manual definition and can be used with arbitrary value syntax like max-w-(--breakpoint-2xl).


**Applied to files:**
- `apps/cms/src/styles/globals.css`

</details>
<details>
<summary>📚 Learning: 2025-08-20T06:18:53.734Z</summary>

Learnt from: brandonmcconnell
Repo: usemarble/marble PR: 113
File: apps/cms/src/components/shared/container.tsx:0-0
Timestamp: 2025-08-20T06:18:53.734Z
Learning: Tailwind CSS v4 provides default CSS variables for breakpoints including --breakpoint-2xl (96rem/1536px), --breakpoint-xl, --breakpoint-lg, --breakpoint-md, and --breakpoint-sm. These are available without manual definition and can be used with arbitrary value syntax like max-w-(--breakpoint-2xl).


**Applied to files:**
- `apps/cms/src/styles/globals.css`

</details>
<details>
<summary>📚 Learning: 2025-08-20T06:22:02.309Z</summary>

Learnt from: brandonmcconnell
Repo: usemarble/marble PR: 113
File: apps/web/src/pages/privacy/index.astro:0-0
Timestamp: 2025-08-20T06:22:02.309Z
Learning: --breakpoint-sm is a default CSS variable provided by Tailwind CSS v4, available without manual definition and can be used with arbitrary value syntax like max-w-(--breakpoint-sm).


**Applied to files:**
- `apps/cms/src/styles/globals.css`

</details>

</details><details>
<summary>🧬 Code graph analysis (36)</summary>

<details>
<summary>apps/cms/src/app/api/categories/route.ts (1)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary>

* `dispatchWebhooks` (33-90)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/components/button.tsx (1)</summary>

* `buttonVariants` (58-58)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/posts/data-grid.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/components/card.tsx (1)</summary>

* `CardContent` (91-91)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/posts/import/route.ts (1)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary>

* `dispatchWebhooks` (33-90)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/auth/register-form.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary>

* `ButtonLoadingSpinner` (31-66)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/categories/[id]/route.ts (1)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary>

* `dispatchWebhooks` (33-90)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/posts/route.ts (1)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary>

* `dispatchWebhooks` (33-90)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/lib/webhooks/usage.ts (1)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/polar/client.ts (1)</summary>

* `createPolarClient` (5-18)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/posts/[id]/route.ts (1)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary>

* `dispatchWebhooks` (33-90)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/upload/complete/route.ts (2)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/polar/client.ts (1)</summary>

* `createPolarClient` (5-18)

</details>
<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary>

* `dispatchWebhooks` (33-90)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/webhooks/create-webhook.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/components/label.tsx (1)</summary>

* `Label` (24-24)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/auth/login-form.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary>

* `ButtonLoadingSpinner` (31-66)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/tags/route.ts (1)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/webhooks/utils.ts (1)</summary>

* `getWebhooks` (78-96)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/home/api-usage-card.tsx (3)</summary><blockquote>

<details>
<summary>apps/cms/src/types/usage-dashboard.ts (1)</summary>

* `UsageDashboardData` (1-40)

</details>
<details>
<summary>packages/ui/src/components/chart.tsx (4)</summary>

* `ChartConfig` (11-19)
* `ChartContainer` (347-347)
* `ChartTooltip` (348-348)
* `ChartTooltipContent` (349-349)

</details>
<details>
<summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary>

* `LoadingSpinner` (8-29)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/metrics/usage/route.ts (2)</summary><blockquote>

<details>
<summary>apps/cms/src/app/api/media/route.ts (1)</summary>

* `GET` (12-111)

</details>
<details>
<summary>apps/cms/src/lib/auth/session.ts (1)</summary>

* `getServerSession` (4-14)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/nav/nav-main.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/components/sidebar.tsx (1)</summary>

* `SidebarGroupLabel` (715-715)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/editor-preferences/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/tags/[id]/route.ts (3)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary>

* `dispatchWebhooks` (33-90)

</details>
<details>
<summary>apps/cms/src/app/api/categories/[id]/route.ts (1)</summary>

* `DELETE` (72-137)

</details>
<details>
<summary>apps/cms/src/lib/auth/session.ts (1)</summary>

* `getServerSession` (4-14)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/schemas/page.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/api/src/middleware/analytics.ts (3)</summary><blockquote>

<details>
<summary>packages/db/src/index.ts (2)</summary>

* `db` (29-29)
* `createClient` (29-29)

</details>
<details>
<summary>apps/cms/src/lib/polar/client.ts (1)</summary>

* `createPolarClient` (5-18)

</details>
<details>
<summary>apps/api/src/lib/polar.ts (1)</summary>

* `createPolarClient` (3-8)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/home/webhook-usage-card.tsx (3)</summary><blockquote>

<details>
<summary>apps/cms/src/types/usage-dashboard.ts (1)</summary>

* `UsageDashboardData` (1-40)

</details>
<details>
<summary>packages/ui/src/components/chart.tsx (1)</summary>

* `ChartConfig` (11-19)

</details>
<details>
<summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary>

* `LoadingSpinner` (8-29)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/keys/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/webhooks/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/media/route.ts (1)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary>

* `dispatchWebhooks` (33-90)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/lib/webhooks/dispatcher.ts (3)</summary><blockquote>

<details>
<summary>apps/cms/src/lib/webhooks/utils.ts (1)</summary>

* `getWebhooks` (78-96)

</details>
<details>
<summary>apps/cms/src/lib/webhooks/webhook-client.ts (1)</summary>

* `WebhookClient` (79-188)

</details>
<details>
<summary>apps/cms/src/lib/webhooks/usage.ts (1)</summary>

* `trackWebhookUsage` (9-71)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/home/media-usage-card.tsx (4)</summary><blockquote>

<details>
<summary>apps/cms/src/types/usage-dashboard.ts (1)</summary>

* `UsageDashboardData` (1-40)

</details>
<details>
<summary>apps/cms/src/utils/string.ts (1)</summary>

* `formatBytes` (13-34)

</details>
<details>
<summary>apps/cms/src/components/ui/hidden-scrollbar.tsx (1)</summary>

* `HiddenScrollbar` (8-31)

</details>
<details>
<summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary>

* `LoadingSpinner` (8-29)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx (7)</summary><blockquote>

<details>
<summary>apps/cms/src/hooks/use-workspace-id.ts (1)</summary>

* `useWorkspaceId` (7-10)

</details>
<details>
<summary>apps/cms/src/lib/queries/keys.ts (1)</summary>

* `QUERY_KEYS` (1-40)

</details>
<details>
<summary>apps/cms/src/types/usage-dashboard.ts (1)</summary>

* `UsageDashboardData` (1-40)

</details>
<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>
<details>
<summary>apps/cms/src/components/home/api-usage-card.tsx (1)</summary>

* `ApiUsageCard` (27-140)

</details>
<details>
<summary>apps/cms/src/components/home/webhook-usage-card.tsx (1)</summary>

* `WebhookUsageCard` (26-139)

</details>
<details>
<summary>apps/cms/src/components/home/media-usage-card.tsx (1)</summary>

* `MediaUsageCard` (47-128)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary>

* `WorkspacePageWrapper` (6-28)

</details>

</blockquote></details>
<details>
<summary>apps/cms/src/components/ui/async-button.tsx (1)</summary><blockquote>

<details>
<summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary>

* `ButtonLoadingSpinner` (31-66)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (56)</summary><blockquote>

<details>
<summary>apps/cms/src/components/nav/workspace-switcher.tsx (1)</summary><blockquote>

`73-73`: **LGTM! Clean styling refactor.**

Switching to semantic design tokens (`sidebar-accent`) improves consistency.

</blockquote></details>
<details>
<summary>apps/cms/src/components/posts/data-grid.tsx (2)</summary><blockquote>

`53-53`: **LGTM: Visual refinement to shadow styling.**

The change from `shadow-sm` to `shadow-xs` is a subtle visual improvement for the card link containers.

---

`66-66`: **LGTM: Fixes malformed flex classes.**

Corrects invalid CSS class syntax. The current `mt-auto flex` properly positions the card footer content.

</blockquote></details>
<details>
<summary>apps/cms/src/components/editor/image-upload-modal.tsx (1)</summary><blockquote>

`210-210`: **LGTM!**

The `fill` prop is appropriate here with the parent container having `position: relative` (line 199). This allows the image to adapt to its container size in the gallery grid.

</blockquote></details>
<details>
<summary>apps/api/src/lib/polar.ts (1)</summary><blockquote>

`1-8`: **LGTM!**

Clean factory function for creating Polar clients with appropriate server selection based on environment.

</blockquote></details>
<details>
<summary>apps/cms/src/components/webhooks/create-webhook.tsx (2)</summary><blockquote>

`256-268`: **Layout refinements look good.**

The spacing adjustments and border removal create a cleaner visual hierarchy for the Events section.

---

`260-260`: **Documentation URL verified as accessible.**

The webhook documentation URL at `https://docs.marblecms.com/guides/features/webhooks` returns an HTTP 200 status code, confirming it is valid and publicly accessible.

</blockquote></details>
<details>
<summary>apps/cms/src/lib/queries/keys.ts (1)</summary><blockquote>

`30-30`: **Migration from removed query keys is complete and verified.**

Comprehensive verification confirms:
- No remaining references to `PUBLISHING_METRICS`, `API_ANALYTICS`, or `WORKSPACE_METRICS` exist in any file across the codebase
- New `USAGE_DASHBOARD` key is properly defined and already in active use
- All usages have been successfully migrated to the unified dashboard approach

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx (2)</summary><blockquote>

`156-165`: **LGTM! Clean refactor.**

Consolidating the upload message logic into `getUploadMessage` eliminates duplication and improves maintainability.

---

`211-214`: **LGTM!**

Consistent with the project-wide compact layout adoption.

</blockquote></details>
<details>
<summary>apps/api/src/middleware/ratelimit.ts (1)</summary><blockquote>

`14-39`: **LGTM! Cleaner structure and sensible rate limiting.**

The conditional rate limiting (200/10s for workspace requests, 10/10s for anonymous) appropriately differentiates authenticated from unauthenticated traffic.

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/tags/[id]/route.ts (2)</summary><blockquote>

`53-67`: **LGTM!**

Webhook dispatch integration follows the fire-and-forget pattern correctly with appropriate error logging.

---

`77-112`: **LGTM!**

Added workspaceId validation prevents unauthorized access, and webhook dispatch mirrors the PATCH handler pattern.

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/metrics/usage/route.ts (4)</summary><blockquote>

`9-14`: **LGTM!**

Standard auth check pattern.

---

`22-82`: **LGTM! Solid metrics aggregation.**

The parallel query pattern, chart bucketing logic, and change percentage calculation (with proper zero-handling) are well-implemented.

---

`84-190`: **LGTM! Comprehensive webhook and media metrics.**

The parallel fetching strategy and chart construction for webhooks mirror the API metrics pattern effectively.

---

`192-225`: **LGTM!**

Response structure is well-organized and aligns with the usage dashboard data type.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx (1)</summary><blockquote>

`50-50`: **LGTM!**

Consistent with project-wide compact layout changes.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx (1)</summary><blockquote>

`133-133`: **LGTM!**

Consistent with project-wide compact layout changes.

</blockquote></details>
<details>
<summary>packages/db/prisma/migrations/20251116173412_new_media_enum_and_alt_text_column/migration.sql (1)</summary><blockquote>

`1-5`: **LGTM!**

The `media_updated` webhook event and `alt` text column are straightforward additions that enhance media tracking and accessibility.

</blockquote></details>
<details>
<summary>apps/cms/src/components/nav/app-sidebar.tsx (1)</summary><blockquote>

`16-16`: **LGTM!**

Removing the border aligns with the streamlined navigation styling.

</blockquote></details>
<details>
<summary>apps/cms/src/components/editor/editor-page.tsx (1)</summary><blockquote>

`34-34`: **LGTM - Import path refactor.**

The HiddenScrollbar import correctly reflects the component relocation to the ui directory.

</blockquote></details>
<details>
<summary>apps/cms/src/components/auth/register-form.tsx (1)</summary><blockquote>

`13-13`: **LGTM - Loading indicator update.**

The ButtonLoadingSpinner correctly replaces the old spinner for GitHub button loading state, aligning with button-specific loading visuals.




Also applies to: 147-147

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/posts/import/route.ts (1)</summary><blockquote>

`8-8`: **LGTM - Webhook dispatch centralization.**

The dispatchWebhooks call properly centralizes webhook delivery with appropriate error handling and contextual logging.




Also applies to: 142-157

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/keys/page-client.tsx (1)</summary><blockquote>

`14-17`: **LGTM - Layout consistency update.**

The size="compact" prop correctly applies compact layout styling to the wrapper.

</blockquote></details>
<details>
<summary>apps/cms/src/components/editor/tabs/metadata-tab.tsx (1)</summary><blockquote>

`6-6`: **LGTM - Import path refactor.**

The HiddenScrollbar import correctly reflects the component relocation to the ui directory.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/schemas/page.tsx (1)</summary><blockquote>

`9-12`: **LGTM - Layout consistency update.**

The size="compact" prop correctly applies compact layout styling to the wrapper.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/editor-preferences/page-client.tsx (1)</summary><blockquote>

`8-8`: **LGTM - Layout consistency update.**

The size="compact" prop correctly applies compact layout styling to the wrapper.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx (1)</summary><blockquote>

`40-43`: **LGTM - Layout consistency update.**

The size="compact" prop and spacing classes correctly apply compact layout styling to the wrapper.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/layout.tsx (1)</summary><blockquote>

`30-30`: **LGTM! Responsive padding enhancement.**

The progressive padding adjustments for larger viewports improve the layout's responsiveness and visual hierarchy.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx (1)</summary><blockquote>

`43-46`: **LGTM! Consistent compact sizing.**

The addition of `size="compact"` aligns with the layout standardization across dashboard pages.

</blockquote></details>
<details>
<summary>apps/cms/src/components/editor/tabs/analysis-tab.tsx (1)</summary><blockquote>

`16-16`: **LGTM! Import path centralization.**

Moving the import to the UI directory standardizes component organization.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx (1)</summary><blockquote>

`13-13`: **LGTM! Consistent compact sizing.**

The compact size prop maintains layout consistency with other settings pages.

</blockquote></details>
<details>
<summary>apps/cms/src/components/nav/nav-main.tsx (1)</summary><blockquote>

`93-106`: **LGTM! Improved navigation semantics and accessibility.**

The changes enhance both accessibility (screen-reader only label) and user experience (Home icon/label is clearer than Overview).

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx (1)</summary><blockquote>

`56-66`: **LGTM! Consistent compact sizing across both states.**

Both rendering branches (categories exist and empty state) correctly apply the compact size prop for layout consistency.

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/tags/route.ts (2)</summary><blockquote>

`5-6`: **LGTM! Updated webhook imports.**

The import changes align with the refactored webhook utilities that now use workspace-scoped operations.

---

`73-73`: **LGTM! Webhook refactor to workspace-scoped context.**

The change from session-based to `workspaceId`-based webhook retrieval correctly aligns with the new `getWebhooks` signature and the PR's centralized webhook dispatching approach.

</blockquote></details>
<details>
<summary>apps/cms/src/lib/validations/webhook.ts (1)</summary><blockquote>

`14-14`: **LGTM!**

The new `media_uploaded` event is properly integrated into both the Zod enum and the events metadata array, following the established pattern for other webhook events.




Also applies to: 132-136

</blockquote></details>
<details>
<summary>apps/cms/src/components/nav/page-header.tsx (1)</summary><blockquote>

`40-40`: **LGTM!**

Simplifying the fallback to a static "Home" heading aligns with the PR's goal of standardizing the dashboard UI.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx (1)</summary><blockquote>

`52-54`: **LGTM!**

Adding `size="compact"` to both wrapper instances standardizes the layout across dashboard pages.




Also applies to: 59-61

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/posts/route.ts (1)</summary><blockquote>

`179-196`: **LGTM!**

The migration to centralized webhook dispatching is well-implemented. The fire-and-forget pattern with error logging is appropriate, ensuring webhook failures don't block the post creation response.

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/categories/route.ts (1)</summary><blockquote>

`77-91`: **LGTM!**

Webhook dispatching follows the same centralized pattern as other routes. The error logging includes helpful context for debugging.

</blockquote></details>
<details>
<summary>apps/cms/src/components/auth/login-form.tsx (1)</summary><blockquote>

`13-16`: **LGTM!**

The loading spinner refactor aligns with the new centralized loading-spinner module while maintaining the existing authentication flow.




Also applies to: 22-22, 133-133

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/upload/complete/route.ts (1)</summary><blockquote>

`133-149`: **LGTM!**

Webhook dispatching follows the established centralized pattern with appropriate fire-and-forget error handling.

</blockquote></details>
<details>
<summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary><blockquote>

`9-9`: **CSS classes verified and properly defined.**

The `workspace-container` (line 295) and `workspace-container-compact` (line 320) classes are both present in `apps/cms/src/styles/globals.css` with responsive breakpoints (sm, md, lg, xl, 2xl) and corresponding CSS variables. The implementation is complete and the new `size` prop has the necessary backing styles.

</blockquote></details>
<details>
<summary>apps/cms/src/lib/polar/client.ts (1)</summary><blockquote>

`3-17`: **Polar client singleton looks good**

The lazy, cached client with a `null` return when `POLAR_ACCESS_TOKEN` is missing is reasonable. Just ensure all call sites treat a `null` client as “Polar not configured” and branch accordingly so you don’t silently skip expected side effects.

</blockquote></details>
<details>
<summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/webhooks/page-client.tsx (1)</summary><blockquote>

`107-110`: **Consistent use of compact workspace wrapper**

Switching both the empty and populated states to `WorkspacePageWrapper` with `size="compact"` (and explicit padding on the list view) keeps layout consistent with the new compact workspace pattern. Looks good.



Also applies to: 133-136

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/categories/[id]/route.ts (1)</summary><blockquote>

`42-51`: **Webhook dispatch is fire‑and‑forget; confirm this is intentional**

Both update and delete now call `dispatchWebhooks(...)` without `await`, relying on the dispatcher + `.catch` handlers to run delivery in the background and log failures. That avoids blocking the HTTP response on webhook delivery, but it also means callers aren’t guaranteed that webhooks have been enqueued before the 200/204 is returned. If that trade‑off is intended, this wiring looks solid.



Also applies to: 53-67, 117-128

</blockquote></details>
<details>
<summary>apps/cms/src/app/api/posts/[id]/route.ts (2)</summary><blockquote>

`196-203`: **Publish/update webhook flow looks coherent**

Using `post_published` only on a draft→published transition and always emitting `post_updated` with the same payload is a clean separation of “first publish” vs “any change”. Fire‑and‑forget dispatch with error logging matches the intent in the comment.



Also applies to: 205-228

---

`245-247`: **Verify Prisma delete filter and workspace scoping**

The DELETE handler now uses:

```ts
const workspaceId = sessionData?.session.activeOrganizationId;
// ...
const deletedPost = await db.post.delete({
  where: { id, workspaceId },
});

In Prisma, delete expects a where matching a unique constraint. Please confirm that your Post model defines a unique or ID constraint that uses both id and workspaceId (e.g. @@id([id, workspaceId]) or @@unique([id, workspaceId])). If id alone is the primary key, this where will not type‑check and you likely want either where: { id } (relying on earlier workspace checks) or deleteMany({ where: { id, workspaceId } }) instead. The new post_deleted dispatch wiring itself looks fine.

Also applies to: 255-255, 262-273

apps/cms/src/lib/webhooks/webhook-client.ts (1)

59-65: Ensure all media.uploaded emitters provide size and type

You’ve tightened the media.uploaded event schema to require size and a type enum. This is good for downstream consumers, but any existing code that calls WebhookClient.send({ event: "media.uploaded", ... }) without these fields will now fail Zod validation at the receiver. Worth checking all emitters to confirm they’re sending both fields with values in ["image", "video", "audio", "document"].

apps/cms/src/app/api/media/route.ts (1)

9-224: Media DELETE scoping + batched webhook dispatch look correct.

Filtering db.media.findMany by workspaceId, building mediaDeletedFromR2 from items actually removed (or missing URLs), and then deleting+tracking only those IDs is a nice tightening of behavior. The new dispatchWebhooks call with validationEvent: "media_deleted" and a single batched payload looks consistent and won’t block the response since it’s fire‑and‑forget with .catch. No issues from my side here.

apps/cms/src/components/home/media-usage-card.tsx (1)

47-127: MediaUsageCard looks solid

Loading, empty, and populated states are handled cleanly, and recent upload rendering (including icons, size, and relative time) is straightforward with no obvious issues.

packages/db/prisma/schema.prisma (1)

34-57: UsageEvent schema and media alt additions look consistent

The new UsageEvent model, its relations/indexes, and the usageEvents back‑reference on Organization align with existing workspace‑scoped tables, and Media.alt plus media_updated event fit cleanly into the current schema with no obvious migration or relational issues.

Also applies to: 150-165, 343-407

apps/cms/src/types/usage-dashboard.ts (1)

1-40: UsageDashboardData shape matches dashboard usage

The structure cleanly covers API, webhook, and media metrics and matches how the dashboard components consume it, so type‑safety around usage data looks good.

apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx (1)

31-56: Usage dashboard layout and card wiring look good

The loading/error branches are straightforward, and the compact WorkspacePageWrapper with the three usage cards wired to data?.api, data?.webhooks, and data?.media matches the UsageDashboardData shape and the cards’ expected props; this composition looks clean.

Comment thread apps/api/package.json
Comment thread apps/api/src/types/env.ts
Comment thread apps/cms/src/app/api/upload/complete/route.ts Outdated
Comment thread apps/cms/src/components/media/media-controls.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a 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

Caution

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

⚠️ Outside diff range comments (1)
apps/cms/src/app/api/upload/complete/route.ts (1)

17-84: Fix user ID access in webhook payload and simplify background tracking

  1. Bug: In the media case, you use sessionData.user.id (Line 75) while the rest of the file uses sessionData.session.userId (e.g., Line 34). Unless getServerSession actually adds a user property, this will be undefined and throw at runtime. For consistency and safety, use the same sessionData.session.userId shape:
-          payload: {
-            id: media.id,
-            name: media.name,
-            userId: sessionData.user.id,
-            size: media.size,
-            type: media.type,
-          },
+          payload: {
+            id: media.id,
+            name: media.name,
+            userId: sessionData.session.userId,
+            size: media.size,
+            type: media.type,
+          },
  1. Optional: trackMediaUpload already wraps both DB and Polar calls in try/catch, so it never rejects. The .catch at the call site (Lines 64–66) is therefore redundant; you can safely fire-and-forget:
-        trackMediaUpload(workspaceId, fileSize, mediaType).catch((err) => {
-          console.error("[Media Upload] Failed to track upload:", err);
-        });
+        void trackMediaUpload(workspaceId, fileSize, mediaType);

Similarly, dispatchWebhooks itself defensive‑logs delivery errors; the extra .catch is only needed if you specifically want to log unexpected top-level failures, otherwise it can be dropped for brevity.

🧹 Nitpick comments (6)
apps/cms/src/components/nav/nav-main.tsx (1)

24-24: Remove unused import.

LayoutIcon is no longer used after replacing "Overview" with "Home" (line 104).

-  LayoutIcon,
apps/cms/src/components/home/api-usage-card.tsx (3)

3-3: Remove unused import.

The Badge import is not used in this component.

-import { Badge } from "@marble/ui/components/badge";

30-35: Remove redundant fallback operator.

The ?? [] on line 35 is redundant since map() always returns an array.

  const chartData =
-    rawChartData.map((item) => ({
+    rawChartData.map((item) => ({
      label: item.label,
      value: item.value,
      date: item.date,
-    })) ?? [];
+    }));

44-44: Remove no-op function.

formatXAxisLabel returns its input unchanged. Remove it and the tickFormatter prop.

-  const formatXAxisLabel = (value: string) => value;
-
              <XAxis
                axisLine={false}
                dataKey="label"
                minTickGap={24}
-                tickFormatter={formatXAxisLabel}
                tickLine={false}
                tickMargin={8}
              />

Also applies to: 111-111

apps/cms/src/components/home/media-usage-card.tsx (1)

47-117: Add accessible label for the icon-only open button

The icon-only Button that opens the media URL (Lines 107–117) has no accessible text, so screen readers won’t know what it does. Add an aria-label (or visually hidden text) describing the action, e.g.:

-                      <Button
+                      <Button
+                        aria-label="Open media in new tab"
                         className="rounded-full bg-sidebar hover:bg-primary/10 hover:text-primary dark:bg-accent/50 dark:hover:text-accent-foreground"
                         onClick={(e) => {
                           e.stopPropagation();
                           openMediaFile(upload.url);
                         }}
                         size="icon"
                         variant="ghost"
                       >
apps/cms/src/utils/usage/media.ts (1)

8-101: Solid non-blocking tracking; a couple of optional refinements

The overall structure (DB + Polar with independent try/catch blocks and workspace fallback) looks good and resilient.

Two small, optional tweaks:

  1. Simplify owner lookup (Lines 23–26):
-    const ownerUserId =
-      organization?.members && organization.members.length > 0
-        ? organization.members[0]?.userId
-        : undefined;
+    const ownerUserId = organization?.members?.[0]?.userId;
  1. Consider running DB and Polar tracking in parallel in trackMediaUpload if ordering doesn’t matter, to reduce tail latency:
-export async function trackMediaUpload(
-  workspaceId: string,
-  fileSize: number,
-  mediaType: string
-): Promise<void> {
-  try {
-    await trackMediaUploadInDB(workspaceId, fileSize);
-  } catch (error) {
-    console.error("[Media Upload] Failed to track in DB:", error);
-  }
-
-  try {
-    const customerId = await getCustomerIdForWorkspace(workspaceId);
-    await trackMediaUploadInPolar(customerId, fileSize, mediaType);
-  } catch (error) {
-    console.error(
-      "[Media Upload] Polar ingestion error (events may still be processed):",
-      error instanceof Error ? error.message : error
-    );
-  }
-}
+export async function trackMediaUpload(
+  workspaceId: string,
+  fileSize: number,
+  mediaType: string
+): Promise<void> {
+  await Promise.allSettled([
+    (async () => {
+      try {
+        await trackMediaUploadInDB(workspaceId, fileSize);
+      } catch (error) {
+        console.error("[Media Upload] Failed to track in DB:", error);
+      }
+    })(),
+    (async () => {
+      try {
+        const customerId = await getCustomerIdForWorkspace(workspaceId);
+        await trackMediaUploadInPolar(customerId, fileSize, mediaType);
+      } catch (error) {
+        console.error(
+          "[Media Upload] Polar ingestion error (events may still be processed):",
+          error instanceof Error ? error.message : error
+        );
+      }
+    })(),
+  ]);
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96a2d98 and 2e5c38e.

📒 Files selected for processing (7)
  • apps/cms/src/app/api/upload/complete/route.ts (3 hunks)
  • apps/cms/src/components/home/api-usage-card.tsx (1 hunks)
  • apps/cms/src/components/home/media-usage-card.tsx (1 hunks)
  • apps/cms/src/components/home/webhook-usage-card.tsx (1 hunks)
  • apps/cms/src/components/nav/nav-devs.tsx (1 hunks)
  • apps/cms/src/components/nav/nav-main.tsx (3 hunks)
  • apps/cms/src/utils/usage/media.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cms/src/components/home/webhook-usage-card.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: taqh
Repo: usemarble/marble PR: 137
File: packages/db/prisma/migrations/20250826194942_make_category_required/migration.sql:11-11
Timestamp: 2025-08-26T20:20:46.367Z
Learning: User "taqh" prefers concise code review comments rather than verbose explanations. Keep reviews short and to the point.
📚 Learning: 2025-09-28T22:18:20.726Z
Learnt from: mezotv
Repo: usemarble/marble PR: 198
File: apps/cms/src/app/(main)/[workspace]/(workspace)/keys/page-client.tsx:23-32
Timestamp: 2025-09-28T22:18:20.726Z
Learning: For Radix UI tooltips with disabled buttons: using asChild with disabled buttons breaks tooltip functionality because disabled elements don't receive the pointer events needed to trigger tooltips. Solutions include: 1) wrapper span with asChild and pointer-events: none on the disabled button, or 2) using aria-disabled instead of disabled to preserve interaction while preventing clicks.

Applied to files:

  • apps/cms/src/components/nav/nav-devs.tsx
🧬 Code graph analysis (4)
apps/cms/src/components/home/api-usage-card.tsx (3)
apps/cms/src/types/usage-dashboard.ts (1)
  • UsageDashboardData (1-40)
packages/ui/src/components/chart.tsx (1)
  • ChartConfig (11-19)
apps/cms/src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (8-29)
apps/cms/src/utils/usage/media.ts (1)
packages/db/src/index.ts (1)
  • db (29-29)
apps/cms/src/app/api/upload/complete/route.ts (2)
apps/cms/src/utils/usage/media.ts (1)
  • trackMediaUpload (82-102)
apps/cms/src/lib/webhooks/dispatcher.ts (1)
  • dispatchWebhooks (33-90)
apps/cms/src/components/home/media-usage-card.tsx (4)
apps/cms/src/types/usage-dashboard.ts (1)
  • UsageDashboardData (1-40)
apps/cms/src/utils/string.ts (1)
  • formatBytes (13-34)
apps/cms/src/components/ui/hidden-scrollbar.tsx (1)
  • HiddenScrollbar (8-31)
apps/cms/src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (8-29)
🔇 Additional comments (7)
apps/cms/src/components/nav/nav-devs.tsx (1)

46-46: LGTM!

Styling simplification is consistent with the broader navigation updates.

apps/cms/src/components/nav/nav-main.tsx (4)

93-93: LGTM!

The sr-only class maintains screen reader accessibility while simplifying the visual UI.


99-106: LGTM!

Overview → Home rebranding with HouseIcon and simplified active state styling are intentional improvements.


113-114: LGTM!

Active state styling simplification is consistent with the broader navigation updates.


135-136: LGTM!

Settings active state styling matches the simplified pattern used throughout the navigation.

apps/cms/src/components/home/api-usage-card.tsx (2)

37-42: LGTM!

Chart configuration is well-structured and properly typed.


72-139: LGTM!

The component properly handles loading, empty, and data states with clean JSX structure.

@taqh taqh merged commit d973176 into main Nov 18, 2025
9 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Nov 24, 2025
@taqh taqh deleted the workspace-analytics-and-usage-tracking branch November 30, 2025 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant