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.3.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 billing is enabled (billingEnabled() — at least one payment provider configured); { 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) (which also enforces the organization-wide AI budget) and AIModelProvider.resolveConfigForScope('utility', orgId). If a budget is exhausted the service returns 429; if the org has no active AI provider it 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