diff --git a/PENDING-ISSUES.md b/PENDING-ISSUES.md
index 99b682ad..4e471554 100644
--- a/PENDING-ISSUES.md
+++ b/PENDING-ISSUES.md
@@ -1,18 +1,11 @@
-# Pending Issues
-
-## 🔴 Django Admin - Logo Missing on Subpages
-
-**Issue:** "IGNY8 Admin" logo with rocket icon appears on homepage but not on model list/detail/edit pages.
-
-**Expected:** Logo should appear consistently across all admin pages (homepage, app index, and all model subpages).
-
----
## 🔴 AI FUunctions progress modals texts and counts to be fixed
## 🔴 AUTOAMTION queue when run manualy completed count to be fixed, and progress abr to be imrpoved and fixed based on actual stage and all other data have bugs
-## 🔴 Improve the metrics below tbale son all module pages, with actiionable metrics and steps
-
## 🔴 Align prompts with teh strategy
-## 🔴 user randomly logs out often,
+## 🔴 user randomly logs out often
+
+## 🔴 MArketing site cotnetn
+
+## 🔴 docuementation adn help update
diff --git a/frontend/src/components/common/PageHeader.tsx b/frontend/src/components/common/PageHeader.tsx
index 7182a030..e884e8d0 100644
--- a/frontend/src/components/common/PageHeader.tsx
+++ b/frontend/src/components/common/PageHeader.tsx
@@ -9,6 +9,7 @@ import { useSectorStore } from '../../store/sectorStore';
import SiteAndSectorSelector from './SiteAndSectorSelector';
import { trackLoading } from './LoadingStateMonitor';
import { useErrorHandler } from '../../hooks/useErrorHandler';
+import { WorkflowInsights, WorkflowInsight } from './WorkflowInsights';
interface PageHeaderProps {
title: string;
@@ -22,6 +23,7 @@ interface PageHeaderProps {
};
hideSiteSector?: boolean; // Hide site/sector selector and info for global pages
navigation?: ReactNode; // Module navigation tabs
+ workflowInsights?: WorkflowInsight[]; // Actionable insights for current page
}
export default function PageHeader({
@@ -33,6 +35,7 @@ export default function PageHeader({
badge,
hideSiteSector = false,
navigation,
+ workflowInsights,
}: PageHeaderProps) {
const { activeSite } = useSiteStore();
const { activeSector, loadSectorsForSite } = useSectorStore();
@@ -98,77 +101,87 @@ export default function PageHeader({
};
return (
-
- {/* Left side: Title, badge, and site/sector info */}
-
-
- {badge && (
-
- {badge.icon && typeof badge.icon === 'object' && 'type' in badge.icon
- ? React.cloneElement(badge.icon as React.ReactElement, { className: 'text-white size-5' })
- : badge.icon}
+
+ {/* Main header row - single row with 3 sections */}
+
+ {/* Left side: Title, badge, and site/sector info */}
+
+
+ {badge && (
+
+ {badge.icon && typeof badge.icon === 'object' && 'type' in badge.icon
+ ? React.cloneElement(badge.icon as React.ReactElement, { className: 'text-white size-5' })
+ : badge.icon}
+
+ )}
+
{title}
+
+ {!hideSiteSector && (
+
+ {lastUpdated && (
+ <>
+
+ Last updated: {lastUpdated.toLocaleTimeString()}
+
+ >
+ )}
+ {activeSite && (
+ <>
+ {lastUpdated &&
• }
+
+ Site: {activeSite.name}
+
+ >
+ )}
+ {activeSector && (
+ <>
+
•
+
+ Sector: {activeSector.name}
+
+ >
+ )}
+ {!activeSector && activeSite && (
+ <>
+
•
+
+ Sector: All Sectors
+
+ >
+ )}
)}
-
{title}
-
- {!hideSiteSector && (
-
- {lastUpdated && (
- <>
-
- Last updated: {lastUpdated.toLocaleTimeString()}
-
- >
- )}
- {activeSite && (
- <>
- {lastUpdated &&
• }
-
- Site: {activeSite.name}
-
- >
- )}
- {activeSector && (
- <>
-
•
-
- Sector: {activeSector.name}
-
- >
- )}
- {!activeSector && activeSite && (
- <>
-
•
-
- Sector: All Sectors
-
- >
- )}
-
- )}
- {hideSiteSector && lastUpdated && (
-
-
- Last updated: {lastUpdated.toLocaleTimeString()}
-
-
- )}
-
-
- {/* Right side: Navigation bar stacked above site/sector selector */}
-
- {navigation &&
{navigation}
}
-
- {!hideSiteSector &&
}
- {showRefresh && onRefresh && (
-
- Refresh
-
+ {hideSiteSector && lastUpdated && (
+
+
+ Last updated: {lastUpdated.toLocaleTimeString()}
+
+
)}
+
+ {/* Middle: Workflow insights - takes available space */}
+ {workflowInsights && workflowInsights.length > 0 && (
+
+
+
+ )}
+
+ {/* Right side: Navigation bar stacked above site/sector selector */}
+
+ {navigation &&
{navigation}
}
+
+ {!hideSiteSector && }
+ {showRefresh && onRefresh && (
+
+ Refresh
+
+ )}
+
+
);
diff --git a/frontend/src/components/common/WorkflowInsights.tsx b/frontend/src/components/common/WorkflowInsights.tsx
new file mode 100644
index 00000000..8b68006a
--- /dev/null
+++ b/frontend/src/components/common/WorkflowInsights.tsx
@@ -0,0 +1,92 @@
+/**
+ * WorkflowInsights Component
+ * Shows actionable insights in a collapsible notification box
+ * Placed between page title and navigation tabs
+ */
+import React, { useState } from 'react';
+import { CheckCircleIcon, AlertIcon, InfoIcon, BoltIcon, CloseIcon } from '../../icons';
+
+export interface WorkflowInsight {
+ type: 'success' | 'warning' | 'info' | 'action';
+ message: string;
+}
+
+interface WorkflowInsightsProps {
+ insights: WorkflowInsight[];
+ className?: string;
+}
+
+export const WorkflowInsights: React.FC
= ({ insights, className = '' }) => {
+ const [isCollapsed, setIsCollapsed] = useState(false);
+
+ if (!insights || insights.length === 0 || isCollapsed) return null;
+
+ // Determine notification color based on most critical insight type
+ const hasCritical = insights.some(i => i.type === 'warning' || i.type === 'action');
+ const hasSuccess = insights.some(i => i.type === 'success');
+
+ const notificationColors = hasCritical
+ ? {
+ bg: 'bg-amber-50 dark:bg-amber-500/10',
+ border: 'border-amber-300 dark:border-amber-700',
+ iconBg: 'bg-amber-100 dark:bg-amber-500/20',
+ }
+ : hasSuccess
+ ? {
+ bg: 'bg-green-50 dark:bg-green-500/10',
+ border: 'border-green-300 dark:border-green-700',
+ iconBg: 'bg-green-100 dark:bg-green-500/20',
+ }
+ : {
+ bg: 'bg-blue-50 dark:bg-blue-500/10',
+ border: 'border-blue-300 dark:border-blue-700',
+ iconBg: 'bg-blue-100 dark:bg-blue-500/20',
+ };
+
+ const getIcon = (type: string) => {
+ switch (type) {
+ case 'success':
+ return ;
+ case 'warning':
+ return ;
+ case 'action':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+ {/* Icon */}
+
+ {getIcon(insights[0].type)}
+
+
+ {/* Content */}
+
+ {insights.map((insight, index) => (
+
+ {insights.length > 1 && (
+
•
+ )}
+
+ {insight.message}
+
+
+ ))}
+
+
+ {/* Close button */}
+
setIsCollapsed(true)}
+ className="flex-shrink-0 p-1 rounded hover:bg-gray-200/50 dark:hover:bg-gray-700/50 transition-colors"
+ aria-label="Dismiss"
+ >
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/header/HeaderMetrics.tsx b/frontend/src/components/header/HeaderMetrics.tsx
index 1d530936..a563d8f7 100644
--- a/frontend/src/components/header/HeaderMetrics.tsx
+++ b/frontend/src/components/header/HeaderMetrics.tsx
@@ -1,5 +1,7 @@
import React from 'react';
import { useHeaderMetrics } from '../../context/HeaderMetricsContext';
+import { Tooltip } from '../ui/tooltip/Tooltip';
+import { InfoIcon } from '../../icons';
export const HeaderMetrics: React.FC = () => {
const { metrics } = useHeaderMetrics();
@@ -8,18 +10,35 @@ export const HeaderMetrics: React.FC = () => {
return (
- {metrics.map((metric, index) => (
-
-
+ {metrics.map((metric, index) => {
+ const metricElement = (
+
-
{metric.label}
+
+ {metric.label}
+ {metric.tooltip && (
+
+ )}
+
{typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value}
- {index < metrics.length - 1 &&
}
-
- ))}
+ );
+
+ return (
+
+ {metric.tooltip ? (
+
+ {metricElement}
+
+ ) : (
+ metricElement
+ )}
+ {index < metrics.length - 1 &&
}
+
+ );
+ })}
);
};
diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx
index ab9312a8..36183c01 100644
--- a/frontend/src/config/pages/clusters.config.tsx
+++ b/frontend/src/config/pages/clusters.config.tsx
@@ -420,28 +420,32 @@ export const createClustersPageConfig = (
],
headerMetrics: [
{
- label: 'Total Clusters',
+ label: 'Clusters',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0,
+ tooltip: 'Topic clusters organizing your keywords. Each cluster should have 3-7 related keywords.',
},
{
- label: 'Active',
- value: 0,
- accentColor: 'green' as const,
- calculate: (data) => data.clusters.filter((c: Cluster) => c.status === 'active').length,
- },
- {
- label: 'Total Keywords',
+ label: 'New',
value: 0,
accentColor: 'amber' as const,
- calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.keywords_count || 0), 0),
+ calculate: (data) => data.clusters.filter((c: Cluster) => (c.ideas_count || 0) === 0).length,
+ tooltip: 'Clusters without content ideas yet. Generate ideas for these clusters to move them into the pipeline.',
},
{
- label: 'Total Volume',
+ label: 'Keywords',
value: 0,
accentColor: 'purple' as const,
+ calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.keywords_count || 0), 0),
+ tooltip: 'Total keywords organized across all clusters. More keywords = better topic coverage.',
+ },
+ {
+ label: 'Volume',
+ value: 0,
+ accentColor: 'green' as const,
calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.volume || 0), 0),
+ tooltip: 'Combined search volume across all clusters. Prioritize high-volume clusters for maximum traffic.',
},
],
};
diff --git a/frontend/src/config/pages/content.config.tsx b/frontend/src/config/pages/content.config.tsx
index 43fbcf17..21196fea 100644
--- a/frontend/src/config/pages/content.config.tsx
+++ b/frontend/src/config/pages/content.config.tsx
@@ -440,22 +440,32 @@ export const createContentPageConfig = (
],
headerMetrics: [
{
- label: 'Total Content',
+ label: 'Content',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0,
+ tooltip: 'Total content pieces generated. Includes drafts, review, and published content.',
},
{
label: 'Draft',
value: 0,
accentColor: 'amber' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'draft').length,
+ tooltip: 'Content in draft stage. Edit and refine before moving to review.',
+ },
+ {
+ label: 'In Review',
+ value: 0,
+ accentColor: 'blue' as const,
+ calculate: (data) => data.content.filter((c: Content) => c.status === 'review').length,
+ tooltip: 'Content awaiting review and approval. Review for quality before publishing.',
},
{
label: 'Published',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'published').length,
+ tooltip: 'Published content ready for WordPress sync. Track your published library.',
},
],
};
diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx
index bf7612a2..19aa86ab 100644
--- a/frontend/src/config/pages/ideas.config.tsx
+++ b/frontend/src/config/pages/ideas.config.tsx
@@ -387,28 +387,32 @@ export const createIdeasPageConfig = (
],
headerMetrics: [
{
- label: 'Total Ideas',
+ label: 'Ideas',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0,
+ tooltip: 'Total content ideas generated. Ideas become tasks in the content queue for writing.',
},
{
label: 'New',
value: 0,
accentColor: 'amber' as const,
calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'new').length,
+ tooltip: 'New ideas waiting for review. Approve ideas to queue them for content creation.',
},
{
label: 'Queued',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'queued').length,
+ tooltip: 'Ideas queued for content generation. These will be converted to writing tasks automatically.',
},
{
label: 'Completed',
value: 0,
accentColor: 'green' as const,
- calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'published').length,
+ calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'completed').length,
+ tooltip: 'Ideas that have been successfully turned into content. Track your content creation progress.',
},
],
};
diff --git a/frontend/src/config/pages/images.config.tsx b/frontend/src/config/pages/images.config.tsx
index 4c669323..b3658eda 100644
--- a/frontend/src/config/pages/images.config.tsx
+++ b/frontend/src/config/pages/images.config.tsx
@@ -217,28 +217,32 @@ export const createImagesPageConfig = (
],
headerMetrics: [
{
- label: 'Total Content',
+ label: 'Content',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0,
+ tooltip: 'Total content pieces with image generation. Track image coverage across all content.',
},
{
label: 'Complete',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'complete').length,
+ tooltip: 'Content with all images generated. Ready for publishing with full visual coverage.',
},
{
label: 'Partial',
value: 0,
- accentColor: 'info' as const,
+ accentColor: 'blue' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'partial').length,
+ tooltip: 'Content with some images missing. Generate remaining images to complete visual assets.',
},
{
label: 'Pending',
value: 0,
accentColor: 'amber' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'pending').length,
+ tooltip: 'Content waiting for image generation. Queue these to start creating visual assets.',
},
],
maxInArticleImages: maxImages,
diff --git a/frontend/src/config/pages/keywords.config.tsx b/frontend/src/config/pages/keywords.config.tsx
index 73ee4f28..3c646499 100644
--- a/frontend/src/config/pages/keywords.config.tsx
+++ b/frontend/src/config/pages/keywords.config.tsx
@@ -476,28 +476,32 @@ export const createKeywordsPageConfig = (
],
headerMetrics: [
{
- label: 'Total Keywords',
+ label: 'Keywords',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0,
+ tooltip: 'Total keywords added to site wrokflow. Minimum 5 Keywords are needed for clustering.',
},
{
- label: 'Mapped',
+ label: 'Clustered',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.keywords.filter((k: Keyword) => k.cluster_id).length,
+ tooltip: 'Keywords grouped into topical clusters. Clustered keywords are ready for content ideation.',
},
{
label: 'Unmapped',
value: 0,
accentColor: 'amber' as const,
calculate: (data) => data.keywords.filter((k: Keyword) => !k.cluster_id).length,
+ tooltip: 'Unclustered keywords waiting to be organized. Select keywords and use Auto-Cluster to group them.',
},
{
- label: 'Total Volume',
+ label: 'Volume',
value: 0,
accentColor: 'purple' as const,
calculate: (data) => data.keywords.reduce((sum: number, k: Keyword) => sum + (k.volume || 0), 0),
+ tooltip: 'Total monthly search volume across all keywords. Higher volume = more traffic potential.',
},
],
// bulkActions and rowActions are now global - defined in table-actions.config.ts
diff --git a/frontend/src/config/pages/published.config.tsx b/frontend/src/config/pages/published.config.tsx
index fd16eed8..d8550a96 100644
--- a/frontend/src/config/pages/published.config.tsx
+++ b/frontend/src/config/pages/published.config.tsx
@@ -305,15 +305,38 @@ export function createPublishedPageConfig(params: {
const headerMetrics: HeaderMetricConfig[] = [
{
- label: 'Total Published',
+ label: 'Published',
accentColor: 'green',
calculate: (data: { totalCount: number }) => data.totalCount,
+ tooltip: 'Total published content. Track your complete content library.',
},
{
- label: 'On WordPress',
+ label: 'Synced',
accentColor: 'blue',
calculate: (data: { content: Content[] }) =>
data.content.filter(c => c.external_id).length,
+ tooltip: 'Content synced to WordPress. Successfully published on your website.',
+ },
+ {
+ label: 'This Month',
+ accentColor: 'purple',
+ calculate: (data: { content: Content[] }) => {
+ const now = new Date();
+ const thisMonth = now.getMonth();
+ const thisYear = now.getFullYear();
+ return data.content.filter(c => {
+ const date = new Date(c.created_at);
+ return date.getMonth() === thisMonth && date.getFullYear() === thisYear;
+ }).length;
+ },
+ tooltip: 'Content published this month. Track your monthly publishing velocity.',
+ },
+ {
+ label: 'Pending Sync',
+ accentColor: 'amber',
+ calculate: (data: { content: Content[] }) =>
+ data.content.filter(c => !c.external_id).length,
+ tooltip: 'Published content not yet synced to WordPress. Sync these to make them live.',
},
];
diff --git a/frontend/src/config/pages/review.config.tsx b/frontend/src/config/pages/review.config.tsx
index 88ec2a35..1cc7e7a9 100644
--- a/frontend/src/config/pages/review.config.tsx
+++ b/frontend/src/config/pages/review.config.tsx
@@ -262,19 +262,28 @@ export function createReviewPageConfig(params: {
],
headerMetrics: [
{
- label: 'Total Ready',
+ label: 'Ready',
accentColor: 'blue',
calculate: ({ totalCount }) => totalCount,
+ tooltip: 'Content ready for final review. Review quality, SEO, and images before publishing.',
},
{
- label: 'Has Images',
+ label: 'Images',
accentColor: 'green',
calculate: ({ content }) => content.filter(c => c.has_generated_images).length,
+ tooltip: 'Content with generated images. Visual assets complete and ready for review.',
},
{
label: 'Optimized',
accentColor: 'purple',
- calculate: ({ content }) => content.filter(c => (c as any).optimization_score >= 80).length,
+ calculate: ({ content }) => content.filter(c => c.optimization_scores && c.optimization_scores.overall_score >= 80).length,
+ tooltip: 'Content with high SEO optimization scores (80%+). Well-optimized for search engines.',
+ },
+ {
+ label: 'Sync Ready',
+ accentColor: 'amber',
+ calculate: ({ content }) => content.filter(c => c.has_generated_images && c.optimization_scores && c.optimization_scores.overall_score >= 70).length,
+ tooltip: 'Content ready for WordPress sync. Has images and good optimization score.',
},
],
};
diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx
index d1240488..bbe3f300 100644
--- a/frontend/src/config/pages/tasks.config.tsx
+++ b/frontend/src/config/pages/tasks.config.tsx
@@ -454,22 +454,39 @@ export const createTasksPageConfig = (
],
headerMetrics: [
{
- label: 'Total Tasks',
+ label: 'Tasks',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0,
+ tooltip: 'Total content generation tasks. Tasks process ideas into written content automatically.',
},
{
- label: 'Queued',
+ label: 'In Queue',
value: 0,
accentColor: 'amber' as const,
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'queued').length,
+ tooltip: 'Tasks queued for processing. These will be picked up by the content generation system.',
+ },
+ {
+ label: 'Processing',
+ value: 0,
+ accentColor: 'blue' as const,
+ calculate: (data) => data.tasks.filter((t: Task) => t.status === 'in_progress').length,
+ tooltip: 'Tasks currently being processed. Content is being generated by AI right now.',
},
{
label: 'Completed',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'completed').length,
+ tooltip: 'Successfully completed tasks. Generated content is ready for review and publishing.',
+ },
+ {
+ label: 'Failed',
+ value: 0,
+ accentColor: 'red' as const,
+ calculate: (data) => data.tasks.filter((t: Task) => t.status === 'failed').length,
+ tooltip: 'Failed tasks that need attention. Review error logs and retry or modify the task.',
},
],
};
diff --git a/frontend/src/context/HeaderMetricsContext.tsx b/frontend/src/context/HeaderMetricsContext.tsx
index a2a38ab6..d3de4a83 100644
--- a/frontend/src/context/HeaderMetricsContext.tsx
+++ b/frontend/src/context/HeaderMetricsContext.tsx
@@ -4,6 +4,7 @@ interface HeaderMetric {
label: string;
value: string | number;
accentColor: 'blue' | 'green' | 'amber' | 'purple';
+ tooltip?: string; // Actionable insight for this metric
}
interface HeaderMetricsContextType {
diff --git a/frontend/src/pages/Planner/Clusters.tsx b/frontend/src/pages/Planner/Clusters.tsx
index d6994a98..dd7d565b 100644
--- a/frontend/src/pages/Planner/Clusters.tsx
+++ b/frontend/src/pages/Planner/Clusters.tsx
@@ -28,6 +28,8 @@ import { usePageSizeStore } from '../../store/pageSizeStore';
import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
+import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
+import { WorkflowInsight } from '../../components/common/WorkflowInsights';
export default function Clusters() {
const toast = useToast();
@@ -75,6 +77,61 @@ export default function Clusters() {
const progressModal = useProgressModal();
const hasReloadedRef = useRef(false);
+ // Calculate workflow insights
+ const workflowInsights: WorkflowInsight[] = useMemo(() => {
+ const insights: WorkflowInsight[] = [];
+ const clustersWithIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length;
+ const totalIdeas = clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0);
+ const emptyClusters = clusters.filter(c => (c.keywords_count || 0) === 0).length;
+ const thinClusters = clusters.filter(c => (c.keywords_count || 0) > 0 && (c.keywords_count || 0) < 3).length;
+ const readyForGeneration = clustersWithIdeas;
+ const generationRate = totalCount > 0 ? Math.round((readyForGeneration / totalCount) * 100) : 0;
+
+ if (totalCount === 0) {
+ insights.push({
+ type: 'info',
+ message: 'Create clusters to organize keywords into topical groups for better content planning',
+ });
+ return insights;
+ }
+
+ // Content generation readiness
+ if (generationRate < 30) {
+ insights.push({
+ type: 'warning',
+ message: `Only ${generationRate}% of clusters have content ideas - Generate ideas to unlock content pipeline`,
+ });
+ } else if (generationRate >= 70) {
+ insights.push({
+ type: 'success',
+ message: `${generationRate}% of clusters have ideas (${totalIdeas} total) - Strong content pipeline ready`,
+ });
+ }
+
+ // Empty or thin clusters
+ if (emptyClusters > 0) {
+ insights.push({
+ type: 'warning',
+ message: `${emptyClusters} clusters have no keywords - Map keywords or delete unused clusters`,
+ });
+ } else if (thinClusters > 2) {
+ insights.push({
+ type: 'info',
+ message: `${thinClusters} clusters have fewer than 3 keywords - Consider adding more related keywords for better coverage`,
+ });
+ }
+
+ // Actionable next step
+ if (totalIdeas === 0) {
+ insights.push({
+ type: 'action',
+ message: 'Select clusters and use Auto-Generate Ideas to create content briefs',
+ });
+ }
+
+ return insights;
+ }, [clusters, totalCount]);
+
// Load clusters - wrapped in useCallback to prevent infinite loops
const loadClusters = useCallback(async () => {
setLoading(true);
@@ -353,6 +410,7 @@ export default function Clusters() {
label: metric.label,
value: metric.calculate({ clusters, totalCount }),
accentColor: metric.accentColor,
+ tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, clusters, totalCount]);
@@ -397,6 +455,7 @@ export default function Clusters() {
title="Keyword Clusters"
badge={{ icon: , color: 'purple' }}
navigation={ }
+ workflowInsights={workflowInsights}
/>
+ {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
+ sum + (c.keywords_count || 0), 0).toLocaleString(),
+ subtitle: `in ${totalCount} clusters`,
+ icon: ,
+ accentColor: 'blue',
+ href: '/planner/keywords',
+ },
+ {
+ title: 'Content Ideas',
+ value: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0).toLocaleString(),
+ subtitle: `across ${clusters.filter(c => (c.ideas_count || 0) > 0).length} clusters`,
+ icon: ,
+ accentColor: 'green',
+ href: '/planner/ideas',
+ },
+ {
+ title: 'Ready to Write',
+ value: clusters.filter(c => (c.ideas_count || 0) > 0 && c.status === 'active').length.toLocaleString(),
+ subtitle: 'clusters with approved ideas',
+ icon: ,
+ accentColor: 'purple',
+ },
+ ]}
+ progress={{
+ label: 'Idea Generation Pipeline: Clusters with content ideas generated (ready for downstream content creation)',
+ value: totalCount > 0 ? Math.round((clusters.filter(c => (c.ideas_count || 0) > 0).length / totalCount) * 100) : 0,
+ color: 'purple',
+ }}
+ />
+
{/* Progress Modal for AI Functions */}
{
+ const insights: WorkflowInsight[] = [];
+ const newCount = ideas.filter(i => i.status === 'new').length;
+ const queuedCount = ideas.filter(i => i.status === 'queued').length;
+ const completedCount = ideas.filter(i => i.status === 'completed').length;
+ const queueActivationRate = totalCount > 0 ? Math.round((queuedCount / totalCount) * 100) : 0;
+ const completionRate = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
+
+ if (totalCount === 0) {
+ insights.push({
+ type: 'info',
+ message: 'Generate ideas from your keyword clusters to build your content pipeline',
+ });
+ return insights;
+ }
+
+ // Queue activation insights
+ if (newCount > 0 && queuedCount === 0) {
+ insights.push({
+ type: 'warning',
+ message: `${newCount} new ideas waiting - Queue them to activate the content pipeline`,
+ });
+ } else if (queueActivationRate > 0 && queueActivationRate < 40) {
+ insights.push({
+ type: 'info',
+ message: `${queueActivationRate}% of ideas queued (${queuedCount} ideas) - Queue more ideas to maintain steady content flow`,
+ });
+ } else if (queuedCount > 0) {
+ insights.push({
+ type: 'success',
+ message: `${queuedCount} ideas in queue - Content pipeline is active and ready for task generation`,
+ });
+ }
+
+ // Completion velocity
+ if (completionRate >= 50) {
+ insights.push({
+ type: 'success',
+ message: `Strong completion rate (${completionRate}%) - ${completedCount} ideas converted to content`,
+ });
+ } else if (completionRate > 0) {
+ insights.push({
+ type: 'info',
+ message: `${completedCount} ideas completed (${completionRate}%) - Continue queuing ideas to grow content library`,
+ });
+ }
+
+ return insights;
+ }, [ideas, totalCount]);
+
// Load clusters for filter dropdown
useEffect(() => {
const loadClusters = async () => {
@@ -258,6 +311,7 @@ export default function Ideas() {
label: metric.label,
value: metric.calculate({ ideas, totalCount }),
accentColor: metric.accentColor,
+ tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, ideas, totalCount]);
@@ -307,6 +361,7 @@ export default function Ideas() {
title="Content Ideas"
badge={{ icon: , color: 'orange' }}
navigation={ }
+ workflowInsights={workflowInsights}
/>
+ {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
+ ,
+ accentColor: 'purple',
+ href: '/planner/clusters',
+ },
+ {
+ title: 'Ready to Queue',
+ value: ideas.filter(i => i.status === 'new').length.toLocaleString(),
+ subtitle: 'awaiting approval',
+ icon: ,
+ accentColor: 'orange',
+ },
+ {
+ title: 'In Queue',
+ value: ideas.filter(i => i.status === 'queued').length.toLocaleString(),
+ subtitle: 'ready for tasks',
+ icon: ,
+ accentColor: 'blue',
+ href: '/writer/tasks',
+ },
+ {
+ title: 'Content Created',
+ value: ideas.filter(i => i.status === 'completed').length.toLocaleString(),
+ subtitle: `${totalCount > 0 ? Math.round((ideas.filter(i => i.status === 'completed').length / totalCount) * 100) : 0}% completion`,
+ icon: ,
+ accentColor: 'green',
+ href: '/writer/content',
+ },
+ ]}
+ progress={{
+ label: 'Idea-to-Content Pipeline: Ideas successfully converted into written content',
+ value: totalCount > 0 ? Math.round((ideas.filter(i => i.status === 'completed').length / totalCount) * 100) : 0,
+ color: 'success',
+ }}
+ />
+
{/* Progress Modal for AI Functions */}
{
+ const insights = [];
+ const clusteredCount = keywords.filter(k => k.cluster_id).length;
+ const unclusteredCount = totalCount - clusteredCount;
+ const pipelineReadiness = totalCount > 0 ? Math.round((clusteredCount / totalCount) * 100) : 0;
+
+ if (totalCount === 0) {
+ insights.push({
+ type: 'info' as const,
+ message: 'Import keywords to begin building your content strategy and unlock SEO opportunities',
+ });
+ return insights;
+ }
+
+ // Pipeline Readiness Score insight
+ if (pipelineReadiness < 30) {
+ insights.push({
+ type: 'warning' as const,
+ message: `Pipeline readiness at ${pipelineReadiness}% - Most keywords need clustering before content ideation can begin`,
+ });
+ } else if (pipelineReadiness < 60) {
+ insights.push({
+ type: 'info' as const,
+ message: `Pipeline readiness at ${pipelineReadiness}% - Clustering progress is moderate, continue organizing keywords`,
+ });
+ } else if (pipelineReadiness >= 85) {
+ insights.push({
+ type: 'success' as const,
+ message: `Excellent pipeline readiness (${pipelineReadiness}%) - Ready for content ideation phase`,
+ });
+ }
+
+ // Clustering Potential (minimum batch size check)
+ if (unclusteredCount >= 5) {
+ insights.push({
+ type: 'action' as const,
+ message: `${unclusteredCount} keywords available for auto-clustering (minimum batch size met)`,
+ });
+ } else if (unclusteredCount > 0 && unclusteredCount < 5) {
+ insights.push({
+ type: 'info' as const,
+ message: `${unclusteredCount} unclustered keywords - Need ${5 - unclusteredCount} more to run auto-cluster`,
+ });
+ }
+
+ // Coverage Gaps - thin clusters that need more research
+ const thinClusters = clusters.filter(c => (c.keywords_count || 0) === 1);
+ if (thinClusters.length > 3) {
+ const thinVolume = thinClusters.reduce((sum, c) => sum + (c.volume || 0), 0);
+ insights.push({
+ type: 'warning' as const,
+ message: `${thinClusters.length} clusters have only 1 keyword each (${thinVolume.toLocaleString()} monthly volume) - Consider expanding research`,
+ });
+ }
+
+ return insights;
+ }, [keywords, totalCount, clusters]);
+
// Handle create/edit
const handleSave = async () => {
try {
@@ -736,6 +796,7 @@ export default function Keywords() {
title="Keywords"
badge={{ icon: , color: 'green' }}
navigation={ }
+ workflowInsights={workflowInsights}
/>
,
accentColor: 'blue',
href: '/planner/keywords',
@@ -867,21 +928,21 @@ export default function Keywords() {
{
title: 'Clustered',
value: keywords.filter(k => k.cluster_id).length.toLocaleString(),
- subtitle: `${Math.round((keywords.filter(k => k.cluster_id).length / Math.max(totalCount, 1)) * 100)}% coverage`,
+ subtitle: `${Math.round((keywords.filter(k => k.cluster_id).length / Math.max(totalCount, 1)) * 100)}% organized`,
icon: ,
accentColor: 'purple',
href: '/planner/clusters',
},
{
- title: 'Active',
- value: keywords.filter(k => k.status === 'active').length.toLocaleString(),
- subtitle: `${keywords.filter(k => k.status === 'pending').length} pending`,
+ title: 'Easy Wins',
+ value: keywords.filter(k => k.difficulty && k.difficulty <= 3 && (k.volume || 0) > 0).length.toLocaleString(),
+ subtitle: `Low difficulty with ${keywords.filter(k => k.difficulty && k.difficulty <= 3).reduce((sum, k) => sum + (k.volume || 0), 0).toLocaleString()} volume`,
icon: ,
accentColor: 'green',
},
]}
progress={{
- label: 'Keyword Clustering Progress',
+ label: 'Keyword Clustering Pipeline: Keywords organized into topical clusters',
value: totalCount > 0 ? Math.round((keywords.filter(k => k.cluster_id).length / totalCount) * 100) : 0,
color: 'primary',
}}
diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx
index 2003c729..4cfda9f4 100644
--- a/frontend/src/pages/Writer/Content.tsx
+++ b/frontend/src/pages/Writer/Content.tsx
@@ -25,6 +25,7 @@ import { useProgressModal } from '../../hooks/useProgressModal';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
+import { WorkflowInsight } from '../../components/common/WorkflowInsights';
export default function Content() {
const toast = useToast();
@@ -55,6 +56,59 @@ export default function Content() {
const progressModal = useProgressModal();
const hasReloadedRef = useRef(false);
+ // Calculate workflow insights
+ const workflowInsights: WorkflowInsight[] = useMemo(() => {
+ const insights: WorkflowInsight[] = [];
+ const draftCount = content.filter(c => c.status === 'draft').length;
+ const reviewCount = content.filter(c => c.status === 'review').length;
+ const publishedCount = content.filter(c => c.status === 'published').length;
+ const publishingRate = totalCount > 0 ? Math.round((publishedCount / totalCount) * 100) : 0;
+
+ if (totalCount === 0) {
+ insights.push({
+ type: 'info',
+ message: 'No content yet - Generate content from tasks to build your content library',
+ });
+ return insights;
+ }
+
+ // Draft vs Review status
+ if (draftCount > reviewCount * 3 && draftCount >= 5) {
+ insights.push({
+ type: 'warning',
+ message: `${draftCount} drafts waiting for review - Move content to review stage for quality assurance`,
+ });
+ } else if (draftCount > 0) {
+ insights.push({
+ type: 'info',
+ message: `${draftCount} drafts in progress - Review and refine before moving to publish stage`,
+ });
+ }
+
+ // Review queue status
+ if (reviewCount > 0) {
+ insights.push({
+ type: 'action',
+ message: `${reviewCount} pieces awaiting final review - Approve and publish when ready`,
+ });
+ }
+
+ // Publishing readiness
+ if (publishingRate >= 60 && publishedCount >= 10) {
+ insights.push({
+ type: 'success',
+ message: `Strong publishing rate (${publishingRate}%) - ${publishedCount} articles ready for WordPress sync`,
+ });
+ } else if (publishedCount > 0) {
+ insights.push({
+ type: 'success',
+ message: `${publishedCount} articles published (${publishingRate}%) - Continue moving content through the pipeline`,
+ });
+ }
+
+ return insights;
+ }, [content, totalCount]);
+
// Load content - wrapped in useCallback
const loadContent = useCallback(async () => {
setLoading(true);
@@ -167,6 +221,7 @@ export default function Content() {
label: metric.label,
value: metric.calculate({ content, totalCount }),
accentColor: metric.accentColor,
+ tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, content, totalCount]);
@@ -238,6 +293,7 @@ export default function Content() {
title="Content Drafts"
badge={{ icon: , color: 'purple' }}
navigation={ }
+ workflowInsights={workflowInsights}
/>
row.title || `Content #${row.id}`}
/>
- {/* Module Metrics Footer */}
+ {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
c.status === 'published').length} published`,
- icon: ,
- accentColor: 'green',
- href: '/writer/content',
+ title: 'Tasks',
+ value: content.length.toLocaleString(),
+ subtitle: 'generated from queue',
+ icon: ,
+ accentColor: 'blue',
+ href: '/writer/tasks',
},
{
title: 'Draft',
value: content.filter(c => c.status === 'draft').length.toLocaleString(),
- subtitle: `${content.filter(c => c.status === 'published').length} published`,
- icon: ,
+ subtitle: 'needs editing',
+ icon: ,
+ accentColor: 'amber',
+ },
+ {
+ title: 'In Review',
+ value: content.filter(c => c.status === 'review').length.toLocaleString(),
+ subtitle: 'awaiting approval',
+ icon: ,
accentColor: 'blue',
+ href: '/writer/review',
+ },
+ {
+ title: 'Published',
+ value: content.filter(c => c.status === 'published').length.toLocaleString(),
+ subtitle: 'ready for sync',
+ icon: ,
+ accentColor: 'green',
+ href: '/writer/published',
},
]}
progress={{
- label: 'Content Publishing Progress',
+ label: 'Content Publishing Pipeline: Content moved from draft through review to published (Draft \u2192 Review \u2192 Published)',
value: totalCount > 0 ? Math.round((content.filter(c => c.status === 'published').length / totalCount) * 100) : 0,
color: 'success',
}}
diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx
index 2f41aa94..3feb2114 100644
--- a/frontend/src/pages/Writer/Images.tsx
+++ b/frontend/src/pages/Writer/Images.tsx
@@ -492,6 +492,7 @@ export default function Images() {
label: metric.label,
value: metric.calculate({ images, totalCount }),
accentColor: metric.accentColor,
+ tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, images, totalCount]);
diff --git a/frontend/src/pages/Writer/Review.tsx b/frontend/src/pages/Writer/Review.tsx
index 39037179..19be6247 100644
--- a/frontend/src/pages/Writer/Review.tsx
+++ b/frontend/src/pages/Writer/Review.tsx
@@ -133,6 +133,7 @@ export default function Review() {
pageConfig.headerMetrics.map(metric => ({
...metric,
value: metric.calculate({ content, totalCount }),
+ tooltip: (metric as any).tooltip,
})),
[pageConfig.headerMetrics, content, totalCount]
);
diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx
index 32613a1e..4c8841b5 100644
--- a/frontend/src/pages/Writer/Tasks.tsx
+++ b/frontend/src/pages/Writer/Tasks.tsx
@@ -31,6 +31,8 @@ import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
+import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
+import { WorkflowInsight } from '../../components/common/WorkflowInsights';
export default function Tasks() {
const toast = useToast();
@@ -79,6 +81,63 @@ export default function Tasks() {
// Progress modal for AI functions
const progressModal = useProgressModal();
+ // Calculate workflow insights
+ const workflowInsights: WorkflowInsight[] = useMemo(() => {
+ const insights: WorkflowInsight[] = [];
+ const queuedCount = tasks.filter(t => t.status === 'queued').length;
+ const processingCount = tasks.filter(t => t.status === 'in_progress').length;
+ const completedCount = tasks.filter(t => t.status === 'completed').length;
+ const failedCount = tasks.filter(t => t.status === 'failed').length;
+ const completionRate = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
+
+ if (totalCount === 0) {
+ insights.push({
+ type: 'info',
+ message: 'No tasks yet - Queue ideas from Planner to start generating content automatically',
+ });
+ return insights;
+ }
+
+ // Queue status
+ if (queuedCount > 10) {
+ insights.push({
+ type: 'warning',
+ message: `Large queue detected (${queuedCount} tasks) - Content generation may take time, consider prioritizing`,
+ });
+ } else if (queuedCount > 0) {
+ insights.push({
+ type: 'info',
+ message: `${queuedCount} tasks in queue - Content generation pipeline is active`,
+ });
+ }
+
+ // Processing status
+ if (processingCount > 0) {
+ insights.push({
+ type: 'action',
+ message: `${processingCount} tasks actively generating content - Check back soon for completed drafts`,
+ });
+ }
+
+ // Failed tasks
+ if (failedCount > 0) {
+ insights.push({
+ type: 'warning',
+ message: `${failedCount} tasks failed - Review errors and retry or adjust task parameters`,
+ });
+ }
+
+ // Completion success
+ if (completionRate >= 70 && completedCount >= 5) {
+ insights.push({
+ type: 'success',
+ message: `High completion rate (${completionRate}%) - ${completedCount} pieces of content ready for review`,
+ });
+ }
+
+ return insights;
+ }, [tasks, totalCount]);
+
// AI Function Logs state
const [aiLogs, setAiLogs] = useState , color: 'indigo' }}
navigation={ }
+ workflowInsights={workflowInsights}
/>
+ {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
+ sum + (c.ideas_count || 0), 0).toLocaleString(),
+ subtitle: 'from planner',
+ icon: ,
+ accentColor: 'orange',
+ href: '/planner/ideas',
+ },
+ {
+ title: 'In Queue',
+ value: tasks.filter(t => t.status === 'queued').length.toLocaleString(),
+ subtitle: 'waiting for processing',
+ icon: ,
+ accentColor: 'amber',
+ },
+ {
+ title: 'Processing',
+ value: tasks.filter(t => t.status === 'in_progress').length.toLocaleString(),
+ subtitle: 'generating content',
+ icon: ,
+ accentColor: 'blue',
+ },
+ {
+ title: 'Ready for Review',
+ value: tasks.filter(t => t.status === 'completed').length.toLocaleString(),
+ subtitle: 'content generated',
+ icon: ,
+ accentColor: 'green',
+ href: '/writer/content',
+ },
+ ]}
+ progress={{
+ label: 'Content Generation Pipeline: Tasks successfully completed (Queued → Processing → Completed)',
+ value: totalCount > 0 ? Math.round((tasks.filter(t => t.status === 'completed').length / totalCount) * 100) : 0,
+ color: 'success',
+ }}
+ />
+
{/* Progress Modal for AI Functions */}