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:
Claude
2026-07-27 19:21:28 +00:00
parent 15716cff5e
commit 7518e6f81e
2 changed files with 65 additions and 1 deletions
+5
View File
@@ -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;