From c90e10fd2e6dae6c36359cb7eeb5b628b42543d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 30 Mar 2026 10:17:04 +0000 Subject: [PATCH] Fix: store reading and exercise session durations in seconds instead of ms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #107 The frontend timer counts in seconds but was converting to milliseconds before sending to the API, which then stored and returned milliseconds. This required the frontend to convert back on display. Changes: - Update duration column comments in UserReadingSession and UserExerciseSession - Fix UserExerciseSession.__init__ to store seconds (remove * 1000) - Update all / 60000 calculations to / 60 for reading/exercise sessions - Rename duration_ms → duration_sec in stats functions for reading/exercise - Update session_history endpoint: format_duration and _calculate_focus_level now expect seconds; browsing/listening durations (still stored in ms) are normalized to seconds before being included in the response - Update macro_reading_session.py to treat duration as seconds - Update activity.py SQL query: remove / 1000 since durations are now seconds Note: browsing and listening sessions still store milliseconds; only reading and exercise sessions are changed as specified in the issue. --- zeeguu/api/endpoints/session_history.py | 27 ++++++++-------- zeeguu/api/endpoints/user_stats.py | 32 +++++++++---------- zeeguu/core/model/user_exercise_session.py | 4 +-- zeeguu/core/model/user_reading_session.py | 4 +-- .../reading_analysis/macro_reading_session.py | 6 ++-- zeeguu/core/user_statistics/activity.py | 2 +- 6 files changed, 38 insertions(+), 37 deletions(-) diff --git a/zeeguu/api/endpoints/session_history.py b/zeeguu/api/endpoints/session_history.py index 5f5cd8086..c4f5ece30 100644 --- a/zeeguu/api/endpoints/session_history.py +++ b/zeeguu/api/endpoints/session_history.py @@ -134,17 +134,17 @@ def _count_interruptions(user_id, start_time, end_time, event_type): return count -def _calculate_focus_level(interruptions, duration_ms, word_count): +def _calculate_focus_level(interruptions, duration_sec, word_count): """ Calculate a focus level based on interruptions, duration, and engagement. Returns: 'focused', 'moderate', or 'distracted' """ - if duration_ms is None or duration_ms == 0: + if duration_sec is None or duration_sec == 0: return None # Calculate words per minute as engagement metric - duration_min = duration_ms / 60000 + duration_min = duration_sec / 60 words_per_min = word_count / duration_min if duration_min > 0 else 0 # Interruptions per 10 minutes @@ -207,14 +207,13 @@ def session_history(): sessions = [] - def format_duration(duration_ms): + def format_duration(duration_sec): """Format duration showing seconds for short durations, minutes for longer ones.""" - if duration_ms is None or duration_ms == 0: + if duration_sec is None or duration_sec == 0: return "0 sec" - seconds = duration_ms / 1000 - if seconds < 60: - return f"{int(seconds)} sec" - minutes = seconds / 60 + if duration_sec < 60: + return f"{int(duration_sec)} sec" + minutes = duration_sec / 60 if minutes < 60: return f"{round(minutes, 1)} min" hours = minutes / 60 @@ -290,12 +289,13 @@ def format_duration(duration_ms): for bs in browsing_sessions: bookmarks = _bookmarks_for_browsing_session(bs.id, learned_language.id) if bookmarks: # Only include browsing sessions with translations in learned language + duration_sec = (bs.duration or 0) // 1000 # stored in ms, normalize to seconds sessions.append( { "session_type": "browsing", "start_time": bs.start_time.isoformat(), - "duration": bs.duration, - "duration_readable": format_duration(bs.duration), + "duration": duration_sec, + "duration_readable": format_duration(duration_sec), "words": bookmarks, "word_count": len(bookmarks), } @@ -319,12 +319,13 @@ def format_duration(duration_ms): else: words = [] + duration_sec = (ls.duration or 0) // 1000 # stored in ms, normalize to seconds sessions.append( { "session_type": "audio", "start_time": ls.start_time.isoformat(), - "duration": ls.duration, # Already in milliseconds - "duration_readable": format_duration(ls.duration), + "duration": duration_sec, + "duration_readable": format_duration(duration_sec), "words": words, "word_count": len(words), "completed": is_completed, diff --git a/zeeguu/api/endpoints/user_stats.py b/zeeguu/api/endpoints/user_stats.py index 114664765..1d9e2939b 100644 --- a/zeeguu/api/endpoints/user_stats.py +++ b/zeeguu/api/endpoints/user_stats.py @@ -123,7 +123,7 @@ def get_exercise_stats_for_user(user_id, start, end): .all() ) - total_duration_ms = sum(s.duration or 0 for s in sessions) + total_duration_sec = sum(s.duration or 0 for s in sessions) session_count = len(sessions) # Get unique words practiced and language info @@ -139,8 +139,8 @@ def get_exercise_stats_for_user(user_id, start, end): return { "session_count": session_count, - "duration_ms": total_duration_ms, - "duration_min": round(total_duration_ms / 60000, 1), + "duration_sec": total_duration_sec, + "duration_min": round(total_duration_sec / 60, 1), "words_by_language": { lang: len(words) for lang, words in words_by_language.items() }, @@ -157,7 +157,7 @@ def get_reading_stats_for_user(user_id, start, end): .all() ) - total_duration_ms = sum(s.duration or 0 for s in sessions) + total_duration_sec = sum(s.duration or 0 for s in sessions) # Group by article and language articles_by_language = defaultdict(set) @@ -169,8 +169,8 @@ def get_reading_stats_for_user(user_id, start, end): return { "session_count": len(sessions), - "duration_ms": total_duration_ms, - "duration_min": round(total_duration_ms / 60000, 1), + "duration_sec": total_duration_sec, + "duration_min": round(total_duration_sec / 60, 1), "articles_by_language": { lang: len(arts) for lang, arts in articles_by_language.items() }, @@ -506,7 +506,7 @@ def user_stats_individual(user_id): { "id": session.id, "start_time": session.start_time.isoformat(), - "duration_min": round((session.duration or 0) / 60000, 1), + "duration_min": round((session.duration or 0) / 60, 1), "language": session_lang, "word_count": len(words), "words": words, @@ -529,7 +529,7 @@ def user_stats_individual(user_id): { "id": session.id, "start_time": session.start_time.isoformat(), - "duration_min": round((session.duration or 0) / 60000, 1), + "duration_min": round((session.duration or 0) / 60, 1), "article_id": session.article_id, "article_title": article.title if article else "Unknown", "article_language": ( @@ -949,8 +949,8 @@ def user_stats_individual_dashboard(user_id): ) # Calculate totals - total_exercise_min = sum((s.duration or 0) / 60000 for s in exercise_sessions) - total_reading_min = sum((s.duration or 0) / 60000 for s in reading_sessions) + total_exercise_min = sum((s.duration or 0) / 60 for s in exercise_sessions) + total_reading_min = sum((s.duration or 0) / 60 for s in reading_sessions) total_audio_min = sum((l.duration_seconds or 0) / 60 for l in audio_lessons) html = f""" @@ -1078,7 +1078,7 @@ def user_stats_individual_dashboard(user_id): if exercise_sessions: for session in exercise_sessions: exercises = Exercise.query.filter(Exercise.session_id == session.id).all() - duration_min = (session.duration or 0) / 60000 + duration_min = (session.duration or 0) / 60 # Determine language from exercises session_lang = None @@ -1126,7 +1126,7 @@ def user_stats_individual_dashboard(user_id): if reading_sessions: for session in reading_sessions: - duration_min = (session.duration or 0) / 60000 + duration_min = (session.duration or 0) / 60 article = session.article article_title = article.title if article else "Unknown article" article_lang = ( @@ -2245,7 +2245,7 @@ def _compute_activity_stats_for_month(month_start, month_end): from sqlalchemy import func # Exercise minutes - exercise_ms = ( + exercise_sec = ( db_session.query(func.sum(UserExerciseSession.duration)) .filter(UserExerciseSession.start_time >= month_start) .filter(UserExerciseSession.start_time < month_end) @@ -2253,7 +2253,7 @@ def _compute_activity_stats_for_month(month_start, month_end): ) or 0 # Reading minutes - reading_ms = ( + reading_sec = ( db_session.query(func.sum(UserReadingSession.duration)) .filter(UserReadingSession.start_time >= month_start) .filter(UserReadingSession.start_time < month_end) @@ -2277,8 +2277,8 @@ def _compute_activity_stats_for_month(month_start, month_end): ) or 0 return { - "exercise_minutes": round(exercise_ms / 60000), - "reading_minutes": round(reading_ms / 60000), + "exercise_minutes": round(exercise_sec / 60), + "reading_minutes": round(reading_sec / 60), "browsing_minutes": round(browsing_ms / 60000), "audio_minutes": round(audio_sec / 60), } diff --git a/zeeguu/core/model/user_exercise_session.py b/zeeguu/core/model/user_exercise_session.py index 76cb733ce..d1d8cf261 100644 --- a/zeeguu/core/model/user_exercise_session.py +++ b/zeeguu/core/model/user_exercise_session.py @@ -26,7 +26,7 @@ class UserExerciseSession(db.Model): user = db.relationship(User) start_time = db.Column(db.DateTime) - duration = db.Column(db.Integer) # Duration time in miliseconds + duration = db.Column(db.Integer) # Duration time in seconds last_action_time = db.Column(db.DateTime) is_active = db.Column(db.Boolean) @@ -46,7 +46,7 @@ def __init__(self, user_id, start_time, current_time=None, platform=None): self.last_action_time = current_time duration = self.last_action_time - self.start_time - self.duration = duration.total_seconds() * 1000 + self.duration = duration.total_seconds() def exercises_in_session_string(self): from zeeguu.core.sql.learner.exercises_history import exercises_in_session diff --git a/zeeguu/core/model/user_reading_session.py b/zeeguu/core/model/user_reading_session.py index 60d924ab2..0954e4933 100644 --- a/zeeguu/core/model/user_reading_session.py +++ b/zeeguu/core/model/user_reading_session.py @@ -30,7 +30,7 @@ class UserReadingSession(db.Model): article = db.relationship(Article) start_time = db.Column(db.DateTime) - duration = db.Column(db.Integer) # Duration time in miliseconds + duration = db.Column(db.Integer) # Duration time in seconds last_action_time = db.Column(db.DateTime) is_active = db.Column(db.Boolean) @@ -54,7 +54,7 @@ def __init__(self, user_id, article_id, current_time=None, reading_source=None, self.platform = platform def human_readable_duration(self): - return human_readable_duration(self.duration) + return str(round(self.duration / 60, 1)) + "min" def human_readable_date(self): return human_readable_date(self.start_time) diff --git a/zeeguu/core/reading_analysis/macro_reading_session.py b/zeeguu/core/reading_analysis/macro_reading_session.py index 61b594585..f8ce1c05d 100644 --- a/zeeguu/core/reading_analysis/macro_reading_session.py +++ b/zeeguu/core/reading_analysis/macro_reading_session.py @@ -26,7 +26,7 @@ def __init__(self, user_article): def append(self, session): self.sessions.append(session) - self.total_time += session.duration / 1000 + self.total_time += session.duration self.reading_speed = int(self.article.word_count * 60 / self.total_time) def start_date(self): @@ -49,7 +49,7 @@ def print_details(self): print("\tSessions: ") for session in self.sessions: - print(f"\t{session.start_time}, {session.duration / 1000}s") + print(f"\t{session.start_time}, {session.duration}s") print(" ") def print_summary(self): @@ -76,7 +76,7 @@ def macro_sessions_for_user(user, language_id): if not user_article: continue - if session.duration < 1000: + if session.duration < 1: # less than 1s is not a session continue diff --git a/zeeguu/core/user_statistics/activity.py b/zeeguu/core/user_statistics/activity.py index 657246751..89443a233 100644 --- a/zeeguu/core/user_statistics/activity.py +++ b/zeeguu/core/user_statistics/activity.py @@ -56,7 +56,7 @@ def _time_by_day(user, table_name, date_field, duration_field): # Safe to use string formatting here since values are validated against whitelist query = ( f" SELECT date({date_field}) as date, " - + f" SUM({duration_field}) / 1000 as duration " + + f" SUM({duration_field}) as duration " + f" FROM {table_name}" + " WHERE user_id = :uid GROUP BY date;" )