Skip to content

Dashboard

The /dashboard surface is a thin composition layer over existing domain services. All aggregation lives in libraries/nestjs-libraries/src/dashboard/; the backend controller (apps/backend/src/api/routes/dashboard.controller.ts) is responsible only for auth, RBAC mapping, and wiring.

Verified against v1.0.0

Architecture

Frontend widgets (apps/frontend/src/components/dashboard/)

  ├─ SWR hooks (hooks/use*.ts)
  │   └─ useFetch -> /dashboard/* endpoints

  ├─ SectionCard kit (kit/section-card.tsx)
  │   └─ useDashboardPrefs (localStorage show/hide)
  │   └─ usePermissions (RBAC gate)

  └─ dashboard.component.tsx (responsive 12-col grid)

Backend controller (apps/backend/src/api/routes/dashboard.controller.ts)

  ├─ DashboardService (libraries/nestjs-libraries/src/dashboard/dashboard.service.ts)
  │   ├─ getSummary(...)            -> Redis-cached per (org, user)
  │   ├─ getSchedule(...)           -> PostsService
  │   ├─ getCampaignSummaries(...)  -> CampaignsService
  │   ├─ getMediaJobs(...)          -> AiSettingsService
  │   ├─ getAttention(...)          -> 8 probes, RBAC-filtered, Redis-cached
  │   ├─ buildUsage(...)            -> SubscriptionService + domain counters
  │   └─ buildPlanUsage(...)        -> plan limits snapshot

  ├─ DashboardBriefService
  │   └─ generateBrief / getCachedBrief -> AI + Redis

  └─ PermissionsService / RolesService -> effective RBAC + billing

No new Prisma models were added; every widget reads existing tables through the appropriate domain service or repository.

Endpoints

MethodRoutePermissionDescription
GET/dashboard/summaryauthTotal posts, scheduled, published next 7 days, connected channels, drafts, upcoming posts, unread comments, provider flags, team size. Cached 60s per (orgId, userId).
GET/dashboard/schedule?days=7&timezone=UTCposts:readDay-bucketed scheduled counts + gap detection. days clamped to 1–30.
GET/dashboard/campaigns?limit=6posts:readActive campaign summaries with post-state counts and goal progress. limit clamped to 1–50.
GET/dashboard/media-jobsmedia:readLatest 20 jobs + { pending, processing, failed7d } counts.
GET/dashboard/usagebilling:readPlan limits/usage when Stripe is enabled; { billingEnabled: false } otherwise. The AI usage widget consumes GET /ai/usage, including the new byProvider breakdown.
GET/dashboard/attentionauth8 attention probes filtered by effective permissions; cached 60s.
GET/dashboard/briefanalytics:readReturns cached brief or { cached: false }.
POST/dashboard/briefanalytics:readGenerates and caches the daily brief. Single-flighted.

Dismiss anomaly uses the unified analytics endpoint: POST /public/v1/analytics/anomalies/:id/dismiss.

Attention probes

DashboardService.getAttention runs eight probes inside try/catch; a single probe failure degrades only that item. The controller maps the member's effective permissions to permittedKinds via KIND_PERMISSION_MAP:

KindRequired permission
failed-postsposts:read
channel-healthposts:read
pending-approvalsposts:update
unread-commentscomments:read
schedule-gapsposts:read
budgetbilling:read
failed-media-jobsmedia:read
anomaliesanalytics:read

Results are sorted by severity (critical > warning > info) then count descending.

Caching

  • dashboard:summary:{orgId}:{userId} — TTL 60s, single-flight miss handling.
  • dashboard:attention:{orgId}:{userId} — TTL 60s.
  • dashboard:brief:{orgId}:{YYYY-MM-DD} — TTL seconds until local midnight in the user's timezone; single-flight on generation.

Cache write failures are swallowed so Redis downtime does not break the dashboard.

Daily Brief

DashboardBriefService builds a prompt from the attention feed, yesterday's analytics, today's scheduled posts, and plan usage. It is generated only on demand, gated by BudgetService.checkBudget('utility', orgId) and AIModelProvider.resolveConfigForScope('utility', orgId). If the org has no active AI provider, the controller returns 503. The brief is cached per org per calendar day.

Frontend RBAC

SectionCard receives an optional permission prop. While usePermissions().isResolved is false the card renders optimistically; once resolved it returns null if the permission is absent. The Customize popover applies the same filter so users cannot toggle sections they are not allowed to see.

Customization

Section visibility is stored in localStorage['dashboard_prefs'] as { hidden: string[], v:1 }. The version field makes it possible to swap in a server-backed store later without breaking existing clients.

  • libraries/nestjs-libraries/src/dashboard/dashboard.service.ts
  • libraries/nestjs-libraries/src/dashboard/dashboard-brief.service.ts
  • apps/backend/src/api/routes/dashboard.controller.ts
  • apps/frontend/src/components/dashboard/dashboard.component.tsx

The AI-native social media management platform — postmill.ai