social-autoposter 1.3.3 → 1.3.4
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 +41 -12
- package/package.json +1 -1
- package/scripts/dm_short_links.py +150 -56
package/bin/server.js
CHANGED
|
@@ -3562,10 +3562,16 @@ async function handleApi(req, res) {
|
|
|
3562
3562
|
const rows = await pq(q);
|
|
3563
3563
|
let events = (rows && rows.length && rows[0].json_agg) ? rows[0].json_agg : [];
|
|
3564
3564
|
// Non-admin: drop events not tagged with an allowed project (including
|
|
3565
|
-
// octolens mentions, which have no project column).
|
|
3565
|
+
// octolens mentions, which have no project column). Strip Claude cost
|
|
3566
|
+
// fields; cost is operator-internal (API spend not charged to clients).
|
|
3566
3567
|
if (!req.user.admin) {
|
|
3567
3568
|
const allowed = new Set(req.user.projects);
|
|
3568
3569
|
events = events.filter(e => e.project && allowed.has(e.project));
|
|
3570
|
+
events.forEach(e => {
|
|
3571
|
+
delete e.cost_usd;
|
|
3572
|
+
delete e.cost_usd_orchestrator;
|
|
3573
|
+
delete e.cost_usd_estimated;
|
|
3574
|
+
});
|
|
3569
3575
|
}
|
|
3570
3576
|
return json(res, { events });
|
|
3571
3577
|
})().catch(e => json(res, { error: e.message }, 500));
|
|
@@ -6510,7 +6516,7 @@ const HTML = `<!DOCTYPE html>
|
|
|
6510
6516
|
<button type="button" class="style-stats-pill" data-value="30d">Last 30d</button>
|
|
6511
6517
|
</div>
|
|
6512
6518
|
</div>
|
|
6513
|
-
<details class="style-stats-section" id="cost-stats" open>
|
|
6519
|
+
<details class="style-stats-section sa-admin-only" id="cost-stats" open>
|
|
6514
6520
|
<summary>
|
|
6515
6521
|
<span class="style-stats-title"><span class="style-stats-caret">▶</span><span id="cost-stats-heading">Cost per Activity (last 24 hours)</span></span>
|
|
6516
6522
|
<span class="style-stats-total" id="cost-stats-total"></span>
|
|
@@ -6785,7 +6791,7 @@ const HTML = `<!DOCTYPE html>
|
|
|
6785
6791
|
<th class="activity-sortable" data-sort="summary">
|
|
6786
6792
|
<span class="activity-header-label">What <span class="activity-sort-arrow" data-sort-arrow="summary"></span></span>
|
|
6787
6793
|
</th>
|
|
6788
|
-
<th style="width:90px;text-align:right;" class="activity-sortable" data-sort="cost_usd">
|
|
6794
|
+
<th style="width:90px;text-align:right;" class="activity-sortable sa-admin-only" data-sort="cost_usd">
|
|
6789
6795
|
<span class="activity-header-label">Cost <span class="activity-sort-arrow" data-sort-arrow="cost_usd"></span></span>
|
|
6790
6796
|
</th>
|
|
6791
6797
|
<th style="width:40px;text-align:center;">
|
|
@@ -9128,6 +9134,27 @@ async function handleDeleteBtnClick(btn) {
|
|
|
9128
9134
|
_saDelFailed.delete(payload.key);
|
|
9129
9135
|
btn.classList.add('is-loading');
|
|
9130
9136
|
btn.setAttribute('title', 'sending...');
|
|
9137
|
+
// Optimistic add: the Resend roundtrip is ~15s and the activity table can
|
|
9138
|
+
// re-render mid-flight, detaching this button node from the DOM. The fresh
|
|
9139
|
+
// button reads _saDelPending at render time, so adding the key up-front
|
|
9140
|
+
// guarantees the new button paints pending immediately instead of
|
|
9141
|
+
// reverting to the default trash icon.
|
|
9142
|
+
_saDelPending.add(payload.key);
|
|
9143
|
+
const applyPendingState = () => {
|
|
9144
|
+
document.querySelectorAll('button.sa-del-btn[data-sa-del-payload]').forEach(b => {
|
|
9145
|
+
let k = null;
|
|
9146
|
+
try {
|
|
9147
|
+
const raw = b.getAttribute('data-sa-del-payload') || '';
|
|
9148
|
+
if (raw) k = JSON.parse(decodeURIComponent(escape(atob(raw)))).key;
|
|
9149
|
+
} catch {}
|
|
9150
|
+
if (k === payload.key) {
|
|
9151
|
+
b.classList.remove('is-loading', 'is-failed');
|
|
9152
|
+
b.classList.add('is-pending');
|
|
9153
|
+
b.setAttribute('title', 'pending deletion');
|
|
9154
|
+
b.setAttribute('aria-label', 'pending deletion');
|
|
9155
|
+
}
|
|
9156
|
+
});
|
|
9157
|
+
};
|
|
9131
9158
|
try {
|
|
9132
9159
|
const r = await fetch('/api/activity/mark-deletion', {
|
|
9133
9160
|
method: 'POST',
|
|
@@ -9135,19 +9162,18 @@ async function handleDeleteBtnClick(btn) {
|
|
|
9135
9162
|
body: JSON.stringify(payload),
|
|
9136
9163
|
});
|
|
9137
9164
|
const j = await r.json().catch(() => ({}));
|
|
9138
|
-
btn.classList.remove('is-loading');
|
|
9139
9165
|
if (r.ok && (j.status === 'pending' || j.already_existed)) {
|
|
9140
|
-
|
|
9141
|
-
btn.classList.add('is-pending');
|
|
9142
|
-
btn.setAttribute('title', 'pending deletion');
|
|
9143
|
-
btn.setAttribute('aria-label', 'pending deletion');
|
|
9166
|
+
applyPendingState();
|
|
9144
9167
|
} else {
|
|
9168
|
+
_saDelPending.delete(payload.key);
|
|
9145
9169
|
_saDelFailed.add(payload.key);
|
|
9170
|
+
btn.classList.remove('is-loading');
|
|
9146
9171
|
btn.classList.add('is-failed');
|
|
9147
9172
|
const msg = (j && j.error) ? String(j.error) : ('HTTP ' + r.status);
|
|
9148
9173
|
btn.setAttribute('title', 'failed: ' + msg + ' (click to retry)');
|
|
9149
9174
|
}
|
|
9150
9175
|
} catch (e) {
|
|
9176
|
+
_saDelPending.delete(payload.key);
|
|
9151
9177
|
btn.classList.remove('is-loading');
|
|
9152
9178
|
_saDelFailed.add(payload.key);
|
|
9153
9179
|
btn.classList.add('is-failed');
|
|
@@ -9395,7 +9421,7 @@ async function loadActivityStats() {
|
|
|
9395
9421
|
// post's first-ever snapshot so day 1 never attributes lifetime counts
|
|
9396
9422
|
// to a capture day; expect those lines to sit at 0 until at least two
|
|
9397
9423
|
// consecutive days of snapshots have accumulated per post.
|
|
9398
|
-
|
|
9424
|
+
let DAILY_METRICS = [
|
|
9399
9425
|
{ id: 'views', label: 'Views', color: '#6366f1', endpoint: '/api/views/per-day', valueKey: 'views_gained', platformAware: true },
|
|
9400
9426
|
{ id: 'upvotes', label: 'Upvotes', color: '#f97316', endpoint: '/api/upvotes/per-day', valueKey: 'upvotes_gained', platformAware: true },
|
|
9401
9427
|
{ id: 'comments', label: 'Comments', color: '#14b8a6', endpoint: '/api/comments/per-day', valueKey: 'comments_gained', platformAware: true },
|
|
@@ -9408,7 +9434,7 @@ const DAILY_METRICS = [
|
|
|
9408
9434
|
// platformAware: filters cost to the chosen platform's activity rows.
|
|
9409
9435
|
// format: 'usd' so the legend pill, Y-axis ticks, bar labels, and
|
|
9410
9436
|
// hover tooltip render as dollars (e.g. $12.34) instead of K/M counts.
|
|
9411
|
-
{ id: 'cost', label: 'Cost (USD)', color: '#dc2626', endpoint: '/api/cost/per-day', valueKey: 'cost_usd', platformAware: true, format: 'usd' },
|
|
9437
|
+
{ id: 'cost', label: 'Cost (USD)', color: '#dc2626', endpoint: '/api/cost/per-day', valueKey: 'cost_usd', platformAware: true, format: 'usd', adminOnly: true },
|
|
9412
9438
|
// All funnel metrics count UNIQUE VISITORS (distinct_id), not raw events.
|
|
9413
9439
|
// A user iterating on the same project (multiple prompts, multiple
|
|
9414
9440
|
// schedule retries, multiple pageviews) is 1, not N. See
|
|
@@ -10108,6 +10134,7 @@ async function loadDailyMetrics() {
|
|
|
10108
10134
|
const qsAware = platformAwareParams.join('&');
|
|
10109
10135
|
const qsProj = projectOnlyParams.join('&');
|
|
10110
10136
|
try {
|
|
10137
|
+
const costAvail = window.SA_IS_ADMIN !== false;
|
|
10111
10138
|
const [views, upvotes, comments, clicks, bookings, funnel, cost] = await Promise.all([
|
|
10112
10139
|
fetchOne('/api/views/per-day?' + qsAware),
|
|
10113
10140
|
fetchOne('/api/upvotes/per-day?' + qsAware),
|
|
@@ -10115,7 +10142,7 @@ async function loadDailyMetrics() {
|
|
|
10115
10142
|
fetchOne('/api/clicks/per-day?' + qsAware),
|
|
10116
10143
|
fetchOne('/api/bookings/per-day?' + qsProj),
|
|
10117
10144
|
fetchOne('/api/funnel/per-day?' + qsProj),
|
|
10118
|
-
fetchOne('/api/cost/per-day?'
|
|
10145
|
+
costAvail ? fetchOne('/api/cost/per-day?' + qsAware) : Promise.resolve({ rows: [], failed: false }),
|
|
10119
10146
|
]);
|
|
10120
10147
|
const allFailed = [views, upvotes, comments, clicks, bookings, funnel, cost].every(r => r.failed);
|
|
10121
10148
|
if (allFailed) {
|
|
@@ -11414,6 +11441,7 @@ function renderCostStats(payload) {
|
|
|
11414
11441
|
let _costStatsLoadedFor = null;
|
|
11415
11442
|
let _costStatsLoading = false;
|
|
11416
11443
|
async function loadCostStats(force) {
|
|
11444
|
+
if (window.SA_IS_ADMIN === false) return;
|
|
11417
11445
|
if (_costStatsLoading) return;
|
|
11418
11446
|
const hours = currentStatusWindow().hours;
|
|
11419
11447
|
const row = document.getElementById('cost-stats-platform-pills');
|
|
@@ -13884,7 +13912,7 @@ function renderActivity(events) {
|
|
|
13884
13912
|
'</div>' +
|
|
13885
13913
|
'</td>' +
|
|
13886
13914
|
'<td class="activity-summary">' + summaryHtml + '</td>' +
|
|
13887
|
-
'<td style="text-align:right;font-variant-numeric:tabular-nums;color:var(--text-secondary);">' + fmtCostCell(e.cost_usd, e.cost_usd_orchestrator, e.cost_usd_estimated) + '</td>' +
|
|
13915
|
+
'<td class="sa-admin-only" style="text-align:right;font-variant-numeric:tabular-nums;color:var(--text-secondary);">' + fmtCostCell(e.cost_usd, e.cost_usd_orchestrator, e.cost_usd_estimated) + '</td>' +
|
|
13888
13916
|
'<td style="text-align:center;">' + renderDeleteBtnHtml(e) + '</td>' +
|
|
13889
13917
|
'</tr>';
|
|
13890
13918
|
}).join('');
|
|
@@ -14180,6 +14208,7 @@ function saStartApp() {
|
|
|
14180
14208
|
document.body.classList.remove('sa-authed-pending');
|
|
14181
14209
|
const isCloud = document.body.classList.contains('sa-cloud');
|
|
14182
14210
|
const isAdmin = window.SA_IS_ADMIN !== false;
|
|
14211
|
+
if (!isAdmin) DAILY_METRICS = DAILY_METRICS.filter(m => !m.adminOnly);
|
|
14183
14212
|
try { window.posthog && window.posthog.capture('dashboard_opened', { is_admin: isAdmin, is_cloud: isCloud }); } catch (e) {}
|
|
14184
14213
|
// Status + pending are local-only (UI hidden by body.sa-cloud). Endpoints
|
|
14185
14214
|
// are admin-only too, so skipping them on cloud also stops 403 spam for
|
package/package.json
CHANGED
|
@@ -522,6 +522,11 @@ def _mint_one_post(conn, *, target_url: str, projects: list, platform: str,
|
|
|
522
522
|
platform UTMs + code in utm_content), so the LLM's URL in the comment text
|
|
523
523
|
is ignored for routing -- visitors always land on the destination we baked
|
|
524
524
|
in. Pool depth managed by scripts/mint_external_pool.py.
|
|
525
|
+
|
|
526
|
+
Routes DB ops through social-autoposter-website /api/v1/post-links/* so
|
|
527
|
+
VMs / sandboxes without DATABASE_URL still mint successfully. The `conn`
|
|
528
|
+
arg is accepted for backward compatibility but ignored. Set
|
|
529
|
+
SOCIAL_AUTOPOSTER_LEGACY_NEON=1 to fall back to the direct-Neon path.
|
|
525
530
|
"""
|
|
526
531
|
target_url = _ensure_scheme((target_url or '').strip())
|
|
527
532
|
if not target_url or target_url == 'https://':
|
|
@@ -540,45 +545,68 @@ def _mint_one_post(conn, *, target_url: str, projects: list, platform: str,
|
|
|
540
545
|
'detail': f"no website for project={project_name!r} in config.json",
|
|
541
546
|
}
|
|
542
547
|
|
|
548
|
+
platform_norm = (platform or '').lower()
|
|
549
|
+
if platform_norm == 'x':
|
|
550
|
+
platform_norm = 'twitter'
|
|
551
|
+
|
|
543
552
|
project_cfg = next((p for p in projects if p.get('name') == project_name), None)
|
|
553
|
+
use_legacy = os.environ.get('SOCIAL_AUTOPOSTER_LEGACY_NEON') == '1'
|
|
554
|
+
|
|
544
555
|
if project_cfg and project_cfg.get('external_short_links'):
|
|
545
|
-
# Pool path. Atomically claim the oldest unclaimed pool row
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
556
|
+
# Pool path. Atomically claim the oldest unclaimed pool row.
|
|
557
|
+
if use_legacy:
|
|
558
|
+
cur = conn.execute(
|
|
559
|
+
"SELECT code, target_url FROM post_links "
|
|
560
|
+
"WHERE project_name = %s AND platform = %s "
|
|
561
|
+
" AND post_id IS NULL AND reply_id IS NULL "
|
|
562
|
+
" AND minted_session LIKE 'pool:%%' "
|
|
563
|
+
"ORDER BY minted_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED",
|
|
564
|
+
(project_name, platform_norm),
|
|
565
|
+
)
|
|
566
|
+
row = cur.fetchone()
|
|
567
|
+
if not row:
|
|
568
|
+
return {
|
|
569
|
+
'ok': False,
|
|
570
|
+
'error': 'pool_exhausted',
|
|
571
|
+
'project': project_name,
|
|
572
|
+
'platform': platform_norm,
|
|
573
|
+
}
|
|
574
|
+
row = dict(row)
|
|
575
|
+
pool_code = row['code']
|
|
576
|
+
pool_target = row['target_url']
|
|
577
|
+
conn.execute(
|
|
578
|
+
"UPDATE post_links SET minted_session = %s WHERE code = %s",
|
|
579
|
+
(minted_session, pool_code),
|
|
580
|
+
)
|
|
581
|
+
conn.commit()
|
|
582
|
+
else:
|
|
583
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
584
|
+
from http_api import api_post
|
|
585
|
+
try:
|
|
586
|
+
resp = api_post(
|
|
587
|
+
"/api/v1/post-links/claim-pool",
|
|
588
|
+
{
|
|
589
|
+
"project_name": project_name,
|
|
590
|
+
"platform": platform_norm,
|
|
591
|
+
"minted_session": minted_session,
|
|
592
|
+
},
|
|
593
|
+
ok_on_conflict=True,
|
|
594
|
+
)
|
|
595
|
+
except Exception as e:
|
|
596
|
+
return {'ok': False, 'error': 'api_unreachable', 'detail': str(e)}
|
|
597
|
+
if not resp or not resp.get('ok'):
|
|
598
|
+
err = (resp or {}).get('error') or {}
|
|
599
|
+
code = err.get('code') if isinstance(err, dict) else None
|
|
600
|
+
return {
|
|
601
|
+
'ok': False,
|
|
602
|
+
'error': code or 'pool_exhausted',
|
|
603
|
+
'project': project_name,
|
|
604
|
+
'platform': platform_norm,
|
|
605
|
+
'detail': err.get('message') if isinstance(err, dict) else str(err),
|
|
606
|
+
}
|
|
607
|
+
data = resp.get('data') or {}
|
|
608
|
+
pool_code = data.get('code')
|
|
609
|
+
pool_target = data.get('target_url')
|
|
582
610
|
return {
|
|
583
611
|
'ok': True,
|
|
584
612
|
'code': pool_code,
|
|
@@ -596,22 +624,54 @@ def _mint_one_post(conn, *, target_url: str, projects: list, platform: str,
|
|
|
596
624
|
platform=platform,
|
|
597
625
|
)
|
|
598
626
|
|
|
599
|
-
#
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
627
|
+
# Fresh mint: try up to 8 random codes before giving up on collision.
|
|
628
|
+
if use_legacy:
|
|
629
|
+
for _ in range(8):
|
|
630
|
+
code = _gen_code()
|
|
631
|
+
try:
|
|
632
|
+
conn.execute(
|
|
633
|
+
"INSERT INTO post_links (code, platform, project_name, "
|
|
634
|
+
" target_url, kind, project_at_mint, minted_session) "
|
|
635
|
+
"VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
|
636
|
+
(code, platform, project_name, final_target, kind,
|
|
637
|
+
matched_project, minted_session),
|
|
638
|
+
)
|
|
639
|
+
conn.commit()
|
|
640
|
+
return {
|
|
641
|
+
'ok': True,
|
|
642
|
+
'code': code,
|
|
643
|
+
'short_url': f"{website}/r/{code}",
|
|
644
|
+
'target_url': final_target,
|
|
645
|
+
'kind': kind,
|
|
646
|
+
}
|
|
647
|
+
except Exception as e:
|
|
648
|
+
if 'duplicate key' in str(e).lower() and 'post_links_pkey' in str(e).lower():
|
|
649
|
+
conn.execute("ROLLBACK")
|
|
650
|
+
continue
|
|
651
|
+
raise
|
|
652
|
+
return {'ok': False, 'error': 'code_collision_after_8_tries'}
|
|
653
|
+
|
|
654
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
655
|
+
from http_api import api_post
|
|
604
656
|
for _ in range(8):
|
|
605
657
|
code = _gen_code()
|
|
606
658
|
try:
|
|
607
|
-
|
|
608
|
-
"
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
659
|
+
resp = api_post(
|
|
660
|
+
"/api/v1/post-links/mint",
|
|
661
|
+
{
|
|
662
|
+
"code": code,
|
|
663
|
+
"platform": platform,
|
|
664
|
+
"project_name": project_name,
|
|
665
|
+
"target_url": final_target,
|
|
666
|
+
"kind": kind,
|
|
667
|
+
"project_at_mint": matched_project,
|
|
668
|
+
"minted_session": minted_session,
|
|
669
|
+
},
|
|
670
|
+
ok_on_conflict=True,
|
|
613
671
|
)
|
|
614
|
-
|
|
672
|
+
except Exception as e:
|
|
673
|
+
return {'ok': False, 'error': 'api_unreachable', 'detail': str(e)}
|
|
674
|
+
if resp and resp.get('ok'):
|
|
615
675
|
return {
|
|
616
676
|
'ok': True,
|
|
617
677
|
'code': code,
|
|
@@ -619,11 +679,15 @@ def _mint_one_post(conn, *, target_url: str, projects: list, platform: str,
|
|
|
619
679
|
'target_url': final_target,
|
|
620
680
|
'kind': kind,
|
|
621
681
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
682
|
+
err = (resp or {}).get('error') or {}
|
|
683
|
+
err_code = err.get('code') if isinstance(err, dict) else None
|
|
684
|
+
if err_code == 'code_collision':
|
|
685
|
+
continue # try another random code
|
|
686
|
+
return {
|
|
687
|
+
'ok': False,
|
|
688
|
+
'error': err_code or 'mint_api_error',
|
|
689
|
+
'detail': err.get('message') if isinstance(err, dict) else str(err),
|
|
690
|
+
}
|
|
627
691
|
return {'ok': False, 'error': 'code_collision_after_8_tries'}
|
|
628
692
|
|
|
629
693
|
|
|
@@ -653,7 +717,11 @@ def wrap_text_for_post(*, text: str, platform: str, project_name: str) -> dict:
|
|
|
653
717
|
|
|
654
718
|
minted_session = str(uuid.uuid4())
|
|
655
719
|
projects = _load_projects()
|
|
656
|
-
|
|
720
|
+
# Conn is only opened for the legacy direct-Neon path (env-gated). The
|
|
721
|
+
# default API path doesn't need it; lazy-open avoids a wasted Neon
|
|
722
|
+
# connection on VMs that don't have DATABASE_URL.
|
|
723
|
+
use_legacy = os.environ.get('SOCIAL_AUTOPOSTER_LEGACY_NEON') == '1'
|
|
724
|
+
conn = dbmod.get_conn() if use_legacy else None
|
|
657
725
|
try:
|
|
658
726
|
seen = {}
|
|
659
727
|
codes = []
|
|
@@ -704,7 +772,26 @@ def wrap_text_for_post(*, text: str, platform: str, project_name: str) -> dict:
|
|
|
704
772
|
'skipped': skipped,
|
|
705
773
|
}
|
|
706
774
|
finally:
|
|
707
|
-
conn
|
|
775
|
+
if conn is not None:
|
|
776
|
+
conn.close()
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _backfill_via_api(*, minted_session: str, post_id: int | None = None,
|
|
780
|
+
reply_id: int | None = None) -> int:
|
|
781
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
782
|
+
from http_api import api_post
|
|
783
|
+
body: dict = {"minted_session": minted_session}
|
|
784
|
+
if post_id is not None:
|
|
785
|
+
body["post_id"] = int(post_id)
|
|
786
|
+
if reply_id is not None:
|
|
787
|
+
body["reply_id"] = int(reply_id)
|
|
788
|
+
try:
|
|
789
|
+
resp = api_post("/api/v1/post-links/backfill", body)
|
|
790
|
+
except Exception:
|
|
791
|
+
return 0
|
|
792
|
+
if not resp or not resp.get('ok'):
|
|
793
|
+
return 0
|
|
794
|
+
return int((resp.get('data') or {}).get('updated') or 0)
|
|
708
795
|
|
|
709
796
|
|
|
710
797
|
def backfill_post_id(*, minted_session: str, post_id: int) -> int:
|
|
@@ -714,9 +801,14 @@ def backfill_post_id(*, minted_session: str, post_id: int) -> int:
|
|
|
714
801
|
Caller should NOT raise on rowcount==0 because some posts have no URLs
|
|
715
802
|
and minted_session was None — the caller should skip the backfill in
|
|
716
803
|
that case.
|
|
804
|
+
|
|
805
|
+
Routes through /api/v1/post-links/backfill by default. Set
|
|
806
|
+
SOCIAL_AUTOPOSTER_LEGACY_NEON=1 for the direct-Neon path.
|
|
717
807
|
"""
|
|
718
808
|
if not minted_session or post_id is None:
|
|
719
809
|
return 0
|
|
810
|
+
if os.environ.get('SOCIAL_AUTOPOSTER_LEGACY_NEON') != '1':
|
|
811
|
+
return _backfill_via_api(minted_session=minted_session, post_id=post_id)
|
|
720
812
|
conn = dbmod.get_conn()
|
|
721
813
|
try:
|
|
722
814
|
cur = conn.execute(
|
|
@@ -735,6 +827,8 @@ def backfill_reply_id(*, minted_session: str, reply_id: int) -> int:
|
|
|
735
827
|
writes to the `replies` table, not `posts`)."""
|
|
736
828
|
if not minted_session or reply_id is None:
|
|
737
829
|
return 0
|
|
830
|
+
if os.environ.get('SOCIAL_AUTOPOSTER_LEGACY_NEON') != '1':
|
|
831
|
+
return _backfill_via_api(minted_session=minted_session, reply_id=reply_id)
|
|
738
832
|
conn = dbmod.get_conn()
|
|
739
833
|
try:
|
|
740
834
|
cur = conn.execute(
|