From db9da26fda25c6d0c714f678019121b606de8b0c Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Wed, 25 Feb 2026 15:31:06 +0100 Subject: [PATCH 01/14] feat(koplugin): flush in-memory stats to DB before sync --- plugins/koinsight.koplugin/db_reader.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/koinsight.koplugin/db_reader.lua b/plugins/koinsight.koplugin/db_reader.lua index 302b00f9..1ef12e73 100644 --- a/plugins/koinsight.koplugin/db_reader.lua +++ b/plugins/koinsight.koplugin/db_reader.lua @@ -109,7 +109,18 @@ function get_md5_by_id(books, target_id) return nil end +local function flush_statistics_to_db() + local ok, ReaderUI = pcall(require, "apps/reader/readerui") + if not ok or not ReaderUI or not ReaderUI.instance then return end + local ui = ReaderUI.instance + if ui and ui.statistics and ui.statistics.is_doc then + pcall(function() ui.statistics:insertDB() end) + logger.info("[KoInsight] Flushed statistics to DB before sync") + end +end + function KoInsightDbReader.progressData() + flush_statistics_to_db() local conn = SQ3.open(db_location) local result, rows = conn:exec("SELECT * FROM page_stat_data") local results = {} From 34f45cb86f11977250e003e50f84dccf8873c008 Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Wed, 25 Feb 2026 15:31:30 +0100 Subject: [PATCH 02/14] fix(koplugin): use os.time() for last_open instead of summary.modified --- plugins/koinsight.koplugin/annotation_reader.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/koinsight.koplugin/annotation_reader.lua b/plugins/koinsight.koplugin/annotation_reader.lua index 7a32f882..6c5847c5 100644 --- a/plugins/koinsight.koplugin/annotation_reader.lua +++ b/plugins/koinsight.koplugin/annotation_reader.lua @@ -216,7 +216,7 @@ function KoInsightAnnotationReader.getBookDataFromSidecar(file_path) pages = total_pages or 0, highlights = (stats and stats.highlights) or 0, notes = (stats and stats.notes) or 0, - last_open = (summary and summary.modified) or os.time(), + last_open = os.time(), } return md5, annotations, total_pages, book_metadata From 70d5187b67f86c6357e33684cd276a85746e637e Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Wed, 25 Feb 2026 15:31:59 +0100 Subject: [PATCH 03/14] fix(server): validate last_open and filter invalid page stats on upload --- apps/server/src/upload/upload-service.ts | 25 ++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/server/src/upload/upload-service.ts b/apps/server/src/upload/upload-service.ts index febf9375..7007cf66 100644 --- a/apps/server/src/upload/upload-service.ts +++ b/apps/server/src/upload/upload-service.ts @@ -97,8 +97,14 @@ export class UploadService { const { book_md5, device_id, total_read_time, total_read_pages, ...otherFields } = bookDevice; - // Always merge these fields - const fieldsToMerge: (keyof BookDevice)[] = ['last_open', 'pages', 'notes', 'highlights']; + // Always merge these fields (last_open added conditionally below) + const fieldsToMerge: (keyof BookDevice)[] = ['pages', 'notes', 'highlights']; + + // Only merge last_open if it's a valid positive Unix timestamp (seconds) + const last_open = bookDevice.last_open; + if (typeof last_open === 'number' && Number.isFinite(last_open) && last_open > 0) { + fieldsToMerge.push('last_open'); + } // Only merge statistics fields if they have actual values (if on statistics.db sync path) // This prevents annotation-only syncs from overwriting with zeros @@ -117,9 +123,20 @@ export class UploadService { ); // Insert page stats (only on stats sync path! there are non for annotation sync path) - if (newPageStats.length > 0) { + const validPageStats = newPageStats.filter( + (s) => + s.duration != null && + typeof s.duration === 'number' && + Number.isFinite(s.duration) && + s.duration > 0 && + s.total_pages != null && + typeof s.total_pages === 'number' && + Number.isFinite(s.total_pages) && + s.total_pages > 0 + ); + if (validPageStats.length > 0) { await Promise.all( - newPageStats.map((pageStat) => + validPageStats.map((pageStat) => trx('page_stat') .insert(pageStat) .onConflict(['device_id', 'book_md5', 'page', 'start_time']) From 6432746c29a3d333b23496174f017b9cf148129a Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Wed, 25 Feb 2026 15:32:24 +0100 Subject: [PATCH 04/14] fix(web): guard avgPerDay against division by zero --- apps/web/src/pages/book-page/book-page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/pages/book-page/book-page.tsx b/apps/web/src/pages/book-page/book-page.tsx index d54f3ea6..35da081a 100644 --- a/apps/web/src/pages/book-page/book-page.tsx +++ b/apps/web/src/pages/book-page/book-page.tsx @@ -157,7 +157,8 @@ function StatsCard({ book }: { book: BookWithData }): JSX.Element { book?.device_data.reduce((acc, device) => Math.max(acc, device.pages), 0) || 0; - const avgPerDay = book ? book.total_read_time / Object.keys(book.read_per_day).length : 0; + const readingDays = book ? Object.keys(book.read_per_day).length : 0; + const avgPerDay = readingDays > 0 ? book!.total_read_time / readingDays : 0; return ( Date: Wed, 25 Feb 2026 15:32:43 +0100 Subject: [PATCH 05/14] fix(server): return 0 from getStartedReading when stats array is empty --- apps/server/src/books/books-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/books/books-service.ts b/apps/server/src/books/books-service.ts index 4532a08c..95480d27 100644 --- a/apps/server/src/books/books-service.ts +++ b/apps/server/src/books/books-service.ts @@ -16,6 +16,7 @@ export class BooksService { } static getStartedReading(stats: PageStat[]): number { + if (stats.length === 0) return 0; return stats.reduce((acc, stat) => Math.min(acc, stat.start_time), Infinity); } From eaf77428ece6bce5568de4e863516a1c782e6859 Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Wed, 25 Feb 2026 15:51:24 +0100 Subject: [PATCH 06/14] fix(server): normalize stats payload to array before filtering --- apps/server/src/upload/upload-service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/server/src/upload/upload-service.ts b/apps/server/src/upload/upload-service.ts index 7007cf66..e5a33b6f 100644 --- a/apps/server/src/upload/upload-service.ts +++ b/apps/server/src/upload/upload-service.ts @@ -45,6 +45,9 @@ export class UploadService { deviceIdOverride?: string // For annotation sync path without stats ) { return db.transaction(async (trx) => { + // Normalize: the plugin sends {} (empty Lua table → JSON object) on the + // annotation-only path, not []. Guard all array operations against this. + const safePageStats = Array.isArray(newPageStats) ? newPageStats : []; // Insert books const newBooks: Partial[] = booksToImport.map((book) => ({ id: book.id, @@ -61,8 +64,8 @@ export class UploadService { // Determine device ID: from stats, override, or fall back to unknown device const deviceId = - newPageStats.length > 0 - ? newPageStats[0].device_id + safePageStats.length > 0 + ? safePageStats[0].device_id : deviceIdOverride || this.UNKNOWN_DEVICE_ID; const hasUnknownDevices = deviceId === this.UNKNOWN_DEVICE_ID; @@ -123,7 +126,7 @@ export class UploadService { ); // Insert page stats (only on stats sync path! there are non for annotation sync path) - const validPageStats = newPageStats.filter( + const validPageStats = safePageStats.filter( (s) => s.duration != null && typeof s.duration === 'number' && From 46fe7154b2a842deaf07219ebb36cc8531aa847c Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Wed, 25 Feb 2026 16:12:44 +0100 Subject: [PATCH 07/14] fix(koplugin): set last_open to 0 in annotation path, statistics sync owns it --- plugins/koinsight.koplugin/annotation_reader.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/koinsight.koplugin/annotation_reader.lua b/plugins/koinsight.koplugin/annotation_reader.lua index 6c5847c5..a78c0803 100644 --- a/plugins/koinsight.koplugin/annotation_reader.lua +++ b/plugins/koinsight.koplugin/annotation_reader.lua @@ -204,7 +204,6 @@ function KoInsightAnnotationReader.getBookDataFromSidecar(file_path) -- Extract book metadata from sidecar local doc_props = doc_settings:readSetting("doc_props") local stats = doc_settings:readSetting("stats") - local summary = doc_settings:readSetting("summary") local percent_finished = doc_settings:readSetting("percent_finished") local book_metadata = { @@ -216,7 +215,7 @@ function KoInsightAnnotationReader.getBookDataFromSidecar(file_path) pages = total_pages or 0, highlights = (stats and stats.highlights) or 0, notes = (stats and stats.notes) or 0, - last_open = os.time(), + last_open = 0, -- not available from sidecar; statistics sync sets this correctly } return md5, annotations, total_pages, book_metadata From ad129edabab8c412f34636b02a57be7eba267cf5 Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Fri, 27 Feb 2026 11:21:22 +0100 Subject: [PATCH 08/14] test(server): add empty-stats case for getStartedReading --- apps/server/src/books/books-service.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/server/src/books/books-service.test.ts b/apps/server/src/books/books-service.test.ts index 26a317f1..b94a7e94 100644 --- a/apps/server/src/books/books-service.test.ts +++ b/apps/server/src/books/books-service.test.ts @@ -72,6 +72,13 @@ describe(BooksService.withData, () => { expect(result.started_reading).toEqual(1720898518000); }); + + it('returns 0 if no stats are available', async () => { + const book1 = await createBook(db, { title: 'Test Book 1' }); + const result = await BooksService.withData(book1); + + expect(result.started_reading).toEqual(0); + }); }); describe('read_per_day', () => { From 25705ab5722645971d0ed74e2f05d6a20475153c Mon Sep 17 00:00:00 2001 From: Tony Fischer Date: Fri, 27 Feb 2026 11:28:30 +0100 Subject: [PATCH 09/14] docs(server): typo in comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/server/src/upload/upload-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/upload/upload-service.ts b/apps/server/src/upload/upload-service.ts index e5a33b6f..ceb3b763 100644 --- a/apps/server/src/upload/upload-service.ts +++ b/apps/server/src/upload/upload-service.ts @@ -125,7 +125,7 @@ export class UploadService { }) ); - // Insert page stats (only on stats sync path! there are non for annotation sync path) + // Insert page stats (only on stats sync path! there are none for annotation sync path) const validPageStats = safePageStats.filter( (s) => s.duration != null && From 9634b3528bca4fa12b24e935e2da1d93938b7089 Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Fri, 27 Feb 2026 11:24:42 +0100 Subject: [PATCH 10/14] fix(server): derive deviceId from first valid stat entry with a string device_id --- apps/server/src/upload/upload-service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/server/src/upload/upload-service.ts b/apps/server/src/upload/upload-service.ts index ceb3b763..df3332fa 100644 --- a/apps/server/src/upload/upload-service.ts +++ b/apps/server/src/upload/upload-service.ts @@ -63,10 +63,10 @@ export class UploadService { ); // Determine device ID: from stats, override, or fall back to unknown device - const deviceId = - safePageStats.length > 0 - ? safePageStats[0].device_id - : deviceIdOverride || this.UNKNOWN_DEVICE_ID; + const firstValidStat = safePageStats.find( + (s) => s != null && typeof s === 'object' && typeof s.device_id === 'string' + ); + const deviceId = firstValidStat?.device_id ?? deviceIdOverride ?? this.UNKNOWN_DEVICE_ID; const hasUnknownDevices = deviceId === this.UNKNOWN_DEVICE_ID; From ae90f63df442e6a85f6b6e72af0e25812f19cac4 Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Fri, 27 Feb 2026 11:26:40 +0100 Subject: [PATCH 11/14] fix(server): guard page stat filter against null/non-object entries --- apps/server/src/upload/upload-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/upload/upload-service.ts b/apps/server/src/upload/upload-service.ts index df3332fa..55bf0763 100644 --- a/apps/server/src/upload/upload-service.ts +++ b/apps/server/src/upload/upload-service.ts @@ -128,11 +128,11 @@ export class UploadService { // Insert page stats (only on stats sync path! there are none for annotation sync path) const validPageStats = safePageStats.filter( (s) => - s.duration != null && + s != null && + typeof s === 'object' && typeof s.duration === 'number' && Number.isFinite(s.duration) && s.duration > 0 && - s.total_pages != null && typeof s.total_pages === 'number' && Number.isFinite(s.total_pages) && s.total_pages > 0 From 9b8da99afbd11d76a206b5281349cd444c68aecc Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Fri, 27 Feb 2026 11:35:14 +0100 Subject: [PATCH 12/14] fix(server): consolidate page stat validation and add input hardening tests --- .../upload/upload-service-annotations.test.ts | 103 +++++++++++++++++- apps/server/src/upload/upload-service.ts | 34 +++--- 2 files changed, 115 insertions(+), 22 deletions(-) diff --git a/apps/server/src/upload/upload-service-annotations.test.ts b/apps/server/src/upload/upload-service-annotations.test.ts index 06a62c38..f585ecda 100644 --- a/apps/server/src/upload/upload-service-annotations.test.ts +++ b/apps/server/src/upload/upload-service-annotations.test.ts @@ -1,8 +1,9 @@ -import { KoReaderAnnotation } from '@koinsight/common/types'; +import { KoReaderAnnotation, KoReaderBook, PageStat } from '@koinsight/common/types'; import { createBook } from '../db/factories/book-factory'; import { createDevice } from '../db/factories/device-factory'; import { db } from '../knex'; import { AnnotationsRepository } from '../annotations/annotations-repository'; +import { UploadService } from './upload-service'; describe('UploadService with annotations', () => { describe('annotation data structure from KoReader', () => { @@ -57,14 +58,14 @@ describe('UploadService with annotations', () => { expect(counts.bookmark).toBe(1); // Verify annotation content - const highlight = importedAnnotations.find(a => a.annotation_type === 'highlight'); + const highlight = importedAnnotations.find((a) => a.annotation_type === 'highlight'); expect(highlight?.text).toBe('First highlight from KoReader'); expect(highlight?.color).toBe('yellow'); - const note = importedAnnotations.find(a => a.annotation_type === 'note'); + const note = importedAnnotations.find((a) => a.annotation_type === 'note'); expect(note?.note).toBe('This is important!'); - const bookmark = importedAnnotations.find(a => a.annotation_type === 'bookmark'); + const bookmark = importedAnnotations.find((a) => a.annotation_type === 'bookmark'); expect(bookmark?.pageno).toBe(50); }); @@ -119,3 +120,97 @@ describe('UploadService with annotations', () => { }); }); }); + +describe('UploadService page stat input hardening', () => { + const makeBook = (md5: string): KoReaderBook => ({ + id: 1, + md5, + title: 'Test Book', + authors: 'Author', + series: '', + language: 'en', + notes: 0, + highlights: 0, + last_open: 0, + pages: 100, + total_read_time: 0, + total_read_pages: 0, + }); + + it('succeeds when stats is a non-array object (annotation-only path)', async () => { + const book = await createBook(db, { md5: 'hardening-test-1' }); + + await expect( + UploadService.uploadStatisticData([makeBook(book.md5)], {} as unknown as PageStat[]) + ).resolves.not.toThrow(); + + const rows = await db('page_stat').where({ book_md5: book.md5 }); + expect(rows).toHaveLength(0); + }); + + it('drops null/undefined/non-object entries in stats array', async () => { + const book = await createBook(db, { md5: 'hardening-test-2' }); + const device = await createDevice(db); + + const statsWithJunk = [ + null, + undefined, + 'not-an-object', + 42, + { + device_id: device.id, + book_md5: book.md5, + page: 1, + start_time: 1000, + duration: 10, + total_pages: 100, + }, + ] as unknown as PageStat[]; + + await expect( + UploadService.uploadStatisticData([makeBook(book.md5)], statsWithJunk) + ).resolves.not.toThrow(); + + const rows = await db('page_stat').where({ book_md5: book.md5 }); + expect(rows).toHaveLength(1); + }); + + it('drops stats with zero or invalid duration/total_pages', async () => { + const book = await createBook(db, { md5: 'hardening-test-3' }); + const device = await createDevice(db); + + const invalidStats: PageStat[] = [ + { + device_id: device.id, + book_md5: book.md5, + page: 1, + start_time: 1000, + duration: 0, + total_pages: 100, + }, + { + device_id: device.id, + book_md5: book.md5, + page: 2, + start_time: 1001, + duration: 10, + total_pages: 0, + }, + { + device_id: device.id, + book_md5: book.md5, + page: 3, + start_time: 1002, + duration: -1, + total_pages: 100, + }, + ]; + + await expect( + UploadService.uploadStatisticData([makeBook(book.md5)], invalidStats) + ).resolves.not.toThrow(); + + const rows = await db('page_stat').where({ book_md5: book.md5 }); + expect(rows).toHaveLength(0); + }); +}); diff --git a/apps/server/src/upload/upload-service.ts b/apps/server/src/upload/upload-service.ts index 55bf0763..723d8b03 100644 --- a/apps/server/src/upload/upload-service.ts +++ b/apps/server/src/upload/upload-service.ts @@ -46,8 +46,19 @@ export class UploadService { ) { return db.transaction(async (trx) => { // Normalize: the plugin sends {} (empty Lua table → JSON object) on the - // annotation-only path, not []. Guard all array operations against this. - const safePageStats = Array.isArray(newPageStats) ? newPageStats : []; + // annotation-only path, not []. Guard all array operations against this, + // and drop clearly invalid page stat rows. + const safePageStats = (Array.isArray(newPageStats) ? newPageStats : []).filter( + (s) => + s != null && + typeof s === 'object' && + typeof s.duration === 'number' && + Number.isFinite(s.duration) && + s.duration > 0 && + typeof s.total_pages === 'number' && + Number.isFinite(s.total_pages) && + s.total_pages > 0 + ); // Insert books const newBooks: Partial[] = booksToImport.map((book) => ({ id: book.id, @@ -63,9 +74,7 @@ export class UploadService { ); // Determine device ID: from stats, override, or fall back to unknown device - const firstValidStat = safePageStats.find( - (s) => s != null && typeof s === 'object' && typeof s.device_id === 'string' - ); + const firstValidStat = safePageStats.find((s) => typeof s.device_id === 'string'); const deviceId = firstValidStat?.device_id ?? deviceIdOverride ?? this.UNKNOWN_DEVICE_ID; const hasUnknownDevices = deviceId === this.UNKNOWN_DEVICE_ID; @@ -126,20 +135,9 @@ export class UploadService { ); // Insert page stats (only on stats sync path! there are none for annotation sync path) - const validPageStats = safePageStats.filter( - (s) => - s != null && - typeof s === 'object' && - typeof s.duration === 'number' && - Number.isFinite(s.duration) && - s.duration > 0 && - typeof s.total_pages === 'number' && - Number.isFinite(s.total_pages) && - s.total_pages > 0 - ); - if (validPageStats.length > 0) { + if (safePageStats.length > 0) { await Promise.all( - validPageStats.map((pageStat) => + safePageStats.map((pageStat) => trx('page_stat') .insert(pageStat) .onConflict(['device_id', 'book_md5', 'page', 'start_time']) From ee48853cc1b06aad9806979eda6b3d92fdceace4 Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Fri, 27 Feb 2026 11:39:07 +0100 Subject: [PATCH 13/14] fix(koplugin): log flush result and warn on insertDB failure --- plugins/koinsight.koplugin/db_reader.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/koinsight.koplugin/db_reader.lua b/plugins/koinsight.koplugin/db_reader.lua index 1ef12e73..76bd0340 100644 --- a/plugins/koinsight.koplugin/db_reader.lua +++ b/plugins/koinsight.koplugin/db_reader.lua @@ -114,8 +114,12 @@ local function flush_statistics_to_db() if not ok or not ReaderUI or not ReaderUI.instance then return end local ui = ReaderUI.instance if ui and ui.statistics and ui.statistics.is_doc then - pcall(function() ui.statistics:insertDB() end) - logger.info("[KoInsight] Flushed statistics to DB before sync") + local ok_flush, err = pcall(function() ui.statistics:insertDB() end) + if ok_flush then + logger.info("[KoInsight] Flushed statistics to DB before sync") + else + logger.warn("[KoInsight] Failed to flush statistics to DB: " .. tostring(err)) + end end end From 6d4a17d55fe47468aa6ea6c2174e37726a7bb5ed Mon Sep 17 00:00:00 2001 From: "Tony Fischer (tku137)" Date: Sun, 1 Mar 2026 15:10:20 +0100 Subject: [PATCH 14/14] fix: less typeof; nullish coalescence instead of non-null assertion --- apps/server/src/upload/upload-service.ts | 6 ++---- apps/web/src/pages/book-page/book-page.tsx | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/server/src/upload/upload-service.ts b/apps/server/src/upload/upload-service.ts index 723d8b03..e799e92d 100644 --- a/apps/server/src/upload/upload-service.ts +++ b/apps/server/src/upload/upload-service.ts @@ -52,10 +52,8 @@ export class UploadService { (s) => s != null && typeof s === 'object' && - typeof s.duration === 'number' && Number.isFinite(s.duration) && s.duration > 0 && - typeof s.total_pages === 'number' && Number.isFinite(s.total_pages) && s.total_pages > 0 ); @@ -74,7 +72,7 @@ export class UploadService { ); // Determine device ID: from stats, override, or fall back to unknown device - const firstValidStat = safePageStats.find((s) => typeof s.device_id === 'string'); + const firstValidStat = safePageStats.find((s) => s.device_id); const deviceId = firstValidStat?.device_id ?? deviceIdOverride ?? this.UNKNOWN_DEVICE_ID; const hasUnknownDevices = deviceId === this.UNKNOWN_DEVICE_ID; @@ -114,7 +112,7 @@ export class UploadService { // Only merge last_open if it's a valid positive Unix timestamp (seconds) const last_open = bookDevice.last_open; - if (typeof last_open === 'number' && Number.isFinite(last_open) && last_open > 0) { + if (Number.isFinite(last_open) && last_open > 0) { fieldsToMerge.push('last_open'); } diff --git a/apps/web/src/pages/book-page/book-page.tsx b/apps/web/src/pages/book-page/book-page.tsx index 35da081a..5b1b4a3f 100644 --- a/apps/web/src/pages/book-page/book-page.tsx +++ b/apps/web/src/pages/book-page/book-page.tsx @@ -158,7 +158,7 @@ function StatsCard({ book }: { book: BookWithData }): JSX.Element { 0; const readingDays = book ? Object.keys(book.read_per_day).length : 0; - const avgPerDay = readingDays > 0 ? book!.total_read_time / readingDays : 0; + const avgPerDay = readingDays > 0 ? (book?.total_read_time ?? 0) / readingDays : 0; return (