feat: bulk post controls - post N or all pending articles from admin UI
This commit is contained in:
parent
1064f3e284
commit
2aa4eb77cf
|
|
@ -657,6 +657,9 @@ function ArticlesPanel({ configId }: { configId: string }) {
|
|||
const [stats, setStats] = useState<ArticleStats | null>(null);
|
||||
const [tab, setTab] = useState<PipelineTab>('discovered');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [bulkCount, setBulkCount] = useState(5);
|
||||
const [posting, setPosting] = useState(false);
|
||||
const [postResult, setPostResult] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
|
||||
const fetchTab = async (t: PipelineTab) => {
|
||||
setLoading(true);
|
||||
|
|
@ -676,6 +679,24 @@ function ArticlesPanel({ configId }: { configId: string }) {
|
|||
|
||||
useEffect(() => { fetchTab(tab); }, [tab]);
|
||||
|
||||
const handleBulkPost = async (count: number | 'all') => {
|
||||
setPosting(true);
|
||||
setPostResult(null);
|
||||
try {
|
||||
const resp = await api.triggerOfficialPost(configId, count);
|
||||
const n = resp.posted?.length || 0;
|
||||
const errs = resp.errors?.length || 0;
|
||||
let msg = `Posted ${n} article(s)`;
|
||||
if (errs > 0) msg += `, ${errs} error(s)`;
|
||||
setPostResult({ ok: errs === 0, msg });
|
||||
if (resp.stats) setStats(resp.stats);
|
||||
fetchTab(tab);
|
||||
} catch (e: any) {
|
||||
setPostResult({ ok: false, msg: e.message });
|
||||
}
|
||||
setPosting(false);
|
||||
};
|
||||
|
||||
const tabConfig: { key: PipelineTab; label: string; color: string; bgColor: string }[] = [
|
||||
{ key: 'discovered', label: 'Pending', color: 'text-blue-700', bgColor: 'bg-blue-100' },
|
||||
{ key: 'posted', label: 'Posted', color: 'text-green-700', bgColor: 'bg-green-100' },
|
||||
|
|
@ -714,6 +735,27 @@ function ArticlesPanel({ configId }: { configId: string }) {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* Bulk post controls — only on discovered tab */}
|
||||
{tab === 'discovered' && stats && stats.discovered > 0 && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-blue-50 border-b border-warm-200">
|
||||
<span className="text-[10px] font-medium text-gray-600">Post:</span>
|
||||
<input type="number" min={1} max={stats.discovered} value={bulkCount}
|
||||
onChange={(e) => setBulkCount(Math.max(1, Number(e.target.value)))}
|
||||
className="w-14 px-1.5 py-1 border border-warm-300 rounded text-xs text-center" />
|
||||
<button onClick={() => handleBulkPost(bulkCount)} disabled={posting}
|
||||
className="px-2.5 py-1 bg-brand-500 text-white rounded text-xs font-medium hover:bg-brand-600 disabled:opacity-50">
|
||||
{posting ? 'Posting...' : `Post ${bulkCount}`}
|
||||
</button>
|
||||
<button onClick={() => handleBulkPost('all')} disabled={posting}
|
||||
className="px-2.5 py-1 bg-green-600 text-white rounded text-xs font-medium hover:bg-green-700 disabled:opacity-50">
|
||||
{posting ? 'Posting...' : `Post All (${stats.discovered})`}
|
||||
</button>
|
||||
{postResult && (
|
||||
<span className={`text-xs ml-auto ${postResult.ok ? 'text-green-600' : 'text-red-600'}`}>{postResult.msg}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Article list */}
|
||||
<div className="max-h-56 overflow-y-auto">
|
||||
{loading ? (
|
||||
|
|
|
|||
|
|
@ -425,8 +425,9 @@ class ApiClient {
|
|||
});
|
||||
}
|
||||
|
||||
async triggerOfficialPost(id: string) {
|
||||
return this.request<any>(`/api/v1/admin/official-accounts/${id}/trigger`, { method: 'POST' });
|
||||
async triggerOfficialPost(id: string, count?: number | 'all') {
|
||||
const q = count !== undefined ? `?count=${count}` : '';
|
||||
return this.request<any>(`/api/v1/admin/official-accounts/${id}/trigger${q}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
async previewOfficialPost(id: string) {
|
||||
|
|
|
|||
|
|
@ -2970,7 +2970,9 @@ func (h *AdminHandler) ToggleOfficialAccount(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, gin.H{"enabled": req.Enabled})
|
||||
}
|
||||
|
||||
// Manually trigger a post for an official account
|
||||
// Manually trigger a post for an official account.
|
||||
// Accepts query param ?count=N to post multiple articles at once.
|
||||
// count=0 or count=all posts ALL pending articles.
|
||||
func (h *AdminHandler) TriggerOfficialPost(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
|
|
@ -2981,37 +2983,69 @@ func (h *AdminHandler) TriggerOfficialPost(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Parse count param: default 1, "all" or "0" = post everything
|
||||
countStr := c.DefaultQuery("count", "1")
|
||||
postAll := countStr == "all" || countStr == "0"
|
||||
count := 1
|
||||
if !postAll {
|
||||
if n, err := strconv.Atoi(countStr); err == nil && n > 0 {
|
||||
count = n
|
||||
}
|
||||
}
|
||||
|
||||
switch cfg.AccountType {
|
||||
case "news", "rss":
|
||||
// Phase 1: Discover new articles
|
||||
_, discErr := h.officialAccountsService.DiscoverArticles(ctx, id)
|
||||
discovered, discErr := h.officialAccountsService.DiscoverArticles(ctx, id)
|
||||
if discErr != nil {
|
||||
// Log but continue — there may be previously discovered articles
|
||||
log.Warn().Err(discErr).Str("config", id).Msg("Discover error during trigger")
|
||||
}
|
||||
|
||||
// Phase 2: Post next article from the queue
|
||||
article, postID, err := h.officialAccountsService.PostNextArticle(ctx, id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
// If posting all, get the pending count
|
||||
if postAll {
|
||||
stats, _ := h.officialAccountsService.GetArticleStats(ctx, id)
|
||||
if stats != nil {
|
||||
count = stats.Discovered
|
||||
}
|
||||
}
|
||||
if article == nil {
|
||||
|
||||
// Phase 2: Post N articles from the queue
|
||||
var posted []gin.H
|
||||
var errors []string
|
||||
for i := 0; i < count; i++ {
|
||||
article, postID, err := h.officialAccountsService.PostNextArticle(ctx, id)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
continue
|
||||
}
|
||||
if article == nil {
|
||||
break // no more articles
|
||||
}
|
||||
posted = append(posted, gin.H{
|
||||
"post_id": postID,
|
||||
"title": article.Title,
|
||||
"link": article.Link,
|
||||
"source": article.SourceName,
|
||||
})
|
||||
}
|
||||
|
||||
if len(posted) == 0 && len(errors) == 0 {
|
||||
msg := "No new articles found"
|
||||
if discErr != nil {
|
||||
msg += " (discover error: " + discErr.Error() + ")"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": msg, "post_id": nil})
|
||||
c.JSON(http.StatusOK, gin.H{"message": msg, "post_id": nil, "discovered": discovered})
|
||||
return
|
||||
}
|
||||
|
||||
stats, _ := h.officialAccountsService.GetArticleStats(ctx, id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Article posted",
|
||||
"post_id": postID,
|
||||
"body": article.Link,
|
||||
"source": article.SourceName,
|
||||
"title": article.Title,
|
||||
"stats": stats,
|
||||
"message": fmt.Sprintf("Posted %d article(s)", len(posted)),
|
||||
"posted": posted,
|
||||
"errors": errors,
|
||||
"discovered": discovered,
|
||||
"stats": stats,
|
||||
})
|
||||
|
||||
default:
|
||||
|
|
|
|||
Loading…
Reference in a new issue