feat: reconcile posted articles against live posts table, revert orphaned articles to discovered

This commit is contained in:
Patrick Britton 2026-02-09 09:01:53 -06:00
parent 52e18daef0
commit 82e9246fdd

View file

@ -514,7 +514,29 @@ func (s *OfficialAccountsService) PostNextArticle(ctx context.Context, configID
return &art, postID, nil return &art, postID, nil
} }
// ReconcilePostedArticles checks for articles marked 'posted' whose post no longer
// exists in the posts table, and reverts them to 'discovered' so they can be re-posted.
func (s *OfficialAccountsService) ReconcilePostedArticles(ctx context.Context, configID string) (int, error) {
tag, err := s.pool.Exec(ctx, `
UPDATE official_account_articles a
SET status = 'discovered', post_id = NULL, posted_at = NULL
WHERE a.config_id = $1
AND a.status = 'posted'
AND a.post_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM public.posts p WHERE p.id::text = a.post_id)
`, configID)
if err != nil {
return 0, err
}
reverted := int(tag.RowsAffected())
if reverted > 0 {
log.Info().Int("reverted", reverted).Str("config", configID).Msg("[OfficialAccounts] Reconciled orphaned articles back to discovered")
}
return reverted, nil
}
// GetArticleQueue returns articles for a config filtered by status. // GetArticleQueue returns articles for a config filtered by status.
// For 'posted' status, only returns articles whose post still exists in the posts table.
func (s *OfficialAccountsService) GetArticleQueue(ctx context.Context, configID string, status string, limit int) ([]CachedArticle, error) { func (s *OfficialAccountsService) GetArticleQueue(ctx context.Context, configID string, status string, limit int) ([]CachedArticle, error) {
if limit <= 0 { if limit <= 0 {
limit = 50 limit = 50
@ -524,12 +546,17 @@ func (s *OfficialAccountsService) GetArticleQueue(ctx context.Context, configID
orderDir = "ASC" // oldest first (next to be posted) orderDir = "ASC" // oldest first (next to be posted)
} }
// For 'posted', reconcile first to catch deleted posts, then query
if status == "posted" {
_, _ = s.ReconcilePostedArticles(ctx, configID)
}
query := fmt.Sprintf(` query := fmt.Sprintf(`
SELECT id, config_id, guid, title, link, source_name, source_url, description, pub_date, SELECT a.id, a.config_id, a.guid, a.title, a.link, a.source_name, a.source_url, a.description, a.pub_date,
status, post_id, error_message, discovered_at, posted_at a.status, a.post_id, a.error_message, a.discovered_at, a.posted_at
FROM official_account_articles FROM official_account_articles a
WHERE config_id = $1 AND status = $2 WHERE a.config_id = $1 AND a.status = $2
ORDER BY discovered_at %s ORDER BY a.discovered_at %s
LIMIT $3 LIMIT $3
`, orderDir) `, orderDir)
@ -564,7 +591,11 @@ type ArticleStats struct {
} }
// GetArticleStats returns article counts by status for a config. // GetArticleStats returns article counts by status for a config.
// Reconciles orphaned articles first so counts reflect reality.
func (s *OfficialAccountsService) GetArticleStats(ctx context.Context, configID string) (*ArticleStats, error) { func (s *OfficialAccountsService) GetArticleStats(ctx context.Context, configID string) (*ArticleStats, error) {
// Reconcile first — revert articles whose posts were deleted
_, _ = s.ReconcilePostedArticles(ctx, configID)
rows, err := s.pool.Query(ctx, ` rows, err := s.pool.Query(ctx, `
SELECT status, COUNT(*) FROM official_account_articles SELECT status, COUNT(*) FROM official_account_articles
WHERE config_id = $1 WHERE config_id = $1