diff --git a/.env.example b/.env.example index 1e83047..6910cb0 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,11 @@ # Backend settings DATABASE_URL=sqlite:///./data/librislog.db -GOOGLE_BOOKS_API_KEY= # REQUIRED for Google Books fallback — without this key Google's API returns 503. - # Create one at https://console.cloud.google.com → APIs & Services → Credentials. +GOOGLE_BOOKS_API_KEY= # optional — see https://codebude.github.io/librislog/guide/api-keys CORS_ORIGINS=["http://localhost", "http://localhost:5173"] LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR | CRITICAL COVERS_DIR=./data/covers # Directory where downloaded cover images are stored locally. -API_KEY_ENCRYPTION_KEY=CHANGE_ME_TO_32PLUS_CHARS +# Generate a real secret: openssl rand -base64 32 +API_KEY_ENCRYPTION_KEY=CHANGE_ME_TO_32PLUS_CHARS # <-- YOU MUST CHANGE THIS AUTH_COOKIE_NAME=librislog_session AUTH_COOKIE_SECURE=false # set true in production (HTTPS) AUTH_COOKIE_SAMESITE=lax # lax | strict | none diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..e41af7f --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,100 @@ +name: Docker Build & Publish + +on: + push: + branches: [develop] + release: + types: [published] + workflow_dispatch: + inputs: + branch: + description: "Branch to build from" + required: false + default: develop + +env: + REGISTRY: ghcr.io + +jobs: + build-and-push: + runs-on: ubuntu-latest + strategy: + matrix: + service: + - name: frontend + image: librislog + dockerfile: ./frontend/Dockerfile + context: ./frontend + - name: backend + image: librislog-api + dockerfile: ./backend/Dockerfile + context: ./backend + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event.inputs.branch || github.ref }} + + - name: Derive version + id: version + run: | + if [ "${{ github.event_name }}" = "release" ]; then + VERSION="${{ github.event.release.tag_name }}" + else + VERSION="$(git describe --tags --always 2>/dev/null || echo 'v0.0.0-dev')" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "sha_short=$(echo '${{ github.sha }}' | cut -c1-7)" >> "$GITHUB_OUTPUT" + + - name: Sanitize version for Docker tag + id: sanitize + run: | + VERSION="${{ steps.version.outputs.version }}" + SANITIZED="$(echo "$VERSION" | sed 's/[^a-zA-Z0-9_.-]/-/g')" + SANITIZED="$(echo "$SANITIZED" | sed 's/^[^a-zA-Z0-9_]\+//')" + echo "sanitized_tag=${SANITIZED:0:128}" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image tags + id: tags + run: | + IMAGE="${{ env.REGISTRY }}/${{ github.repository }}/${{ matrix.service.image }}" + SHA_SHORT="${{ steps.version.outputs.sha_short }}" + + if [ "${{ github.event_name }}" = "release" ]; then + SANITIZED="${{ steps.sanitize.outputs.sanitized_tag }}" + TAGS="${IMAGE}:${SANITIZED},${IMAGE}:latest" + else + TAGS="${IMAGE}:develop,${IMAGE}:${SHA_SHORT}" + fi + + echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: ${{ matrix.service.context }} + file: ${{ matrix.service.dockerfile }} + push: true + platforms: linux/amd64 + tags: ${{ steps.tags.outputs.tags }} + build-args: | + APP_VERSION=${{ steps.version.outputs.version }} + GIT_SHA=${{ github.sha }} + ${{ matrix.service.name == 'frontend' && 'PUBLIC_DEFAULT_LOCALE=en' || '' }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..9c73808 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,109 @@ +name: Docs + +on: + push: + branches: [develop] + release: + types: [published] + +concurrency: + group: docs-deploy + cancel-in-progress: true + +env: + NODE_VERSION: "22" + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + path: release + + - uses: actions/checkout@v6 + with: + ref: develop + path: develop + + - name: Determine release ref + id: release-ref + working-directory: release + run: | + if [ "${{ github.event_name }}" = "release" ]; then + REF="${{ github.event.release.tag_name }}" + else + DEFAULT_BRANCH=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p') + REF=$(git tag --list 'v*' --sort=-version:refname | head -1) + REF="${REF:-$DEFAULT_BRANCH}" + fi + echo "ref=${REF}" >> "$GITHUB_OUTPUT" + + - name: Checkout release ref + working-directory: release + run: git checkout "${{ steps.release-ref.outputs.ref }}" + + - name: Check if release ref has docs + id: check-release-docs + working-directory: release + run: | + if [ -f docs/package.json ] && [ -f docs/.vitepress/config.ts ]; then + echo "has_docs=true" >> "$GITHUB_OUTPUT" + else + echo "has_docs=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: | + release/docs/package-lock.json + develop/docs/package-lock.json + + - name: Build release docs + if: steps.check-release-docs.outputs.has_docs == 'true' + working-directory: release/docs + run: | + npm ci + npx vitepress build + mv .vitepress/dist /tmp/dist-release + + - name: Build nightly docs + working-directory: develop/docs + run: | + npm ci + cp .vitepress/config.ts .vitepress/config.original.ts + cp .vitepress/config.nightly.ts .vitepress/config.ts + npx vitepress build + mv .vitepress/dist /tmp/dist-nightly + + - name: Prepare full deploy (release + nightly) + if: steps.check-release-docs.outputs.has_docs == 'true' + run: | + mv /tmp/dist-release ./dist + mkdir -p ./dist/next + cp -a /tmp/dist-nightly/. ./dist/next/ + + - name: Prepare nightly-only deploy + if: steps.check-release-docs.outputs.has_docs != 'true' + run: cp -a /tmp/dist-nightly/. ./dist/ + + - uses: peaceiris/actions-gh-pages@v4 + if: steps.check-release-docs.outputs.has_docs == 'true' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + keep_files: false + + - uses: peaceiris/actions-gh-pages@v4 + if: steps.check-release-docs.outputs.has_docs != 'true' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + destination_dir: next + keep_files: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..cb453d1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,279 @@ +name: Tests + +on: + push: + branches: [develop, main] + pull_request: + branches: [develop, main] + workflow_dispatch: + +env: + PYTHON_VERSION: "3.14" + NODE_VERSION: "24" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + +jobs: + backend: + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: uv sync --frozen + + - name: Run tests + run: uv run pytest --junitxml=report.xml --cov=app --cov-report=term-missing + + - name: Upload report + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-report-backend + path: backend/report.xml + + cli: + runs-on: ubuntu-latest + defaults: + run: + working-directory: cli + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: uv sync --frozen + + - name: Run tests + run: uv run pytest --junitxml=report.xml + + - name: Upload report + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-report-cli + path: cli/report.xml + + frontend: + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v6 + + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npx vitest run --reporter=junit --reporter=default --outputFile=report.xml + + - name: Upload report + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-report-frontend + path: frontend/report.xml + + e2e: + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Prepare E2E directories + run: | + docker run --rm -v ${{ github.workspace }}/data-e2e:/data alpine sh -c 'rm -rf /data/*' 2>/dev/null || true + mkdir -p playwright-report test-results + + - name: Build backend image + uses: docker/build-push-action@v7 + with: + context: ./backend + tags: librislog-e2e-backend + load: true + build-args: | + APP_VERSION=v0.0.0-dev + GIT_SHA=unknown + + - name: Build frontend image + uses: docker/build-push-action@v7 + with: + context: ./frontend + tags: librislog-e2e-frontend + load: true + build-args: | + APP_VERSION=v0.0.0-dev + GIT_SHA=unknown + PUBLIC_DEFAULT_LOCALE=en + + - name: Build test-runner image + uses: docker/build-push-action@v7 + with: + context: ./frontend + file: frontend/Dockerfile.e2e + tags: librislog-e2e-test-runner + load: true + + - name: Run E2E tests + run: docker compose -f ../docker-compose.e2e.yml up --abort-on-container-exit --attach test-runner --no-log-prefix + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v7 + with: + name: playwright-report + path: frontend/playwright-report/ + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v7 + with: + name: test-results + path: frontend/test-results/ + include-hidden-files: true + + report: + needs: [backend, cli, frontend, e2e] + if: always() + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + steps: + - uses: actions/download-artifact@v8 + with: + path: artifacts + pattern: "*report*" + + - name: Initialize git for test reporter + run: | + git init + git config user.email "ci@github.com" + git config user.name "CI" + git add -A + git commit --allow-empty -m "ci" + + - name: Publish backend results + id: backend + uses: dorny/test-reporter@v3 + continue-on-error: true + with: + name: Backend Tests (pytest) + path: artifacts/test-report-backend/report.xml + reporter: java-junit + fail-on-error: "false" + + - name: Publish CLI results + id: cli + uses: dorny/test-reporter@v3 + continue-on-error: true + with: + name: CLI Tests (pytest) + path: artifacts/test-report-cli/report.xml + reporter: java-junit + fail-on-error: "false" + + - name: Publish frontend results + id: frontend + uses: dorny/test-reporter@v3 + continue-on-error: true + with: + name: Frontend Tests (vitest) + path: artifacts/test-report-frontend/report.xml + reporter: java-junit + fail-on-error: "false" + + - name: Publish E2E results + id: e2e + uses: dorny/test-reporter@v3 + continue-on-error: true + with: + name: E2E Tests (Playwright) + path: artifacts/playwright-report/report.xml + reporter: java-junit + fail-on-error: "false" + + - name: Post PR comment + if: github.event_name == 'pull_request' + uses: actions/github-script@v9 + env: + backend_passed: ${{ steps.backend.outputs.passed }} + backend_failed: ${{ steps.backend.outputs.failed }} + backend_skipped: ${{ steps.backend.outputs.skipped }} + backend_conclusion: ${{ steps.backend.outputs.conclusion }} + cli_passed: ${{ steps.cli.outputs.passed }} + cli_failed: ${{ steps.cli.outputs.failed }} + cli_skipped: ${{ steps.cli.outputs.skipped }} + cli_conclusion: ${{ steps.cli.outputs.conclusion }} + frontend_passed: ${{ steps.frontend.outputs.passed }} + frontend_failed: ${{ steps.frontend.outputs.failed }} + frontend_skipped: ${{ steps.frontend.outputs.skipped }} + frontend_conclusion: ${{ steps.frontend.outputs.conclusion }} + e2e_passed: ${{ steps.e2e.outputs.passed }} + e2e_failed: ${{ steps.e2e.outputs.failed }} + e2e_skipped: ${{ steps.e2e.outputs.skipped }} + e2e_conclusion: ${{ steps.e2e.outputs.conclusion }} + with: + script: | + function suite(label, prefix) { + const passed = parseInt(process.env[`${prefix}_passed`] || '0'); + const failed = parseInt(process.env[`${prefix}_failed`] || '0'); + const skipped = parseInt(process.env[`${prefix}_skipped`] || '0'); + const conclusion = process.env[`${prefix}_conclusion`] || 'skipped'; + const total = passed + failed + skipped; + const emoji = conclusion === 'success' ? '✅' : conclusion === 'failure' ? '❌' : '⏭️'; + return { passed, failed, skipped, total, line: `${emoji} **${label}** — ${passed} passed, ${failed} failed, ${skipped} skipped (${total} total)` }; + } + + const results = [ + suite('Backend (pytest)', 'backend'), + suite('CLI (pytest)', 'cli'), + suite('Frontend (vitest)', 'frontend'), + suite('E2E (Playwright)', 'e2e'), + ]; + + const sha = context.sha; + const shaLink = `[\`${sha.slice(0, 7)}\`](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${sha})`; + const totalPassed = results.reduce((s, r) => s + r.passed, 0); + const totalFailed = results.reduce((s, r) => s + r.failed, 0); + const totalSkipped = results.reduce((s, r) => s + r.skipped, 0); + const total = totalPassed + totalFailed + totalSkipped; + + const summary = totalFailed === 0 && totalSkipped === 0 + ? `**Summary:** ✅ All ${total} tests passed` + : `**Summary:** ${totalPassed} ✅ passed` + (totalFailed > 0 ? `, ${totalFailed} ❌ failed` : '') + (totalSkipped > 0 ? `, ${totalSkipped} ⏭️ skipped` : '') + ` (${total} total)`; + + const body = `## 🧪 Test Results — ${shaLink}\n\n` + + results.map(r => r.line).join('\n') + + `\n\n${summary}`; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); diff --git a/.gitignore b/.gitignore index 927829d..edabd7d 100644 --- a/.gitignore +++ b/.gitignore @@ -222,8 +222,12 @@ __marimo__/ /ideas.txt /backend/data/ /data/ +/data-e2e/ /backend/data/ !frontend/src/lib cookies.txt profile-snapshot -.plan/ \ No newline at end of file +.plan/ +node_modules/ +/*.png +*-snapshot.md \ No newline at end of file diff --git a/README.md b/README.md index db94057..384d042 100644 --- a/README.md +++ b/README.md @@ -1,235 +1,136 @@ # LibrisLog -**Single-user book tracking webapp** — maintain three reading lists (Want to Read, Currently Reading, Read), import books from Open Library & Google Books, scrape cover art, and manage your collection through a modern Svelte dashboard. - - -[](https://github.com/astral-sh/uv) - - - - - - - -## AI-Assisted Development Disclaimer - -This project was developed with the assistance of AI coding tools (OpenCode CLI) under the following human-supervised workflow: - -1. **Requirements engineering** — human specifies the feature -2. **Agent drafts** — AI agent generates an initial implementation for larger changes -3. **Plan review** — human reviews and corrects the plan iteratively -4. **Implementation** — agent writes code guided by the approved plan -5. **Code review** — agent runs a separate code-review AI model, reports findings -6. **Human review** — all changes are reviewed and corrected by a human before commit - -No AI-generated code is committed without human review and approval. +
+ 📚 Full Documentation + · + Quick Start + · + API Reference + · + Nightly Docs +
+ + + +**Multi-user book tracking webapp** — maintain four reading lists, import books from Open Library & Google Books, scrape cover art, and get rich reading analytics — all on your own hardware. + +> `docker compose up -d` → full data ownership, no vendor lock-in, no API keys required. --- -## Quick Start (Docker) +## Quick Start ```bash -cp .env.example .env # review and adjust values -docker compose up --build -d +mkdir librislog && cd librislog +curl -O https://raw.githubusercontent.com/codebude/librislog/main/docker-compose.yml +curl -O https://raw.githubusercontent.com/codebude/librislog/main/.env.example +cp .env.example .env +# generate a random secret key +sed -i "s/CHANGE_ME_TO_32PLUS_CHARS/$(openssl rand -base64 32)/" .env +docker compose up -d ``` -The frontend is available at **http://localhost:8001** and the API at **http://localhost:8000**. Health check: `GET /api/health`. +Open **http://localhost:8001** and create your account. --- -## Configuration - -All configuration is done through the `.env` file in the project root. See `.env.example` for defaults. - -### Core - -| Variable | Default | Description | -|---|---|---| -| `DATABASE_URL` | `sqlite:///./data/librislog.db` | SQLite database path | -| `CORS_ORIGINS` | `["http://localhost", …]` | Allowed CORS origins | -| `LOG_LEVEL` | `INFO` | Python log level | -| `API_KEY_ENCRYPTION_KEY` | — | **Required.** 32+ char secret for API key encryption | -| `FORWARDED_ALLOW_IPS` | `*` | Trusted proxy IPs for forwarded headers. `*` trusts all (recommended behind your own TLS proxy). Set to specific IPs to restrict. | - -### Authentication - -| Variable | Default | Description | -|---|---|---| -| `AUTH_COOKIE_NAME` | `librislog_session` | Session cookie name | -| `AUTH_COOKIE_SECURE` | `false` | Set `true` in production (HTTPS) | -| `AUTH_COOKIE_SAMESITE` | `lax` | `lax` \| `strict` \| `none` | - -### OIDC (optional) - -| Variable | Default | Description | -|---|---|---| -| `OIDC_ENABLED` | `false` | Enable OpenID Connect login | -| `OIDC_CLIENT_ID` | — | OIDC client ID | -| `OIDC_CLIENT_SECRET` | — | OIDC client secret | -| `OIDC_WELL_KNOWN_URL` | — | OIDC discovery URL | - -### Book import +## Screenshots -| Variable | Default | Description | -|---|---|---| -| `GOOGLE_BOOKS_API_KEY` | — | Google Books API key (required for Google fallback) | -| `HARDCOVER_APP_API_TOKEN` | — | Hardcover.app API token (optional source) | + + -### Cover scraping - -| Variable | Default | Description | -|---|---|---| -| `COVERS_DIR` | `./data/covers` | Local cover image storage directory | -| `THALIA_COVER_SEARCH_ENABLED` | `false` | Enable Thalia.de cover scraping. **Research-only:** users must ensure compliance with Thalia's ToS. The author assumes no liability for misuse. | - -### Dashboard - -| Variable | Default | Description | -|---|---|---| -| `DASHBOARD_QUOTE_ENABLED` | `true` | Show motivational quote on dashboard | -| `DASHBOARD_QUOTE_URL` | *(spark API)* | Quote API endpoint | -| `DASHBOARD_QUOTE_CACHE_TTL` | `86400` | Quote cache TTL in seconds | +--- -### Frontend (build-time) +## Why LibrisLog? -| Variable | Default | Description | -|---|---|---| -| `PUBLIC_DEFAULT_LOCALE` | `en` | UI default locale: `en` \| `de` | +- **Your data, your rules.** Fully self-hosted under MIT license — no ads, no tracking, no vendor lock-in. A single SQLite file you can back up anytime. +- **No API keys required.** Works with Open Library out of the box. Add Google Books or Hardcover.app tokens optionally for richer search results. +- **Rich insights from day one.** Calendar heatmap, language/status/page distribution charts, books finished per month/year, top authors — all on your hardware. +- **Multi-user from the start.** User roles (admin/user), optional OIDC SSO, per-user libraries. One instance works for your whole household or small group. +- **Import any format you have.** Goodreads CSV with automatic field mapping, generic CSV with per-field Python transforms, JSON, ZIP with covers. +- **Point your phone at an ISBN barcode.** Real-time barcode scanning in the browser — no native app required. +- **Cover art from multiple sources.** Automatic search across AbeBooks, Open Library, Amazon, and Hardcover — plus manual upload or URL paste. +- **Full REST API.** OpenAPI-documented backend you can script against — build your own frontend, connect home automation, or pipe data into your own tools. +- **Lightweight.** Two Docker containers, one SQLite database. +- **Bilingual UI.** English and German with a localization framework ready for more languages. -### Build-time version injection +--- -```bash -export APP_VERSION=$(git describe --tags --always) -export GIT_SHA=$(git rev-parse HEAD) -docker compose up --build -d -``` +## Features -Omitting these vars leaves the fallback `v0.0.0-dev` / `unknown`. Version is shown in the sidebar and exposed on the health endpoint. +- **Library** — Grid/list view, search and sort, four reading statuses (Want to Read, Currently Reading, Read, Did Not Finish) +- **Reading progress** — Page-level slider, full progress timeline per book with edit/history +- **Statistics dashboard** — Calendar heatmap, distribution charts, books finished per period, top authors +- **Book import** — Search Open Library, Google Books, Hardcover.app. Scan ISBN barcodes on mobile. Manual entry for anything not found +- **Data portability** — Export as JSON, CSV, or ZIP with covers. Import from Goodreads or any CSV with custom field mapping +- **Cover management** — Automatic multi-source cover search with manual override, URL paste, or file upload +- **Data hygiene** — Find and fix missing metadata (covers, page counts, authors) in bulk +- **Multi-user** — Admin/user roles, per-user libraries, optional OIDC login +- **Themes** — Light, dark, and custom DaisyUI themes with persistent preferences +- **Administration** — Full backup/restore of the SQLite database, user management, API key management --- -## Development +## API -### Prerequisites +The backend is a standalone FastAPI application. The full API is documented via Swagger UI at `/api/docs` when the server is running. -- Python 3.14+ -- Node.js 26+ — use `nvm use` inside `frontend/` (`.nvmrc` is provided) -- [uv](https://github.com/astral-sh/uv) - -### Backend +Create API keys from the web UI (Profile → API Keys) for headless access. See the [API Reference](https://codebude.github.io/librislog/api/) for details. ```bash cd backend uv sync uv run alembic upgrade head -uv run uvicorn app.main:app --reload # http://localhost:8000 +uv run uvicorn app.main:app --reload ``` -All routes are documented at `/docs` (Swagger UI) when the server is running. - -### Frontend - -```bash -cd frontend -nvm use -npm install -npm run dev # http://localhost:5173 -``` - -The dev server proxies `/api` requests to `http://localhost:8000`. - --- -## Testing - -### Backend (pytest, 628 tests) - -```bash -cd backend -uv run pytest # runs tests with coverage -``` +## Stack -### Frontend (Vitest, 290 tests) +| Layer | Technology | +|---|---| +| **Backend** | FastAPI, SQLModel, SQLite, Alembic, Pydantic v2 | +| **Frontend** | Svelte 5, SvelteKit, Tailwind CSS v4, DaisyUI v5 | +| **Auth** | Session cookies, optional OIDC (Authlib) | +| **Deployment** | Docker, Docker Compose | +| **Package managers** | `uv` (Python), `npm` (Node) | +| **Testing** | pytest + pytest-cov (backend), Vitest + Testing Library (frontend), Playwright (E2E) | -```bash -cd frontend -npm test # runs tests -npm run test:coverage # runs tests with coverage report -``` +--- -### Frontend type-checking (Svelte validation) +## Contributing -```bash -cd frontend -npm run check # runs svelte-check -``` +See the [Developer Setup](https://codebude.github.io/librislog/guide/developer-setup) guide for instructions on running LibrisLog locally, running tests, and using the CLI tool. ---- +This project was developed with the assistance of AI coding tools under a human-supervised workflow. No AI-generated code is committed without human review and approval. -## Project Structure +## License -``` -librislog/ -├── backend/ -│ ├── app/ -│ │ ├── main.py # FastAPI entry point -│ │ ├── config.py # pydantic-settings configuration -│ │ ├── models.py # SQLModel ORM models -│ │ ├── schemas.py # Pydantic request/response schemas -│ │ ├── database.py # DB engine & session dependency -│ │ ├── auth.py # Authentication & session logic -│ │ ├── oidc.py # OpenID Connect integration -│ │ ├── routers/ # API route handlers -│ │ │ ├── books.py # Book CRUD -│ │ │ ├── auth.py # Login/logout -│ │ │ ├── covers.py # Cover upload/import -│ │ │ ├── cover_candidates.py # Auto-search covers by ISBN -│ │ │ ├── data.py # Data export/import -│ │ │ ├── import_.py # Book import (Open Library, Google Books) -│ │ │ ├── progress.py # Reading progress -│ │ │ ├── statistics.py # Dashboard statistics -│ │ │ ├── profile.py # User profile & settings -│ │ │ ├── admin.py # Admin endpoints -│ │ │ ├── users.py # User management -│ │ │ ├── health.py # Health check -│ │ │ └── docs.py # Documentation routes -│ │ └── services/ # Business logic -│ │ ├── book_import.py # Open Library & Google Books search -│ │ ├── cover_import.py # Cover download & processing -│ │ ├── cover_storage.py # Local cover file storage -│ │ ├── data_export.py # Export to JSON/CSV/ZIP -│ │ ├── data_import.py # Import from JSON/CSV -│ │ ├── backup_restore.py # Full DB backup & restore -│ │ ├── tags.py # Tag management -│ │ ├── quote_cache.py # Dashboard quote caching -│ │ ├── isbn_utils.py # ISBN-10/13 conversion -│ │ └── user_deletion.py # Account deletion -│ ├── alembic/ # Database migrations -│ ├── tests/ # 628 pytest tests -│ └── Dockerfile -├── frontend/ -│ ├── src/ -│ │ ├── lib/ -│ │ │ ├── api.ts # Typed fetch wrappers -│ │ │ ├── types.ts # TypeScript interfaces -│ │ │ ├── toasts.ts # Toast notification store -│ │ │ ├── i18n/ # Internationalisation (en, de) -│ │ │ ├── stores/ # Svelte stores (auth, timezone) -│ │ │ ├── components/ # 27 Svelte components (41 files incl. tests) -│ │ │ └── test/ # Test setup & mocks -│ │ └── routes/ # SvelteKit pages -│ └── Dockerfile -├── docker-compose.yml # 2 services: backend + frontend -└── .env.example # All configurable variables -``` +MIT -## Stack +## Star History -| Layer | Technology | -|---|---| -| **Backend** | FastAPI, SQLModel, SQLite, Alembic, Pydantic v2 | -| **Frontend** | Svelte 5, SvelteKit, Tailwind CSS v4, DaisyUI v5 | -| **Auth** | Session cookies, optional OIDC (Authlib) | -| **Reverse proxy** | nginx (embedded in frontend container) | -| **Package managers** | `uv` (Python), `npm` (Node) | -| **Testing** | pytest + pytest-cov (backend), Vitest + Testing Library (frontend) | + +{$_('book.autoSearchInfo')}
- {#if candidates.length === 0} + {#if sorted.length === 0}