social-autoposter 1.3.9 → 1.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/server.js +84 -12
- package/package.json +1 -1
- package/scripts/reddit_browser.py +11 -2
package/bin/server.js
CHANGED
|
@@ -2454,6 +2454,12 @@ const bookingsPerDayCache = new Map();
|
|
|
2454
2454
|
// post_link_clicks (per-hit log) filtered to is_bot=false so we count humans
|
|
2455
2455
|
// only; joins to post_links + posts so platform/project filters apply.
|
|
2456
2456
|
const clicksPerDayCache = new Map();
|
|
2457
|
+
// Posts-per-day: cached by days|platform|project. Backs the Trends-tab
|
|
2458
|
+
// "Views / Post" ratio denominator. Counts posts.posted_at grouped by UTC date,
|
|
2459
|
+
// excluding moltbook/github/github_issues to match the views/upvotes/comments
|
|
2460
|
+
// scope (those platforms don't surface views, so including them would
|
|
2461
|
+
// understate views-per-post for the comparable platforms).
|
|
2462
|
+
const postsPerDayCache = new Map();
|
|
2457
2463
|
// Funnel-per-day (PostHog-backed metrics): cached by days.
|
|
2458
2464
|
const funnelPerDayCache = new Map();
|
|
2459
2465
|
// Cost-per-day: Claude API session cost attributed to days an activity row
|
|
@@ -4317,6 +4323,56 @@ async function handleApi(req, res) {
|
|
|
4317
4323
|
})().catch(e => json(res, { error: e.message }, 500));
|
|
4318
4324
|
}
|
|
4319
4325
|
|
|
4326
|
+
// GET /api/posts/per-day?days=N&platform=X&project=Y - count of posts we
|
|
4327
|
+
// made per day, sourced from posts.posted_at. Same platform/project filter
|
|
4328
|
+
// shape as views/upvotes/comments so Trends-tab filters behave identically.
|
|
4329
|
+
// Excludes moltbook/github/github_issues to match the views denominator
|
|
4330
|
+
// scope (those platforms don't surface views, so including their posts
|
|
4331
|
+
// would understate the views-per-post ratio). Backs the Trends-tab
|
|
4332
|
+
// "Views / Post" pill.
|
|
4333
|
+
if (p === '/api/posts/per-day' && req.method === 'GET') {
|
|
4334
|
+
if (!req.user.admin) return json(res, { error: 'forbidden' }, 403);
|
|
4335
|
+
const url = new URL(req.url, 'http://localhost');
|
|
4336
|
+
const days = Math.max(1, Math.min(365, parseInt(url.searchParams.get('days') || '30', 10) || 30));
|
|
4337
|
+
const rawPlatform = (url.searchParams.get('platform') || '').trim().toLowerCase();
|
|
4338
|
+
const platform = (rawPlatform === '' || rawPlatform === 'all') ? '' :
|
|
4339
|
+
(rawPlatform === 'x' ? 'twitter' : rawPlatform);
|
|
4340
|
+
const platformOk = platform === '' || /^[a-z0-9_]{1,32}$/.test(platform);
|
|
4341
|
+
if (!platformOk) return json(res, { error: 'invalid platform' }, 400);
|
|
4342
|
+
const rawProject = (url.searchParams.get('project') || '').trim();
|
|
4343
|
+
const project = (rawProject === '' || rawProject.toLowerCase() === 'all') ? '' : rawProject;
|
|
4344
|
+
const projectOk = project === '' || /^[A-Za-z0-9_\-]{1,64}$/.test(project);
|
|
4345
|
+
if (!projectOk) return json(res, { error: 'invalid project' }, 400);
|
|
4346
|
+
const cacheKey = days + '|' + platform + '|' + project;
|
|
4347
|
+
const cached = postsPerDayCache.get(cacheKey);
|
|
4348
|
+
if (cached && Date.now() - cached.at < 300000) {
|
|
4349
|
+
return json(res, { days, rows: cached.value, cachedAt: cached.at });
|
|
4350
|
+
}
|
|
4351
|
+
const platformFilter = platform
|
|
4352
|
+
? " AND CASE WHEN LOWER(p.platform) = 'x' THEN 'twitter' ELSE LOWER(p.platform) END = '" + platform + "'"
|
|
4353
|
+
: '';
|
|
4354
|
+
const projectFilter = project
|
|
4355
|
+
? " AND p.project_name = '" + project.replace(/'/g, "''") + "'"
|
|
4356
|
+
: '';
|
|
4357
|
+
const q =
|
|
4358
|
+
"SELECT json_agg(row_to_json(r)) FROM (" +
|
|
4359
|
+
"SELECT to_char((p.posted_at AT TIME ZONE 'UTC')::date, 'YYYY-MM-DD') AS day, " +
|
|
4360
|
+
"COUNT(*)::bigint AS posts_made " +
|
|
4361
|
+
"FROM posts p " +
|
|
4362
|
+
"WHERE p.posted_at IS NOT NULL " +
|
|
4363
|
+
"AND p.posted_at >= CURRENT_DATE - INTERVAL '" + days + " days' " +
|
|
4364
|
+
"AND LOWER(p.platform) NOT IN ('moltbook', 'github', 'github_issues')" +
|
|
4365
|
+
platformFilter + projectFilter + " " +
|
|
4366
|
+
"GROUP BY day ORDER BY day ASC" +
|
|
4367
|
+
") r";
|
|
4368
|
+
return (async () => {
|
|
4369
|
+
const rows = await pq(q);
|
|
4370
|
+
const value = (rows && rows.length && rows[0].json_agg) ? rows[0].json_agg : [];
|
|
4371
|
+
postsPerDayCache.set(cacheKey, { at: Date.now(), value });
|
|
4372
|
+
return json(res, { days, rows: value });
|
|
4373
|
+
})().catch(e => json(res, { error: e.message }, 500));
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4320
4376
|
// GET /api/bookings/per-day?days=N - real Cal.com bookings per day from
|
|
4321
4377
|
// the separate BOOKINGS_DATABASE_URL Neon DB. Filters out test bookings
|
|
4322
4378
|
// the same way project_stats_json.py does (attendee_email NOT ILIKE
|
|
@@ -9989,6 +10045,11 @@ function renderDailyMetrics() {
|
|
|
9989
10045
|
// no new fetch needed. Values are percentages (0-100), formatted to one
|
|
9990
10046
|
// decimal place; days with views=0 are dropped (ratios are undefined).
|
|
9991
10047
|
let RATIO_METRICS = [
|
|
10048
|
+
// Views per post: how many views a post earns on average per day in the
|
|
10049
|
+
// window. Numerator is views_gained that day; denominator is posts_made
|
|
10050
|
+
// that day. format='count' renders as plain K/M numbers (e.g. "1.2K")
|
|
10051
|
+
// since the value is a count, not a percentage or dollar figure.
|
|
10052
|
+
{ id: 'views_per_post', label: 'Views / Post', color: '#a855f7', numerator: 'views', denominator: 'posts', format: 'count', scaleFactor: 1 },
|
|
9992
10053
|
{ id: 'upvotes_per_view', label: 'Upvotes / Views', color: '#f97316', numerator: 'upvotes', denominator: 'views', format: 'pct', scaleFactor: 100 },
|
|
9993
10054
|
{ id: 'comments_per_view', label: 'Comments / Views', color: '#14b8a6', numerator: 'comments', denominator: 'views', format: 'pct', scaleFactor: 100 },
|
|
9994
10055
|
{ id: 'clicks_per_view', label: 'Clicks / Views', color: '#0ea5e9', numerator: 'clicks', denominator: 'views', format: 'pct', scaleFactor: 100 },
|
|
@@ -10007,11 +10068,11 @@ let RATIO_METRICS = [
|
|
|
10007
10068
|
{ id: 'cost_per_kviews', label: 'Cost / 1k Views', color: '#dc2626', numerator: 'cost', denominator: 'views', format: 'usd', scaleFactor: 1000, adminOnly: true },
|
|
10008
10069
|
{ id: 'cost_per_kvisitors', label: 'Cost / 1k Visitors', color: '#7c3aed', numerator: 'cost', denominator: 'pageviews', format: 'usd', scaleFactor: 1000, adminOnly: true },
|
|
10009
10070
|
];
|
|
10010
|
-
const RATIO_METRICS_DEFAULTS = ['upvotes_per_view', 'comments_per_view', 'clicks_per_view', 'email_signups_per_session', 'schedule_clicks_per_session', 'get_started_per_session', 'cost_per_kviews', 'cost_per_kvisitors'];
|
|
10011
|
-
// .
|
|
10012
|
-
//
|
|
10013
|
-
//
|
|
10014
|
-
const RATIO_METRICS_STORAGE_KEY = 'ratioMetricsActive.
|
|
10071
|
+
const RATIO_METRICS_DEFAULTS = ['views_per_post', 'upvotes_per_view', 'comments_per_view', 'clicks_per_view', 'email_signups_per_session', 'schedule_clicks_per_session', 'get_started_per_session', 'cost_per_kviews', 'cost_per_kvisitors'];
|
|
10072
|
+
// .v3: ratio set expanded to include views_per_post at the head. Bumping
|
|
10073
|
+
// the storage key seeds the new defaults exactly once so existing users
|
|
10074
|
+
// see the new ratio pre-selected the next time they open Trends.
|
|
10075
|
+
const RATIO_METRICS_STORAGE_KEY = 'ratioMetricsActive.v3';
|
|
10015
10076
|
let _ratioMetricsActive = null;
|
|
10016
10077
|
function _loadRatioMetricsActive() {
|
|
10017
10078
|
if (_ratioMetricsActive) return _ratioMetricsActive;
|
|
@@ -10036,6 +10097,12 @@ function _fmtPct(n) {
|
|
|
10036
10097
|
function _fmtForRatio(r, n) {
|
|
10037
10098
|
if (n == null || !isFinite(n)) return '—';
|
|
10038
10099
|
if (r && r.format === 'usd') return _fmtUsd(n);
|
|
10100
|
+
if (r && r.format === 'count') {
|
|
10101
|
+
// Whole-number K/M counts (no decimals) for ratios like Views / Post.
|
|
10102
|
+
if (n >= 1_000_000) return Math.round(n / 1_000_000) + 'M';
|
|
10103
|
+
if (n >= 1_000) return Math.round(n / 1_000) + 'K';
|
|
10104
|
+
return String(Math.round(n));
|
|
10105
|
+
}
|
|
10039
10106
|
return _fmtPct(n);
|
|
10040
10107
|
}
|
|
10041
10108
|
|
|
@@ -10203,10 +10270,13 @@ function renderRatioMetrics() {
|
|
|
10203
10270
|
const day = days[idx];
|
|
10204
10271
|
const rows = visible.map(r => {
|
|
10205
10272
|
const v = ratioSeries[r.id][day];
|
|
10206
|
-
// "no views" / "no visitors" depending on the
|
|
10207
|
-
// ratios use the right unit so the empty-day
|
|
10208
|
-
// ratio's meaning ("no views" for
|
|
10209
|
-
|
|
10273
|
+
// "no views" / "no visitors" / "no posts" depending on the
|
|
10274
|
+
// denominator. Cost ratios use the right unit so the empty-day
|
|
10275
|
+
// message matches the ratio's meaning ("no views" for
|
|
10276
|
+
// cost_per_kviews, etc.).
|
|
10277
|
+
const emptyLabel = (r.denominator === 'pageviews') ? 'no visitors'
|
|
10278
|
+
: (r.denominator === 'posts') ? 'no posts'
|
|
10279
|
+
: 'no views';
|
|
10210
10280
|
const display = (v == null || !isFinite(v)) ? emptyLabel : _fmtForRatio(r, v);
|
|
10211
10281
|
return '<div class="tt-row"><span class="swatch" style="background:' + r.color + ';"></span>' +
|
|
10212
10282
|
'<span>' + escapeHtml(r.label) + '</span>' +
|
|
@@ -10355,7 +10425,7 @@ async function loadDailyMetrics() {
|
|
|
10355
10425
|
const qsProj = projectOnlyParams.join('&');
|
|
10356
10426
|
try {
|
|
10357
10427
|
const costAvail = window.SA_IS_ADMIN !== false;
|
|
10358
|
-
const [views, upvotes, comments, clicks, bookings, funnel, cost] = await Promise.all([
|
|
10428
|
+
const [views, upvotes, comments, clicks, bookings, funnel, cost, posts] = await Promise.all([
|
|
10359
10429
|
fetchOne('/api/views/per-day?' + qsAware),
|
|
10360
10430
|
fetchOne('/api/upvotes/per-day?' + qsAware),
|
|
10361
10431
|
fetchOne('/api/comments/per-day?' + qsAware),
|
|
@@ -10363,8 +10433,9 @@ async function loadDailyMetrics() {
|
|
|
10363
10433
|
fetchOne('/api/bookings/per-day?' + qsProj),
|
|
10364
10434
|
fetchOne('/api/funnel/per-day?' + qsProj),
|
|
10365
10435
|
costAvail ? fetchOne('/api/cost/per-day?' + qsAware) : Promise.resolve({ rows: [], failed: false }),
|
|
10436
|
+
fetchOne('/api/posts/per-day?' + qsAware),
|
|
10366
10437
|
]);
|
|
10367
|
-
const allFailed = [views, upvotes, comments, clicks, bookings, funnel, cost].every(r => r.failed);
|
|
10438
|
+
const allFailed = [views, upvotes, comments, clicks, bookings, funnel, cost, posts].every(r => r.failed);
|
|
10368
10439
|
if (allFailed) {
|
|
10369
10440
|
if (chartEl) chartEl.innerHTML = '<div class="views-chart-empty">Unable to load daily metrics (all endpoints failed).</div>';
|
|
10370
10441
|
return;
|
|
@@ -10380,6 +10451,7 @@ async function loadDailyMetrics() {
|
|
|
10380
10451
|
intoSeries('clicks', clicks.rows, 'clicks_gained');
|
|
10381
10452
|
intoSeries('bookings', bookings.rows, 'bookings_gained');
|
|
10382
10453
|
intoSeries('cost', cost.rows, 'cost_usd');
|
|
10454
|
+
intoSeries('posts', posts.rows, 'posts_made');
|
|
10383
10455
|
DAILY_METRICS.filter(m => m.funnel).forEach(m => {
|
|
10384
10456
|
intoSeries(m.id, funnel.rows, m.valueKey);
|
|
10385
10457
|
});
|
|
@@ -10402,7 +10474,7 @@ async function loadDailyMetrics() {
|
|
|
10402
10474
|
// Stash a list of failed endpoints so renderDailyMetrics can surface a
|
|
10403
10475
|
// small "(N timed out)" hint in the status pill rather than silently
|
|
10404
10476
|
// showing flat zeros for those series.
|
|
10405
|
-
const fetchResults = { views, upvotes, comments, clicks, bookings, funnel, cost };
|
|
10477
|
+
const fetchResults = { views, upvotes, comments, clicks, bookings, funnel, cost, posts };
|
|
10406
10478
|
_dailyMetricsFailed = Object.keys(fetchResults)
|
|
10407
10479
|
.filter(k => fetchResults[k].failed)
|
|
10408
10480
|
.map(k => ({ key: k, timedOut: !!fetchResults[k].timedOut }));
|
package/package.json
CHANGED
|
@@ -61,14 +61,23 @@ def _diag_log(msg):
|
|
|
61
61
|
pass
|
|
62
62
|
VIEWPORT = {"width": 911, "height": 1016}
|
|
63
63
|
|
|
64
|
-
# Load Reddit username from config
|
|
64
|
+
# Load Reddit username from config.
|
|
65
|
+
# Prefers the new top-level `reddit_account.username` (2026-05-15) over the
|
|
66
|
+
# legacy `accounts.reddit.username` path. Drift between the two silently
|
|
67
|
+
# broke the post-permalink lookup on the VM (wrong username → JS finds 0
|
|
68
|
+
# matching comments → permalink=None → pipeline records `failed` despite
|
|
69
|
+
# the comment landing on Reddit).
|
|
65
70
|
_config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")
|
|
66
71
|
OUR_USERNAME = "Deep_Ad1959"
|
|
67
72
|
if os.path.exists(_config_path):
|
|
68
73
|
try:
|
|
69
74
|
with open(_config_path) as f:
|
|
70
75
|
_cfg = json.load(f)
|
|
71
|
-
OUR_USERNAME =
|
|
76
|
+
OUR_USERNAME = (
|
|
77
|
+
(_cfg.get("reddit_account") or {}).get("username")
|
|
78
|
+
or _cfg.get("accounts", {}).get("reddit", {}).get("username")
|
|
79
|
+
or OUR_USERNAME
|
|
80
|
+
)
|
|
72
81
|
except Exception:
|
|
73
82
|
pass
|
|
74
83
|
|