Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
0e9ceb9
Fix calendar-day splits in daily page statistics (instead of 24h slots)
codebude Sep 4, 2026
73fdf12
Fixed user timezone input and improved usability
codebude Sep 4, 2026
fd97692
Merge pull request #91 from codebude/bugfix/harden-timezone-handling
codebude Sep 4, 2026
12b8758
Improved visibility of selected item in suggestion input
codebude Sep 5, 2026
d61156a
fix(#79): improve contrast and add visual border to selected dropdown…
codebude Sep 5, 2026
875cdfd
Better border for selected tag/author items
codebude Sep 5, 2026
53e763c
Merge pull request #92 from codebude/feature/import-suggestioninput-ux
codebude Sep 5, 2026
f2d5c7d
Adaptive date input with additional validation
codebude Sep 5, 2026
a3e4e2d
Fix native date picker opening next to the field
codebude Sep 5, 2026
b3de732
Merge pull request #93 from codebude/feature/improve-date-input-ux
codebude Sep 5, 2026
80f0e87
Add secure context checks to barcode scanner
codebude Sep 7, 2026
d7d89d2
Add notes on secure context and camera usage to the docs
codebude Sep 7, 2026
9dfac05
Fix wrong secure context urls in tests
codebude Sep 7, 2026
68266dd
Merge pull request #95 from codebude/feature/add-secure-context-warning
codebude Sep 7, 2026
7c7f377
Implement close on esc keypress for various dialogs
codebude Sep 7, 2026
c9ba979
Add cancelable book search to the import modal
codebude Sep 7, 2026
49e26dc
Add visually grouping of duplicate entries in "edition" groups to boo…
codebude Sep 8, 2026
10efc95
Merge pull request #96 from codebude/feature/ux-improvements
codebude Sep 8, 2026
ef6b487
Heimdall integration documentation
codebude Sep 8, 2026
1a467d6
Reformatted text
codebude Sep 8, 2026
cfc0309
Update release notes
codebude Sep 8, 2026
b2f8caa
Unified release title headings in release notes
codebude Sep 8, 2026
cd5cad2
Merge pull request #97 from codebude/feature/heimdall-integration
codebude Sep 8, 2026
646cce2
Lower telemetry ping from every 24h to every 23h to avoid day misses …
codebude Sep 8, 2026
59bdbb4
Add current camera name label to scanner view
codebude Sep 8, 2026
692a082
Fix progress date editing in configured timezone
codebude Sep 8, 2026
b9021a4
Fix tele interval test case
codebude Sep 8, 2026
aaf6bec
Merge pull request #98 from codebude/feature/various-minor-improvements
codebude Sep 8, 2026
5739720
Add selectable time range to statistics
codebude Sep 8, 2026
bca4dd4
Merge pull request #99 from codebude/feature/customizable-reading-sta…
codebude Sep 8, 2026
8bf610c
Add optional medium attribute to books
codebude Sep 9, 2026
1c82741
Support localized medium and possession search values
codebude Sep 9, 2026
de010ef
Merge pull request #100 from codebude/feature/medium-type-attribute
codebude Sep 9, 2026
29b5632
First shot of url sharing
codebude Sep 10, 2026
0dc71dc
Add copy and open actions for share links
codebude Sep 10, 2026
a159462
Add backend database to .gitignore
codebude Sep 10, 2026
1ec7f12
Add validation and scrolling to share link dialog
codebude Sep 10, 2026
ddb14c3
Add language selection to public profile share links
codebude Sep 10, 2026
d0f2e90
Add show more button to public profile timeline
codebude Sep 10, 2026
a07458c
Add search to public profile library
codebude Sep 10, 2026
bb02e50
Add time range selection to public profile trends, fix and improve de…
codebude Sep 10, 2026
fe928c4
Improve public profile design with avatars, timeline cards and distri…
codebude Sep 10, 2026
32139cc
Document public profile sharing
codebude Sep 10, 2026
d198f8a
Merge pull request #101 from codebude/feature/public-profile-pages
codebude Sep 10, 2026
c1d44c2
Added basket functionality
codebude Sep 11, 2026
7455a7b
Highlight the selected edition in import search
codebude Sep 11, 2026
c26e235
Document import basket and selected edition highlight in release notes
codebude Sep 11, 2026
95f5be6
Merge pull request #102 from codebude/#feature/search-n-add-multiple-…
codebude Sep 11, 2026
d7cc3c2
Improved dark theme contrast and improved spacing in share url list
codebude Sep 13, 2026
bc1f4fd
Switch statistics ranges to calendar-based options
codebude Sep 13, 2026
f730985
Disable telemetry in development environment
codebude Sep 13, 2026
ce0ec97
Add medium to data hygiene missing attributes
codebude Sep 13, 2026
8dd106e
Merge pull request #103 from codebude/feature/various-minor-ui-improv…
codebude Sep 13, 2026
d71bab8
Add configurable reading date automation to profile settings
codebude Sep 15, 2026
3a0d684
Allow clearing finish dates when automation is disabled
codebude Sep 15, 2026
733e9df
Merge pull request #104 from codebude/feature/optional-state-transitions
codebude Sep 15, 2026
6adb6a6
Support multiple parallel searches in the add book dialog
codebude Sep 15, 2026
7f506d7
Share medium and acquisition status across parallel search panels
codebude Sep 15, 2026
3d661dc
Focus newly added parallel search panels
codebude Sep 15, 2026
b7fac41
Give each parallel search panel a distinct color accent
codebude Sep 15, 2026
2e0e8c6
Merge pull request #105 from codebude/feature/parallel-searches
codebude Sep 15, 2026
998007a
Add Excel (XLSX) import support
codebude Sep 15, 2026
05e07e6
Add a Bookstats import preset
codebude Sep 15, 2026
bb739b6
Release notes for release v1.9.0
codebude Sep 15, 2026
0f1fad3
Merge pull request #106 from codebude/feature/enhanced-data-import
codebude Sep 15, 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ __marimo__/

/ideas.txt
/backend/data/
/backend/librislog.db
/data/
/data-e2e/
/backend/data/
Expand All @@ -235,4 +236,4 @@ node_modules/
/.playwright-mcp
/.sverklo
.plan/
/.opencode
/.opencodebackend/librislog.db
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Open **http://localhost:8001** and create your account.
- **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.
- **Import any format you have.** Goodreads or Bookstats exports with automatic field mapping, generic CSV or Excel (XLSX) 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.
Expand All @@ -81,7 +81,7 @@ Open **http://localhost:8001** and create your account.
- **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
- **Data portability** — Export as JSON, CSV, or ZIP with covers. Import from the Goodreads or Bookstats presets, or any CSV/Excel file 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""normalize statistics range settings

Revision ID: 0a1b2c3d4e5f
Revises: d3e4f5a6b7c8
Create Date: 2026-09-13 00:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "0a1b2c3d4e5f"
down_revision: Union[str, Sequence[str], None] = "d3e4f5a6b7c8"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# The old rolling ranges have no direct equivalent in the calendar-based selector.
op.execute(
sa.text(
"UPDATE usersettings "
"SET statistics_range = 'alltime' "
"WHERE statistics_range IN ('6months', '30days')"
)
)
op.execute(
sa.text(
"UPDATE usersettings "
"SET statistics_range = 'this_year' "
"WHERE statistics_range = '1year'"
)
)


def downgrade() -> None:
op.execute(
sa.text(
"UPDATE usersettings "
"SET statistics_range = '1year' "
"WHERE statistics_range = 'this_year'"
)
)
29 changes: 29 additions & 0 deletions backend/alembic/versions/7a8b9c0d1e2f_add_medium_to_books.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add optional medium to books

Revision ID: 7a8b9c0d1e2f
Revises: f3a5b7c9d1e2
Create Date: 2026-09-08 23:40:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = "7a8b9c0d1e2f"
down_revision: Union[str, Sequence[str], None] = "f3a5b7c9d1e2"
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("medium", sa.String(length=32), nullable=True))
batch_op.create_index("ix_book_medium", ["medium"], unique=False)


