From d7cc3c263e7c2312be5e0b5878cc47a1b6097f48 Mon Sep 17 00:00:00 2001 From: codebude Date: Sun, 13 Sep 2026 14:13:10 +0200 Subject: [PATCH 1/4] Improved dark theme contrast and improved spacing in share url list --- frontend/src/app.css | 8 +++++++ frontend/src/routes/profile/+page.svelte | 27 ++++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index fd4fd9c2..7c62b8cd 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -61,6 +61,14 @@ --noise: 0; } +/* Keep dark cards calm while giving the page and nested surfaces clearer depth. */ +html[data-theme="dark"] { + --color-base-100: oklch(25.5% 0.018 255); + --color-base-200: oklch(19.5% 0.016 255); + --color-base-300: oklch(15.5% 0.014 255); + --color-base-content: oklch(86% 0.012 255); +} + html { scroll-behavior: smooth; } diff --git a/frontend/src/routes/profile/+page.svelte b/frontend/src/routes/profile/+page.svelte index 7d3c106e..0ae71149 100644 --- a/frontend/src/routes/profile/+page.svelte +++ b/frontend/src/routes/profile/+page.svelte @@ -452,7 +452,20 @@ import ShareLinkDialog from '$lib/components/ShareLinkDialog.svelte'; let copiedShareLinkId = $state(null); async function loadShareLinks() { - shareLinks = await api.profile.listShareLinks(); + const links = await api.profile.listShareLinks(); + shareLinks = links; + + const tokens = await Promise.all( + links.map(async (link) => { + try { + const result = await api.profile.revealShareLink(link.id); + return [link.id, result.token] as const; + } catch { + return null; + } + }) + ); + revealedTokens = Object.fromEntries(tokens.filter((entry): entry is [number, string] => entry !== null)); } function openCreateShareLink() { @@ -1030,14 +1043,14 @@ import ShareLinkDialog from '$lib/components/ShareLinkDialog.svelte'; (createdShareToken = null)} duration={0}>
{$_('publicProfile.tokenShownOnce')} -
+
{publicShareUrl(createdShareToken)}
- - + {$_('publicProfile.openLink')}
@@ -1047,11 +1060,11 @@ import ShareLinkDialog from '$lib/components/ShareLinkDialog.svelte'; {#if shareLinks.length === 0}

{$_('profile.shareProfile.empty')}

{:else} -
    +
      {#each shareLinks as link}
    • -
      -

      +

      +

      {link.name} {link.audience === 'public' ? $_('publicProfile.audiencePublic') : $_('publicProfile.audienceAuthenticated')} From bc1f4fd7acca708bff8ff2ad420429b6848e1f88 Mon Sep 17 00:00:00 2001 From: codebude Date: Sun, 13 Sep 2026 14:24:02 +0200 Subject: [PATCH 2/4] Switch statistics ranges to calendar-based options --- ...a1b2c3d4e5f_normalize_statistics_ranges.py | 46 +++++++++++++++++++ backend/app/schemas.py | 5 +- backend/app/services/statistics.py | 39 ++++------------ backend/tests/test_statistics.py | 32 +++++++++++-- .../components/StatisticsRangeSelector.svelte | 5 +- .../StatisticsRangeSelector.test.ts | 5 +- frontend/src/lib/i18n/locales/de.json | 5 +- frontend/src/lib/i18n/locales/en.json | 5 +- frontend/src/lib/i18n/locales/es.json | 5 +- frontend/src/lib/i18n/locales/fr.json | 5 +- frontend/src/lib/i18n/locales/zh.json | 5 +- frontend/src/lib/types.ts | 2 +- 12 files changed, 101 insertions(+), 58 deletions(-) create mode 100644 backend/alembic/versions/0a1b2c3d4e5f_normalize_statistics_ranges.py diff --git a/backend/alembic/versions/0a1b2c3d4e5f_normalize_statistics_ranges.py b/backend/alembic/versions/0a1b2c3d4e5f_normalize_statistics_ranges.py new file mode 100644 index 00000000..2298c45a --- /dev/null +++ b/backend/alembic/versions/0a1b2c3d4e5f_normalize_statistics_ranges.py @@ -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'" + ) + ) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index e3f600c6..a06a2b06 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -262,10 +262,9 @@ class YearlyBooks(SQLModel): class StatisticsRange(str, Enum): """Shared statistics time-range selector options.""" alltime = "alltime" + this_year = "this_year" + last_year = "last_year" three_years = "3years" - one_year = "1year" - six_months = "6months" - thirty_days = "30days" custom = "custom" diff --git a/backend/app/services/statistics.py b/backend/app/services/statistics.py index bab3454a..da1a9c77 100644 --- a/backend/app/services/statistics.py +++ b/backend/app/services/statistics.py @@ -112,26 +112,6 @@ def _naive_utc(dt: datetime) -> datetime: return dt -def _subtract_months(dt: datetime, months: int) -> datetime: - """Return *dt* shifted back by *months*, clamping the day if needed.""" - year, month = dt.year, dt.month - months - while month <= 0: - month += 12 - year -= 1 - last_dom = calendar.monthrange(year, month)[1] - day = min(dt.day, last_dom) - return dt.replace(year=year, month=month, day=day) - - -def _subtract_years(dt: datetime, years: int) -> datetime: - """Return *dt* shifted back by *years*, handling Feb 29 gracefully.""" - year = dt.year - years - try: - return dt.replace(year=year) - except ValueError: - return dt.replace(year=year, month=2, day=28) - - def _statistics_window( range_value: StatisticsRange, custom_from: date | None, @@ -147,7 +127,9 @@ def _statistics_window( - Custom -> from start of the custom *from* day to end of the custom *to* day (inclusive) in *tz*. - - Predefined -> ``now - delta`` (inclusive) to ``now``. + - This year -> the start of the current calendar year to ``now``. + - Last year -> the complete previous calendar year. + - Last 3 years -> the start of the calendar year two years ago to ``now``. """ if range_value == StatisticsRange.alltime: return (None, None) @@ -164,14 +146,13 @@ def _statistics_window( return (_naive_utc(start), _naive_utc(end)) end = now - if range_value == StatisticsRange.thirty_days: - start = now - timedelta(days=30) - elif range_value == StatisticsRange.six_months: - start = _subtract_months(now, 6) - elif range_value == StatisticsRange.one_year: - start = _subtract_years(now, 1) + if range_value == StatisticsRange.this_year: + start = datetime(now.year, 1, 1, tzinfo=tz) + elif range_value == StatisticsRange.last_year: + start = datetime(now.year - 1, 1, 1, tzinfo=tz) + end = datetime(now.year - 1, 12, 31, 23, 59, 59, 999999, tzinfo=tz) elif range_value == StatisticsRange.three_years: - start = _subtract_years(now, 3) + start = datetime(now.year - 2, 1, 1, tzinfo=tz) else: start = now return (_naive_utc(start), _naive_utc(end)) @@ -944,4 +925,4 @@ def _rating_sort_key(book: Book) -> tuple[int, float]: average_rating=average_rating, top_rated_books=top_rated_books, worst_rated_books=worst_rated_books, - ) \ No newline at end of file + ) diff --git a/backend/tests/test_statistics.py b/backend/tests/test_statistics.py index c5552b78..f586e953 100644 --- a/backend/tests/test_statistics.py +++ b/backend/tests/test_statistics.py @@ -10,7 +10,8 @@ from sqlmodel import Session, select from app.models import Book, ReadingProgress, ReadingStatus, UserSettings -from app.services.statistics import _extract_book_level_daily_pages +from app.schemas import StatisticsRange +from app.services.statistics import _extract_book_level_daily_pages, _statistics_window def _create_book(client: Any, **overrides: Any) -> dict[str, Any]: @@ -718,8 +719,8 @@ def test_statistics_range_filters_finished_books(client: Any) -> None: client, title="Outside", reading_status="read", - date_started=(now - timedelta(days=45)).isoformat(), - date_finished=(now - timedelta(days=40)).isoformat(), + date_started=f"{now.year - 1}-01-01T10:00:00+00:00", + date_finished=f"{now.year - 1}-01-02T10:00:00+00:00", ) _create_book( client, @@ -729,13 +730,36 @@ def test_statistics_range_filters_finished_books(client: Any) -> None: date_finished=(now - timedelta(days=2)).isoformat(), ) - response = client.get("/api/statistics?range=30days") + response = client.get("/api/statistics?range=this_year") assert response.status_code == 200 data = response.json() assert sum(item["count"] for item in data["books_finished_per_month"]) == 1 assert sum(item["count"] for item in data["books_finished_per_year"]) == 1 +def test_statistics_calendar_range_windows() -> None: + now = datetime(2026, 9, 13, 12, 0, tzinfo=timezone.utc) + tz = ZoneInfo("UTC") + + this_year_start, this_year_end = _statistics_window( + StatisticsRange.this_year, None, None, tz, now + ) + assert this_year_start == datetime(2026, 1, 1) + assert this_year_end == datetime(2026, 9, 13, 12, 0) + + last_year_start, last_year_end = _statistics_window( + StatisticsRange.last_year, None, None, tz, now + ) + assert last_year_start == datetime(2025, 1, 1) + assert last_year_end == datetime(2025, 12, 31, 23, 59, 59, 999999) + + three_years_start, three_years_end = _statistics_window( + StatisticsRange.three_years, None, None, tz, now + ) + assert three_years_start == datetime(2024, 1, 1) + assert three_years_end == datetime(2026, 9, 13, 12, 0) + + def test_statistics_custom_range_and_validation(client: Any) -> None: _create_book( client, diff --git a/frontend/src/lib/components/StatisticsRangeSelector.svelte b/frontend/src/lib/components/StatisticsRangeSelector.svelte index fd3b03f0..9fdd56f1 100644 --- a/frontend/src/lib/components/StatisticsRangeSelector.svelte +++ b/frontend/src/lib/components/StatisticsRangeSelector.svelte @@ -21,10 +21,9 @@ const options: Array<[StatisticsRange, string]> = [ ['alltime', 'statistics.rangeAllTime'], + ['this_year', 'statistics.rangeThisYear'], + ['last_year', 'statistics.rangeLastYear'], ['3years', 'statistics.rangeLast3Years'], - ['1year', 'statistics.rangeLastYear'], - ['6months', 'statistics.rangeLast6Months'], - ['30days', 'statistics.rangeLast30Days'], ['custom', 'statistics.rangeCustom'] ]; diff --git a/frontend/src/lib/components/StatisticsRangeSelector.test.ts b/frontend/src/lib/components/StatisticsRangeSelector.test.ts index b89222e2..e91abc10 100644 --- a/frontend/src/lib/components/StatisticsRangeSelector.test.ts +++ b/frontend/src/lib/components/StatisticsRangeSelector.test.ts @@ -8,10 +8,9 @@ describe('StatisticsRangeSelector', () => { const options = screen.getByRole('combobox').querySelectorAll('option'); expect([...options].map((option) => option.textContent)).toEqual([ 'All time', - 'Last 3 years', + 'This year', 'Last year', - 'Last 6 months', - 'Last 30 days', + 'Last 3 years', 'Custom' ]); }); diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json index 91b12dd2..6ff36826 100644 --- a/frontend/src/lib/i18n/locales/de.json +++ b/frontend/src/lib/i18n/locales/de.json @@ -39,10 +39,9 @@ "rangeLast12Months": "Letzte 12 Monate", "refreshing": "Statistiken werden aktualisiert", "rangeAllTime": "Gesamte Zeit", - "rangeLast3Years": "Letzte 3 Jahre", + "rangeThisYear": "Dieses Jahr", "rangeLastYear": "Letztes Jahr", - "rangeLast6Months": "Letzte 6 Monate", - "rangeLast30Days": "Letzte 30 Tage", + "rangeLast3Years": "Letzte 3 Jahre", "rangeCustom": "Benutzerdefiniert", "from": "Von", "to": "Bis", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index bdd88a9b..61c5dc8d 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -41,10 +41,9 @@ "rangeLast12Months": "Last 12 months", "refreshing": "Updating statistics", "rangeAllTime": "All time", - "rangeLast3Years": "Last 3 years", + "rangeThisYear": "This year", "rangeLastYear": "Last year", - "rangeLast6Months": "Last 6 months", - "rangeLast30Days": "Last 30 days", + "rangeLast3Years": "Last 3 years", "rangeCustom": "Custom", "from": "From", "to": "To", diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index 8d74fe30..cfb11c8b 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -39,10 +39,9 @@ "rangeLast12Months": "Últimos 12 meses", "refreshing": "Actualizando estadísticas", "rangeAllTime": "Todo el tiempo", + "rangeThisYear": "Este año", + "rangeLastYear": "El año pasado", "rangeLast3Years": "Últimos 3 años", - "rangeLastYear": "Último año", - "rangeLast6Months": "Últimos 6 meses", - "rangeLast30Days": "Últimos 30 días", "rangeCustom": "Personalizado", "from": "Desde", "to": "Hasta", diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json index cdb6a4ef..034a7b93 100644 --- a/frontend/src/lib/i18n/locales/fr.json +++ b/frontend/src/lib/i18n/locales/fr.json @@ -39,10 +39,9 @@ "rangeLast12Months": "12 derniers mois", "refreshing": "Mise à jour des statistiques", "rangeAllTime": "Depuis toujours", + "rangeThisYear": "Cette année", + "rangeLastYear": "L'année dernière", "rangeLast3Years": "3 dernières années", - "rangeLastYear": "Dernière année", - "rangeLast6Months": "6 derniers mois", - "rangeLast30Days": "30 derniers jours", "rangeCustom": "Personnalisé", "from": "Du", "to": "Au", diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json index 81980c4f..d6e0813f 100644 --- a/frontend/src/lib/i18n/locales/zh.json +++ b/frontend/src/lib/i18n/locales/zh.json @@ -39,10 +39,9 @@ "rangeLast12Months": "最近12个月", "refreshing": "正在更新统计数据", "rangeAllTime": "全部时间", + "rangeThisYear": "今年", + "rangeLastYear": "去年", "rangeLast3Years": "最近3年", - "rangeLastYear": "最近一年", - "rangeLast6Months": "最近6个月", - "rangeLast30Days": "最近30天", "rangeCustom": "自定义", "from": "从", "to": "到", diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 2a03e1a0..3598c385 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -214,7 +214,7 @@ export interface StatisticsResponse { worst_rated_books: TopRatedBook[]; } -export type StatisticsRange = 'alltime' | '3years' | '1year' | '6months' | '30days' | 'custom'; +export type StatisticsRange = 'alltime' | 'this_year' | 'last_year' | '3years' | 'custom'; export type UserRole = 'admin' | 'user'; From f730985dfa810bcf7e6e1881dc00fdd12ffebbb7 Mon Sep 17 00:00:00 2001 From: codebude Date: Sun, 13 Sep 2026 14:28:29 +0200 Subject: [PATCH 3/4] Disable telemetry in development environment --- docker-compose.dev.yml | 1 + docs/guide/developer-setup.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8b4d093d..2b09470f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -13,6 +13,7 @@ services: - ./data:/app/data - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro # only needed if you use custom certificates in you environment environment: + TELEMETRY_DISABLED: "true" # Development runs must not send telemetry. REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt # only needed if you use custom certificates in you environment SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt # only needed if you use custom certificates in you environment restart: unless-stopped diff --git a/docs/guide/developer-setup.md b/docs/guide/developer-setup.md index fb96b6b3..5367b826 100644 --- a/docs/guide/developer-setup.md +++ b/docs/guide/developer-setup.md @@ -64,7 +64,7 @@ Steps: uv sync cd backend uv run alembic upgrade head -uv run uvicorn app.main:app --reload --port 8000 +TELEMETRY_DISABLED=true uv run uvicorn app.main:app --reload --port 8000 ``` The backend runs on http://localhost:8000 with auto-reload on code changes. From ce0ec9798cabfd5cd8094aa48b955af15e0d3257 Mon Sep 17 00:00:00 2001 From: codebude Date: Sun, 13 Sep 2026 16:12:11 +0200 Subject: [PATCH 4/4] Add medium to data hygiene missing attributes --- backend/app/schemas.py | 1 + backend/tests/test_hygiene.py | 16 +++++++++++++++- frontend/src/lib/i18n/locales/de.json | 3 ++- frontend/src/lib/i18n/locales/en.json | 3 ++- frontend/src/lib/i18n/locales/es.json | 3 ++- frontend/src/lib/i18n/locales/fr.json | 3 ++- frontend/src/lib/i18n/locales/zh.json | 3 ++- frontend/src/lib/types.ts | 3 ++- frontend/src/routes/data-hygiene/+page.svelte | 1 + frontend/src/routes/data-hygiene/page.test.ts | 10 +++++----- 10 files changed, 34 insertions(+), 12 deletions(-) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index a06a2b06..4edaff95 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -572,6 +572,7 @@ class HygieneAttribute(str, Enum): subtitle = "subtitle" page_count = "page_count" cover_url = "cover_url" + medium = "medium" class HygieneMissingBook(SQLModel): diff --git a/backend/tests/test_hygiene.py b/backend/tests/test_hygiene.py index 8928b334..25defe0d 100644 --- a/backend/tests/test_hygiene.py +++ b/backend/tests/test_hygiene.py @@ -5,7 +5,7 @@ from pytest import MonkeyPatch from sqlmodel import Session, col, select -from app.models import Author, Book, BookAuthor, ReadingStatus, User +from app.models import Author, Book, BookAuthor, Medium, ReadingStatus, User from app.routers import hygiene as hygiene_router from app.services.authors import normalize_author_list @@ -28,6 +28,7 @@ def _create_book(session: Session, user_id: int, **overrides: object) -> Book: "blurb": "A test book.", "cover_url": None, "reading_status": ReadingStatus.want_to_read, + "medium": Medium.print, "user_id": user_id, } defaults.update(overrides) @@ -158,6 +159,19 @@ def test_missing_page_count_zero_treated_as_missing(self, client: TestClient, se assert data["total"] == 1 assert data["books"][0]["title"] == "Zero Pages" + def test_missing_medium(self, client: TestClient, session: Session) -> None: + """Medium is reported as missing when it has not been set.""" + user_id = 1 + _create_book(session, user_id, title="Print book", medium=Medium.print) + _create_book(session, user_id, title="Missing medium", medium=None) + + resp = client.get("/api/hygiene/missing?attributes=medium") + assert resp.status_code == 200 + data = resp.json() + assert data["total"] == 1 + assert data["books"][0]["title"] == "Missing medium" + assert data["total_missing_per_attribute"]["medium"] == 1 + class TestBatchUpdate: def test_batch_update_single_field(self, client: TestClient, session: Session) -> None: diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json index 6ff36826..23d0437d 100644 --- a/frontend/src/lib/i18n/locales/de.json +++ b/frontend/src/lib/i18n/locales/de.json @@ -802,7 +802,8 @@ "language": "Sprache", "subtitle": "Untertitel", "page_count": "Seitenanzahl", - "cover_url": "Cover" + "cover_url": "Cover", + "medium": "Medium" }, "matchAny": "Beliebiges", "matchAll": "Alle", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 61c5dc8d..8e3f5ba9 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -802,7 +802,8 @@ "language": "Language", "subtitle": "Subtitle", "page_count": "Page count", - "cover_url": "Cover" + "cover_url": "Cover", + "medium": "Medium" }, "matchAny": "Match any", "matchAll": "Match all", diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index cfb11c8b..da8bfb16 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -802,7 +802,8 @@ "language": "Idioma", "subtitle": "Subtítulo", "page_count": "Nº de páginas", - "cover_url": "Portada" + "cover_url": "Portada", + "medium": "Medio" }, "matchAny": "Coincidir cualquiera", "matchAll": "Coincidir todos", diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json index 034a7b93..284e3948 100644 --- a/frontend/src/lib/i18n/locales/fr.json +++ b/frontend/src/lib/i18n/locales/fr.json @@ -802,7 +802,8 @@ "language": "Langue", "subtitle": "Sous-titre", "page_count": "Nombre de pages", - "cover_url": "Couverture" + "cover_url": "Couverture", + "medium": "Support" }, "matchAny": "Correspond à l'un", "matchAll": "Correspond à tous", diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json index d6e0813f..d431dbbe 100644 --- a/frontend/src/lib/i18n/locales/zh.json +++ b/frontend/src/lib/i18n/locales/zh.json @@ -802,7 +802,8 @@ "language": "语言", "subtitle": "副标题", "page_count": "页数", - "cover_url": "封面" + "cover_url": "封面", + "medium": "媒介" }, "matchAny": "匹配任一", "matchAll": "匹配全部", diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 3598c385..bc765674 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -433,7 +433,8 @@ export type HygieneAttribute = | 'language' | 'subtitle' | 'page_count' - | 'cover_url'; + | 'cover_url' + | 'medium'; export interface HygieneMissingBook { id: number; diff --git a/frontend/src/routes/data-hygiene/+page.svelte b/frontend/src/routes/data-hygiene/+page.svelte index a20d60c5..67c53728 100644 --- a/frontend/src/routes/data-hygiene/+page.svelte +++ b/frontend/src/routes/data-hygiene/+page.svelte @@ -22,6 +22,7 @@ { key: 'subtitle', labelKey: 'dataHygiene.attributes.subtitle' }, { key: 'page_count', labelKey: 'dataHygiene.attributes.page_count' }, { key: 'cover_url', labelKey: 'dataHygiene.attributes.cover_url' }, + { key: 'medium', labelKey: 'dataHygiene.attributes.medium' }, ]; let selectedAttributes = $state([]); diff --git a/frontend/src/routes/data-hygiene/page.test.ts b/frontend/src/routes/data-hygiene/page.test.ts index 5d62533b..7a940a30 100644 --- a/frontend/src/routes/data-hygiene/page.test.ts +++ b/frontend/src/routes/data-hygiene/page.test.ts @@ -47,7 +47,7 @@ function mockBook(id: number, overrides?: Partial): HygieneM const emptyPerAttribute = { author: 0, isbn: 0, publisher: 0, published_year: 0, - blurb: 0, language: 0, subtitle: 0, page_count: 0, cover_url: 0, + blurb: 0, language: 0, subtitle: 0, page_count: 0, cover_url: 0, medium: 0, }; describe('DataHygienePage', () => { @@ -88,7 +88,7 @@ describe('DataHygienePage', () => { expect(mockListMissing).toHaveBeenCalledWith({ attributes: [ 'author', 'isbn', 'publisher', 'published_year', - 'blurb', 'language', 'subtitle', 'page_count', 'cover_url', + 'blurb', 'language', 'subtitle', 'page_count', 'cover_url', 'medium', ], match: 'any', offset: 0, @@ -96,14 +96,14 @@ describe('DataHygienePage', () => { }); }); - it('displays all 9 attribute chips', async () => { + it('displays all 10 attribute chips', async () => { render(DataHygienePage); const chips = await screen.findAllByRole('button'); const attrChips = chips.filter(c => - /Author|ISBN|Publisher|Year|Description|Language|Subtitle|Page count|Cover/.test(c.textContent || '') + /Author|ISBN|Publisher|Year|Description|Language|Subtitle|Page count|Cover|Medium/.test(c.textContent || '') ); - expect(attrChips).toHaveLength(9); + expect(attrChips).toHaveLength(10); }); it('shows per-attribute missing counts on chips', async () => {