feat(admin): compact scrape page layout; add Changelog page
Some checks failed
CI / Backend (push) Successful in 48s
CI / UI (push) Successful in 35s
Release / Test backend (push) Successful in 39s
Release / Check ui (push) Successful in 36s
CI / Backend (pull_request) Successful in 41s
Release / Docker / caddy (push) Successful in 1m31s
CI / UI (pull_request) Successful in 49s
Release / Docker / backend (push) Failing after 1m9s
Release / Docker / ui (push) Successful in 2m18s
Release / Docker / runner (push) Successful in 3m49s
Release / Gitea Release (push) Has been skipped
Some checks failed
CI / Backend (push) Successful in 48s
CI / UI (push) Successful in 35s
Release / Test backend (push) Successful in 39s
Release / Check ui (push) Successful in 36s
CI / Backend (pull_request) Successful in 41s
Release / Docker / caddy (push) Successful in 1m31s
CI / UI (pull_request) Successful in 49s
Release / Docker / backend (push) Failing after 1m9s
Release / Docker / ui (push) Successful in 2m18s
Release / Docker / runner (push) Successful in 3m49s
Release / Gitea Release (push) Has been skipped
- Scrape page: replace three large cards + genre card with a compact bordered table of rows (label + inline controls per action). Visually much lighter, all controls visible without scrolling. - Admin sidebar: add Changelog link after Audio. - New /admin/changelog page: fetches releases from Gitea API and renders them as a clean list (tag, title, date, body).
This commit is contained in:
@@ -231,131 +231,110 @@
|
||||
<title>Scrape tasks — libnovel admin</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-zinc-100">Scrape tasks</h1>
|
||||
<p class="text-zinc-400 text-sm mt-1">
|
||||
Job status:
|
||||
{#if running}
|
||||
<span class="text-amber-400 font-medium animate-pulse">Running</span>
|
||||
{:else}
|
||||
<span class="text-green-400 font-medium">Idle</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h1 class="text-xl font-semibold text-zinc-100 flex-1">Scrape</h1>
|
||||
<span class="text-xs {running ? 'text-amber-400 animate-pulse' : 'text-green-500'}">
|
||||
● {running ? 'Running' : 'Idle'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Scrape controls -->
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Compact controls -->
|
||||
<div class="divide-y divide-zinc-800 border border-zinc-800 rounded-xl overflow-hidden">
|
||||
<!-- Full catalogue -->
|
||||
<div class="bg-zinc-800 rounded-xl border border-zinc-700 p-5 space-y-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-zinc-300">Scrape full catalogue</h2>
|
||||
<p class="text-xs text-zinc-500 mt-1">Re-crawls all novelfire.net pages and picks up new books.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 px-4 py-3 bg-zinc-900">
|
||||
<span class="text-sm text-zinc-400 w-36 shrink-0">Full catalogue</span>
|
||||
<button
|
||||
onclick={triggerCatalogueScrape}
|
||||
disabled={running || cataloguing}
|
||||
class="w-full px-4 py-2 rounded-lg bg-amber-600 text-zinc-900 font-semibold text-sm hover:bg-amber-500 transition-colors disabled:opacity-50"
|
||||
class="px-3 py-1.5 rounded-md bg-amber-600 text-zinc-900 font-semibold text-xs hover:bg-amber-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{cataloguing ? 'Queuing…' : running ? 'Already running…' : 'Start catalogue scrape'}
|
||||
{cataloguing ? 'Queuing…' : running ? 'Running…' : 'Start scrape'}
|
||||
</button>
|
||||
{#if catalogueError}
|
||||
<p class="text-sm text-red-400">{catalogueError}</p>
|
||||
{/if}
|
||||
{#if catalogueError}<span class="text-xs text-red-400">{catalogueError}</span>{/if}
|
||||
</div>
|
||||
|
||||
<!-- Single book -->
|
||||
<div id="book-form" class="bg-zinc-800 rounded-xl border border-zinc-700 p-5 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-zinc-300">Scrape a single book</h2>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="url"
|
||||
bind:value={scrapeUrl}
|
||||
placeholder="https://novelfire.net/book/…"
|
||||
class="flex-1 min-w-0 bg-zinc-700 border border-zinc-600 rounded-lg px-3 py-2 text-zinc-100 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-amber-400"
|
||||
/>
|
||||
<button
|
||||
onclick={() => triggerBookScrape(scrapeUrl)}
|
||||
disabled={!scrapeUrl.trim() || running || scraping}
|
||||
class="shrink-0 px-4 py-2 rounded-lg bg-zinc-600 text-zinc-100 font-semibold text-sm hover:bg-zinc-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{scraping ? 'Queuing…' : 'Scrape'}
|
||||
</button>
|
||||
</div>
|
||||
{#if scrapeError}
|
||||
<p class="text-sm text-red-400">{scrapeError}</p>
|
||||
{/if}
|
||||
<div id="book-form" class="flex items-center gap-3 px-4 py-3 bg-zinc-900">
|
||||
<span class="text-sm text-zinc-400 w-36 shrink-0">Single book</span>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={scrapeUrl}
|
||||
placeholder="https://novelfire.net/book/…"
|
||||
class="flex-1 min-w-0 bg-zinc-800 border border-zinc-700 rounded-md px-3 py-1.5 text-zinc-100 text-sm placeholder-zinc-600 focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
/>
|
||||
<button
|
||||
onclick={() => triggerBookScrape(scrapeUrl)}
|
||||
disabled={!scrapeUrl.trim() || running || scraping}
|
||||
class="shrink-0 px-3 py-1.5 rounded-md bg-zinc-700 text-zinc-100 font-medium text-xs hover:bg-zinc-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{scraping ? 'Queuing…' : 'Scrape'}
|
||||
</button>
|
||||
{#if scrapeError}<span class="text-xs text-red-400">{scrapeError}</span>{/if}
|
||||
</div>
|
||||
|
||||
<!-- Range scrape -->
|
||||
<div id="range-form" class="bg-zinc-800 rounded-xl border border-zinc-700 p-5 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-zinc-300">Scrape chapter range</h2>
|
||||
<div id="range-form" class="flex items-center gap-3 px-4 py-3 bg-zinc-900 flex-wrap">
|
||||
<span class="text-sm text-zinc-400 w-36 shrink-0">Chapter range</span>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={rangeUrl}
|
||||
placeholder="https://novelfire.net/book/…"
|
||||
class="w-full bg-zinc-700 border border-zinc-600 rounded-lg px-3 py-2 text-zinc-100 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-amber-400"
|
||||
class="flex-1 min-w-0 bg-zinc-800 border border-zinc-700 rounded-md px-3 py-1.5 text-zinc-100 text-sm placeholder-zinc-600 focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
bind:value={rangeFrom}
|
||||
min="1"
|
||||
placeholder="From ch."
|
||||
class="w-full bg-zinc-700 border border-zinc-600 rounded-lg px-3 py-2 text-zinc-100 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-amber-400"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
bind:value={rangeTo}
|
||||
min="1"
|
||||
placeholder="To ch. (opt)"
|
||||
class="w-full bg-zinc-700 border border-zinc-600 rounded-lg px-3 py-2 text-zinc-100 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-amber-400"
|
||||
/>
|
||||
<button
|
||||
onclick={triggerRangeScrape}
|
||||
disabled={!rangeUrl.trim() || rangeFrom === null || running || ranging}
|
||||
class="shrink-0 px-4 py-2 rounded-lg bg-zinc-600 text-zinc-100 font-semibold text-sm hover:bg-zinc-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{ranging ? 'Queuing…' : 'Go'}
|
||||
</button>
|
||||
</div>
|
||||
{#if rangeError}
|
||||
<p class="text-sm text-red-400">{rangeError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick-scrape genre links -->
|
||||
<div class="bg-zinc-800 rounded-xl border border-zinc-700 p-5 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-zinc-300">Quick genre refresh</h2>
|
||||
<p class="text-xs text-zinc-500">Paste one of these into the single-book scraper to re-index a genre, or use them as starting points for range scrapes.</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each quickScrapes as qs}
|
||||
<button
|
||||
onclick={() => { scrapeUrl = qs.url; }}
|
||||
class="px-3 py-1.5 rounded-lg text-xs font-medium bg-zinc-700 text-zinc-300 border border-zinc-600 hover:border-amber-400/60 hover:text-amber-300 transition-colors"
|
||||
>
|
||||
{qs.label}
|
||||
</button>
|
||||
{/each}
|
||||
<a
|
||||
href="https://novelfire.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="px-3 py-1.5 rounded-lg text-xs font-medium bg-zinc-700/50 text-zinc-400 border border-zinc-600/50 hover:text-amber-300 hover:border-amber-400/40 transition-colors"
|
||||
<input
|
||||
type="number"
|
||||
bind:value={rangeFrom}
|
||||
min="1"
|
||||
placeholder="From"
|
||||
class="w-20 bg-zinc-800 border border-zinc-700 rounded-md px-3 py-1.5 text-zinc-100 text-sm placeholder-zinc-600 focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
bind:value={rangeTo}
|
||||
min="1"
|
||||
placeholder="To"
|
||||
class="w-20 bg-zinc-800 border border-zinc-700 rounded-md px-3 py-1.5 text-zinc-100 text-sm placeholder-zinc-600 focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
/>
|
||||
<button
|
||||
onclick={triggerRangeScrape}
|
||||
disabled={!rangeUrl.trim() || rangeFrom === null || running || ranging}
|
||||
class="shrink-0 px-3 py-1.5 rounded-md bg-zinc-700 text-zinc-100 font-medium text-xs hover:bg-zinc-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Browse novelfire.net ↗
|
||||
</a>
|
||||
{ranging ? 'Queuing…' : 'Go'}
|
||||
</button>
|
||||
{#if rangeError}<span class="text-xs text-red-400 w-full pl-40">{rangeError}</span>{/if}
|
||||
</div>
|
||||
|
||||
<!-- Quick genre chips -->
|
||||
<div class="flex items-center gap-3 px-4 py-3 bg-zinc-900 flex-wrap">
|
||||
<span class="text-sm text-zinc-400 w-36 shrink-0">Quick genres</span>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#each quickScrapes as qs}
|
||||
<button
|
||||
onclick={() => { scrapeUrl = qs.url; }}
|
||||
class="px-2.5 py-1 rounded text-xs font-medium bg-zinc-800 text-zinc-400 border border-zinc-700 hover:border-amber-400/50 hover:text-amber-300 transition-colors"
|
||||
>
|
||||
{qs.label}
|
||||
</button>
|
||||
{/each}
|
||||
<a
|
||||
href="https://novelfire.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="px-2.5 py-1 rounded text-xs font-medium text-zinc-500 border border-zinc-700/50 hover:text-amber-300 hover:border-amber-400/40 transition-colors"
|
||||
>
|
||||
novelfire.net ↗
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tasks table -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h2 class="text-lg font-semibold text-zinc-100 flex-1">Task history</h2>
|
||||
<h2 class="text-sm font-semibold text-zinc-400 flex-1 uppercase tracking-widest">Task history</h2>
|
||||
<input
|
||||
type="search"
|
||||
bind:value={q}
|
||||
|
||||
Reference in New Issue
Block a user