def downgrade() -> None:
with op.batch_alter_table("book") as batch_op:
batch_op.drop_index("ix_book_medium")
batch_op.drop_column("medium")
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add raw token to public_profile_link

Revision ID: b2c3d4e5f6a7
Revises: c9a4b7d8e3f1
Create Date: 2026-09-10 12:00:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "b2c3d4e5f6a7"
down_revision: Union[str, Sequence[str], None] = "c9a4b7d8e3f1"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
with op.batch_alter_table("public_profile_link") as batch_op:
batch_op.add_column(sa.Column("token", sa.String(length=255), nullable=True))


def downgrade() -> None:
with op.batch_alter_table("public_profile_link") as batch_op:
batch_op.drop_column("token")
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""add public_profile_link table for shareable public profiles

Revision ID: c9a4b7d8e3f1
Revises: 7a8b9c0d1e2f
Create Date: 2026-09-09 23:40:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "c9a4b7d8e3f1"
down_revision: Union[str, Sequence[str], None] = "7a8b9c0d1e2f"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Create the public_profile_link table."""
op.create_table(
"public_profile_link",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("token_prefix", sa.String(length=255), nullable=False),
sa.Column("token_hash", sa.String(length=255), nullable=False),
sa.Column("audience", sa.String(length=32), nullable=False),
sa.Column("visibility_config_json", sa.Text(), nullable=False),
sa.Column("expires_at", sa.DateTime(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("revoked_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_public_profile_link_user_id", "public_profile_link", ["user_id"], unique=False)
op.create_index("ix_public_profile_link_token_prefix", "public_profile_link", ["token_prefix"], unique=False)
op.create_index("ix_public_profile_link_token_hash", "public_profile_link", ["token_hash"], unique=True)


def downgrade() -> None:
"""Drop the public_profile_link table."""
op.drop_index("ix_public_profile_link_token_hash", table_name="public_profile_link")
op.drop_index("ix_public_profile_link_token_prefix", table_name="public_profile_link")
op.drop_index("ix_public_profile_link_user_id", table_name="public_profile_link")
op.drop_table("public_profile_link")
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add language to public_profile_link

Revision ID: d3e4f5a6b7c8
Revises: b2c3d4e5f6a7
Create Date: 2026-09-10 13:00:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "d3e4f5a6b7c8"
down_revision: Union[str, Sequence[str], None] = "b2c3d4e5f6a7"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
with op.batch_alter_table("public_profile_link") as batch_op:
batch_op.add_column(sa.Column("language", sa.String(length=10), nullable=True))


def downgrade() -> None:
with op.batch_alter_table("public_profile_link") as batch_op:
batch_op.drop_column("language")
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add reading date automation settings

Revision ID: d7e8f9a0b1c2
Revises: 0a1b2c3d4e5f
Create Date: 2026-09-14 00:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = "d7e8f9a0b1c2"
down_revision: Union[str, Sequence[str], None] = "0a1b2c3d4e5f"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
"usersettings",
sa.Column("auto_set_date_started", sa.Boolean(), nullable=False, server_default=sa.true()),
)
op.add_column(
"usersettings",
sa.Column("auto_set_date_finished", sa.Boolean(), nullable=False, server_default=sa.true()),
)


def downgrade() -> None:
op.drop_column("usersettings", "auto_set_date_finished")
op.drop_column("usersettings", "auto_set_date_started")
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""add statistics range to usersettings

Revision ID: f3a5b7c9d1e2
Revises: c3d4e5f6a7b8
Create Date: 2026-09-08 15:30:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'f3a5b7c9d1e2'
down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b8'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column('usersettings', sa.Column('statistics_range', sa.String(length=20), nullable=False, server_default='alltime'))
op.add_column('usersettings', sa.Column('statistics_custom_from', sa.Date(), nullable=True))
op.add_column('usersettings', sa.Column('statistics_custom_to', sa.Date(), nullable=True))


def downgrade() -> None:
op.drop_column('usersettings', 'statistics_custom_to')
op.drop_column('usersettings', 'statistics_custom_from')
op.drop_column('usersettings', 'statistics_range')
24 changes: 24 additions & 0 deletions backend/app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,30 @@ def get_embed_token_prefix(token: str) -> str:
return token[:12]


# --- Public profile share-link token utilities ---

PUBLIC_PROFILE_TOKEN_PREFIX = "lp_"


def generate_public_profile_token() -> str:
"""Generate a new random public profile token prefixed with 'lp_'."""
return f"{PUBLIC_PROFILE_TOKEN_PREFIX}{secrets.token_urlsafe(32)}"


def hash_public_profile_token(value: str) -> str:
"""Return a HMAC-SHA256 hex digest of a public profile token."""
return hmac.new(
settings.api_key_encryption_key.encode("utf-8"),
value.encode("utf-8"),
hashlib.sha256,
).hexdigest()


def get_public_profile_token_prefix(token: str) -> str:
"""Return the first 12 characters of the public profile token (visible prefix)."""
return token[:12]


# --- Password reset token utilities ---

_password_reset_serializer = URLSafeTimedSerializer(
Expand Down
13 changes: 13 additions & 0 deletions backend/app/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@
"avg_pages": "Seiten/Buch"
}
},
"medium": {
"print": "Print",
"ebook": "eBook",
"audiobook": "Hörbuch",
"comic_graphic_novel": "Comic / Graphic Novel",
"magazine_newspaper": "Magazin / Zeitung"
},
"acquisition": {
"owned": "Im Besitz",
"borrowed": "Geliehen",
"digital_access": "Digital verfügbar",
"to_acquire": "Muss noch beschafft werden"
},
"email": {
"passwordResetSubject": "Passwort zurücksetzen – LibrisLog",
"passwordResetBody": "<html>\n<body>\n<p>Du hast das Zurücksetzen deines Passworts für dein LibrisLog-Konto beantragt.</p>\n<p>Klicke auf den Link unten, um dein Passwort zurückzusetzen. Dieser Link ist {duration_minutes} Minuten gültig.</p>\n<p><a href=\"{reset_url}\">{reset_url}</a></p>\n<p>Falls du dies nicht angefordert hast, ignoriere bitte diese E-Mail.</p>\n</body>\n</html>"
Expand Down
13 changes: 13 additions & 0 deletions backend/app/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@
"avg_pages": "Avg/Book"
}
},
"medium": {
"print": "Print",
"ebook": "eBook",
"audiobook": "Audiobook",
"comic_graphic_novel": "Comic / Graphic Novel",
"magazine_newspaper": "Magazine / Newspaper"
},
"acquisition": {
"owned": "Owned",
"borrowed": "Borrowed",
"digital_access": "Digital access",
"to_acquire": "Needs to be acquired"
},
"email": {
"passwordResetSubject": "Password Reset – LibrisLog",
"passwordResetBody": "<html>\n<body>\n<p>You have requested a password reset for your LibrisLog account.</p>\n<p>Click the link below to reset your password. This link is valid for {duration_minutes} minutes.</p>\n<p><a href=\"{reset_url}\">{reset_url}</a></p>\n<p>If you did not request this, please ignore this email.</p>\n</body>\n</html>"
Expand Down
13 changes: 13 additions & 0 deletions backend/app/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@
"avg_pages": "Páginas/Libro"
}
},
"medium": {
"print": "Impreso",
"ebook": "eBook",
"audiobook": "Audiolibro",
"comic_graphic_novel": "Cómic / Novela gráfica",
"magazine_newspaper": "Revista / Periódico"
},
"acquisition": {
"owned": "En propiedad",
"borrowed": "Prestado",
"digital_access": "Acceso digital",
"to_acquire": "Por adquirir"
},
"email": {
"passwordResetSubject": "Restablecer contraseña – LibrisLog",
"passwordResetBody": "<html>\n<body>\n<p>Has solicitado un restablecimiento de contraseña para tu cuenta de LibrisLog.</p>\n<p>Haz clic en el enlace de abajo para restablecer tu contraseña. Este enlace es válido por {duration_minutes} minutos.</p>\n<p><a href=\"{reset_url}\">{reset_url}</a></p>\n<p>Si no solicitaste esto, ignora este correo electrónico.</p>\n</body>\n</html>"
Expand Down
13 changes: 13 additions & 0 deletions backend/app/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@
"avg_pages": "Pages/Livre"
}
},
"medium": {
"print": "Imprimé",
"ebook": "eBook",
"audiobook": "Livre audio",
"comic_graphic_novel": "Bande dessinée / Roman graphique",
"magazine_newspaper": "Magazine / Journal"
},
"acquisition": {
"owned": "Possédé",
"borrowed": "Emprunté",
"digital_access": "Accès numérique",
"to_acquire": "À acquérir"
},
"email": {
"passwordResetSubject": "Réinitialisation du mot de passe – LibrisLog",
"passwordResetBody": "<html>\n<body>\n<p>Vous avez demandé une réinitialisation de mot de passe pour votre compte LibrisLog.</p>\n<p>Cliquez sur le lien ci-dessous pour réinitialiser votre mot de passe. Ce lien est valable {duration_minutes} minutes.</p>\n<p><a href=\"{reset_url}\">{reset_url}</a></p>\n<p>Si vous n'avez pas demandé cela, veuillez ignorer cet e-mail.</p>\n</body>\n</html>"
Expand Down
Loading
Loading