Skip to Content
Apps & PortalsDm PortalScreens — DM Portal

Screens — DM Portal

Audience: Internal digital marketers and HITL reviewers managing multiple client accounts Platform: Web only (Next.js) Auth: Custom JWT (dm_access_token httpOnly cookie) — reviewer, admin, or super_admin role required; cross-tenant access based on TenantMember assignment

Status notation: [Live] = confirmed working · [To Build] = specified, not yet implemented · [Unverified] = not yet checked against running app

The DM Portal is a separate Next.js app from the Dashboard. The sidebar is collapsible (toggle button collapses to icon-only w-16; expanded is w-56). The sidebar tenant switcher is mandatory — content is gated until a client is selected. “All Tenants” view is not available.

Top Bar

[Live]apps/dm/src/components/topbar.tsx

Components (left → right):

  • Theme toggle (cycles light → dark → system)
  • Calendar link (/calendar)CalendarDays icon — navigates to the Content Calendar page.
  • Notification bell — violet unread badge; opens dropdown panel
  • Profile avatar dropdown (initials, name, email, DM Reviewer badge, Profile Settings, Sign out)

Notification Dropdown

  • Fetches from GET /api/dm/notifications?tenantId={activeTenantId} on mount (once per page load)
  • Subscribes to Socket.IO notification:new events via /tenant namespace for real-time updates
  • Shows last 50 notifications for the active tenant (unread highlighted in violet)
  • “Mark all read” → POST /api/dm/notifications/read-all with { tenantId }
  • Shows “Select a client to see notifications” when no tenant is active
  • activeTenantId is passed as a prop from the server layout (reads dm_active_tenant cookie)

Socket.IO room join for DM reviewers: DM reviewers have tenantId: null in their JWT, so the server-side connection handler skips the normal tenant:${tenantId} room join. After connecting, the topbar emits tenant:join with activeTenantId; the server handler (role-gated to reviewer/admin/super_admin) calls socket.join("tenant:${tenantId}") so real-time notification:new events are received correctly.

API routes (Next.js proxy):

  • GET /api/dm/notifications?tenantId= → proxies to GET /dm/v1/notifications?tenantId=
  • POST /api/dm/notifications/read-all → proxies to POST /dm/v1/notifications/read-all

Backend routes (/dm/v1/, access-checked by requireDMAccess):

  • GET /dm/v1/notifications?tenantId= — last 50 notifications for the tenant; reviewer must be a member
  • POST /dm/v1/notifications/read-all — marks all unread as read for { tenantId } in body

The DM portal sidebar includes the following nav groups (in addition to individual items):

