Add "Reissue an article" panel to the Retention admin tab
Wires up POST /api/admin/articles/:id/reissue (added alongside the blank-article fix) as a UI panel instead of requiring curl: paste an article ID, it deletes the article and requeues its source items for re-publish. Verified live in a browser against a real backend/DB — both the success path and the "no article with that ID" 404 case.
This commit is contained in:
@@ -107,6 +107,11 @@ export const pollSourceNow = (id: string, fetchFn?: typeof fetch) =>
|
||||
export const reissueSourceContent = (id: string, fetchFn?: typeof fetch) =>
|
||||
request<{ articlesDeleted: number; itemsRequeued: number }>(`/api/admin/sources/${id}/reissue`, { method: 'POST' }, fetchFn);
|
||||
|
||||
// Fixes one specific bad article regardless of how many sources it merged — unlike
|
||||
// reissueSourceContent above, which deliberately won't touch a multi-source article.
|
||||
export const reissueArticle = (id: string, fetchFn?: typeof fetch) =>
|
||||
request<{ articlesDeleted: number; itemsRequeued: number }>(`/api/admin/articles/${id}/reissue`, { method: 'POST' }, fetchFn);
|
||||
|
||||
// Content clearing — wipe articles/media/a source's raw items so they can be repopulated fresh.
|
||||
export const clearSourceContent = (id: string, fetchFn?: typeof fetch) =>
|
||||
request<{ itemsDeleted: number; articlesDeleted: number }>(`/api/admin/content/sources/${id}`, { method: 'DELETE' }, fetchFn);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { AdminSettings } from '$lib/adminTypes';
|
||||
import { updateSettings, clearAllArticles, clearAllMedia } from '$lib/adminApi';
|
||||
import { updateSettings, clearAllArticles, clearAllMedia, reissueArticle } from '$lib/adminApi';
|
||||
import SaveStatus from './SaveStatus.svelte';
|
||||
|
||||
let { settings }: { settings: AdminSettings } = $props();
|
||||
@@ -11,6 +11,11 @@
|
||||
let clearing = $state<'articles' | 'media' | null>(null);
|
||||
let clearResult = $state<string | null>(null);
|
||||
|
||||
let reissueArticleId = $state('');
|
||||
let reissuing = $state(false);
|
||||
let reissueResult = $state<string | null>(null);
|
||||
let reissueError = $state<string | null>(null);
|
||||
|
||||
let telegramMediaMode = $state(settings.telegramMediaMode);
|
||||
let telegramMediaStatus = $state<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
||||
let telegramMediaSaveTimer: ReturnType<typeof setTimeout>;
|
||||
@@ -60,6 +65,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReissueArticle() {
|
||||
const id = reissueArticleId.trim();
|
||||
if (!id) return;
|
||||
reissuing = true;
|
||||
reissueResult = null;
|
||||
reissueError = null;
|
||||
try {
|
||||
const { itemsRequeued } = await reissueArticle(id);
|
||||
reissueResult = `Deleted — ${itemsRequeued} source item(s) requeued for re-publish`;
|
||||
reissueArticleId = '';
|
||||
} catch (err) {
|
||||
reissueError = /\(404\)/.test((err as Error).message) ? 'No article with that ID' : (err as Error).message;
|
||||
} finally {
|
||||
reissuing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSave() {
|
||||
status = 'saving';
|
||||
clearTimeout(saveTimer);
|
||||
@@ -216,6 +238,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<span class="panel-title">Reissue an article</span>
|
||||
<p class="hint">
|
||||
Deletes one specific published article and requeues every source item it was built from, so
|
||||
they re-cluster and re-synthesize fresh on the next scheduler tick — for fixing a single bad
|
||||
publish (e.g. a garbled AI merge) without wiping anything else. Unlike a source's own "Clear
|
||||
content" action, this works regardless of how many different sources the article merged
|
||||
together. Find the article ID in its URL or via <code>GET /api/article/:id</code>.
|
||||
</p>
|
||||
<div class="clear-row">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="art-…"
|
||||
bind:value={reissueArticleId}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleReissueArticle()}
|
||||
style="flex: 1; min-width: 220px"
|
||||
/>
|
||||
<button
|
||||
class="danger-btn"
|
||||
onclick={handleReissueArticle}
|
||||
disabled={reissuing || !reissueArticleId.trim()}
|
||||
>
|
||||
{reissuing ? 'Reissuing…' : 'Reissue'}
|
||||
</button>
|
||||
{#if reissueResult}
|
||||
<span class="usage-label">{reissueResult}</span>
|
||||
{/if}
|
||||
{#if reissueError}
|
||||
<span class="error-label">{reissueError}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
background: var(--surface-1);
|
||||
@@ -276,6 +331,10 @@
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.error-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-danger);
|
||||
}
|
||||
.bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
|
||||
Reference in New Issue
Block a user