feat: P4 metrics collector + /api/metrics; P5 from-url generator (cheerio)
This commit is contained in:
@@ -111,4 +111,23 @@ router.post('/topics-ideas', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/generate/from-url — прочитать URL и написать пост в стиле канала
|
||||
router.post('/from-url', async (req, res) => {
|
||||
try {
|
||||
const { channelId, url } = req.body;
|
||||
const userId = parseInt(req.headers['x-user-id']) || null;
|
||||
if (!channelId || !url) return res.status(400).json({ error: 'channelId and url required' });
|
||||
|
||||
const channel = await channelsSvc.getChannel(userId, channelId);
|
||||
if (!channel) return res.status(404).json({ error: 'Channel not found' });
|
||||
|
||||
const { generateFromUrl } = require('../services/fromUrl');
|
||||
const result = await generateFromUrl({ url, channelId, channel });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error('[Route] POST /generate/from-url', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* GET /api/metrics/channel/:channelId — статистика канала
|
||||
* POST /api/metrics/collect — принудительный сбор (admin/cron)
|
||||
* GET /api/metrics/best-time/:channelId — лучшие дни/часы публикаций
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { query } = require('../config/db');
|
||||
const { collectMetrics } = require('../services/metricsCollector');
|
||||
|
||||
// ── POST /api/metrics/collect — принудительный сбор ──────────────────────────
|
||||
router.post('/collect', async (req, res) => {
|
||||
try {
|
||||
const result = await collectMetrics();
|
||||
res.json({ ok: true, ...result });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /api/metrics/channel/:channelId — сводка канала ──────────────────────
|
||||
router.get('/channel/:channelId', async (req, res) => {
|
||||
try {
|
||||
const { channelId } = req.params;
|
||||
const days = parseInt(req.query.days) || 30;
|
||||
const since = new Date(Date.now() - days * 86400_000);
|
||||
|
||||
// Посты по статусам
|
||||
const { rows: statusStats } = await query(`
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM posts
|
||||
WHERE channel_id=$1 AND created_at > $2
|
||||
GROUP BY status
|
||||
`, [channelId, since]);
|
||||
|
||||
// Реакции топ-5 по постам
|
||||
const { rows: topPosts } = await query(`
|
||||
SELECT p.id, p.tg_message_id, p.published_at,
|
||||
p.reactions, p.forwards,
|
||||
left(p.content, 100) AS preview
|
||||
FROM posts p
|
||||
WHERE p.channel_id=$1
|
||||
AND p.published_at > $2
|
||||
AND p.reactions != '{}'
|
||||
ORDER BY (
|
||||
SELECT COALESCE(SUM(value::int), 0)
|
||||
FROM jsonb_each_text(p.reactions)
|
||||
) DESC
|
||||
LIMIT 5
|
||||
`, [channelId, since]);
|
||||
|
||||
// Общие реакции за период
|
||||
const { rows: reactionTotals } = await query(`
|
||||
SELECT key as emoji, SUM(value::int) as total
|
||||
FROM posts p, jsonb_each_text(p.reactions)
|
||||
WHERE p.channel_id=$1 AND p.published_at > $2
|
||||
GROUP BY key
|
||||
ORDER BY total DESC
|
||||
LIMIT 10
|
||||
`, [channelId, since]);
|
||||
|
||||
// Всего публикаций за период
|
||||
const { rows: totals } = await query(`
|
||||
SELECT COUNT(*) as total_posts,
|
||||
COUNT(CASE WHEN tg_message_id IS NOT NULL THEN 1 END) as published_to_tg,
|
||||
SUM(COALESCE(forwards,0)) as total_forwards
|
||||
FROM posts
|
||||
WHERE channel_id=$1 AND published_at > $2
|
||||
`, [channelId, since]);
|
||||
|
||||
res.json({
|
||||
channel_id: parseInt(channelId),
|
||||
days,
|
||||
totals: totals[0],
|
||||
by_status: statusStats,
|
||||
top_posts: topPosts,
|
||||
reaction_totals: reactionTotals,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /api/metrics/best-time/:channelId — лучший день/час ──────────────────
|
||||
router.get('/best-time/:channelId', async (req, res) => {
|
||||
try {
|
||||
const { channelId } = req.params;
|
||||
const days = parseInt(req.query.days) || 90;
|
||||
const since = new Date(Date.now() - days * 86400_000);
|
||||
|
||||
// Публикации по дням недели
|
||||
const { rows: byDow } = await query(`
|
||||
SELECT EXTRACT(ISODOW FROM published_at AT TIME ZONE 'Europe/Moscow') AS dow,
|
||||
COUNT(*) as count
|
||||
FROM posts
|
||||
WHERE channel_id=$1 AND published_at > $2 AND status='published'
|
||||
GROUP BY dow
|
||||
ORDER BY dow
|
||||
`, [channelId, since]);
|
||||
|
||||
// Публикации по часам (МСК)
|
||||
const { rows: byHour } = await query(`
|
||||
SELECT EXTRACT(HOUR FROM published_at AT TIME ZONE 'Europe/Moscow') AS hour,
|
||||
COUNT(*) as count
|
||||
FROM posts
|
||||
WHERE channel_id=$1 AND published_at > $2 AND status='published'
|
||||
GROUP BY hour
|
||||
ORDER BY hour
|
||||
`, [channelId, since]);
|
||||
|
||||
const DOW_LABELS = ['', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
||||
|
||||
res.json({
|
||||
channel_id: parseInt(channelId),
|
||||
days,
|
||||
by_dow: byDow.map(r => ({ dow: r.dow, label: DOW_LABELS[r.dow] || '?', count: parseInt(r.count) })),
|
||||
by_hour: byHour.map(r => ({ hour: parseInt(r.hour), count: parseInt(r.count) })),
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── GET /api/metrics/user-posts/:channelId — метрики user_posts ──────────────
|
||||
router.get('/user-posts/:channelId', async (req, res) => {
|
||||
try {
|
||||
const userId = parseInt(req.headers['x-user-id']);
|
||||
if (!userId) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
const { channelId } = req.params;
|
||||
const days = parseInt(req.query.days) || 30;
|
||||
const since = new Date(Date.now() - days * 86400_000);
|
||||
|
||||
const { rows } = await query(`
|
||||
SELECT id, tg_message_id, status, published_at,
|
||||
reactions, forwards, metrics_at,
|
||||
left(content, 120) AS preview, topic, image_url
|
||||
FROM user_posts
|
||||
WHERE channel_id=$1 AND user_id=$2
|
||||
AND published_at > $3
|
||||
ORDER BY published_at DESC
|
||||
LIMIT 50
|
||||
`, [channelId, userId, since]);
|
||||
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user