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) =>
|
export const reissueSourceContent = (id: string, fetchFn?: typeof fetch) =>
|
||||||
request<{ articlesDeleted: number; itemsRequeued: number }>(`/api/admin/sources/${id}/reissue`, { method: 'POST' }, fetchFn);
|
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.
|
// Content clearing — wipe articles/media/a source's raw items so they can be repopulated fresh.
|
||||||
export const clearSourceContent = (id: string, fetchFn?: typeof fetch) =>
|
export const clearSourceContent = (id: string, fetchFn?: typeof fetch) =>
|
||||||
request<{ itemsDeleted: number; articlesDeleted: number }>(`/api/admin/content/sources/${id}`, { method: 'DELETE' }, fetchFn);
|
request<{ itemsDeleted: number; articlesDeleted: number }>(`/api/admin/content/sources/${id}`, { method: 'DELETE' }, fetchFn);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { AdminSettings } from '$lib/adminTypes';
|
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';
|
import SaveStatus from './SaveStatus.svelte';
|
||||||
|
|
||||||
let { settings }: { settings: AdminSettings } = $props();
|
let { settings }: { settings: AdminSettings } = $props();
|
||||||
@@ -11,6 +11,11 @@
|
|||||||
let clearing = $state<'articles' | 'media' | null>(null);
|
let clearing = $state<'articles' | 'media' | null>(null);
|
||||||
let clearResult = $state<string | 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 telegramMediaMode = $state(settings.telegramMediaMode);
|
||||||
let telegramMediaStatus = $state<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
let telegramMediaStatus = $state<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
||||||
let telegramMediaSaveTimer: ReturnType<typeof setTimeout>;
|
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() {
|
function scheduleSave() {
|
||||||
status = 'saving';
|
status = 'saving';
|
||||||
clearTimeout(saveTimer);
|
clearTimeout(saveTimer);
|
||||||
@@ -216,6 +238,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<style>
|
||||||
.panel {
|
.panel {
|
||||||
background: var(--surface-1);
|
background: var(--surface-1);
|
||||||
@@ -276,6 +331,10 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
.error-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-danger);
|
||||||
|
}
|
||||||
.bar {
|
.bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
|
|||||||
Reference in New Issue
Block a user