feat: Sprint 1 - Deep discovery, lead persistence, and dashboard nav

- Add BacklinkOutreachScraper (Exa + DuckDuckGo deep scraping)
- Extend DB and Pydantic models for lead enrichment columns
- Add StorageService methods for lead CRUD with auto-migration
- Add backend endpoints: deep discover, campaign detail, lead management
- Extend frontend API client and store with discovery + lead actions
- Create BacklinkOutreachDashboard component with campaigns/discover/leads tabs
- Register route at /backlink-outreach under SEO feature flag
- Add nav entry under Enterprise & Advanced in tool categories
This commit is contained in:
ajaysi
2026-05-23 17:07:33 +05:30
parent 816d59a30a
commit 090d69761f
22 changed files with 3494 additions and 48 deletions

View File

@@ -104,3 +104,87 @@ export const fetchBacklinkReportingSnapshot = async (): Promise<BacklinkReportin
export const createBacklinkCampaign = async (payload: BacklinkCampaignCreateRequest): Promise<BacklinkCampaignCreateResponse> => (await apiClient.post('/api/backlink-outreach/campaigns', payload)).data;
export const listBacklinkCampaigns = async (user_id: string, workspace_id: string): Promise<BacklinkCampaignListResponse> => (await apiClient.get('/api/backlink-outreach/campaigns', { params: { user_id, workspace_id } })).data;
// -- Deep Discovery --
export interface EnrichedOpportunity {
url: string;
domain: string;
page_title: string;
snippet: string;
full_text: string;
email: string | null;
contact_page: string | null;
confidence_score: number;
quality_score: number;
word_count: number;
has_guest_post_guidelines: boolean;
discovery_source: string;
}
export interface DeepDiscoveryRequest {
keyword: string;
max_results?: number;
campaign_id?: string;
}
export interface DeepDiscoveryResponse {
keyword: string;
source: string;
total_found: number;
opportunities: EnrichedOpportunity[];
}
export const discoverDeepBacklinkOpportunities = async (payload: DeepDiscoveryRequest): Promise<DeepDiscoveryResponse> => (await apiClient.post('/api/backlink-outreach/discover/deep', payload)).data;
// -- Leads --
export interface LeadRecord {
lead_id: string;
campaign_id: string;
url: string | null;
domain: string;
page_title: string;
snippet: string;
email: string | null;
confidence_score: number;
discovery_source: string;
status: string;
notes: string | null;
created_at: string | null;
}
export interface LeadListResponse {
leads: LeadRecord[];
total: number;
}
export interface LeadCreateRequest {
campaign_id: string;
url: string;
domain: string;
email?: string;
page_title?: string;
snippet?: string;
confidence_score?: number;
notes?: string;
}
export interface LeadStatusUpdateRequest {
status: string;
notes?: string;
}
export interface CampaignDetailResponse {
campaign_id: string;
name: string;
status: string;
created_at: string | null;
lead_count: number;
leads: LeadRecord[];
}
export const fetchCampaignDetail = async (campaign_id: string, user_id: string): Promise<CampaignDetailResponse> => (await apiClient.get(`/api/backlink-outreach/campaigns/${campaign_id}`, { params: { user_id } })).data;
export const fetchCampaignLeads = async (campaign_id: string, user_id: string, status?: string): Promise<LeadListResponse> => (await apiClient.get(`/api/backlink-outreach/campaigns/${campaign_id}/leads`, { params: { user_id, status } })).data;
export const addLeadToCampaign = async (campaign_id: string, payload: LeadCreateRequest): Promise<LeadRecord> => (await apiClient.post(`/api/backlink-outreach/campaigns/${campaign_id}/leads`, payload)).data;
export const updateLeadStatus = async (lead_id: string, payload: LeadStatusUpdateRequest): Promise<LeadRecord> => (await apiClient.patch(`/api/backlink-outreach/leads/${lead_id}/status`, payload)).data;

View File

@@ -0,0 +1,65 @@
import { apiClient } from './client';
export interface BlogAsset {
id: number;
title: string | null;
description: string | null;
tags: string[];
phase: string;
research_keywords: string | null;
topic: string | null;
selected_title: string | null;
word_count_target: number | null;
has_research: boolean;
has_outline: boolean;
has_content: boolean;
has_seo: boolean;
has_publish: boolean;
created_at: string | null;
updated_at: string | null;
}
export interface BlogAssetFull extends BlogAsset {
research_data?: any;
outline_data?: any;
content_data?: any;
seo_data?: any;
publish_data?: any;
}
export interface CreateAssetParams {
research_keywords: string;
topic?: string;
word_count_target?: number;
}
export interface UpdateAssetParams {
phase?: 'research' | 'outline' | 'content' | 'seo' | 'publish';
topic?: string;
selected_title?: string;
word_count_target?: number;
research_data?: any;
outline_data?: any;
content_data?: any;
seo_data?: any;
publish_data?: any;
}
class BlogAssetAPI {
async create(params: CreateAssetParams): Promise<{ success: boolean; asset: BlogAsset; existing: boolean }> {
const res = await apiClient.post('/api/blog/asset', params);
return res.data;
}
async update(assetId: number, params: UpdateAssetParams): Promise<{ success: boolean; asset: BlogAsset }> {
const res = await apiClient.put(`/api/blog/asset/${assetId}`, params);
return res.data;
}
async get(assetId: number): Promise<{ success: boolean; asset: BlogAssetFull }> {
const res = await apiClient.get(`/api/blog/asset/${assetId}`);
return res.data;
}
}
export const blogAssetAPI = new BlogAssetAPI();