From 66e1b0e29e59f14854363ca6215b365e843ad7c4 Mon Sep 17 00:00:00 2001 From: grigo Date: Thu, 11 Jun 2026 12:34:35 +0300 Subject: [PATCH] fixed reminder --- backend/app/character/card.py | 3 +- backend/app/reminders/service.py | 13 +++++- backend/app/tools/registry.py | 9 ++-- backend/scripts/seed_holiday_reminders.py | 52 +++++++++++++++++++++++ frontend/nginx.conf | 4 ++ frontend/src/api/client.ts | 41 ++++++++++++------ frontend/src/pages/Reminders.tsx | 1 + 7 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 backend/scripts/seed_holiday_reminders.py diff --git a/backend/app/character/card.py b/backend/app/character/card.py index 4254e44..6b62345 100644 --- a/backend/app/character/card.py +++ b/backend/app/character/card.py @@ -27,7 +27,8 @@ TOOLS_INSTRUCTIONS = """ - Покупки: list_shopping_lists, create_shopping_list, add_shopping_items, check_shopping_item, remove_shopping_item, delete_shopping_list. - «Добавь в список покупок» → add_shopping_items (list_name + товары). «Что купить» → list_shopping_lists. Не выдумывай списки. - Напоминания: list_reminders, create_reminder, update_reminder, delete_reminder, complete_reminder. -- «Напомни через 15 минут», «завтра утром», «12 мая 2027 в 12:16» → create_reminder с due_at в ISO (часовой пояс из [Текущее время]). +- «Напомни через 15 минут», «завтра утром», «12 мая в 9:00» → create_reminder с due_at в ISO (часовой пояс из [Текущее время]). +- День рождения, Новый год и другие праздники → recurrence yearly. - Относительное время считай от «Сейчас» в контексте. «Утром» ≈ 09:00, «вечером» ≈ 19:00, если не уточнено иначе. """.strip() diff --git a/backend/app/reminders/service.py b/backend/app/reminders/service.py index aad231a..1fccd9c 100644 --- a/backend/app/reminders/service.py +++ b/backend/app/reminders/service.py @@ -14,7 +14,14 @@ RECURRENCE_NONE = "none" RECURRENCE_DAILY = "daily" RECURRENCE_WEEKLY = "weekly" RECURRENCE_MONTHLY = "monthly" -VALID_RECURRENCE = frozenset({RECURRENCE_NONE, RECURRENCE_DAILY, RECURRENCE_WEEKLY, RECURRENCE_MONTHLY}) +RECURRENCE_YEARLY = "yearly" +VALID_RECURRENCE = frozenset({ + RECURRENCE_NONE, + RECURRENCE_DAILY, + RECURRENCE_WEEKLY, + RECURRENCE_MONTHLY, + RECURRENCE_YEARLY, +}) def _utcnow() -> datetime: @@ -50,6 +57,10 @@ def _advance_due(due_at: datetime, recurrence: str) -> datetime: year += 1 day = min(due_at.day, calendar.monthrange(year, month)[1]) return due_at.replace(year=year, month=month, day=day) + if recurrence == RECURRENCE_YEARLY: + year = due_at.year + 1 + day = min(due_at.day, calendar.monthrange(year, due_at.month)[1]) + return due_at.replace(year=year, day=day) return due_at diff --git a/backend/app/tools/registry.py b/backend/app/tools/registry.py index 855df7e..9b83953 100644 --- a/backend/app/tools/registry.py +++ b/backend/app/tools/registry.py @@ -649,8 +649,8 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [ "all_day": {"type": "boolean"}, "recurrence": { "type": "string", - "enum": ["none", "daily", "weekly", "monthly"], - "description": "Повтор", + "enum": ["none", "daily", "weekly", "monthly", "yearly"], + "description": "Повтор (yearly — день рождения, Новый год)", }, }, "required": ["title", "due_at"], @@ -670,7 +670,10 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [ "due_at": {"type": "string"}, "notes": {"type": "string"}, "all_day": {"type": "boolean"}, - "recurrence": {"type": "string", "enum": ["none", "daily", "weekly", "monthly"]}, + "recurrence": { + "type": "string", + "enum": ["none", "daily", "weekly", "monthly", "yearly"], + }, "enabled": {"type": "boolean"}, }, "required": ["reminder_id"], diff --git a/backend/scripts/seed_holiday_reminders.py b/backend/scripts/seed_holiday_reminders.py new file mode 100644 index 0000000..05ab0a5 --- /dev/null +++ b/backend/scripts/seed_holiday_reminders.py @@ -0,0 +1,52 @@ +"""Идемпотентно добавляет ежегодные напоминания: ДР 12 мая и Новый год.""" + +from sqlalchemy import select + +from app.db.base import SessionLocal +from app.db.models import Reminder +from app.reminders.service import RECURRENCE_YEARLY, RemindersService + +DEFAULTS = ( + { + "title": "День рождения", + "due_at": "2027-05-12T09:00:00+03:00", + "notes": "С днём рождения!", + "all_day": True, + "recurrence": RECURRENCE_YEARLY, + }, + { + "title": "Новый год", + "due_at": "2026-12-31T23:55:00+03:00", + "notes": "С наступающим Новым годом!", + "all_day": False, + "recurrence": RECURRENCE_YEARLY, + }, +) + + +def main() -> None: + db = SessionLocal() + try: + service = RemindersService(db) + for item in DEFAULTS: + exists = db.scalar( + select(Reminder.id) + .where( + Reminder.title == item["title"], + Reminder.recurrence == RECURRENCE_YEARLY, + Reminder.enabled.is_(True), + ) + .limit(1) + ) + if exists: + print(f"skip: {item['title']} (id={exists})") + continue + result = service.create(**item) + reminder = result["reminder"] + print(f"created: {reminder['title']} · {reminder['due_at_local']} · yearly") + finally: + db.close() + + +if __name__ == "__main__": + main() diff --git a/frontend/nginx.conf b/frontend/nginx.conf index f647f0f..10b23be 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -9,9 +9,13 @@ server { proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; chunked_transfer_encoding off; + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; } location / { diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index db1255f..c9ce3f8 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -271,22 +271,35 @@ export const api = { } }; - while (true) { - const { done, value } = await reader.read(); - if (value) { - buffer += decoder.decode(value, { stream: !done }); - } - - const parts = buffer.split("\n\n"); - buffer = parts.pop() ?? ""; - yield* flushParts(parts); - - if (done) { - if (buffer.trim()) { - yield* flushParts([buffer]); + try { + while (true) { + let done = false; + let value: Uint8Array | undefined; + try { + ({ done, value } = await reader.read()); + } catch { + throw new Error( + "Соединение прервалось (таймаут прокси). Обновите чат — ответ мог уже сохраниться.", + ); + } + + if (value) { + buffer += decoder.decode(value, { stream: !done }); + } + + const parts = buffer.split("\n\n"); + buffer = parts.pop() ?? ""; + yield* flushParts(parts); + + if (done) { + if (buffer.trim()) { + yield* flushParts([buffer]); + } + break; } - break; } + } finally { + reader.releaseLock(); } }, diff --git a/frontend/src/pages/Reminders.tsx b/frontend/src/pages/Reminders.tsx index 52019e2..56963a6 100644 --- a/frontend/src/pages/Reminders.tsx +++ b/frontend/src/pages/Reminders.tsx @@ -252,6 +252,7 @@ export default function Reminders() { +