From 37d4226beb97a55a7adad68924b790b56cf4ac2f Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 2 Mar 2026 20:02:20 +0300 Subject: [PATCH] chore: update project documentation and configuration files - Added AGENTS.md for AI agent documentation and maintainers, outlining project structure and conventions. - Updated CONTRIBUTING.md to specify that all project documentation must be in English, including README and docstrings. - Enhanced README.md to reference documentation guidelines and the new AGENTS.md file. - Cleaned up .gitignore by removing unnecessary entries for cursor-related files. - Introduced new .cursor rules for backend, frontend, project architecture, and testing to standardize development practices. --- .cursor/rules/backend.mdc | 150 +++++++++++++++++++++++++++++++++++++ .cursor/rules/frontend.mdc | 93 +++++++++++++++++++++++ .cursor/rules/project.mdc | 113 ++++++++++++++++++++++++++++ .cursor/rules/testing.mdc | 103 +++++++++++++++++++++++++ .cursor/worktrees.json | 5 ++ .gitignore | 3 +- AGENTS.md | 49 ++++++++++++ CONTRIBUTING.md | 9 +++ README.md | 1 + docs/index.md | 2 + 10 files changed, 526 insertions(+), 2 deletions(-) create mode 100644 .cursor/rules/backend.mdc create mode 100644 .cursor/rules/frontend.mdc create mode 100644 .cursor/rules/project.mdc create mode 100644 .cursor/rules/testing.mdc create mode 100644 .cursor/worktrees.json create mode 100644 AGENTS.md diff --git a/.cursor/rules/backend.mdc b/.cursor/rules/backend.mdc new file mode 100644 index 0000000..b203050 --- /dev/null +++ b/.cursor/rules/backend.mdc @@ -0,0 +1,150 @@ +--- +description: Rules for working with the Python backend (duty_teller/) +globs: + - duty_teller/** + - alembic/** + - tests/** +--- + +# Backend — Python + +## Package layout + +``` +duty_teller/ +├── main.py / run.py # Entry point: bot + uvicorn +├── config.py # Settings from env vars (python-dotenv) +├── cache.py # TTL caches with pattern invalidation +├── api/ # FastAPI app, routes, auth, ICS endpoints +│ ├── app.py # FastAPI app creation, route registration, static mount +│ ├── dependencies.py # get_db_session, require_miniapp_username, get_validated_dates +│ ├── telegram_auth.py # initData HMAC validation +│ ├── calendar_ics.py # External ICS fetch & parse +│ └── personal_calendar_ics.py +├── db/ # Database layer +│ ├── models.py # SQLAlchemy ORM models (Base) +│ ├── repository.py # CRUD functions (receive Session) +│ ├── schemas.py # Pydantic response schemas +│ └── session.py # session_scope, get_engine, get_session +├── handlers/ # Telegram bot command/message handlers +│ ├── commands.py # /start, /help, /set_phone, /calendar_link, /set_role +│ ├── common.py # is_admin_async, invalidate_is_admin_cache +│ ├── errors.py # Global error handler +│ ├── group_duty_pin.py # Pinned duty message in group chats +│ └── import_duty_schedule.py +├── i18n/ # Translations +│ ├── core.py # get_lang, t() +│ ├── lang.py # normalize_lang +│ └── messages.py # MESSAGES dict (ru/en) +├── importers/ # File parsers +│ └── duty_schedule.py # parse_duty_schedule +├── services/ # Business logic (receives Session) +│ ├── import_service.py # run_import +│ └── group_duty_pin_service.py +└── utils/ # Shared helpers + ├── dates.py + ├── handover.py + ├── http_client.py + └── user.py +``` + +## Imports + +- Use absolute imports from the `duty_teller` package: `from duty_teller.db.repository import get_or_create_user`. +- Never import handler modules from services or repository — dependency flows + downward: handlers → services → repository → models. + +## DB access pattern + +Handlers are async; SQLAlchemy sessions are synchronous. Two patterns: + +### In bot handlers — `session_scope` + `run_in_executor` + +```python +def do_work(): + with session_scope(config.DATABASE_URL) as session: + # synchronous DB code here + ... + +await asyncio.get_running_loop().run_in_executor(None, do_work) +``` + +### In FastAPI endpoints — dependency injection + +```python +@router.get("/api/duties") +def get_duties(session: Session = Depends(get_db_session)): + ... +``` + +`get_db_session` yields a `session_scope` context — FastAPI closes it after the request. + +## Handler patterns + +### Admin-only commands + +```python +if not await is_admin_async(update.effective_user.id): + await update.message.reply_text(t(lang, "import.admin_only")) + return +``` + +`is_admin_async` is cached for 60 s and invalidated on role changes. + +### i18n in handlers + +```python +lang = get_lang(update.effective_user) +text = t(lang, "start.greeting") +``` + +`t(lang, key, **kwargs)` substitutes `{placeholders}` and falls back to English → raw key. + +### Error handling + +The global `error_handler` (registered via `app.add_error_handler`) logs the +exception and sends a generic localized error reply. + +## Service layer + +- Services receive a `Session` — they never open their own. +- Services call repository functions, never handler code. +- After mutations that affect cached data, call `invalidate_duty_related_caches()`. + +## Cache invalidation + +Three TTL caches in `cache.py`: + +| Cache | TTL | Invalidation trigger | +|-------|-----|---------------------| +| `ics_calendar_cache` | 600 s | After duty import | +| `duty_pin_cache` | 90 s | After duty import | +| `is_admin_cache` | 60 s | After `set_user_role` | + +- `invalidate_duty_related_caches()` clears ICS and pin caches (call after any duty mutation). +- `invalidate_is_admin_cache(telegram_user_id)` clears a single admin entry. +- Pattern invalidation: `cache.invalidate_pattern(key_prefix_tuple)`. + +## Alembic migrations + +- Config in `pyproject.toml` under `[tool.alembic]`, script location: `alembic/`. +- **Naming convention:** `NNN_description.py` — sequential zero-padded number (`001`, `002`, …). +- Revision IDs are the string number: `revision = "008"`, `down_revision = "007"`. +- Run: `alembic -c pyproject.toml upgrade head` / `downgrade -1`. +- `entrypoint.sh` runs `alembic upgrade head` before starting the app in Docker. + +## Configuration + +- All config comes from environment variables, loaded with `python-dotenv`. +- `duty_teller/config.py` exposes module-level constants (`BOT_TOKEN`, `DATABASE_URL`, etc.) + built from `Settings.from_env()`. +- Never hardcode secrets or URLs — always use `config.*` constants. +- Key variables: `BOT_TOKEN`, `DATABASE_URL`, `MINI_APP_BASE_URL`, `HTTP_HOST`, `HTTP_PORT`, + `ADMIN_USERNAMES`, `ALLOWED_USERNAMES`, `DUTY_DISPLAY_TZ`, `DEFAULT_LANGUAGE`. + +## Code style + +- Formatter: Black (line-length 120). +- Linter: Ruff (`ruff check duty_teller tests`). +- Follow PEP 8 and Google Python Style Guide for docstrings. +- Max line length: 120 characters. diff --git a/.cursor/rules/frontend.mdc b/.cursor/rules/frontend.mdc new file mode 100644 index 0000000..fe4a2b1 --- /dev/null +++ b/.cursor/rules/frontend.mdc @@ -0,0 +1,93 @@ +--- +description: Rules for working with the Telegram Mini App frontend (webapp/) +globs: + - webapp/** +--- + +# Frontend — Telegram Mini App + +## Module structure + +All source lives in `webapp/js/`. Each module has a single responsibility: + +| Module | Responsibility | +|--------|---------------| +| `main.js` | Entry point: theme init, auth gate, `loadMonth`, navigation, swipe gestures, sticky scroll | +| `dom.js` | Lazy DOM getters (`getCalendarEl()`, `getDutyListEl()`, etc.) and shared mutable `state` | +| `i18n.js` | `MESSAGES` dictionary (en/ru), `getLang()`, `t()`, `monthName()`, `weekdayLabels()` | +| `auth.js` | Telegram `initData` extraction (`getInitData`), `isLocalhost`, hash/query fallback | +| `theme.js` | Theme detection (`getTheme`), CSS variable injection (`applyThemeParamsToCss`), `initTheme` | +| `api.js` | `apiGet`, `fetchDuties`, `fetchCalendarEvents`; timeout, auth header, `Accept-Language` | +| `calendar.js` | `dutiesByDate`, `calendarEventsByDate`, `renderCalendar` (6-week grid with indicators) | +| `dutyList.js` | `dutyTimelineCardHtml`, `dutyItemHtml`, `renderDutyList` (monthly duty timeline cards) | +| `dayDetail.js` | Day detail panel — popover (desktop) or bottom sheet (mobile), `buildDayDetailContent` | +| `hints.js` | Tooltip positioning, duty marker hint content, info-button tooltips | +| `dateUtils.js` | Date helpers: `localDateString`, `dutyOverlapsLocalDay/Range`, `getMonday`, `formatHHMM`, etc. | +| `utils.js` | `escapeHtml` utility | +| `constants.js` | `FETCH_TIMEOUT_MS`, `RETRY_DELAY_MS`, `RETRY_AFTER_ACCESS_DENIED_MS` | + +## State management + +A single mutable `state` object is exported from `dom.js`: + +```js +export const state = { + current: new Date(), // currently displayed month + lastDutiesForList: [], // duties array for the duty list + todayRefreshInterval: null, // interval handle + lang: "ru" // 'ru' | 'en' +}; +``` + +- **No store / pub-sub / reactivity.** `main.js` mutates `state`, then calls + render functions (`renderCalendar`, `renderDutyList`) imperatively. +- Other modules read `state` but should not mutate it directly. + +## HTML rendering + +- Rendering functions build HTML strings (concatenation + `escapeHtml`) or use + `createElement` + `setAttribute`. +- Always escape user-controlled text with `escapeHtml()` before inserting via `innerHTML`. +- `setAttribute()` handles attribute quoting automatically — do not manually escape + quotes for data attributes. + +## i18n + +```js +t(lang, key, params?) // → translated string +``` + +- `lang` is `'ru'` or `'en'`, stored in `state.lang`. +- `getLang()` resolves language from: Telegram `initData` user → `navigator.language` → `"ru"`. +- Params use named placeholders: `t(lang, "duty.until", { time: "14:00" })`. +- Fallback chain: `MESSAGES[lang][key]` → `MESSAGES.en[key]` → raw key string. +- All user-visible text must go through `t()` — never hardcode Russian strings in JS. + +## Telegram WebApp SDK + +- Loaded via `