Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
116f3e7
Update .gitignore
codebude Aug 22, 2026
2d7b29a
Fix star chart in readme
codebude Aug 22, 2026
1d19504
Merge pull request #61 from codebude/bugfix/star-chart-readme
codebude Aug 22, 2026
1767690
Add notifications and handler for reading progress changes to keep ca…
codebude Aug 22, 2026
eddaa83
Fix timezone handling in progress chart
codebude Aug 22, 2026
b5509a1
Add ui dialog to ask for start date when transitioning to "read" stat…
codebude Aug 22, 2026
9f19b0d
Prevent "set to 100%" dialog from opening when transforming to "did n…
codebude Aug 22, 2026
79efcf8
Prevent "set to 100%" dialog from opening when transforming to "read"…
codebude Aug 22, 2026
e0a7540
Added state of possession indicator to the data model and frontend
codebude Aug 23, 2026
f8513cf
Fixed missing translation
codebude Aug 23, 2026
a4c6cc0
Add cart badge to book cards for books that have status to acquire
codebude Aug 23, 2026
b1044f2
Improved sort selector ui
codebude Aug 23, 2026
2b74cfe
Added possession status to statistics page
codebude Aug 23, 2026
5d5aa6f
Merge pull request #62 from codebude/feature/progress-chart-improvements
codebude Aug 23, 2026
cc66ad4
Upgrade Python packages
codebude Aug 23, 2026
b56151c
Apply frontend dependencies security patches
codebude Aug 23, 2026
d839c57
Upgrade docs packages/dependencies
codebude Aug 23, 2026
b5705be
Upgrade playwright in e2e test image
codebude Aug 23, 2026
4264301
Update setup test mocks
codebude Aug 23, 2026
cdbace3
Fix backend type errors with ty and add config
codebude Aug 23, 2026
ed9bb4d
Pytest for uncovered code branches
codebude Aug 23, 2026
de7e33f
Fixed use of deprecated http status constant
codebude Aug 23, 2026
3c59850
Fix mail pytests by setting test data for environments without mail s…
codebude Aug 23, 2026
68afcbe
Fix uv project setup
codebude Aug 23, 2026
0fe8429
Merge pull request #63 from codebude/feature/improve-code-quality-and…
codebude Aug 23, 2026
1e0ba82
Rewrote docs in regards to uv sync usage
codebude Aug 23, 2026
33b2249
Merge pull request #64 from codebude/feature/mixed-minor-improvements
codebude Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Exclude everything from the Docker context by default.
# The backend image is built from the repository root because uv keeps the
# workspace lockfile at the root level.
*

# Workspace dependency files
!pyproject.toml
!uv.lock

