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 isw-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) —CalendarDaysicon — 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:newevents via/tenantnamespace for real-time updates - Shows last 50 notifications for the active tenant (unread highlighted in violet)
- “Mark all read” →
POST /api/dm/notifications/read-allwith{ tenantId } - Shows “Select a client to see notifications” when no tenant is active
activeTenantIdis passed as a prop from the server layout (readsdm_active_tenantcookie)
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 toGET /dm/v1/notifications?tenantId=POST /api/dm/notifications/read-all→ proxies toPOST /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 memberPOST /dm/v1/notifications/read-all— marks all unread as read for{ tenantId }in body
Navigation Structure (DM Portal)
The DM portal sidebar includes the following nav groups (in addition to individual items):
| Route | Label | Icon | Notes |
|---|---|---|---|
| (Content group) | Content | Layers | NavGroup — auto-expands when on any content/* route |
/blog | Blog Posts | BookOpen | |
/landing-pages | Landing Pages | Globe | |
/content-briefs | Content Briefs | ClipboardPen | |
/social | Social Posts | Images | |
/newsletters | Newsletters | ||
/occasions | Occasions | PartyPopper | Upcoming important days; DM triggers occasion posts |
/media | Media Library | GalleryHorizontalEnd | |
| (SEO group) | SEO | SearchCode | NavGroup — auto-expands when on any /seo/* route |
/seo/backlinks | Backlinks | Link2 | |
/seo/link-building | Link Building | Hammer | Backlink outreach campaigns |
/seo/keywords | Keywords | KeyRound | |
/seo/keyword-groups | Keyword Groups | FolderSearch |
All icons from lucide-react.
| Screen | Status |
|---|---|
| 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, optionaldeliverablePeriodId).
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 view —
ContentCalendarfrom@leadmetrics/ui; violet tiles; date fromscheduledAt ?? 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:
| Platform | Preview style |
|---|---|
| White card; avatar header; square image; ♥ 💬 ➦ / 🔖 actions; caption + hashtags | |
| ”YB” avatar; name + followers + “Just now”; body text; image below; reactions row | |
| Avatar; timestamp; body text; image; Like/Comment/Share | |
| X (Twitter) | Avatar; @yourbrand; tweet text + image inline; reply/repost/like/share |
| TikTok | Black 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.
Status-gated actions (sticky footer)
| Status | Actions |
|---|---|
dm_review | Reject Copy (opens feedback modal → POST /api/dm/social/{id}/dm-reject) · Approve Copy (→ POST /api/dm/social/{id}/dm-approve → enqueues designer) |
client_review | Reject Design (opens feedback modal → POST /api/dm/social/{id}/design-reject) · read-only “sent to client” note |
| other | Status badge + approved-by info (read-only) |
Playwright note: Sticky footer buttons do not fire React handlers via normal
browser_click. Usebrowser_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 Overview —
AgentRunStatscomponent: 4 stat cards (Total Runs · Total Time · Total Cost · Avg Duration) + 30-day bar chart scoped to active tenant. Data fromGET /dm/v1/agents/:agentId/run-stats?tenantId=. - Agent Milestones —
ActivityLogentries whereactorType = "agent"andactor = agent.role, scoped to active tenant. Shows action, detail (2-line clamp), View link, timeAgo timestamp. - Assigned Activities —
Activityrows whereagentQueue = 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.tsx → triggerContextPdfDownload)
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.tsx → triggerPdfDownload)
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.5teal bar, 2-col grid ofDeliverableItemCardlinks - 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 modal — GenerateReportModal:
- 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/generate→router.refresh()
Upload Report modal — UploadReportModal:
- 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 placeholderfailed: 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)
| Column | Details |
|---|---|
| Difficulty | Badge: low (emerald), medium (amber), high (red) |
| Intent | Badge: informational / commercial / transactional / navigational |
| Source | Badge: 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/keywords→POST /dm/v1/keywordsPATCH /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-groups→POST /dm/v1/keyword-groupsPATCH /api/dm/keyword-groups/[id]/approve→PATCH /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]/keywords→POST /dm/v1/keyword-groups/[id]/keywordsDELETE /api/dm/keyword-groups/[id]/keywords/[keywordId]→DELETE /dm/v1/keyword-groups/[id]/keywords/[keywordId]PATCH /api/dm/keyword-groups/[id]/approve→PATCH /dm/v1/keyword-groups/[id]/approve
Screen P17 — Backlinks (/seo/backlinks) [Live]
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
| Column | Content |
|---|---|
| Domain | Source domain + ExternalLink to sourceUrl |
| Type | Prospect type (capitalized, dashes → spaces) |
| Score | Relevance score badge (/10; emerald ≥8, amber ≥5, slate otherwise) |
| Status | Status badge |
| Rationale | Pitch rationale excerpt (truncated) |
| → | ChevronRight row hint |
| Status value | Label | Badge colour |
|---|---|---|
prospecting | Prospecting | slate |
outreach_sent | Outreach Sent | blue |
responded | Responded | violet |
agreed | Agreed | amber |
published | Published | emerald |
rejected | Rejected | red |
no_response | No Response | slate (muted) |
API (proxy route)
GET /dm/v1/backlinks?tenantId=&limit=100 — returns up to 100 backlink rows ordered newest first.
Screen P17b — Backlink Detail (/seo/backlinks/[id]) [Live]
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
outreachStatusfrom the status dropdown below. - Update Status & Notes card —
outreachStatusdropdown (all 7 values) + notes textarea + Save Changes button (violet). CallsPATCH /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 withcampaign+campaignEmailrelationsPATCH /api/dm/backlinks/[id]→PATCH /dm/v1/backlinks/:id— updateoutreachStatusand/ornotes
Screen P18 — Link Building (/seo/link-building) [Live]
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
| Status | Label | Badge colour |
|---|---|---|
draft | Draft | slate |
dm_review | Needs Review | amber |
sent | Sent | emerald |
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 view —
ContentCalendarfrom@leadmetrics/ui; orange tiles; date fromsentAt ?? createdAt. Popup shows Mail icon, Newsletter badge, status badge, subject, previewText, audience, recipient count (if sent), “View Newsletter →” link.
Table columns
| Column | Content |
|---|---|
| Newsletter | Mail icon (orange) + subject + previewText |
| Status | Status badge (In Review / Approved / Sent / etc.) |
| Recipients | Recipient count if sent, — otherwise |
| Date | sentAt 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.
Screen P18b — Link Building Detail (/seo/link-building/[id]) [Live]
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):
draftstate: Approve (emerald Check) · Skip (slate X) · Edit (Pencil)approvedstate: Send (blue Send) · Edit (Pencil)skippedstate: Approve (to un-skip)sentstate: read-only “Sent” badge with timestamp
Edit mode: Inline editing of subject, body, recipientEmail, recipientName. Save / Cancel buttons.
Outreach status lifecycle (CampaignEmail)
draft → approved → sent (or draft → skipped)
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]/send→POST /dm/v1/campaigns/[campaignId]/emails/[emailId]/sendPOST /api/campaigns/[campaignId]/send-all→POST /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/healthhas the same layout withreadonly={false}, showing reconnect/connect buttons. - Shared
buildHealthSummary(tenantId)function inapps/api/src/routers/tenant/channel-health-summary.tspowers both endpoints.
Files
| File | Notes |
|---|---|
apps/dm/src/app/(dm)/channels/health/page.tsx | Server component; fetches from API |
apps/dm/src/app/(dm)/channels/health/ChannelHealthClient.tsx | Client component; readonly=true default |
apps/api/src/routers/dm/channels.ts | GET /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→ createsActivity+SocialPost(contentType: “occasion”) → enqueuessocial-post-writeragent
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 includedimportantDay.countriesincludestenant.country→ included
Occasions with no matching countries (and not global) are excluded.
Post flow
Clicking “Create post” creates:
ActivitywithdeliverableType: "social_post",contentType: "occasion",dueDate= next occurrenceSocialPostwithcontentType: "occasion",status: "queued"- BullMQ job on
social-post-writerqueue
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)
| Method | Route | Proxies to |
|---|---|---|
GET | /api/important-days | GET /dm/v1/important-days/upcoming?tenantId= |
POST | /api/important-days/[id]/create-post | POST /dm/v1/important-days/:id/create-post |
Backend routes (/dm/v1/, requireDMAccess)
| Method | Route | Description |
|---|---|---|
GET | /dm/v1/important-days/upcoming?tenantId= | Returns occasions in next 30 days matching tenant country |
POST | /dm/v1/important-days/:id/create-post | Creates activity + social post + enqueues writer |
Files
| File | Notes |
|---|---|
apps/dm/src/app/(dm)/occasions/page.tsx | Server component; fetches upcoming occasions |
apps/dm/src/app/(dm)/occasions/OccasionsClient.tsx | Client component; search + cards |
apps/dm/src/app/api/important-days/route.ts | Proxy: GET upcoming |
apps/dm/src/app/api/important-days/[id]/create-post/route.ts | Proxy: POST create-post |
apps/api/src/routers/dm/important-days.ts | Fastify DM routes |