| name | radar |
| description | Competitor/industry monitoring feature - track social accounts, detect viral content, analyze trends. |
Radar Feature Development
Radar is OpenPromo's competitor monitoring and content discovery tool. It tracks TikTok and Instagram accounts, syncs their content, detects anomalies (viral spikes, outperformers), and helps users discover trending content ideas.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Dashboard UI │
│ RadarPage → RadarSourcesPanel, RadarContentFeed, RadarDigest │
└────────────────────────────┬────────────────────────────────────┘
│ orpc
┌────────────────────────────▼────────────────────────────────────┐
│ Worker (orpc routes) │
│ packages/dash/worker/src/orpc/routes/radar.ts │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────────▼────────────────────────────────────┐
│ Core Domain Logic │
│ packages/core/src/domain/radar/ │
│ ├── entity/ │
│ │ ├── EntRadarSource.ts (tracked accounts) │
│ │ ├── EntRadarContentItem.ts (synced posts) │
│ │ ├── EntRadarSavedIdea.ts (bookmarked content) │
│ │ └── EntRadarDigest.ts (AI summaries) │
│ ├── radar-sync-service.ts (fetches & processes content) │
│ └── radar-digest-service.ts (generates AI summaries) │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────────▼────────────────────────────────────┐
│ External APIs (TikHub) │
│ packages/core/src/providers/tikhub/ │
│ ├── client.ts (HTTP client with KV caching) │
│ ├── tikhub.ts (unified exports) │
│ └── tikhub-tiktok-web.ts (TikTok Web API types) │
└─────────────────────────────────────────────────────────────────┘
Key Files
Data Models (Drizzle)
packages/core/src/schemas/radar.sql.ts- Table definitionsradarSourcesTable- Tracked accounts (TikTok, Instagram)radarContentItemsTable- Synced posts/videosradarSavedIdeasTable- User bookmarksradarDigestsTable- AI-generated summaries
Entities
EntRadarSource- Add/remove/pause tracked sources, validationEntRadarContentItem- Store/query synced content, anomaly flagsEntRadarSavedIdea- Save/unsave content as ideasEntRadarDigest- Generate/store weekly digests
Services
radar-sync-service.ts- Core sync logicsyncSource(source)- Fetch posts, detect anomalies, storesyncWorkspace(workspaceId)- Sync all sources for workspace- Anomaly detection: viral_spike (5x avg), outperformer (2x avg)
API Routes
packages/dash/worker/src/orpc/routes/radar.ts- Sources:
addSource,listSources,removeSource,pauseSource,resumeSource - Content:
listContent,listAnomalies - Ideas:
saveIdea,listIdeas,unsaveIdea - Digests:
getLatestDigest,listDigests,generateDigest - Sync:
triggerSync
- Sources:
UI Components
packages/dash/ui/src/components/radar/RadarPage.tsx- Main page with tabs (Anomalies, All, Saved)RadarSourcesPanel.tsx- Grid of tracked source cardsRadarAddSourceForm.tsx- URL input to add sourcesRadarContentFeed.tsx- Grid of content cardsRadarEmptyState.tsx- Empty state with onboardingRadarDigestCard.tsx- AI digest displayradar-url-parser.ts- Parse TikTok/IG profile URLs
Query Hooks
packages/dash/ui/src/queries/radar.tsuseRadarSources,useAddRadarSource,useRemoveRadarSourceuseRadarContent,useRadarAnomaliesuseRadarIdeas,useSaveRadarIdea,useUnsaveRadarIdeauseTriggerRadarSync,useGenerateRadarDigest
TikHub Integration
TikHub provides the social media APIs. API calls are cached in KV (24h TTL).
TikTok
// Get secUid from username (required for fetching posts)
TikHub.TikTokWeb.getSecUid(username)
// Get user profile
TikHub.TikTokWeb.fetchUserInfo({ user_id: secUid })
// Get user posts (returns up to 20)
TikHub.TikTokWeb.fetchUserPosts({ secUid, count: 20 })
// Get user profile (data nested under data.data)
TikHub.InstagramV2.fetchUserInfo({ username })
// Get user posts (data nested under data.data.items)
TikHub.InstagramV2.fetchUserPosts({ username })
Important: Instagram API responses are nested under data.data, not data directly.
Adding New Source Types
- Add type to
radarSourceTypePgEnuminradar.sql.ts - Add validation method to
TikHubPlatformValidatorinEntRadarSource.ts - Add fetch method to
TikHubPlatformFetcherinradar-sync-service.ts - Update
fetchContent()switch in sync service - Update URL parser in
radar-url-parser.ts - Update UI icons in components
Anomaly Detection
Content is flagged as anomaly based on views vs baseline:
- viral_spike: 5x+ average views (score 0.8-1.0)
- outperformer: 2x-5x average views (score 0.3-0.79)
Baseline is calculated from last 20 posts with min 5 posts required.
Sync Flow
User adds source via URL →
EntRadarSource.add()- Validates account exists via TikHub API
- Extracts profile data (avatar, stats)
- Stores source with
status: 'active' - Auto-triggers sync for new source
Sync runs (manual or cron) →
radarSyncService.syncSource()- Fetches latest posts from platform API
- Calculates/updates baseline metrics
- Detects anomalies based on views
- Upserts content items with anomaly flags
- Updates
lastSyncedAttimestamp
UI Patterns
Source Cards
<SourceCard source={source} />
// Shows: avatar, @username, platform icon, follower count, video count
// Actions: pause/resume, remove (with ConfirmDialog)
Content Cards
<RadarContentCard item={item} />
// Uses GridCard building blocks from @/components/common/grid-card
// Shows: thumbnail, metrics (views/likes/comments), anomaly badge
// Actions: Save Idea (hover), Open Original (dropdown)
Add Source Flow
- User pastes TikTok/IG profile URL
- URL parsed → preview card shown
- User clicks "Add Source"
- Source validated, created, auto-synced
Common Tasks
Debug sync issues
// Check logs for:
console.log("[radar] syncing source", { sourceId, sourceType, identifier });
console.log("[radar] fetched content", { count });
log.info("instagram posts fetched", { count });
Test API responses
Use labs route or add temp logging in validator/fetcher functions.
Clear KV cache
// Cache keys are prefixed with "tikhub:"
const key = `tikhub:${url}`;
await kv.delete(key);
Route Configuration
Route: /workspaces/:workspaceSlug/radar
Search params (zod validated):
const radarSearchSchema = z.object({
tab: z.enum(["anomalies", "all", "saved"]).catch("anomalies"),
});