Skip to content

fix(notifications): paginate and index the notification list endpoints#1

Closed
tagpro wants to merge 1 commit into
masterfrom
fix/notifications-pagination
Closed

fix(notifications): paginate and index the notification list endpoints#1
tagpro wants to merge 1 commit into
masterfrom
fix/notifications-pagination

Conversation

@tagpro

@tagpro tagpro commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Fork-side copy of maxdorninger#564, so the change can be tested on our own instance.

The branch is based on upstream/master, not this fork's master, so it does not carry the fork-specific commits (CI/Quay, MissingGreenlet fix). Merging it here brings the pagination work onto the fork's master alongside those.

Problem

Both notification list endpoints return the entire notification table:

# media_manager/notification/repository.py
stmt = select(Notification).order_by(Notification.timestamp.desc())   # no limit/offset

Every visit to the tab (and every 30s poll) fetches every row, pydantic-validates each one, serialises the whole list to JSON and ships it to the browser. On our instance the notification table had ~437,000 rows.

There is also no index beyond the primary key on id, so the ORDER BY timestamp DESC sorts the whole table on every load and the unread query is a full scan.

Changes

  • GET /notification and GET /notification/unread take limit (default 50, max 200) and offset; total count comes back in an X-Total-Count header (exposed via CORS).
  • GET /notification takes an optional read filter — the UI used to build its "read" list by fetching everything and filtering client-side.
  • Migration b3d51c7fa9e2 (off e60ae827ed98): index on timestamp DESC, partial index WHERE read = false.
  • New PATCH /notification/read bulk-marks all unread as read in one UPDATE; the old "Mark All as Read" looped a PATCH per notification and would otherwise have marked only the loaded page.
  • Notifications page loads 50 at a time with "Load more"; read notifications are not fetched until that section is expanded.
  • Adds a small pytest harness (tests/, async SQLite) plus repository tests for limit/offset/ordering/unread/bulk-read.

Testing locally

git fetch origin fix/notifications-pagination
git checkout fix/notifications-pagination
uv sync --group test && uv run pytest
uv run alembic upgrade head     # adds the two indexes
make up

Note the migration creates two indexes on a ~437k-row table — quick, but it does take a lock briefly.

The notification flood (identical failure notifications written every import cycle) is a separate root cause and is not addressed here; this only makes the endpoint bounded and indexed.

🤖 Generated with Claude Code

https://claude.ai/code/session_019wrZPxJDip5x3mzjWnpwos

Summary by CodeRabbit

  • New Features

    • Added paginated notification lists with newest-first ordering and optional read-status filtering.
    • Added total notification counts and “Load More” controls for unread and read notifications.
    • Added a bulk “Mark All as Read” action.
    • Improved notification refreshing to load only the necessary data.
  • Bug Fixes

    • Notification list loading is more efficient and supports larger notification histories.
  • Tests

    • Added coverage for pagination, filtering, counting, ordering, and bulk read updates.

The notification tab loaded slowly because both list endpoints returned the
entire notification table. GET /notification and GET /notification/unread ran
`select(Notification).order_by(timestamp desc)` with no limit, so every page
load fetched every row, pydantic-validated each one, serialised the lot to JSON
and shipped it to the browser. On the instance this was found on the table had
~437,000 rows.

The table also had no index beyond the primary key on `id`, so the ORDER BY
sorted the whole table on every load and the unread filter was a full scan.

Backend:
- Both list endpoints now take `limit` (default 50, max 200) and `offset`
  (>= 0), threaded through the service into `.limit()/.offset()` in the SQL.
  Ordering stays timestamp descending. The number of matching notifications is
  returned in the `X-Total-Count` response header (exposed via CORS) so the UI
  can paginate.
- GET /notification takes an optional `read` filter, so the UI can page through
  read notifications rather than fetching everything and filtering client-side.
- New migration indexes the table for these queries: an index on `timestamp`
  descending for the ORDER BY, and a partial index for the unread query.
- New PATCH /notification/read marks every unread notification read in a single
  statement. The UI used to do this by looping a PATCH over every notification
  it held, which only worked because it held all of them.

Frontend:
- The notifications page now loads a page of 50 with "load more", and does not
  fetch read notifications at all until that section is expanded.

Tests:
- Repository tests covering the limit cap, offset, timestamp-descending
  ordering, the unread query, and the bulk mark-as-read, seeded with more rows
  than the page size so pagination is actually exercised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wrZPxJDip5x3mzjWnpwos
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c908a9f9-ee62-40fa-afa3-3e75ff19015d

📥 Commits

Reviewing files that changed from the base of the PR and between 29aad53 and 434bfed.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • alembic/versions/b3d51c7fa9e2_index_notification_timestamp_and_read.py
  • media_manager/main.py
  • media_manager/notification/models.py
  • media_manager/notification/repository.py
  • media_manager/notification/router.py
  • media_manager/notification/service.py
  • pyproject.toml
  • tests/__init__.py
  • tests/conftest.py
  • tests/test_notification_repository.py
  • web/src/lib/api/api.d.ts
  • web/src/routes/dashboard/notifications/+page.svelte

📝 Walkthrough

Walkthrough

Notification retrieval now supports paginated read/unread queries, total-count headers, indexed ordering, bulk read updates, repository tests, generated API types, and dashboard load-more behavior.

Changes

Notification pagination and read state

Layer / File(s) Summary
Notification indexes and test database
media_manager/notification/models.py, alembic/versions/..., tests/conftest.py, pyproject.toml
Notification timestamp and unread indexes were added, with migration support and shared async SQLite test configuration.
Repository and service pagination
media_manager/notification/repository.py, media_manager/notification/service.py, tests/test_notification_repository.py
Notification queries now support ordering, pagination, read filtering, total counts, and bulk marking unread records as read, with repository coverage for these behaviors.
Paginated notification API
media_manager/notification/router.py, media_manager/main.py, web/src/lib/api/api.d.ts
Notification endpoints accept pagination parameters, return X-Total-Count, expose bulk read updates, and publish updated generated API types.
Dashboard notification paging
web/src/routes/dashboard/notifications/+page.svelte
The dashboard loads unread and read pages separately, displays server totals, adds load-more controls, updates local ordering, and polls unread notifications conditionally.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dashboard as Notifications dashboard
  participant Router as Notification router
  participant Service as NotificationService
  participant Repository as NotificationRepository
  participant Database as Notification database

  Dashboard->>Router: Request paginated notifications
  Router->>Service: Count matching notifications
  Service->>Repository: Count by read filter
  Repository->>Database: Execute count query
  Router->>Service: Fetch page with limit and offset
  Service->>Repository: Query ordered notification page
  Repository->>Database: Execute filtered paginated query
  Router-->>Dashboard: Notifications plus X-Total-Count
Loading
sequenceDiagram
  participant Dashboard as Notifications dashboard
  participant Router as Notification router
  participant Service as NotificationService
  participant Repository as NotificationRepository
  participant Database as Notification database

  Dashboard->>Router: PATCH mark all as read
  Router->>Service: Mark all notifications as read
  Service->>Repository: Update unread notifications
  Repository->>Database: Execute bulk read update
  Router-->>Dashboard: 204 No Content
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/notifications-pagination
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/notifications-pagination

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tagpro

tagpro commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

Superseded: opening a fork-specific branch that merges cleanly with the fork's master (the branch here is based on upstream/master and conflicts with the fork's existing tests/conftest.py).

@tagpro tagpro closed this Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant