diff --git a/VERSION b/VERSION index 25fea79..3b2dba6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0+74 +0.1.0+75 diff --git a/bot.py b/bot.py index 5fa8d32..e19ede2 100644 --- a/bot.py +++ b/bot.py @@ -56,6 +56,7 @@ from user_store import ( get_recent_post_history_text, get_tone_of_voice, get_training_summary, + get_training_updated_at, save_training_summary, init_db, is_subscribed, @@ -597,6 +598,7 @@ async def _refresh_training_summary(user_id: int, profile: str, notes: list[str] if not summary: return await asyncio.to_thread(save_training_summary, user_id, summary) + logger.info("Training summary updated for user %s", user_id) TRAINING_INFO_TITLES = { @@ -607,7 +609,10 @@ TRAINING_INFO_TITLES = { } -def training_info_rich_html(summary: dict[str, str]) -> str: +TRAINING_INFO_TIMEZONE = datetime.timezone(datetime.timedelta(hours=2)) + + +def training_info_rich_html(summary: dict[str, str], updated_at: str = "") -> str: blocks: list[str] = [] for field, title in TRAINING_INFO_TITLES.items(): value = (summary.get(field) or "").strip() @@ -616,12 +621,26 @@ def training_info_rich_html(summary: dict[str, str]) -> str: blocks.append(f"

{html.escape(title)}

{html.escape(value)}

") if not blocks: return "" - return ( - "

🎓 Что я о тебе поняла

" - + "".join(blocks) - + "

Это краткая выжимка: для генерации я использую полный профиль и все примеры постов.

" - "

Дополнить знания можно в любой момент — просто пришли новые посты или сведения.

" - ) + footer = "

Это краткая выжимка: для генерации я использую полный профиль и все примеры постов.

" + stamp = training_time_label(updated_at) + if stamp: + footer += f"

Последнее обучение: {html.escape(stamp)}.

" + footer += "

Дополнить знания можно в любой момент — просто пришли новые посты или сведения.

" + return "".join(blocks) + footer + + +def training_time_label(updated_at: str) -> str: + """Локальная дата и время последнего обучения; '' если время неизвестно.""" + raw = (updated_at or "").strip() + if not raw: + return "" + try: + moment = datetime.datetime.fromisoformat(raw.replace("Z", "+00:00")) + except ValueError: + return "" + if moment.tzinfo is None: + moment = moment.replace(tzinfo=datetime.timezone.utc) + return moment.astimezone(TRAINING_INFO_TIMEZONE).strftime("%d.%m.%Y, %H:%M") async def training_info_command(message: Message, state: FSMContext) -> None: @@ -644,7 +663,10 @@ async def training_info_command(message: Message, state: FSMContext) -> None: summary = await asyncio.to_thread(get_training_summary, message.from_user.id) finally: await status.finish(None) - rich_html = training_info_rich_html(summary) + rich_html = training_info_rich_html( + summary, + await asyncio.to_thread(get_training_updated_at, message.from_user.id), + ) if not rich_html: await message.answer( "🎓 Пока не знаю о тебе почти ничего\n\n" diff --git a/tests/test_writer_features.py b/tests/test_writer_features.py index c87a8d2..d67f1a1 100644 --- a/tests/test_writer_features.py +++ b/tests/test_writer_features.py @@ -1184,12 +1184,15 @@ class TrainingSummaryTests(unittest.IsolatedAsyncioTestCase): def test_rich_html_contains_only_filled_blocks(self) -> None: rendered = bot.training_info_rich_html( - {"about": "Канал про ИИ", "topics": "", "style": "сухо", "audience": ""} + {"about": "Канал про ИИ", "topics": "", "style": "сухо", "audience": ""}, + "2026-10-29 18:11:35", ) - self.assertIn("

", rendered) + self.assertNotIn("

", rendered) + self.assertTrue(rendered.startswith("

")) self.assertIn("Канал про ИИ", rendered) self.assertNotIn("Основные темы", rendered) - self.assertEqual(bot.training_info_rich_html({}), "") + self.assertIn("Последнее обучение: 29.10.2026, 20:11", rendered) + self.assertEqual(bot.training_info_rich_html({}, ""), "") async def test_summary_is_saved_after_profile_rebuild(self) -> None: with patch.object(bot, "create_tone_profile", new=AsyncMock(return_value="профиль")), \ diff --git a/user_store.py b/user_store.py index d88077c..02a4a27 100644 --- a/user_store.py +++ b/user_store.py @@ -590,6 +590,15 @@ def get_training_summary(user_id: int, db_path: str | None = None) -> dict[str, } +def get_training_updated_at(user_id: int, db_path: str | None = None) -> str: + """Время последнего обучения (UTC, naive) — updated_at строки профиля.""" + with closing(connect(db_path)) as connection: + row = connection.execute( + "SELECT updated_at FROM users WHERE telegram_user_id = ?", (user_id,) + ).fetchone() + return str(row["updated_at"] or "") if row else "" + + def save_training_summary( user_id: int, summary: dict[str, str], db_path: str | None = None ) -> None: