Compare commits

...

3 Commits

Author SHA1 Message Date
Admin
45f5c51da6 fix(ci): strip 'build' from .dockerignore before docker-ui build
All checks were successful
Release / Test backend (push) Successful in 41s
Release / Check ui (push) Successful in 1m41s
Release / Docker / caddy (push) Successful in 41s
Release / Docker / backend (push) Successful in 3m23s
Release / Docker / runner (push) Successful in 2m53s
Release / Upload source maps (push) Successful in 1m27s
Release / Docker / ui (push) Successful in 2m32s
Release / Gitea Release (push) Successful in 42s
When PREBUILT=1 the pre-built artifact is downloaded into ui/build/ but
.dockerignore excludes 'build', so Docker never sees it and /app/build
doesn't exist in the builder stage — causing the runtime COPY to fail.

Fix: rewrite ui/.dockerignore on the CI runner (grep -v '^build$') so the
pre-built directory is included in the Docker context.

Also in this commit:
- book page: gate EPUB download on isPro (UI upsell + server 403 guard)
- book page: chapter names default pattern changed to '{scene}'
2026-04-05 21:26:05 +05:00
Admin
55df88c3e5 fix(player): remove duplicate inline player and declutter bottom bar
Some checks failed
Release / Test backend (push) Successful in 37s
Release / Check ui (push) Successful in 1m47s
Release / Docker / caddy (push) Successful in 50s
Release / Docker / backend (push) Successful in 3m11s
Release / Docker / runner (push) Successful in 2m42s
Release / Upload source maps (push) Successful in 1m30s
Release / Docker / ui (push) Failing after 1m35s
Release / Gitea Release (push) Has been skipped
When the mini-player is active on the current chapter, the collapsible
'Listen to this chapter' panel now shows a brief note instead of rendering
a full second AudioPlayer.

Bottom bar track info column: removed chapter title and book title lines
so the time display fits on one line without crowding the controls.
2026-04-05 20:31:01 +05:00
Admin
eb137fdbf5 fix: source maps — ship pre-built artifact with injected debug IDs to Docker
Some checks failed
Release / Test backend (push) Successful in 46s
Release / Check ui (push) Successful in 1m38s
Release / Docker / caddy (push) Successful in 49s
Release / Docker / backend (push) Successful in 2m54s
Release / Docker / runner (push) Successful in 2m52s
Release / Upload source maps (push) Successful in 1m39s
Release / Docker / ui (push) Failing after 1m34s
Release / Gitea Release (push) Has been skipped
The docker-ui job was rebuilding the UI from scratch inside Docker, producing
chunk hashes and JS files that didn't match the source maps uploaded to
GlitchTip, causing all stack traces to appear minified.

Fix:
- ui/Dockerfile: add PREBUILT=1 ARG; skip npm run build when set
- release.yaml upload-sourcemaps: re-upload artifact after sentry-cli inject
- release.yaml docker-ui: download injected artifact into ui/build/ and pass
  PREBUILT=1 so Docker reuses the exact same JS files whose debug IDs are in
  GlitchTip
2026-04-05 20:25:27 +05:00
7 changed files with 73 additions and 32 deletions

View File

@@ -171,6 +171,13 @@ jobs:
SENTRY_ORG: libnovel
SENTRY_PROJECT: ui
- name: Upload injected build (for docker-ui)
uses: actions/upload-artifact@v3
with:
name: ui-build-injected
path: build
retention-days: 1
- name: Create GlitchTip release
run: sentry-cli releases new ${{ steps.ver.outputs.version }}
env:
@@ -226,6 +233,17 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Download injected build (debug IDs already embedded)
uses: actions/download-artifact@v3
with:
name: ui-build-injected
path: ui/build
- name: Allow build/ into Docker context (override .dockerignore)
run: |
grep -v '^build$' ui/.dockerignore > ui/.dockerignore.tmp
mv ui/.dockerignore.tmp ui/.dockerignore
- uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
@@ -255,6 +273,7 @@ jobs:
BUILD_VERSION=${{ steps.meta.outputs.version }}
BUILD_COMMIT=${{ gitea.sha }}
BUILD_TIME=${{ gitea.event.head_commit.timestamp }}
PREBUILT=1
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-ui:latest
cache-to: type=inline

View File

@@ -21,7 +21,11 @@ ENV PUBLIC_BUILD_VERSION=$BUILD_VERSION
ENV PUBLIC_BUILD_COMMIT=$BUILD_COMMIT
ENV PUBLIC_BUILD_TIME=$BUILD_TIME
RUN npm run build
# PREBUILT=1 skips npm run build — used in CI when the build/ directory has
# already been compiled (and debug IDs injected) by a prior job. The caller
# must copy the pre-built build/ into the Docker context before building.
ARG PREBUILT=0
RUN [ "$PREBUILT" = "1" ] || npm run build
# ── Runtime image ──────────────────────────────────────────────────────────────
# adapter-node bundles most server-side code, but packages with dynamic

View File

