bugfix: third-pass audit follow-ups (F1-F4 + dockerignore)

- F1: backend/Dockerfile now copies Cargo.lock alongside Cargo.toml
  and builds with --locked, so the production image runs against the
  exact crate versions CI tested. Without this, cargo silently
  resolved fresh on each image build and "we tested it" stopped being
  true for the binary you ship.
- F2: POST /api/v1/mangas/{id}/chapters rejects chapter `number < 1`
  with 422 validation_failed. Mirrors the bookmark page>=1 rule from
  0.9.4 — chapter numbers are 1-indexed everywhere (URLs, upload
  form, reader) and 0/negative numbers had no legitimate use. Three
  cases (0, -1, -100) in api_uploads.rs.
- F3: bookmarks/+page.ts no longer re-throws non-401 ApiErrors as
  SvelteKit's generic 500 page. Surfaces the error message inline via
  a new `data.error` field; the page renders an alert when present.
  Same UX shape as the home page's existing error handling.
- F4: dropped Space from the reader keyboard binding. On portrait
  phones and narrow desktop windows the page image overflows the
  viewport and the user expects Space to scroll — preventDefaulting
  it skipped past unread content. ArrowRight + j remain.
- New backend/.dockerignore and frontend/.dockerignore so the local
  target/ and node_modules/ don't get shipped into the build context
  on every `docker compose build`.

Lockstep version bump to 0.10.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-17 00:13:14 +02:00
parent 18e2cb610a
commit 49f6d4d213
11 changed files with 93 additions and 11 deletions

13
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
# Shrink the build context the daemon ships to Docker. node_modules in
# particular would otherwise be re-uploaded on every build despite the
# Dockerfile re-installing it from scratch inside the builder.
node_modules/
build/
.svelte-kit/
e2e/
test-results/
playwright-report/
*.md
.env
.env.*

View File

@@ -1,6 +1,6 @@
{
"name": "mangalord-frontend",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"scripts": {

View File

@@ -2,6 +2,7 @@
let { data } = $props();
const authenticated = $derived(data.authenticated);
const bookmarks = $derived(data.bookmarks);
const error = $derived(data.error);
</script>
<svelte:head>
@@ -10,7 +11,11 @@
<h1>Bookmarks</h1>
{#if !authenticated}
{#if error}
<p role="alert" data-testid="bookmarks-error">
Couldn't load bookmarks: {error}
</p>
{:else if !authenticated}
<p data-testid="bookmarks-signin">
<a href="/login">Sign in</a> to see your bookmarks.
</p>

View File

@@ -7,10 +7,18 @@ export const ssr = false;
export const load: PageLoad = async () => {
try {
const page = await listMyBookmarks();
return { bookmarks: page.items, authenticated: true };
return { bookmarks: page.items, authenticated: true, error: null };
} catch (e) {
if (e instanceof ApiError && e.status === 401) {
return { bookmarks: [], authenticated: false };
return { bookmarks: [], authenticated: false, error: null };
}
// Anything else (502 upstream_unavailable from a backend
// restart, 500 internal_error) is rendered inline rather than
// re-thrown — SvelteKit's generic error.html is not the right
// UX for a transient API blip and the user is already
// authenticated as far as we know.
if (e instanceof ApiError) {
return { bookmarks: [], authenticated: true, error: e.message };
}
throw e;
}

View File

@@ -32,10 +32,14 @@
// Don't hijack keys while the user is typing in an input.
const target = e.target as HTMLElement | null;
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) return;
// Space is deliberately *not* bound — on viewports where a page
// image overflows (portrait phones, narrow desktop windows),
// users expect Space to scroll the page, and stealing it for
// next-page would skip past unread content. ArrowRight + j
// are the discoverable bindings.
switch (e.key) {
case 'ArrowRight':
case 'j':
case ' ':
e.preventDefault();
next();
break;