vinos-engine / analytics-engine.js
VinOS Agent
Phase 5: Autonomous Traffic-to-Sales Engine
f30cee0
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 = `📊 <b>Daily Analytics Summary</b>\n\n`;
msg += `<b>Posts Published:</b> ${published.length}\n`;
msg += `<b>With Analytics:</b> ${withAnalytics.length}\n\n`;
if (patterns.best_pillar) msg += `🏆 <b>Best Pillar:</b> ${patterns.best_pillar}\n`;
if (patterns.best_posting_time) msg += `⏰ <b>Best Time:</b> ${patterns.best_posting_time}\n`;
if (patterns.top_posts?.length > 0) {
msg += `\n<b>Top Performers:</b>\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();