# Backend source and config
!backend/
!backend/pyproject.toml
!backend/alembic.ini
!backend/alembic/
!backend/alembic/**
!backend/app/
!backend/app/**
!backend/entrypoint.sh

# Do not ship test code in the image
backend/tests/
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
- name: backend
image: librislog-api
dockerfile: ./backend/Dockerfile
context: ./backend
context: .

arch: [amd64, arm64]

Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ jobs:
- name: Build backend image
uses: docker/build-push-action@v7
with:
context: ./backend
context: .
file: ./backend/Dockerfile
tags: librislog-e2e-backend
load: true
build-args: |
Expand Down
13 changes: 9 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,6 @@ __marimo__/

# Streamlit
.streamlit/secrets.toml
/.memsearch
/.playwright-mcp

/ideas.txt
/backend/data/
Expand All @@ -227,7 +225,14 @@ __marimo__/
!frontend/src/lib
cookies.txt
profile-snapshot
.plan/

node_modules/
/*.png
*-snapshot.md
*-snapshot.md

# AI tools
/.memsearch
/.playwright-mcp
/.sverklo
.plan/
/.opencode
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<a href="https://docs.librislog.app/"><img src="https://github.com/codebude/librislog/actions/workflows/docs.yml/badge.svg" alt="Docs Build"></a>
<img src="https://img.shields.io/badge/python-3.14-%233776AB?logo=python" alt="Python">
<img src="https://img.shields.io/badge/svelte-5-%23FF3E00?logo=svelte" alt="Svelte">
<img src="https://img.shields.io/badge/FastAPI-0.136-%23009688?logo=fastapi" alt="FastAPI">
<img src="https://img.shields.io/badge/FastAPI-0.141-%23009688?logo=fastapi" alt="FastAPI">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
</p>

Expand Down Expand Up @@ -95,8 +95,8 @@ The backend is a standalone FastAPI application. The full API is documented via
Create API keys from the web UI (Profile → API Keys) for headless access. See the [API Reference](https://docs.librislog.app/api/) for details.

```bash
cd backend
uv sync
cd backend
uv run alembic upgrade head
uv run uvicorn app.main:app --reload
```
Expand Down Expand Up @@ -128,10 +128,12 @@ MIT

## Star History

## Star History

<a href="https://www.star-history.com/?repos=codebude%2Flibrislog&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=codebude/librislog&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=codebude/librislog&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=codebude/librislog&type=date&legend=top-left" />
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=codebude/librislog&type=date&theme=dark&legend=top-left&sealed_token=63MQnBuCo06yuka4CK7jSufogWTsYJDymR_l4uRyMUp_LWPSC65IxbPKjm2UeEPNaU4GKsdNJG308hwGsMjSqtjKwc6Br0SiEkEx-UkS4-7OKgvFQfRqR8rB8bFTNi8eWhacZ2clPYvs_oKBPqUsbCDUXDqGQxuneV_1LAEC3AoIiqlsodcwv_RAvCvm" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=codebude/librislog&type=date&legend=top-left&sealed_token=63MQnBuCo06yuka4CK7jSufogWTsYJDymR_l4uRyMUp_LWPSC65IxbPKjm2UeEPNaU4GKsdNJG308hwGsMjSqtjKwc6Br0SiEkEx-UkS4-7OKgvFQfRqR8rB8bFTNi8eWhacZ2clPYvs_oKBPqUsbCDUXDqGQxuneV_1LAEC3AoIiqlsodcwv_RAvCvm" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=codebude/librislog&type=date&legend=top-left&sealed_token=63MQnBuCo06yuka4CK7jSufogWTsYJDymR_l4uRyMUp_LWPSC65IxbPKjm2UeEPNaU4GKsdNJG308hwGsMjSqtjKwc6Br0SiEkEx-UkS4-7OKgvFQfRqR8rB8bFTNi8eWhacZ2clPYvs_oKBPqUsbCDUXDqGQxuneV_1LAEC3AoIiqlsodcwv_RAvCvm" />
</picture>
</a>
28 changes: 17 additions & 11 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,32 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

WORKDIR /app

# Copy dependency files first for layer caching
# Copy workspace dependency files first for layer caching.
# The backend is a uv workspace member, so the lockfile lives at the repo root.
COPY pyproject.toml uv.lock ./
COPY backend/pyproject.toml ./backend/

# Install production dependencies only (no dev extras)
RUN uv sync --no-dev --frozen
# Sync only the backend workspace member's production dependencies.
# The workspace root keeps the lockfile, so we build from the repo root context.
RUN uv sync --project backend --no-dev --frozen

# Copy application source
COPY alembic.ini ./
COPY alembic/ ./alembic/
COPY app/ ./app/
COPY backend/alembic.ini ./backend/
COPY backend/alembic/ ./backend/alembic/
COPY backend/app/ ./backend/app/

# Make the backend source importable from the workspace root
ENV PYTHONPATH=/app/backend

# Inject version from build args (overwrites fallback in _build_info.py)
ARG APP_VERSION=v0.0.0-dev
ARG GIT_SHA=unknown
RUN echo "__version__ = \"$APP_VERSION\"" > app/_build_info.py && \
echo "__git_sha__ = \"$GIT_SHA\"" >> app/_build_info.py
RUN echo "__version__ = \"$APP_VERSION\"" > ./backend/app/_build_info.py && \
echo "__git_sha__ = \"$GIT_SHA\"" >> ./backend/app/_build_info.py

# Entrypoint: run migrations then start server
COPY entrypoint.sh ./
RUN chmod +x entrypoint.sh
COPY backend/entrypoint.sh ./backend/
RUN chmod +x ./backend/entrypoint.sh

EXPOSE 8000
ENTRYPOINT ["./entrypoint.sh"]
ENTRYPOINT ["./backend/entrypoint.sh"]
32 changes: 32 additions & 0 deletions backend/alembic/versions/e2f3a4b5c6d7_add_acquisition_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add acquisition status to books

Revision ID: e2f3a4b5c6d7
Revises: 1a2b3c4d5e6f
Create Date: 2026-08-22 20:00:00.000000
"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = "e2f3a4b5c6d7"
down_revision: Union[str, Sequence[str], None] = "784de5d2bf69"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
with op.batch_alter_table("book") as batch_op:
batch_op.add_column(sa.Column("acquisition_status", sa.String(length=32), nullable=True))
op.execute("UPDATE book SET acquisition_status = 'owned' WHERE acquisition_status IS NULL")
with op.batch_alter_table("book") as batch_op:
batch_op.alter_column("acquisition_status", nullable=False)
batch_op.create_index("ix_book_acquisition_status", ["acquisition_status"])


def downgrade() -> None:
with op.batch_alter_table("book") as batch_op:
batch_op.drop_index("ix_book_acquisition_status")
batch_op.drop_column("acquisition_status")
6 changes: 3 additions & 3 deletions backend/app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from fastapi.security import APIKeyHeader
from passlib.exc import UnknownHashError
from passlib.context import CryptContext
from sqlmodel import Session, select
from sqlmodel import Session, col, select

from itsdangerous import URLSafeTimedSerializer

Expand All @@ -27,7 +27,7 @@ class _BcryptAbout:
__version__: str = getattr(bcrypt, "__version__", "")


bcrypt.__about__ = _BcryptAbout() # type: ignore[attr-defined]
bcrypt.__about__ = _BcryptAbout() # ty: ignore[unresolved-attribute]

bcrypt_context: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto")
fallback_context: CryptContext = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
Expand Down Expand Up @@ -190,7 +190,7 @@ def require_user_by_api_key(

key_hash = hash_api_key(x_api_key)
key = session.exec(
select(ApiKey).where(ApiKey.key_hash == key_hash, ApiKey.revoked_at.is_(None))
select(ApiKey).where(ApiKey.key_hash == key_hash, col(ApiKey.revoked_at).is_(None))
).first()
if not key:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
Expand Down
20 changes: 15 additions & 5 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ class ReadingStatus(str, Enum):
did_not_finish = "did_not_finish"


class AcquisitionStatus(str, Enum):
"""Enum of a book's current availability to the user."""

owned = "owned"
borrowed = "borrowed"
digital_access = "digital_access"
to_acquire = "to_acquire"


class UserRole(str, Enum):
"""Enum of possible user roles."""

Expand Down Expand Up @@ -79,6 +88,7 @@ def normalize_empty_cover_url(cls, data: dict) -> dict:
blurb: Optional[str] = None
rating: Optional[int] = Field(default=None, ge=1, le=5)
reading_status: ReadingStatus = Field(default=ReadingStatus.want_to_read, index=True)
acquisition_status: AcquisitionStatus = Field(default=AcquisitionStatus.owned, index=True)
user_id: Optional[int] = Field(default=None, foreign_key="user.id", index=True)
date_added: datetime = Field(
default_factory=utcnow,
Expand All @@ -97,7 +107,7 @@ def normalize_empty_cover_url(cls, data: dict) -> dict:
class Tag(SQLModel, table=True):
"""A user-specific tag that can be applied to books."""

__tablename__ = "tag"
__tablename__: str = "tag"
__table_args__ = (sa.UniqueConstraint("user_id", "name", name="uq_tag_user_id_name"),)

id: Optional[int] = Field(default=None, primary_key=True)
Expand All @@ -112,7 +122,7 @@ class Tag(SQLModel, table=True):
class BookTag(SQLModel, table=True):
"""Many-to-many association between books and tags."""

__tablename__ = "book_tag"
__tablename__: str = "book_tag"

book_id: int = Field(foreign_key="book.id", primary_key=True)
tag_id: int = Field(foreign_key="tag.id", primary_key=True, index=True)
Expand Down Expand Up @@ -178,7 +188,7 @@ class ApiKey(SQLModel, table=True):
class ReadingProgress(SQLModel, table=True):
"""A page-number reading progress entry for a book."""

__tablename__ = "reading_progress"
__tablename__: str = "reading_progress"

id: Optional[int] = Field(default=None, primary_key=True)
book_id: int = Field(foreign_key="book.id", index=True)
Expand Down Expand Up @@ -212,7 +222,7 @@ class OidcLink(SQLModel, table=True):
class EmbedToken(SQLModel, table=True):
"""A scoped embed token for iframe/dashboard integrations."""

__tablename__ = "embed_token"
__tablename__: str = "embed_token"

id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id", index=True)
Expand Down Expand Up @@ -242,7 +252,7 @@ class EmbedToken(SQLModel, table=True):
class ImportMapping(SQLModel, table=True):
"""A saved column-mapping configuration for data import."""

__tablename__ = "import_mapping"
__tablename__: str = "import_mapping"
__table_args__ = (
sa.UniqueConstraint("user_id", "name", name="uq_import_mapping_user_id_name"),
)
Expand Down
2 changes: 2 additions & 0 deletions backend/app/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def setup(
session.add(user)
session.commit()
session.refresh(user)
assert user.id is not None

session.add(UserSettings(user_id=user.id, language="en"))
session.commit()
Expand All @@ -81,6 +82,7 @@ def login(
user = session.exec(select(User).where(User.email == credentials.email)).first()
if not user or not verify_password(credentials.password, user.hashed_password):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password")
assert user.id is not None

start_browser_session(http_request, user.id, user.credentials_version)
return {"user": UserRead.model_validate(user)}
Expand Down
Loading
Loading