const fs = require('fs'); const path = require('path'); const zernioPoster = require('./zernio-poster'); const postProxyPoster = require('./postproxy-poster'); const apiCaller = require('./skills/api_caller'); const DB_PATH = path.join(__dirname, 'database/social_db.json'); class AnalyticsEngine { _loadDB() { return JSON.parse(fs.readFileSync(DB_PATH, 'utf8')); } _saveDB(db) { fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2)); } /** * Pull analytics for all published posts from Zernio + PostProxy. */ async pullDailyAnalytics() { console.log('[Analytics] Pulling daily analytics...'); const db = this._loadDB(); const published = db.posts.filter(p => p.status === 'published' || p.status === 'scheduled'); let updated = 0; for (const post of published) { const cp = post.channel_posts || {}; let totalLikes = 0, totalComments = 0, totalShares = 0, totalReach = 0; for (const [channel, info] of Object.entries(cp)) { if (info.status !== 'published' || !info.postId) continue; try { let analytics = null; const zernioChs = ['instagram', 'threads', 'facebook', 'pinterest']; if (zernioChs.includes(channel)) { const result = await zernioPoster.getAnalytics(info.postId); if (result.success) analytics = result.analytics; } else { // PostProxy channels const result = await postProxyPoster.getPostAnalytics(info.postId); if (result.success) analytics = result.analytics; } if (analytics) { info.analytics = analytics; totalLikes += analytics.likes || 0; totalComments += analytics.comments || 0; totalShares += analytics.shares || 0; totalReach += analytics.reach || analytics.impressions || 0; } } catch (e) { console.error(`[Analytics] Failed for ${post.id}/${channel}: ${e.message}`); } } // Aggregate per-post analytics if (totalLikes || totalComments || totalShares) { const reach = totalReach || 1; post.analytics = { likes: totalLikes, comments: totalComments, shares: totalShares, reach: totalReach, engagementRate: ((totalLikes + totalComments + totalShares) / reach * 100).toFixed(2), score: this.scorePost({ likes: totalLikes, comments: totalComments, shares: totalShares, reach: totalReach }), lastSync: new Date().toISOString() }; updated++; } } // Compute patterns if (updated > 0 || published.length > 0) { db.analytics_patterns = this._computePatterns(published); } this._saveDB(db); console.log(`[Analytics] Updated ${updated} posts, patterns computed.`); return { success: true, updated }; } /** * Score a post based on engagement metrics. */ scorePost(metrics) { const { likes = 0, comments = 0, shares = 0, reach = 1 } = metrics; // Weighted: comments 3x, shares 2x, likes 1x return parseFloat(((comments * 3 + shares * 2 + likes) / Math.max(reach, 1) * 100).toFixed(2)); } /** * Compute patterns from published posts. */ _computePatterns(posts) { const withAnalytics = posts.filter(p => p.analytics); if (withAnalytics.length === 0) { return { last_computed: new Date().toISOString(), insights_text: 'No analytics data yet.' }; } // Group by pillar const pillarScores = {}; const hookScores = {}; const timeScores = {}; for (const post of withAnalytics) { const pillar = post.v1?.pillar || 'unknown'; if (!pillarScores[pillar]) pillarScores[pillar] = []; pillarScores[pillar].push(post.analytics.score || 0); const time = post.v1?.best_time_to_post || 'unknown'; if (!timeScores[time]) timeScores[time] = []; timeScores[time].push(post.analytics.score || 0); } const avg = arr => arr.length ? (arr.reduce((a, b) => a + b, 0) / arr.length).toFixed(2) : 0; const bestPillar = Object.entries(pillarScores).sort((a, b) => avg(b[1]) - avg(a[1]))[0]; const bestTime = Object.entries(timeScores).sort((a, b) => avg(b[1]) - avg(a[1]))[0]; const topPosts = withAnalytics .sort((a, b) => (b.analytics.score || 0) - (a.analytics.score || 0)) .slice(0, 5) .map(p => ({ id: p.id, title: p.v1?.title, score: p.analytics.score, pillar: p.v1?.pillar })); // Auto-offer trigger: high-scoring posts without offers get one created (fire-and-forget) try { const salesEngine = require('./skills/sales_engine'); const postsToOffer = withAnalytics.filter(p => (p.analytics.score || 0) > 3 && !p.offerCreated); if (postsToOffer.length > 0) { const self = this; (async () => { for (const post of postsToOffer) { try { console.log(`[Analytics] Auto-offer trigger: ${post.v1?.title || post.id} (score: ${post.analytics.score})`); const offerResult = await salesEngine.createOfferFromWinner(post); post.offerCreated = true; post.offerResult = offerResult.success ? offerResult.offer?.id : offerResult.verdict; } catch (e) { console.error(`[Analytics] Offer failed for ${post.id}:`, e.message); } } self._saveDB(db); })(); } } catch (e) { console.error('[Analytics] Auto-offer trigger error:', e.message); } const insightsText = [ bestPillar ? `Best pillar: "${bestPillar[0]}" (avg score: ${avg(bestPillar[1])})` : '', bestTime ? `Best posting time: ${bestTime[0]}` : '', `Top post: "${topPosts[0]?.title || 'N/A'}" (score: ${topPosts[0]?.score || 0})`, `Total analyzed: ${withAnalytics.length} posts` ].filter(Boolean).join('. '); return { last_computed: new Date().toISOString(), best_pillar: bestPillar ? bestPillar[0] : null, best_posting_time: bestTime ? bestTime[0] : null, top_posts: topPosts, insights_text: insightsText }; } /** * Generate a Telegram-friendly daily summary. */ async generateDailySummary() { const db = this._loadDB(); const patterns = db.analytics_patterns; if (!patterns || patterns.insights_text === 'No analytics data yet.') { return { success: true, message: 'No analytics data yet. Publish some posts first!' }; } const published = db.posts.filter(p => p.status === 'published'); const withAnalytics = published.filter(p => p.analytics); let msg = `📊 Daily Analytics Summary\n\n`; msg += `Posts Published: ${published.length}\n`; msg += `With Analytics: ${withAnalytics.length}\n\n`; if (patterns.best_pillar) msg += `🏆 Best Pillar: ${patterns.best_pillar}\n`; if (patterns.best_posting_time) msg += `⏰ Best Time: ${patterns.best_posting_time}\n`; if (patterns.top_posts?.length > 0) { msg += `\nTop Performers:\n`; for (const tp of patterns.top_posts.slice(0, 3)) { msg += `• ${tp.title || tp.id} — Score: ${tp.score}\n`; } } // Send via Telegram if (process.env.TELEGRAM_CHAT_ID) { await apiCaller.sendTelegramMessage(process.env.TELEGRAM_CHAT_ID, msg); } return { success: true, message: msg }; } /** * Get compact insights string for AI remix prompt injection. */ getInsightsForAI() { try { const db = this._loadDB(); return db.analytics_patterns?.insights_text || ''; } catch (e) { return ''; } } /** * Get analytics data for dashboard. */ getDashboardData() { const db = this._loadDB(); const published = db.posts.filter(p => p.analytics); return { patterns: db.analytics_patterns || null, posts: published.map(p => ({ id: p.id, title: p.v1?.title, pillar: p.v1?.pillar, ts: p.ts, analytics: p.analytics })) }; } } module.exports = new AnalyticsEngine();