Compare commits

...

4 Commits

Author SHA1 Message Date
Admin
023b1f7fec feat(observability): add GlitchTip source map uploads for un-minified stack traces
Some checks failed
CI / Check ui (pull_request) Failing after 11s
CI / Docker / caddy (pull_request) Failing after 11s
CI / Docker / ui (pull_request) Has been skipped
CI / Test backend (pull_request) Successful in 30s
Release / Check ui (push) Failing after 38s
Release / Upload source maps (push) Has been skipped
Release / Docker / ui (push) Has been skipped
Release / Test backend (push) Successful in 48s
Release / Docker / caddy (push) Successful in 45s
CI / Docker / backend (pull_request) Has been cancelled
CI / Docker / runner (pull_request) Has been cancelled
Release / Docker / runner (push) Has been cancelled
Release / Gitea Release (push) Has been cancelled
Release / Docker / backend (push) Has been cancelled
- Enable sourcemap:true in vite.config.ts
- Add sentryVitePlugin: uploads maps to errors.libnovel.cc, deletes them post-upload so they never ship in the Docker image
- Wire release: PUBLIC_BUILD_VERSION in both hooks.client.ts and hooks.server.ts so events correlate to the correct artifact
- Add upload-sourcemaps CI job in release.yaml (parallel to docker-ui, uses GLITCHTIP_AUTH_TOKEN secret)
2026-03-25 20:26:19 +05:00
Admin
7e99fc6d70 fix(runner): fix audio task infinite loop and semaphore race
Some checks failed
Release / Check ui (push) Successful in 22s
Release / Test backend (push) Successful in 33s
Release / Docker / backend (push) Failing after 30s
Release / Docker / caddy (push) Successful in 1m9s
Release / Docker / ui (push) Successful in 1m34s
Release / Docker / runner (push) Failing after 1m15s
Release / Gitea Release (push) Has been skipped
Two bugs caused audio tasks to loop endlessly:

1. claimRecord never set heartbeat_at — newly claimed tasks had
   heartbeat_at=null, which matched the reaper's stale filter
   (heartbeat_at=null || heartbeat_at<threshold). Tasks were reaped
   and reset to pending within seconds of being claimed, before the
   30s heartbeat goroutine had a chance to write a timestamp.
   Fix: set heartbeat_at=now() in claimRecord alongside status=running.

2. Audio semaphore was checked AFTER claiming the task. When the
   semaphore was full the select/break only broke the inner select,
   not the for loop — the code fell through and launched an uncapped
   goroutine that blocked forever on <-audioSem drain. The task also
   stayed status=running with no heartbeat, feeding bug #1.
   Fix: pre-acquire a semaphore slot BEFORE claiming the task; release
   it immediately if the queue is empty or claim fails.
2026-03-25 15:09:52 +05:00
Admin
12d6d30fb0 feat: add /terms page and make disclaimer/privacy/dmca/terms public routes
Some checks failed
CI / Check ui (pull_request) Successful in 20s
Release / Test backend (push) Successful in 42s
Release / Check ui (push) Successful in 38s
CI / Docker / caddy (pull_request) Successful in 3m2s
Release / Docker / caddy (push) Successful in 1m36s
CI / Docker / ui (pull_request) Successful in 1m19s
Release / Docker / backend (push) Successful in 1m42s
CI / Test backend (pull_request) Successful in 6m54s
Release / Docker / ui (push) Successful in 1m38s
Release / Docker / runner (push) Successful in 2m28s
Release / Gitea Release (push) Failing after 2s
CI / Docker / backend (pull_request) Failing after 43s
CI / Docker / runner (pull_request) Successful in 1m20s
2026-03-25 13:55:27 +05:00
Admin
f9c14685b3 feat(auth): make /disclaimer, /privacy, /dmca public routes
Some checks failed
CI / Test backend (pull_request) Successful in 28s
CI / Check ui (pull_request) Successful in 33s
Release / Test backend (push) Successful in 18s
Release / Check ui (push) Successful in 29s
CI / Docker / backend (pull_request) Failing after 20s
Release / Docker / caddy (push) Successful in 1m31s
CI / Docker / ui (pull_request) Successful in 1m8s
CI / Docker / runner (pull_request) Successful in 2m17s
Release / Docker / backend (push) Successful in 1m31s
Release / Docker / runner (push) Successful in 2m24s
Release / Docker / ui (push) Successful in 1m25s
CI / Docker / caddy (pull_request) Successful in 6m47s
Release / Gitea Release (push) Failing after 1s
2026-03-25 13:51:36 +05:00
10 changed files with 137 additions and 14 deletions

