From e1b29b8c28aeb232859edd876b80cb435f45db16 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 14 Apr 2026 15:36:03 +0200 Subject: [PATCH] Dashboard: Improve handling of read news (saving each id individually) Fixes #1338 --- CHANGELOG.md | 3 ++ src/serena/config/serena_config.py | 6 ++- src/serena/dashboard.py | 68 ++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ce06696..ee9434b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: When scanning for `.gitignore` files, the presence of files that could not be made relative to the project root would cause the scan to fail. #1317 +Dashboard: + - Fix handling of read news, saving each read news entry separately #1338 + JetBrains: - Improve handling of `relative_path` parameter - Improve its documentation to avoid usage errors diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 987d8cf6..a9cefd3b 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -83,7 +83,11 @@ class SerenaPaths: If a name of a mode matches a name of a mode in SERENAS_OWN_MODES_YAML_DIR, the user mode will override the default mode definition. """ - self.news_snippet_id_file: str = os.path.join(self.serena_user_home_dir, "last_read_news_snippet_id.txt") + self.news_legacy_last_read_id_file: str = os.path.join(self.serena_user_home_dir, "last_read_news_snippet_id.txt") + """ + file containing the ID of the last read news snippet + """ + self.news_read_items_file: str = os.path.join(self.serena_user_home_dir, "news_read.pkl") """ file containing the ID of the last read news snippet """ diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 3c33f1a2..8ed27a5f 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -15,6 +15,7 @@ from flask import Flask, Response, redirect, request, send_from_directory from PIL import Image from pydantic import BaseModel from sensai.util import logging +from sensai.util.pickle import dump_pickle, load_pickle from serena.analytics import ToolUsageStats from serena.config.serena_config import SerenaConfig, SerenaPaths @@ -132,6 +133,54 @@ class QueuedExecution(BaseModel): ) +class ReadNews: + def __init__(self, read_ids: list[str], legacy_last_read_id: str | None = None): + self._read_ids = set(read_ids) + self._legacy_last_read_id = legacy_last_read_id + + @staticmethod + def load() -> "ReadNews": + read_news_path = SerenaPaths().news_read_items_file + legacy_last_read_id_path = SerenaPaths().news_legacy_last_read_id_file + + def load_legacy_last_read_id() -> str | None: + if not os.path.exists(legacy_last_read_id_path): + return None + with open(legacy_last_read_id_path, encoding="utf-8") as f: + last_read_news_id = f.read().strip() + if last_read_news_id == "20262103": + last_read_news_id = "20260321" # fix originally misnamed news id + return last_read_news_id + + if os.path.exists(read_news_path): + return load_pickle(read_news_path) + else: + instance = ReadNews(read_ids=[], legacy_last_read_id=load_legacy_last_read_id()) + instance._save() + try: + os.unlink(legacy_last_read_id_path) + except: + pass + return instance + + def _save(self) -> None: + dump_pickle(self, SerenaPaths().news_read_items_file) + + def is_read(self, identifier: str) -> bool: + if identifier in self._read_ids: + return True + if self._legacy_last_read_id is not None and identifier <= self._legacy_last_read_id: + return True + return False + + def mark_read(self, identifier: str) -> None: + """ + Marks the given news snippet as read, saving the new state to disk + """ + self._read_ids.add(identifier) + self._save() + + class SerenaDashboardAPI: log = logging.getLogger(__qualname__) @@ -150,6 +199,7 @@ class SerenaDashboardAPI: self._loaded_news: dict[str, str] = {} self._news_ready = threading.Event() self._setup_routes() + self._read_news = ReadNews.load() # Fetch remote news in background on startup (non-blocking) threading.Thread(target=self._fetch_news, daemon=True).start() @@ -362,7 +412,6 @@ class SerenaDashboardAPI: self._news_ready.wait() all_news = self._loaded_news - # Filter news items by installation date serena_config_creation_date = SerenaConfig.get_config_file_creation_date() if serena_config_creation_date is None: # should not normally happen, since config file should exist when the dashboard is started @@ -370,17 +419,12 @@ class SerenaDashboardAPI: log.error("Serena config file not found when starting the dashboard") return {} serena_config_creation_date = serena_config_creation_date.strftime("%Y%m%d") - # Only include news items published on or after the installation date + + # filter for news after the installation date post_installation_news = {k: v for k, v in all_news.items() if k >= serena_config_creation_date} - news_snippet_id_file = SerenaPaths().news_snippet_id_file - if not os.path.exists(news_snippet_id_file): - return post_installation_news - with open(news_snippet_id_file, encoding="utf-8") as f: - last_read_news_id = f.read().strip() - if last_read_news_id == "20262103": - last_read_news_id = "20260321" # fix originally misnamed news id - return {k: v for k, v in post_installation_news.items() if k > last_read_news_id} + # read unread news + return {k: v for k, v in post_installation_news.items() if not self._read_news.is_read(k)} try: unread_news = _fetch_unread_news() @@ -393,9 +437,7 @@ class SerenaDashboardAPI: try: request_data = request.get_json() news_snippet_id = str(request_data.get("news_snippet_id")) - news_snippet_id_file = SerenaPaths().news_snippet_id_file - with open(news_snippet_id_file, "w", encoding="utf-8") as f: - f.write(news_snippet_id) + self._read_news.mark_read(news_snippet_id) return {"status": "success", "message": f"Marked news snippet {news_snippet_id} as read"} except Exception as e: return {"status": "error", "message": str(e)}