RouteLabelIconNotes
(Content group)ContentLayersNavGroup — auto-expands when on any content/* route
  /blogBlog PostsBookOpen
  /landing-pagesLanding PagesGlobe
  /content-briefsContent BriefsClipboardPen
  /socialSocial PostsImages
  /newslettersNewslettersMail
  /occasionsOccasionsPartyPopperUpcoming important days; DM triggers occasion posts
  /mediaMedia LibraryGalleryHorizontalEnd
(SEO group)SEOSearchCodeNavGroup — auto-expands when on any /seo/* route
  /seo/backlinksBacklinksLink2
  /seo/link-buildingLink BuildingHammerBacklink outreach campaigns
  /seo/keywordsKeywordsKeyRound
  /seo/keyword-groupsKeyword GroupsFolderSearch

All icons from lucide-react.

ScreenStatus
P1 — Mission Control[Live] — stat cards + pending approvals (max 10) + running agents (max 10); polls every 60s
P2 — Activities[Live] — 4-view (list/grouped/calendar/kanban) activity browser; API-driven
P3 — Activity Detail[To Build] — live SSE streaming from agent requires agent infra
P4 — Approvals Queue[Live] — unified blog+social dm_review queue; type filter tabs; clickable rows
P5 — Approval Review (Blog)[Live] — blog post review with approve/reject; DM notes
P5a — Social Posts List[Live] — list/calendar view toggle; violet calendar tiles; status + platform filters; paginated
P5b — Approval Review (Social)[Live] — social post review with approve; advances to design_pending
P6 — Agents[Live] — cross-tenant agent monitoring grid with real-time Socket.IO events
P6b — Agent Detail[Live] — per-agent detail: milestones + assigned activities scoped to active tenant
P7 — Users[Live] — cross-tenant user list
P8 — Reports[Unverified]
P9 — Context[Live] — gradient header, timeline/details/revise tabs; DM can request revision; cannot approve
P10 — Strategy[Live] — full strategy viewer + status dropdown (draft/pending_review/rejected only) + revise tab
P10b — Deliverable Plan[Live] — plan viewer with goals + monthly deliverables + est. credits; no approve button
P11 — Goals[Live] — stat cards, goal cards with progress bars; “Archived Plans” link
P11b — Archived Goals[Live] — expandable plan accordion cards; GET /dm/v1/goals/archived?tenantId=
P12 — Deliverables[Live] — expandable rows with per-item deliverable cards; prev/next period picker
P13 — Activity Log[Live] — read-only timeline of the 100 most recent activity log entries
P14 — Calendar[Live] — react-big-calendar; sidebar checkbox filters (sources + platforms); floating event popup (read-only, no approve); accessed via topbar CalendarDays icon not sidebar
P15 — Help Center[Live] — /help directory with full-text search + 18 topic pages covering all DM portal features; sidebar “Help Center” leaf link
P16 — Keywords[Live] — keyword table; add/edit/delete; source badge (agent/manual)
P16b — Keyword Groups[Live] — group list with approve button per pending_review group
P16c — Keyword Group Detail[Live] — keywords in group; add/remove; approve group button
P17 — Backlinks[Live] — backlink prospect table; clickable rows; search + status filter; chevron hint
P17b — Backlink Detail[Live] — prospect details; 5-step timeline; editable status + notes; campaign + email cards
P18 — Outreach Campaigns[Live] — campaign list; status draft/dm_review/sent; email progress counter
P18b — Campaign Detail[Live] — per-email cards; approve/skip/edit/send; bulk “Send All Approved”
P19 — Newsletters[Live] — list/calendar view toggle; status filter tabs; approve/reject; orange calendar tiles
P20 — Channel Health[Live] — read-only channel health dashboard; overall score gauge, KPI cards, attention items, channel grid; amber badge when pendingSuggestionsCount > 0
P21 — Occasions[Live] — upcoming occasions grid (next 30 days); per-card platform picker + “Create post” button; triggers social-post-writer agent
P22 — Channel Detail[Live] — /channels/[id]; Suggestions tab (default) + Insights tab; DM can approve/dismiss/execute/mark-done/restore; Regenerate button re-runs suggester

Screen P1 — Mission Control (/overview)

Status: [Live]

Purpose: Overview of all pending work and live agent activity across all assigned tenants.

┌─────────────────────────────────────────────────────────────┐ │ DM Portal 🔔15 Reviewer [All Tenants ▾] │ ├──────────┬──────────┬──────────┬──────────────────────────── │ │ Pending │ Running │ Escalati-│ Tenants │ │ Approvals│ Agents │ ons │ Active │ │ 15 │ 0 │ 0 │ 8 │ ├──────────┴──────────┴──────────┴──────────────────────────── │ │ │ │ │ Pending Approvals │ Running Agents │ │ (sorted by age) │ ───────────────────── │ │ 📝 Blog Post Technovate │ (empty when idle) │ │ 📱 Social Post Acme │ │ │ ... │ │ │ [View All Approvals →] │ │ └──────────────────────────────┴──────────────────────────────┘

Data sources (all reviewer-accessible via /dm/v1/):

  • Stat cards: GET /dm/v1/overview-stats{ pendingApprovals, runningAgents, escalations, activeTenants }
  • Pending approvals list: GET /dm/v1/overview-approvals?limit=10
  • Running agents list: GET /dm/v1/overview-running

Notification bell: The topbar bell is a full dropdown (see Top Bar section). The P1 stat cards poll the overview endpoints independently — the bell is not wired to /api/dm/stats. See Notification Dropdown section at the top of this document.

Update cadence: Page polls every 30s; bell polls every 60s. No SSE required.


Screen P2 — Activities (/activities)

Status: [Live]

Purpose: Cross-tenant activity list. All tasks across all assigned tenants in one view.

Filter: tenant | campaign | agent | status | date | type.

Table columns: tenant, campaign, activity type, agent, status, created, duration, cost.

Click row → Activity Detail (P3).

Toolbar: Includes an Add Activity button (violet) that opens a CreateActivityModal. Activities created by the DM are assigned to the client role (assignedRole: "client", createdByRole: "dm"). The modal sets isManual=true, agentQueue="manual", deliverableType="custom".

Manual activities: Rows with isManual=true display a “Manual” badge in the list view. On the Activity Detail page, manual activities with status != "done" show a “Mark as Done” button.

See Dashboard D6 for the full activity model field additions (isManual, assignedRole, createdByRole, notes, optional deliverablePeriodId).


Screen P3 — Activity Detail (/activities/[id])

Purpose: Live monitoring and intervention interface for a single agent activity.

Split view:

Left: Live output — terminal-style streaming pane. SSE stream from event_logs collection. Auto-scroll toggle. Pause stream button.

Right: Metadata

  • Tenant, campaign, activity type, agent, model, session ID
  • Input payload (what was sent to the agent)
  • Cost running counter
  • Output validators: live feedback as output streams (character limit check, brand voice scan)
  • Actions: Intervene (pause agent + leave note) | Retry with note | Cancel

Screen P4 — Approvals Queue (/approvals)

Status: [Live]

Purpose: Cross-tenant unified queue of all content pending DM review.

Data source: GET /dm/v1/overview-approvals — blog posts + social posts in dm_review status, scoped to accessible tenants. Each item includes a link field pointing to the review screen.

Autonomy note: Items only appear here when the corresponding PlatformSetting approval toggle is ON. If requireBlogApproval or requireSocialApproval is disabled in the Manage → Autonomy page, those content types skip dm_review entirely and never appear in this queue.

Layout:

  • Subtitle: “Content pending DM review — N items”
  • Filter tabs: All | Blog Posts | Social Posts (sets ?type= URL param)
  • Table: Content label | Type badge | Client name | Age
  • Each row is a clickable link → navigates to the review screen for that item

Type badges: Blog Post (blue), Social Post (violet)

Notification entry point: The topbar bell icon links here. Count shown in bell = pendingApprovals from stats endpoint.

Not yet implemented: Bulk approve, risk level sorting, assigned reviewer filter.


Screen P5 — Approval Review Blog (/approvals/[id])

Purpose: Full-screen blog post review with DM approve/reject.

See P5b below for the social post review screen.


Screen P5a — Social Posts List (/social) [Live]

Status: [Live] Purpose: Browse all social posts for the active client across all statuses and platforms.

View toggle

List/Calendar toggle in the header (violet active state).

  • List view — paginated table; status filter tabs (All / Needs Review / Design / Approved / Sent) + platform dropdown.
  • Calendar viewContentCalendar from @leadmetrics/ui; violet tiles; date from scheduledAt ?? activity.dueDate ?? createdAt. Popup shows platform icon, status badge, body text preview, format + char count, “View Social Post →” link.

Data source

GET /dm/v1/social?tenantId=&page=&limit=20&status=&platform= — paginated list. A second request (limit=200, status filter only) feeds the calendar view as allPosts.


Screen P5b — Social Post Review (/social/[id]) [Live]

Status: [Live] Purpose: Two-panel review of a social media post — copy on the left, platform preview + poster images on the right. DM reviewer approves the copy (→ designer enqueued) or rejects with feedback (→ writer re-runs).

Layout

┌─────────────────────────────────┬──────────────────────────────────┐ │ LEFT PANEL — Copy Review │ RIGHT PANEL — Preview + Assets │ │ │ │ │ [Platform badge] [Format] │ [Platform Preview Card] │ │ [Status badge] [Version] │ ┌──────────────────────┐ │ │ ───────────────────────────── │ │ @yourbrand │ │ │ Character count progress bar │ │ │ │ │ │ │ [Poster Image] │ │ │ [Client feedback] (if any) │ │ │ │ │ [Previous DM note] (if any) │ │ Caption text... │ │ │ │ │ #hashtags │ │ │ ┌─ Hook callout (purple) ───┐ │ └──────────────────────┘ │ │ │ "Opening hook text" │ │ │ │ └───────────────────────────┘ │ [All Slides ({n})] │ │ │ (raw poster carousel; │ │ Copy text body │ shown only after approval) │ │ (hashtags highlighted blue) │ │ │ │ [Note: poster pending…] │ │ Hashtag pills row │ (if copy not yet approved) │ │ Alt text (if provided) │ │ └─────────────────────────────────┴──────────────────────────────────┘ │ STICKY FOOTER — Action buttons │ │ [Reject Copy] [Approve Copy] (when status=dm_review) │ │ [Reject Design] (status note) (when status=client_review)│ └────────────────────────────────────────────────────────────────────┘

Platform Preview Card

Embedded in the right panel; shows how the post will look on the target platform using platform-specific UI mockups:

PlatformPreview style
InstagramWhite card; avatar header; square image; ♥ 💬 ➦ / 🔖 actions; caption + hashtags
LinkedIn”YB” avatar; name + followers + “Just now”; body text; image below; reactions row
FacebookAvatar; timestamp; body text; image; Like/Comment/Share
X (Twitter)Avatar; @yourbrand; tweet text + image inline; reply/repost/like/share
TikTokBlack card; image fills frame; caption + hashtags overlaid; right-side action buttons

The actual poster image (medias[0].url) is embedded inside the platform UI where applicable. If no image exists yet (copy not approved), a branded placeholder is shown.

StatusActions
dm_reviewReject Copy (opens feedback modal → POST /api/dm/social/{id}/dm-reject) · Approve Copy (→ POST /api/dm/social/{id}/dm-approve → enqueues designer)
client_reviewReject Design (opens feedback modal → POST /api/dm/social/{id}/design-reject) · read-only “sent to client” note
otherStatus badge + approved-by info (read-only)

Playwright note: Sticky footer buttons do not fire React handlers via normal browser_click. Use browser_evaluate + fetch('/api/dm/social/{id}/dm-approve', { method: 'POST' }) or JS .click() workaround.

Platform icons

All platform icons use react-icons/fa6 with brand colors: Instagram (#E1306C), LinkedIn (#0A66C2), Facebook (#1877F2), X (dark/currentColor), TikTok (#EE1D52). Icon size w-6 h-6 in the detail page.


Screen P6 — Agents (/agents)

Status: [Live]

Purpose: Cross-tenant view of all active agents and their live status, with real-time monitoring.

Implementation: AgentsClient (client component) + server component page. Socket.IO events (agent:event) received via getSocket(). Live state keyed by ${tenantId}:${agentRole} for cross-tenant tracking.

Live Activity Banner — shown when any agents are running. Lists all active runs with spinner, step count, elapsed timer, and tool description.

Agent card grid — grouped by category (Onboarding, Strategy, Orchestration, SEO, Content, Reporting, Research, Reactive). Each card is a clickable link to /agents/[agentId]:

  • Bot icon with status dot (violet pulse = running, green = recently completed, red = recently failed)
  • Agent name + plan tier badge + “Live” pill when running
  • Live: tool description + step count + elapsed time
  • Idle: agent description text + “Active” indicator

Data source: GET /dm/v1/agents — all active agentConfig rows (read-only, monitoring only).

Real-time: Socket.IO agent:event updates — agent:started, agent:progress, agent:completed, agent:failed. Completed/failed states auto-clear after 30s/60s respectively.


Screen P6b — Agent Detail (/agents/[agentId])

Status: [Live]

Purpose: Per-agent detail view scoped to the active tenant — milestones logged by the agent and deliverable activities assigned to it.

Layout:

  • Back link → /agents
  • Header card — agent name, description, plan tier badge, category badge, Active status dot
  • Config row (2 cols) — Role (monospace) · Activities count with done/active/overdue breakdown
  • Activity OverviewAgentRunStats component: 4 stat cards (Total Runs · Total Time · Total Cost · Avg Duration) + 30-day bar chart scoped to active tenant. Data from GET /dm/v1/agents/:agentId/run-stats?tenantId=.
  • Agent MilestonesActivityLog entries where actorType = "agent" and actor = agent.role, scoped to active tenant. Shows action, detail (2-line clamp), View link, timeAgo timestamp.
  • Assigned ActivitiesActivity rows where agentQueue = agent.role, scoped to active tenant. Shows type emoji, label, deliverableType, due date (red + ⚠ if overdue), status badge.

APIs:

  • GET /dm/v1/agents/:agentId?tenantId= — returns { agent, stats, activities, agentLogs }
  • GET /dm/v1/agents/:agentId/run-stats?tenantId= — returns daily run aggregates for the bar chart

Both access-controlled to reviewer’s assigned tenants.


Screen P7 — Users (/users)

Status: [Live]

Purpose: Cross-tenant user list for the DM reviewer.

Data source: GET /dm/v1/users — queries TenantMember (single source of truth). Scope-aware:

  • super_admin: all TenantMember records (optionally filtered by ?tenantId=)
  • reviewer/admin: TenantMember records for tenants they are assigned to

View: Since “All Tenants” is no longer available, always filtered to the active tenant cookie. 3-column table — User (avatar + name + email), Role, Joined. Heading shows {TenantName} — Users.

Role badges (TenantMember roles): owner=amber, admin=emerald, reviewer=blue, member=slate.

Consistency: Dashboard Team Members page and DM Portal Users page both use TenantMember and show the same list for a given tenant.


Screen P9 — Context (/context)

Status: [Live]

Purpose: View, download, and request revision of the client context file for the selected tenant.

Data source: GET /dm/v1/context?tenantId= — returns context with status and change log.

Layout: Mirrors dashboard ContextViewer:

  • Header bar: “Client Context” + Download as PDF button (visible whenever content exists) + status badge + Locked indicator (when approved)
  • Blue gradient header card (tenant name + version)
  • Optional banners: amber “Awaiting client approval” (completed), green “Approved” (approved), blue “Revision submitted” (post-revise)
  • Dashed “Context Preview” divider → MarkdownRenderer
  • Right sidebar: Timeline / Details / Revise tabs

Download as PDF: Same branded PDF engine as dashboard — cover page, TOC, running footer on every page. Opens browser print dialog. (ContextViewer.tsxtriggerContextPdfDownload)

DM restrictions: DM can request revision (Revise tab → POST /api/context/revise/dm/v1/context/revise → enqueues context-file-writer). DM cannot approve — if status is pending_review, shows “Awaiting client approval” info banner only.


Screen P10 — Strategy (/strategy)

Status: [Live]

Purpose: View, download, revise, and change status of the active strategy for the selected client tenant.

Data source: GET /dm/v1/strategy?tenantId= — returns strategy with all versions and change log.

Layout: Mirrors dashboard StrategyViewer:

  • Header bar: “Strategies” + Download as PDF button + StatusDropdown
  • Purple gradient header card (strategy title + version)
  • Version picker (if multiple versions)
  • Dashed “Strategy Preview” divider → MarkdownRenderer
  • Right sidebar: Timeline / Details / Revise tabs

Download as PDF: Same branded PDF engine as dashboard — brand purple cover page, TOC page, content with running footer. Opens browser print dialog. (StrategyViewer.tsxtriggerPdfDownload)

Status dropdown: DM can set draft / pending_review / rejected. If status is approved, shows a static approved badge (cannot be changed by DM). Posts to POST /api/strategy/status/dm/v1/strategy/status.

Revise tab: DM can request strategy regeneration with notes. Posts to POST /api/strategy/revise/dm/v1/strategy/revise → logs change + sets pending_review + enqueues strategy-writer.

After any mutation: window.location.reload() (no server actions in DM portal).


Screen P10b — Deliverable Plan (/strategy/deliverable-plan)

Status: [Live]

Purpose: View the deliverable plan for the selected client tenant. Read-only — approval is client-only.

Data source: GET /dm/v1/deliverable-plan?tenantId= — returns plan with goals, templates, and strategyStatus.

Layout: Matches dashboard deliverable plan viewer:

  • Header with plan version + date + status badge (Approved / Pending Review)
  • 3 summary cards: Goals · Deliverable Types · Est. Monthly Credits
  • Goals section: goal cards with linked deliverable type badge pills
  • Monthly Deliverables: icon + label + priority + rationale + platforms + volume + credits/unit

DM restrictions: If status is pending_review, shows “Awaiting client approval” banner. No approve button — only the client dashboard has that action.


Screen P11 — Goals (/goals)

Status: [Live]

Purpose: View goals from the active deliverable plan for the selected client tenant.

Data source: GET /dm/v1/goals?tenantId= — returns goals with current-period activity progress and planStatus.

Layout: Matches dashboard GoalsClient:

  • Header with “Archived Plans” button → /goals/archived + “View Strategy” button
  • 3 summary stat cards: Total Goals · Ahead · On Track
  • Goal cards with channel badge, red-gradient progress bar, status badge (Ahead/On Track/Behind)
  • Empty states: amber Clock if plan is pending_review; violet Flag if no plan at all

hasPendingPlan derived client-side: planStatus === "pending_review" (API returns the latest non-archived plan, which could be pending or approved).


Screen P11b — Archived Goals (/goals/archived)

Status: [Live]

Purpose: Browse goals from past (archived) deliverable plan versions.

Data source: GET /dm/v1/goals/archived?tenantId= — returns archived plans with their goals.

Layout: Matches dashboard ArchivedGoalsClient:

  • Back button → /goals · plan count in header
  • Expandable accordion cards per archived plan (version, created date, archived date, goal count)
  • Expanded: read-only goal rows with channel badge, target, metric, timeframe, linked types

Screen P12 — Deliverables (/deliverables)

Status: [Live]

Purpose: Monthly deliverable progress with per-item details for the selected client tenant.

Data source: GET /dm/v1/deliverables?tenantId=&period=YYYY-MM — returns templates with items[] (from Deliverable model), inProgress count, totalInProgress.

Layout: Matches dashboard DeliverablesClient (client component):

  • Header + period picker: [←] [📅 Month ▼] [→] with prev/next arrows
  • Summary stat cards: Deliverables · Published · In Progress (if > 0) · Completion %
  • Period status banner if periodStatus === "preview"
  • Expandable rows: progress summary, h-1.5 teal bar, 2-col grid of DeliverableItemCard links
  • Item links: blog → /blog/[id], social → /social/[id], other → /activities

Screen P13 — Activity Log (/activity-log)

Status: [Live]

Purpose: Read-only timeline of the 100 most recent activity log entries for the selected client tenant.

Data source: GET /dm/v1/activity-log?tenantId=

Layout:

  • Icon-coded timeline: Bot (agent actions), User (human actions), Cpu (system actions) based on actorType
  • Each entry: action label, actor name, relative timestamp (timeAgo), detail text if present, link if present

API: GET /dm/v1/activity-log — requires tenantId. Returns last 100 ActivityLog rows ordered by createdAt desc.


Screen P8 — Reports (/reports)

Status: [Live] Purpose: Browse, upload, and generate AI performance reports for the active tenant.

Layout

Page header with two action buttons: Generate with AI (violet, Sparkles) and Upload (outline, Plus). Scrollable report table below.

Components

Reports table

Columns: Report Name | Date Range | Status | (chevron)

Each row is a clickable link to /reports/[id]. Row shows:

  • Violet BarChart2 icon
  • Report name + creator sub-line (Bot = agent, Sparkles = AI-generated, Upload = DM-uploaded)
  • Date range
  • Status badge: Published (green) · Generating… (amber, spinning) · Failed (red)

Generate with AI modalGenerateReportModal:

  • Textarea for natural-language prompt + 4 example chips
  • Date range picker (This Month / Last Month quick-fill)
  • Optional title field
  • Submit → POST /api/dm/reports/generaterouter.refresh()

Upload Report modalUploadReportModal:

  • Drag-and-drop file picker (.md or .pdf, max 10 MB)
  • Report name, date range (with quick-fill), notes textarea
  • Submit → POST /api/dm/reports

States

Empty state: FileText icon, “No reports yet.”


Screen P8b — Report Detail (/reports/[id])

Status: [Live] Purpose: View a full performance report for a tenant.

Layout

Sticky top bar + scrollable content (max-w-4xl).

Components

Sticky top bar

  • ArrowLeft back → /reports
  • BarChart2 icon + report title
  • Download PDF button (markdown reports, hidden while generating) — triggerPdfDownload() via marked + window.print()
  • Open PDF link (uploaded PDFs) — Spaces CDN URL

Cover banner (violet gradient)

  • “Performance Report” eyebrow
  • Title, tenant name, month/year, date range
  • Creator line with Bot/Sparkles/Upload icon

Banners

  • Amber: DM notes (uploads only)
  • Violet: original AI request prompt (AI-generated only)

Body states

  • generating: spinner placeholder
  • failed: error message
  • PDF: <iframe> 80vh
  • Markdown: <MarkdownRenderer>

API

All data fetched server-side in page.tsx via JWT → GET /dm/v1/reports/[id]?tenantId=. Generate via POST /dm/v1/reports/generate (DM access required).


Screen P16 — Keywords (/seo/keywords) [Live]

Status: [Live] Purpose: View, add, edit, and delete SEO keywords for the active client tenant. Mirrors the dashboard Keywords page with DM-level access. Access: Via sidebar SEO group → “Keywords” link.

Layout

Page header (“Keywords”) with an Add Keyword button (violet, opens a right-side drawer). Below: a searchable, filterable table of all tenant keywords.

Components

Keywords table

Columns: Keyword | Search Volume | Difficulty | Intent | Category | Source | (row actions)

ColumnDetails
DifficultyBadge: low (emerald), medium (amber), high (red)
IntentBadge: informational / commercial / transactional / navigational
SourceBadge: agent (violet) or manual (slate)

Row actions: Edit (pencil icon) | Delete (trash icon, with confirmation).

Add / Edit Keyword drawer (right-side): Same form fields as dashboard — keyword, search volume, difficulty, intent, category, notes. DM can add and edit keywords directly.

API (proxy routes)

  • GET /api/dm/keywords?tenantId=GET /dm/v1/keywords?tenantId=
  • POST /api/dm/keywordsPOST /dm/v1/keywords
  • PATCH /api/dm/keywords/[id]PATCH /dm/v1/keywords/[id]
  • DELETE /api/dm/keywords/[id]DELETE /dm/v1/keywords/[id]

Screen P16b — Keyword Groups (/seo/keyword-groups) [Live]

Status: [Live] Purpose: View keyword groups for the active client tenant, and approve groups that are in pending_review status. DM is the only role that can approve keyword groups. Access: Via sidebar SEO group → “Keyword Groups” link.

Layout

Page header (“Keyword Groups”) with a New Group button (violet). Below: list of all keyword groups.

Components

Keyword groups list

Each row: Group Name | Purpose | Status badge | Keyword count | (action buttons) | chevron link.

For groups with status=pending_review, an Approve button (emerald) appears inline. Clicking it calls PATCH /api/dm/keyword-groups/[id]/approve → changes status to approved. The row updates immediately on success.

Status badges: pending_review (amber), approved (emerald), rejected (red).

Clicking a row navigates to /seo/keyword-groups/[id].

API (proxy routes)

  • GET /api/dm/keyword-groups?tenantId=GET /dm/v1/keyword-groups?tenantId=
  • POST /api/dm/keyword-groupsPOST /dm/v1/keyword-groups
  • PATCH /api/dm/keyword-groups/[id]/approvePATCH /dm/v1/keyword-groups/[id]/approve

Screen P16c — Keyword Group Detail (/seo/keyword-groups/[id]) [Live]

Status: [Live] Purpose: View keywords in a group, add/remove keywords, and approve the group. DM has full edit access plus the ability to approve. Access: Click any row on the Keyword Groups list.

Layout

Page header: group name + status badge + purpose label. Back link to /seo/keyword-groups.

When status=pending_review, an Approve Group button (emerald) appears in the header. Clicking it calls PATCH /api/dm/keyword-groups/[id]/approve and refreshes the page.

Below header: table of keywords in the group + Add Keyword button (violet) that opens a right-side picker panel.

Components

Keywords-in-group table

Columns: Keyword | Difficulty | Intent | Category | Primary (star icon) | (Remove button)

  • Remove button removes the keyword from the group (does not delete the keyword itself).
  • DM can add keywords from any tenant keyword not yet in the group.

Add Keyword picker panel (right drawer)

Shows all tenant keywords not yet in this group. Each row has an Add button. Table refreshes after add.

Approval flow

Only the DM can approve keyword groups. Approval changes status from pending_review to approved. Once approved, the client dashboard shows the group as approved. The Approve Group button is only shown when status=pending_review — it is not shown for approved or rejected groups.

API (proxy routes)

  • GET /api/dm/keyword-groups/[id]?tenantId=GET /dm/v1/keyword-groups/[id]?tenantId=
  • POST /api/dm/keyword-groups/[id]/keywordsPOST /dm/v1/keyword-groups/[id]/keywords
  • DELETE /api/dm/keyword-groups/[id]/keywords/[keywordId]DELETE /dm/v1/keyword-groups/[id]/keywords/[keywordId]
  • PATCH /api/dm/keyword-groups/[id]/approvePATCH /dm/v1/keyword-groups/[id]/approve

Status: [Live] Purpose: Browse all backlink prospects for the active client. Click any row to open the detail page. Access: Via sidebar SEO group → “Backlinks” link.

Layout

Header with total count. Search input + status filter dropdown. Table with chevron column. Row click → /seo/backlinks/[id].

Table columns

ColumnContent
DomainSource domain + ExternalLink to sourceUrl
TypeProspect type (capitalized, dashes → spaces)
ScoreRelevance score badge (/10; emerald ≥8, amber ≥5, slate otherwise)
StatusStatus badge
RationalePitch rationale excerpt (truncated)
ChevronRight row hint
Status valueLabelBadge colour
prospectingProspectingslate
outreach_sentOutreach Sentblue
respondedRespondedviolet
agreedAgreedamber
publishedPublishedemerald
rejectedRejectedred
no_responseNo Responseslate (muted)

API (proxy route)

GET /dm/v1/backlinks?tenantId=&limit=100 — returns up to 100 backlink rows ordered newest first.


Status: [Live] Purpose: Full view of a single backlink prospect with editable status and notes, plus the linked campaign and outreach email.

Layout

Back link → /seo/backlinks. Header: source domain + status/DR/prospect type/link-type badges. Two-column grid (2:1).

Left column (2/3)

  • Prospect Details card — target URL, anchor text, relevance score bar, pitch rationale.
  • Outreach Timeline card — 5-step vertical timeline (Identified → Outreach Sent → Responded → Agreed → Link Live). Reflects the current outreachStatus from the status dropdown below.
  • Update Status & Notes cardoutreachStatus dropdown (all 7 values) + notes textarea + Save Changes button (violet). Calls PATCH /api/dm/backlinks/[id]. Shows a “Saved” confirmation on success.

Right column (1/3)

  • Campaign card — campaign name, status, prospect count, “View Campaign →” link. Empty state if none.
  • Outreach Email card — email status badge, sent date, recipient, subject, scrollable body. “Edit in Campaign →” link to /seo/link-building/[id] for full editing. Empty state if no email drafted.

API

  • GET /dm/v1/backlinks/:id?tenantId= — fetch single backlink with campaign + campaignEmail relations
  • PATCH /api/dm/backlinks/[id]PATCH /dm/v1/backlinks/:id — update outreachStatus and/or notes

Status: [Live] Purpose: List of all backlink outreach campaigns created by the backlink-researcher agent for the active client tenant. Each campaign holds a set of AI-drafted outreach emails ready for DM review. Access: Via sidebar SEO group → “Campaigns” link.

Layout

Page header “Outreach Campaigns” with campaign count. List of clickable campaign cards. Click row → Campaign Detail.

Components

Campaign card list

Each card row: Campaign name | Type badge | Status badge | email progress ({drafted}/{total}) | date | ChevronRight

StatusLabelBadge colour
draftDraftslate
dm_reviewNeeds Reviewamber
sentSentemerald

Empty state: “No campaigns yet — campaigns are created automatically when backlink research completes.”

API (proxy route)

GET /api/dm/campaigns?tenantId=GET /dm/v1/campaigns?tenantId= — returns Campaign rows with email count.


Screen P19 — Newsletters (/newsletters) [Live]

Status: [Live] Purpose: Review, approve, and reject AI-generated email newsletters for the active client.

View toggle

List/Calendar toggle in the page header (violet active state). Filters and pagination only visible in list view.

  • List view — status filter tabs (All / In Review / Approved / Sent) + paginated table. Clicking a row opens the newsletter detail page.
  • Calendar viewContentCalendar from @leadmetrics/ui; orange tiles; date from sentAt ?? createdAt. Popup shows Mail icon, Newsletter badge, status badge, subject, previewText, audience, recipient count (if sent), “View Newsletter →” link.

Table columns

ColumnContent
NewsletterMail icon (orange) + subject + previewText
StatusStatus badge (In Review / Approved / Sent / etc.)
RecipientsRecipient count if sent, otherwise
DatesentAt if sent, otherwise createdAt

Data source

GET /dm/v1/newsletters?tenantId=&page=&limit=20&status= — paginated, status-filtered list. A second request (limit=200, no status filter) fetches allItems for the calendar view.


Status: [Live] Purpose: Review and act on individual AI-drafted outreach emails within a campaign. DM can edit, approve, skip, or send each email. Bulk “Send All Approved” action available. Access: Click any campaign row on the Campaigns list.

Layout

Back link → /seo/campaigns. Campaign name header with status badge and email progress counter. Send All Approved button (violet, top-right) when any approved emails exist. Scrollable list of email cards.

Components

Email card

Each card shows:

  • Header: Source domain (hyperlink) + prospect type badge + relevance score + status badge
  • Recipient: name + email address (if crawled; editable)
  • Subject line (editable in edit mode)
  • Body (editable in edit mode — monospace textarea)
  • Actions (sticky card footer):
    • draft state: Approve (emerald Check) · Skip (slate X) · Edit (Pencil)
    • approved state: Send (blue Send) · Edit (Pencil)
    • skipped state: Approve (to un-skip)
    • sent state: read-only “Sent” badge with timestamp

Edit mode: Inline editing of subject, body, recipientEmail, recipientName. Save / Cancel buttons.

Outreach status lifecycle (CampaignEmail)

draftapprovedsent (or draftskipped)

Sending a single email: POST /api/campaigns/[campaignId]/emails/[emailId]/send

Bulk send all approved: POST /api/campaigns/[campaignId]/send-all

API (proxy routes)

  • GET /api/dm/campaigns/[id]?tenantId=GET /dm/v1/campaigns/[id]?tenantId=
  • PATCH /api/campaigns/[campaignId]/emails/[emailId]PATCH /dm/v1/campaigns/[campaignId]/emails/[emailId]
  • POST /api/campaigns/[campaignId]/emails/[emailId]/sendPOST /dm/v1/campaigns/[campaignId]/emails/[emailId]/send
  • POST /api/campaigns/[campaignId]/send-allPOST /dm/v1/campaigns/[campaignId]/send-all

Screen P20 — Channel Health (/channels/health) [Live]

Status: [Live] Purpose: Read-only view of a client’s channel connection status and health scores. Gives DM reviewers an at-a-glance view of which channels are working, which are at risk, and what needs the client’s attention. Access: Via sidebar “Channel Health” leaf item (HeartPulse icon), positioned after Activities.

Layout

Full-width page with a max-w-7xl content container.

Page header: “Channel Health” title + <Link> to /help (HelpCircle icon).

Top section (two columns on lg+):

  • Left — Overall Health Gauge: Semi-circle SVG gauge (radius 80, arc 251.33). Displays overall score (0–100) or ”—” if no scored channels. Score-coloured arc (green ≥70, amber 50–69, red <50). Trend badge below (↑ Improving / → Stable / ↓ Declining). “Based on N connected channels” subtitle.
  • Right — KPI Cards (2×2 grid): Total Channels / Connected / At Risk / Disconnected.

Attention Panel (amber border): Lists channels needing immediate action. critical items (red badge) = disconnected or score <30; warning items (amber badge) = score 30–49. Shows “Client action required — reconnect channel” text (read-only, no buttons).

Channel Grid (below): One card per connected channel. Card header: platform icon + channel name + connection status badge. Score arc indicator + score number. Health trend badge. “View Insights →” link to /channels/{channelId}.

Never-Connected Channels section: Separate list of channels in neverConnected state with “Never connected” label.

Data

All data fetched server-side in page.tsx via:

GET /dm/v1/channels/health-summary?tenantId={activeTenantId} Authorization: Bearer {token}

Response shape:

{ overallScore: number | null, overallTrend: string | null, // "improving" | "stable" | "declining" counts: { total, connected, disconnected, neverConnected, atRisk, scored }, channels: ChannelSummary[], attentionItems: AttentionItem[], }

Parity notes

  • No approve/reconnect buttons — DM portal is read-only; action items show “Client action required” text instead of buttons.
  • Dashboard portal /channels/health has the same layout with readonly={false}, showing reconnect/connect buttons.
  • Shared buildHealthSummary(tenantId) function in apps/api/src/routers/tenant/channel-health-summary.ts powers both endpoints.

Files

FileNotes
apps/dm/src/app/(dm)/channels/health/page.tsxServer component; fetches from API
apps/dm/src/app/(dm)/channels/health/ChannelHealthClient.tsxClient component; readonly=true default
apps/api/src/routers/dm/channels.tsGET /dm/v1/channels/health-summary

Screen P21 — Occasions (/occasions) [Live]

Purpose: Shows upcoming public holidays and cultural occasions relevant to the active tenant’s region (next 30 days). DMs can trigger occasion social posts directly from this screen.

Access: Sidebar → Content group → “Occasions” (PartyPopper icon)

Layout

Full-width page, p-6 padding.

Header: “Upcoming Occasions” title + subtitle “Important days in the next 30 days — create occasion posts for your client.”

Search input: Client-side text filter on occasion name.

Occasion grid: grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4

Each card contains:

  • PartyPopper icon in violet circle
  • Occasion name + optional description
  • Calendar date (formatted as “1 May 2026”)
  • Urgency badge — colour-coded: rose = ≤3 days, amber = ≤7 days, violet = >7 days. Text: “Today” / “Tomorrow” / “In N days”
  • Platform dropdown — instagram / linkedin / facebook / google_business_profile / x
  • “Create post” button — POSTs to /api/important-days/:id/create-post → creates Activity + SocialPost (contentType: “occasion”) → enqueues social-post-writer agent

On success the button is replaced by “Post queued — agents are writing it now” (green).

Empty state: Globe icon + “No upcoming occasions” message (shows when no occasions in next 30 days for this tenant’s country, or search filter matches nothing).

Country matching

Backend (GET /dm/v1/important-days/upcoming?tenantId=) filters by:

  • importantDay.isGlobal = true → always included
  • importantDay.countries includes tenant.country → included

Occasions with no matching countries (and not global) are excluded.

Post flow

Clicking “Create post” creates:

  1. Activity with deliverableType: "social_post", contentType: "occasion", dueDate = next occurrence
  2. SocialPost with contentType: "occasion", status: "queued"
  3. BullMQ job on social-post-writer queue

The social-post-writer uses a festive prompt for contentType === "occasion". The social-post-designer uses the SCENE_MAP["occasion"] scene: “Festive graphic composition with bold celebratory typography, brand colours prominent…”

After writing, the post enters the normal dm_review → client_review → client_approved → publish flow.

API routes (Next.js proxy)

MethodRouteProxies to
GET/api/important-daysGET /dm/v1/important-days/upcoming?tenantId=
POST/api/important-days/[id]/create-postPOST /dm/v1/important-days/:id/create-post

Backend routes (/dm/v1/, requireDMAccess)

MethodRouteDescription
GET/dm/v1/important-days/upcoming?tenantId=Returns occasions in next 30 days matching tenant country
POST/dm/v1/important-days/:id/create-postCreates activity + social post + enqueues writer

Files

FileNotes
apps/dm/src/app/(dm)/occasions/page.tsxServer component; fetches upcoming occasions
apps/dm/src/app/(dm)/occasions/OccasionsClient.tsxClient component; search + cards
apps/dm/src/app/api/important-days/route.tsProxy: GET upcoming
apps/dm/src/app/api/important-days/[id]/create-post/route.tsProxy: POST create-post
apps/api/src/routers/dm/important-days.tsFastify DM routes

© 2026 Leadmetrics — Internal use only