Skip to content

Commit 78e2ee9

Browse files
authored
Merge pull request #65 from codebude/develop
vNext: UI enhancements, reading progress sync, possession tracking, and dependency upgrades
2 parents 42cb5ca + 33b2249 commit 78e2ee9

101 files changed

Lines changed: 4207 additions & 2344 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.dockerignore‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Exclude everything from the Docker context by default.
2+
# The backend image is built from the repository root because uv keeps the
3+
# workspace lockfile at the root level.
4+
*
5+
6+
# Workspace dependency files
7+
!pyproject.toml
8+
!uv.lock
9+
10+
# Backend source and config
11+
!backend/
12+
!backend/pyproject.toml
13+
!backend/alembic.ini
14+
!backend/alembic/
15+
!backend/alembic/**
16+
!backend/app/
17+
!backend/app/**
18+
!backend/entrypoint.sh
19+
20+
# Do not ship test code in the image
21+
backend/tests/

‎.github/workflows/docker.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575
- name: backend
7676
image: librislog-api
7777
dockerfile: ./backend/Dockerfile
78-
context: ./backend
78+
context: .
7979

8080
arch: [amd64, arm64]
8181

‎.github/workflows/tests.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ jobs:
112112
- name: Build backend image
113113
uses: docker/build-push-action@v7
114114
with:
115-
context: ./backend
115+
context: .
116+
file: ./backend/Dockerfile
116117
tags: librislog-e2e-backend
117118
load: true
118119
build-args: |

‎.gitignore‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,6 @@ __marimo__/
216216

217217
# Streamlit
218218
.streamlit/secrets.toml
219-
/.memsearch
220-
/.playwright-mcp
221219

222220
/ideas.txt
223221
/backend/data/
@@ -227,7 +225,14 @@ __marimo__/
227225
!frontend/src/lib
228226
cookies.txt
229227
profile-snapshot
230-
.plan/
228+
231229
node_modules/
232230
/*.png
233-
*-snapshot.md
231+
*-snapshot.md
232+
233+
# AI tools
234+
/.memsearch
235+
/.playwright-mcp
236+
/.sverklo
237+
.plan/
238+
/.opencode

‎README.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<a href="https://docs.librislog.app/"><img src="https://github.com/codebude/librislog/actions/workflows/docs.yml/badge.svg" alt="Docs Build"></a>
1717
<img src="https://img.shields.io/badge/python-3.14-%233776AB?logo=python" alt="Python">
1818
<img src="https://img.shields.io/badge/svelte-5-%23FF3E00?logo=svelte" alt="Svelte">
19-
<img src="https://img.shields.io/badge/FastAPI-0.136-%23009688?logo=fastapi" alt="FastAPI">
19+
<img src="https://img.shields.io/badge/FastAPI-0.141-%23009688?logo=fastapi" alt="FastAPI">
2020
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
2121
</p>
2222

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

9797
```bash
98-
cd backend
9998
uv sync
99+
cd backend
100100
uv run alembic upgrade head
101101
uv run uvicorn app.main:app --reload
102102
```
@@ -128,10 +128,12 @@ MIT
128128

129129
## Star History
130130

131+
## Star History
132+
131133
<a href="https://www.star-history.com/?repos=codebude%2Flibrislog&type=date&legend=top-left">
132134
<picture>
133-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=codebude/librislog&type=date&theme=dark&legend=top-left" />
134-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=codebude/librislog&type=date&legend=top-left" />
135-
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=codebude/librislog&type=date&legend=top-left" />
135+
<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" />
136+
<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" />
137+
<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" />
136138
</picture>
137139
</a>

‎backend/Dockerfile‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,32 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
55

66
WORKDIR /app
77

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

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

1417
# Copy application source
15-
COPY alembic.ini ./
16-
COPY alembic/ ./alembic/
17-
COPY app/ ./app/
18+
COPY backend/alembic.ini ./backend/
19+
COPY backend/alembic/ ./backend/alembic/
20+
COPY backend/app/ ./backend/app/
21+
22+
# Make the backend source importable from the workspace root
23+
ENV PYTHONPATH=/app/backend
1824

1925
# Inject version from build args (overwrites fallback in _build_info.py)
2026
ARG APP_VERSION=v0.0.0-dev
2127
ARG GIT_SHA=unknown
22-
RUN echo "__version__ = \"$APP_VERSION\"" > app/_build_info.py && \
23-
echo "__git_sha__ = \"$GIT_SHA\"" >> app/_build_info.py
28+
RUN echo "__version__ = \"$APP_VERSION\"" > ./backend/app/_build_info.py && \
29+
echo "__git_sha__ = \"$GIT_SHA\"" >> ./backend/app/_build_info.py
2430

2531
# Entrypoint: run migrations then start server
26-
COPY entrypoint.sh ./
27-
RUN chmod +x entrypoint.sh
32+
COPY backend/entrypoint.sh ./backend/
33+
RUN chmod +x ./backend/entrypoint.sh
2834

2935
EXPOSE 8000
30-
ENTRYPOINT ["./entrypoint.sh"]
36+
ENTRYPOINT ["./backend/entrypoint.sh"]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""add acquisition status to books
2+
3+
Revision ID: e2f3a4b5c6d7
4+
Revises: 1a2b3c4d5e6f
5+
Create Date: 2026-08-22 20:00:00.000000
6+
"""
7+
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
revision: str = "e2f3a4b5c6d7"
15+
down_revision: Union[str, Sequence[str], None] = "784de5d2bf69"
16+
branch_labels: Union[str, Sequence[str], None] = None
17+
depends_on: Union[str, Sequence[str], None] = None
18+
19+
20+
def upgrade() -> None:
21+
with op.batch_alter_table("book") as batch_op:
22+
batch_op.add_column(sa.Column("acquisition_status", sa.String(length=32), nullable=True))
23+
op.execute("UPDATE book SET acquisition_status = 'owned' WHERE acquisition_status IS NULL")
24+
with op.batch_alter_table("book") as batch_op:
25+
batch_op.alter_column("acquisition_status", nullable=False)
26+
batch_op.create_index("ix_book_acquisition_status", ["acquisition_status"])
27+
28+
29+
def downgrade() -> None:
30+
with op.batch_alter_table("book") as batch_op:
31+
batch_op.drop_index("ix_book_acquisition_status")
32+
batch_op.drop_column("acquisition_status")

‎backend/app/auth.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from fastapi.security import APIKeyHeader
1313
from passlib.exc import UnknownHashError
1414
from passlib.context import CryptContext
15-
from sqlmodel import Session, select
15+
from sqlmodel import Session, col, select
1616

1717
from itsdangerous import URLSafeTimedSerializer
1818

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

2929

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

3232
bcrypt_context: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto")
3333
fallback_context: CryptContext = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
@@ -190,7 +190,7 @@ def require_user_by_api_key(
190190

191191
key_hash = hash_api_key(x_api_key)
192192
key = session.exec(
193-
select(ApiKey).where(ApiKey.key_hash == key_hash, ApiKey.revoked_at.is_(None))
193+
select(ApiKey).where(ApiKey.key_hash == key_hash, col(ApiKey.revoked_at).is_(None))
194194
).first()
195195
if not key:
196196
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")

‎backend/app/models.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ class ReadingStatus(str, Enum):
4444
did_not_finish = "did_not_finish"
4545

4646

47+
class AcquisitionStatus(str, Enum):
48+
"""Enum of a book's current availability to the user."""
49+
50+
owned = "owned"
51+
borrowed = "borrowed"
52+
digital_access = "digital_access"
53+
to_acquire = "to_acquire"
54+
55+
4756
class UserRole(str, Enum):
4857
"""Enum of possible user roles."""
4958

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

100-
__tablename__ = "tag"
110+
__tablename__: str = "tag"
101111
__table_args__ = (sa.UniqueConstraint("user_id", "name", name="uq_tag_user_id_name"),)
102112

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

115-
__tablename__ = "book_tag"
125+
__tablename__: str = "book_tag"
116126

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

181-
__tablename__ = "reading_progress"
191+
__tablename__: str = "reading_progress"
182192

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

215-
__tablename__ = "embed_token"
225+
__tablename__: str = "embed_token"
216226

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

245-
__tablename__ = "import_mapping"
255+
__tablename__: str = "import_mapping"
246256
__table_args__ = (
247257
sa.UniqueConstraint("user_id", "name", name="uq_import_mapping_user_id_name"),
248258
)

‎backend/app/routers/auth.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def setup(
6363
session.add(user)
6464
session.commit()
6565
session.refresh(user)
66+
assert user.id is not None
6667

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

8587
start_browser_session(http_request, user.id, user.credentials_version)
8688
return {"user": UserRead.model_validate(user)}

0 commit comments

Comments
 (0)