View File

@@ -135,6 +135,35 @@ jobs:
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-runner:latest
cache-to: type=inline
# ── ui: source map upload ─────────────────────────────────────────────────────
# Builds the UI with source maps and uploads them to GlitchTip so that error
# stack traces resolve to original .svelte/.ts file names and line numbers.
# Runs in parallel with docker-ui (both need check-ui to pass first).
upload-sourcemaps:
name: Upload source maps
runs-on: ubuntu-latest
needs: [check-ui]
defaults:
run:
working-directory: ui
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: ui/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build with source maps and upload to GlitchTip
env:
SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
BUILD_VERSION: ${{ gitea.ref_name }}
run: npm run build
# ── docker: ui ────────────────────────────────────────────────────────────────
docker-ui:
name: Docker / ui
@@ -213,7 +242,7 @@ jobs:
release:
name: Gitea Release
runs-on: ubuntu-latest
needs: [docker-backend, docker-runner, docker-ui, docker-caddy]
needs: [docker-backend, docker-runner, docker-ui, docker-caddy, upload-sourcemaps]
steps:
- uses: actions/checkout@v4
with:

View File

@@ -248,23 +248,30 @@ func (r *Runner) poll(ctx context.Context, scrapeSem, audioSem chan struct{}, wg
}
// ── Audio tasks ───────────────────────────────────────────────────────
// Only claim tasks when there is a free slot in the semaphore.
// This avoids the old bug where we claimed (status→running) a task and
// then couldn't dispatch it, leaving it orphaned until the reaper fired.
audioLoop:
for {
if ctx.Err() != nil {
return
}
// Check capacity before claiming to avoid orphaning tasks.
select {
case audioSem <- struct{}{}:
// Slot acquired — proceed to claim a task.
default:
// All slots busy; leave remaining pending tasks for next tick.
break audioLoop
}
task, ok, err := r.deps.Consumer.ClaimNextAudioTask(ctx, r.cfg.WorkerID)
if err != nil {
<-audioSem // release the pre-acquired slot
r.deps.Log.Error("runner: ClaimNextAudioTask failed", "err", err)
break
}
if !ok {
break
}
select {
case audioSem <- struct{}{}:
default:
r.deps.Log.Warn("runner: audio semaphore full, will retry next tick",
"task_id", task.ID)
<-audioSem // release the pre-acquired slot; queue empty
break
}
r.tasksRunning.Add(1)

View File

@@ -247,8 +247,9 @@ func (c *pbClient) claimRecord(ctx context.Context, collection, workerID string,
}
claim := map[string]any{
"status": string(domain.TaskStatusRunning),
"worker_id": workerID,
"status": string(domain.TaskStatusRunning),
"worker_id": workerID,
"heartbeat_at": time.Now().UTC().Format(time.RFC3339),
}
for k, v := range extraClaim {
claim[k] = v

1
ui/package-lock.json generated
View File

@@ -17,6 +17,7 @@
"pocketbase": "^0.26.8"
},
"devDependencies": {
"@sentry/vite-plugin": "^5.1.1",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.50.2",

View File

@@ -12,6 +12,7 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sentry/vite-plugin": "^5.1.1",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.50.2",

View File

@@ -6,7 +6,10 @@ import { env } from '$env/dynamic/public';
if (env.PUBLIC_GLITCHTIP_DSN) {
Sentry.init({
dsn: env.PUBLIC_GLITCHTIP_DSN,
tracesSampleRate: 0.1
tracesSampleRate: 0.1,
// Must match the release name used when uploading source maps in CI
// (BUILD_VERSION injected by Dockerfile as PUBLIC_BUILD_VERSION).
release: env.PUBLIC_BUILD_VERSION || undefined
});
}

View File

@@ -13,7 +13,10 @@ import { drain as drainPresignCache } from '$lib/server/presignCache';
if (pubEnv.PUBLIC_GLITCHTIP_DSN) {
Sentry.init({
dsn: pubEnv.PUBLIC_GLITCHTIP_DSN,
tracesSampleRate: 0.1
tracesSampleRate: 0.1,
// Must match the release name used when uploading source maps in CI
// (BUILD_VERSION injected by Dockerfile as PUBLIC_BUILD_VERSION).
release: pubEnv.PUBLIC_BUILD_VERSION || undefined
});
}

View File

@@ -4,7 +4,7 @@ import { getSettings } from '$lib/server/pocketbase';
import { log } from '$lib/server/logger';
// Routes that are accessible without being logged in
const PUBLIC_ROUTES = new Set(['/login']);
const PUBLIC_ROUTES = new Set(['/login', '/disclaimer', '/privacy', '/dmca', '/terms']);
export const load: LayoutServerLoad = async ({ locals, url }) => {
// Allow /auth/* (OAuth initiation + callbacks) without login

View File

@@ -0,0 +1,51 @@
<svelte:head>
<title>Terms of Service — libnovel</title>
</svelte:head>
<div class="max-w-2xl mx-auto py-10 px-4">
<h1 class="text-2xl font-bold text-zinc-100 mb-6">Terms of Service</h1>
<div class="space-y-5 text-sm text-zinc-400 leading-relaxed">
<p>
By using libnovel you agree to these terms. If you do not agree, please do not use the service.
</p>
<h2 class="text-base font-semibold text-zinc-200 mt-6">Use of the service</h2>
<ul class="list-disc list-inside space-y-2 pl-1">
<li>libnovel is provided for personal, non-commercial reading use only.</li>
<li>You may not scrape, crawl, or systematically download content from the site.</li>
<li>You may not use the service for any unlawful purpose.</li>
<li>Accounts may be suspended or terminated for abuse.</li>
</ul>
<h2 class="text-base font-semibold text-zinc-200 mt-6">Content</h2>
<p>
libnovel aggregates publicly available web novel content from third-party sources for
personal reading convenience. We do not claim ownership of any novel content displayed on
the site. If you are a rights holder and wish to have content removed, please see our
<a href="/dmca" class="text-amber-400 hover:text-amber-300 transition-colors">DMCA policy</a>.
</p>
<h2 class="text-base font-semibold text-zinc-200 mt-6">Accounts</h2>
<p>
You are responsible for maintaining the security of your account. libnovel is not liable
for any loss or damage resulting from unauthorised access to your account.
</p>
<h2 class="text-base font-semibold text-zinc-200 mt-6">Disclaimer of warranties</h2>
<p>
The service is provided "as is" without warranty of any kind. We do not guarantee
availability, accuracy, or completeness of any content. See our full
<a href="/disclaimer" class="text-amber-400 hover:text-amber-300 transition-colors">disclaimer</a>
for details.
</p>
<h2 class="text-base font-semibold text-zinc-200 mt-6">Changes to these terms</h2>
<p>
We may update these terms at any time. Continued use of the service after changes are
posted constitutes acceptance of the revised terms.
</p>
<p class="text-zinc-600 text-xs mt-8">Last updated: {new Date().getFullYear()}</p>
</div>
</div>

View File

@@ -1,9 +1,36 @@
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import { sentryVitePlugin } from '@sentry/vite-plugin';
// Source maps are always generated so that the CI pipeline can upload them to
// GlitchTip after a release build. The sentryVitePlugin upload step is only
// active when SENTRY_AUTH_TOKEN is present (i.e. in the release CI job).
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
build: {
sourcemap: true
},
plugins: [
tailwindcss(),
sveltekit(),
sentryVitePlugin({
org: 'libnovel',
project: 'libnovel-ui',
url: 'https://errors.libnovel.cc/',
// Auth token injected by CI via SENTRY_AUTH_TOKEN env var.
// When the env var is absent the plugin is a no-op.
authToken: process.env.SENTRY_AUTH_TOKEN,
// Release name matches the Docker image tag (e.g. "1.2.3").
release: { name: process.env.BUILD_VERSION },
// Don't upload source maps in local dev or CI type-check jobs.
disable: !process.env.SENTRY_AUTH_TOKEN,
// Source maps are uploaded to GlitchTip; do not ship them in the
// production bundle served to browsers.
sourceMapsUploadOptions: {
filesToDeleteAfterUpload: ['./build/**/*.map']
}
})
],
ssr: {
// Force these packages to be bundled into the server output rather than
// treated as external requires. The production Docker image has no