@@ -857,12 +857,6 @@
aria-label={audioStore.chapters.length > 0 ? m.player_toggle_chapter_list() : undefined}
title={audioStore.chapters.length > 0 ? m.player_chapter_list_label() : undefined}
>
{#if audioStore.chapterTitle}
<p class="text-xs text-(--color-text) truncate leading-tight">{audioStore.chapterTitle}</p>
{/if}
{#if audioStore.bookTitle}
<p class="text-xs text-(--color-muted) truncate leading-tight">{audioStore.bookTitle}</p>
{/if}
{#if audioStore.status === 'generating'}
<p class="text-xs text-(--color-brand) leading-tight">
{m.player_generating({ percent: String(Math.round(audioStore.progress)) })}

View File

@@ -2,7 +2,11 @@ import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { backendFetch } from '$lib/server/scraper';
export const GET: RequestHandler = async ({ params, url }) => {
export const GET: RequestHandler = async ({ params, url, locals }) => {
if (!locals.isPro) {
error(403, 'EPUB download requires a Pro subscription');
}
const { slug } = params;
const from = url.searchParams.get('from');
const to = url.searchParams.get('to');

View File

@@ -64,8 +64,9 @@ export const load: PageServerLoad = async ({ params, locals }) => {
lastChapter: null,
userRating: 0,
ratingAvg: { avg: 0, count: 0 },
isAdmin: locals.user?.role === 'admin',
isLoggedIn: !!locals.user,
isAdmin: locals.user?.role === 'admin',
isPro: locals.isPro,
isLoggedIn: !!locals.user,
currentUserId: locals.user?.id ?? '',
scraping: true,
taskId: body.task_id

View File

@@ -326,7 +326,7 @@
let chapNamesPreview = $state<{ number: number; old_title: string; new_title: string; edited: string }[]>([]);
let chapNamesApplying = $state(false);
let chapNamesResult = $state<'applied' | 'error' | ''>('');
let chapNamesPattern = $state('Chapter {n}: {scene}');
let chapNamesPattern = $state('{scene}');
let chapNamesBatchProgress = $state('');
let chapNamesBatchWarnings = $state<string[]>([]);
@@ -342,7 +342,7 @@
const res = await fetch('/api/admin/text-gen/chapter-names', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, pattern: chapNamesPattern.trim() || 'Chapter {n}: {scene}' })
body: JSON.stringify({ slug, pattern: chapNamesPattern.trim() || '{scene}' })
});
if (!res.ok) {
chapNamesResult = 'error';
@@ -835,13 +835,22 @@
<p class="text-sm font-medium text-(--color-text)">Download</p>
<p class="text-xs text-(--color-muted)">All {chapterList.length} chapters as EPUB</p>
</div>
<a
href="/api/export/{book.slug}"
download="{book.slug}.epub"
class="px-3 py-1.5 rounded-lg bg-(--color-surface-2) border border-(--color-border) text-sm font-medium text-(--color-muted) hover:text-(--color-text) hover:border-zinc-500 transition-colors flex-shrink-0"
>
.epub
</a>
{#if data.isPro}
<a
href="/api/export/{book.slug}"
download="{book.slug}.epub"
class="px-3 py-1.5 rounded-lg bg-(--color-surface-2) border border-(--color-border) text-sm font-medium text-(--color-muted) hover:text-(--color-text) hover:border-zinc-500 transition-colors flex-shrink-0"
>
.epub
</a>
{:else}
<a
href="/profile"
class="px-3 py-1.5 rounded-lg bg-(--color-brand)/15 border border-(--color-brand)/30 text-sm font-medium text-(--color-brand) hover:bg-(--color-brand)/25 transition-colors flex-shrink-0"
>
Pro
</a>
{/if}
</div>
{/if}
@@ -1135,7 +1144,7 @@
<input
type="text"
bind:value={chapNamesPattern}
placeholder="Chapter {'{n}'}: {'{scene}'}"
placeholder="{'{scene}'}"
class="w-full px-2 py-1.5 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
/>
<div class="flex items-center gap-3 flex-wrap">

View File

@@ -471,18 +471,28 @@
</button>
{#if audioExpanded}
<div class="border border-t-0 border-(--color-border) rounded-b-lg overflow-hidden">
<AudioPlayer
slug={data.book.slug}
chapter={data.chapter.number}
chapterTitle={data.chapter.title || m.reader_chapter_n({ n: String(data.chapter.number) })}
bookTitle={data.book.title}
cover={data.book.cover}
nextChapter={data.next}
chapters={data.chapters}
voices={data.voices}
playerStyle={layout.playerStyle}
onProRequired={() => { audioProRequired = true; }}
/>
{#if audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number && audioStore.active}
<!-- Mini-player is already playing this chapter — don't duplicate controls -->
<div class="px-4 py-3 flex items-center gap-2 text-sm text-(--color-muted)">
<svg class="w-4 h-4 text-(--color-brand) shrink-0" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 3v10.55A4 4 0 1014 17V7h4V3h-6z"/>
</svg>
<span>Controls are in the player bar below.</span>
</div>
{:else}
<AudioPlayer
slug={data.book.slug}
chapter={data.chapter.number}
chapterTitle={data.chapter.title || m.reader_chapter_n({ n: String(data.chapter.number) })}
bookTitle={data.book.title}
cover={data.book.cover}
nextChapter={data.next}
chapters={data.chapters}
voices={data.voices}
playerStyle={layout.playerStyle}
onProRequired={() => { audioProRequired = true; }}
/>
{/if}
</div>
{/if}
</div>