- Import task: persist object_key, author, cover_url, genres, summary, book_status in PocketBase so the runner can fetch the file and write book metadata on completion - Runner poll mode: pass task.ObjectKey instead of empty string - Runner: write BookMeta + UpsertBook in Meilisearch after chapter ingest so imported books appear in catalogue and search - Import UI: add author, cover URL, genres, summary, status fields; add AI tasks panel (chapter names, description, image gen, tagline) after import completes; add AI tasks button on each done task in the list - Admin nav: add Notifications entry to sidebar (all 5 locales) - Logout: delete user_sessions row on sign-out so sessions don't accumulate as phantoms after each login/logout cycle
195 lines
8.6 KiB
Svelte
195 lines
8.6 KiB
Svelte
<script lang="ts">
|
|
import { page } from '$app/state';
|
|
import * as m from '$lib/paraglide/messages.js';
|
|
|
|
const internalLinks = [
|
|
{
|
|
href: '/admin/scrape',
|
|
label: () => m.admin_nav_scrape(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />`
|
|
},
|
|
{
|
|
href: '/admin/audio',
|
|
label: () => m.admin_nav_audio(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />`
|
|
},
|
|
{
|
|
href: '/admin/translation',
|
|
label: () => m.admin_nav_translation(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />`
|
|
},
|
|
{
|
|
href: '/admin/import',
|
|
label: () => m.admin_nav_import(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />`
|
|
},
|
|
{
|
|
href: '/admin/image-gen',
|
|
label: () => m.admin_nav_image_gen(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />`
|
|
},
|
|
{
|
|
href: '/admin/text-gen',
|
|
label: () => m.admin_nav_text_gen(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />`
|
|
},
|
|
{
|
|
href: '/admin/ai-jobs',
|
|
label: () => m.admin_nav_ai_jobs(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />`
|
|
},
|
|
{
|
|
href: '/admin/notifications',
|
|
label: () => m.admin_nav_notifications(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />`
|
|
},
|
|
{
|
|
href: '/admin/catalogue-tools',
|
|
label: () => m.admin_nav_catalogue_tools(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />`
|
|
},
|
|
{
|
|
href: '/admin/changelog',
|
|
label: () => m.admin_nav_changelog(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4h10a2 2 0 012 2v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6a2 2 0 012-2z" />`
|
|
}
|
|
];
|
|
|
|
const externalLinks = [
|
|
{
|
|
href: 'https://feedback.libnovel.cc',
|
|
label: () => m.admin_nav_feedback(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />`
|
|
},
|
|
{
|
|
href: 'https://errors.libnovel.cc',
|
|
label: () => m.admin_nav_errors(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />`
|
|
},
|
|
{
|
|
href: 'https://analytics.libnovel.cc',
|
|
label: () => m.admin_nav_analytics(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />`
|
|
},
|
|
{
|
|
href: 'https://logs.libnovel.cc',
|
|
label: () => m.admin_nav_logs(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h10" />`
|
|
},
|
|
{
|
|
href: 'https://grafana.libnovel.cc',
|
|
label: () => m.admin_nav_grafana(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />`
|
|
},
|
|
{
|
|
href: 'https://uptime.libnovel.cc',
|
|
label: () => m.admin_nav_uptime(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />`
|
|
},
|
|
{
|
|
href: 'https://push.libnovel.cc',
|
|
label: () => m.admin_nav_push(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />`
|
|
},
|
|
{
|
|
href: 'https://gitea.kalekber.cc/kamil/libnovel',
|
|
label: () => m.admin_nav_gitea(),
|
|
icon: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />`
|
|
}
|
|
];
|
|
|
|
interface Props {
|
|
children?: import('svelte').Snippet;
|
|
}
|
|
let { children }: Props = $props();
|
|
|
|
let sidebarOpen = $state(false);
|
|
</script>
|
|
|
|
<!-- Mobile sidebar overlay -->
|
|
{#if sidebarOpen}
|
|
<button
|
|
class="fixed inset-0 z-40 bg-black/50 md:hidden"
|
|
onclick={() => (sidebarOpen = false)}
|
|
aria-label="Close sidebar"
|
|
></button>
|
|
{/if}
|
|
|
|
<div class="flex min-h-[calc(100vh-4rem)] gap-0">
|
|
<!-- Sidebar -->
|
|
<aside
|
|
class="
|
|
fixed top-0 left-0 h-full z-50 w-56 shrink-0 border-r border-(--color-border) px-2 py-5 flex flex-col gap-5
|
|
bg-(--color-surface) transition-transform duration-200
|
|
{sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
|
md:relative md:translate-x-0 md:w-52 md:z-auto md:top-auto md:h-auto
|
|
"
|
|
>
|
|
<!-- Internal pages -->
|
|
<div>
|
|
<p class="px-3 mb-1.5 text-[10px] font-semibold text-(--color-muted) uppercase tracking-widest">{m.admin_pages_label()}</p>
|
|
<nav class="flex flex-col gap-0.5">
|
|
{#each internalLinks as link}
|
|
{@const active = page.url.pathname.startsWith(link.href)}
|
|
<a
|
|
href={link.href}
|
|
onclick={() => (sidebarOpen = false)}
|
|
class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors flex items-center gap-2.5
|
|
{active
|
|
? 'bg-(--color-surface-2) text-(--color-text)'
|
|
: 'text-(--color-muted) hover:bg-(--color-surface-2)/60 hover:text-(--color-text)'}"
|
|
>
|
|
<svg class="w-3.5 h-3.5 shrink-0 {active ? 'text-(--color-brand)' : 'opacity-50'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
{@html link.icon}
|
|
</svg>
|
|
{link.label()}
|
|
</a>
|
|
{/each}
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="border-t border-(--color-border)"></div>
|
|
|
|
<!-- External tools -->
|
|
<div>
|
|
<p class="px-3 mb-1.5 text-[10px] font-semibold text-(--color-muted) uppercase tracking-widest">{m.admin_tools_label()}</p>
|
|
<nav class="flex flex-col gap-0.5">
|
|
{#each externalLinks as link}
|
|
<a
|
|
href={link.href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="px-3 py-1.5 rounded-md text-sm font-medium text-(--color-muted) hover:bg-(--color-surface-2)/60 hover:text-(--color-text) transition-colors flex items-center gap-2.5"
|
|
>
|
|
<svg class="w-3.5 h-3.5 shrink-0 opacity-40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
{@html link.icon}
|
|
</svg>
|
|
<span class="flex-1">{link.label()}</span>
|
|
<svg class="w-2.5 h-2.5 shrink-0 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
</a>
|
|
{/each}
|
|
</nav>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main content -->
|
|
<main class="flex-1 min-w-0 px-4 py-6 md:px-8">
|
|
<!-- Mobile nav toggle -->
|
|
<button
|
|
onclick={() => (sidebarOpen = true)}
|
|
class="md:hidden mb-4 flex items-center gap-2 text-sm text-(--color-muted) hover:text-(--color-text) transition-colors"
|
|
aria-label="Open navigation"
|
|
>
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
Admin menu
|
|
</button>
|
|
|
|
{@render children?.()}
|
|
</main>
|
|
</div>
|