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.
This commit is contained in:
150
.cursor/rules/backend.mdc
Normal file
150
.cursor/rules/backend.mdc
Normal file
@@ -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.
|
||||
93
.cursor/rules/frontend.mdc
Normal file
93
.cursor/rules/frontend.mdc
Normal file
@@ -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 `<script>` in `index.html` from `https://telegram.org/js/telegram-web-app.js`.
|
||||
- `main.js` calls `Telegram.WebApp.ready()` and `expand()` on startup.
|
||||
- Auth: `initData` from SDK → hash param `tgWebAppData` → query string (fallback chain in `auth.js`).
|
||||
- The `X-Telegram-Init-Data` header carries initData to the backend API.
|
||||
- `requireTelegramOrLocalhost` gates access; localhost is allowed for dev.
|
||||
- Theme changes subscribed via `Telegram.WebApp.onEvent("theme_changed", applyTheme)`.
|
||||
|
||||
## Theme system
|
||||
|
||||
- CSS variables in `:root`: `--bg`, `--surface`, `--text`, `--muted`, `--accent`,
|
||||
`--duty`, `--today`, `--unavailable`, `--vacation`, `--error`, etc.
|
||||
- `[data-theme="light"]` and `[data-theme="dark"]` override base variables.
|
||||
- `theme.js` resolves scheme via:
|
||||
1. `Telegram.WebApp.colorScheme`
|
||||
2. CSS `--tg-color-scheme`
|
||||
3. `prefers-color-scheme` media query
|
||||
- `applyThemeParamsToCss()` maps Telegram `themeParams` to `--tg-theme-*` CSS variables.
|
||||
- Prefer `var(--token)` over hardcoded colors; use `color-mix()` for alpha variants.
|
||||
|
||||
## Testing (frontend)
|
||||
|
||||
- **Runner:** Vitest (`webapp/vitest.config.js`), environment: `happy-dom`.
|
||||
- **Test files:** `webapp/js/**/*.test.js`.
|
||||
- **DOM setup:** Element refs are resolved lazily (getters). Set `document.body.innerHTML`
|
||||
with required DOM structure in `beforeAll`/`beforeEach`; import order of modules using `dom.js` is flexible.
|
||||
- **Run:** `npm run test` (in `webapp/`).
|
||||
113
.cursor/rules/project.mdc
Normal file
113
.cursor/rules/project.mdc
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
description: Overall project architecture and context for duty-teller
|
||||
globs:
|
||||
- "**"
|
||||
---
|
||||
|
||||
# Project — duty-teller
|
||||
|
||||
A Telegram bot for team duty shift calendar management and group reminders,
|
||||
with a Telegram Mini App (webapp) for calendar visualization.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────┐ polling ┌──────────────────────┐
|
||||
│ Telegram │◄────────────►│ python-telegram-bot │
|
||||
│ Bot API │ │ (handlers/) │
|
||||
└──────────────┘ └──────────┬───────────┘
|
||||
│
|
||||
┌──────────────┐ HTTP ┌──────────▼───────────┐
|
||||
│ Telegram │◄────────────►│ FastAPI (api/) │
|
||||
│ Mini App │ initData │ + static webapp │
|
||||
│ (webapp/) │ auth └──────────┬───────────┘
|
||||
└──────────────┘ │
|
||||
┌──────────▼───────────┐
|
||||
│ SQLite + SQLAlchemy │
|
||||
│ (db/) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
- **Bot:** python-telegram-bot v22, async polling mode.
|
||||
- **API:** FastAPI served by uvicorn in a daemon thread alongside the bot.
|
||||
- **Database:** SQLite via SQLAlchemy 2.x ORM; Alembic for migrations.
|
||||
- **Frontend:** Vanilla JS Telegram Mini App at `/app`, no framework.
|
||||
|
||||
## Key packages
|
||||
|
||||
| Package | Role |
|
||||
|---------|------|
|
||||
| `duty_teller/handlers/` | Telegram bot command and message handlers |
|
||||
| `duty_teller/api/` | FastAPI app, REST endpoints, Telegram initData auth, ICS feeds |
|
||||
| `duty_teller/db/` | ORM models, repository (CRUD), session management, Pydantic schemas |
|
||||
| `duty_teller/services/` | Business logic (import, duty pin) — receives Session |
|
||||
| `duty_teller/importers/` | Duty schedule file parsers |
|
||||
| `duty_teller/i18n/` | Bilingual translations (ru/en), `t()` function |
|
||||
| `duty_teller/utils/` | Date helpers, user utilities, HTTP client |
|
||||
| `duty_teller/cache.py` | TTL caches with pattern-based invalidation |
|
||||
| `duty_teller/config.py` | Environment-based configuration |
|
||||
| `webapp/` | Telegram Mini App (HTML + JS + CSS) |
|
||||
|
||||
## API endpoints
|
||||
|
||||
| Method | Path | Auth | Purpose |
|
||||
|--------|------|------|---------|
|
||||
| GET | `/health` | None | Health check |
|
||||
| GET | `/api/duties` | initData | Duties for date range |
|
||||
| GET | `/api/calendar-events` | initData | External calendar events |
|
||||
| GET | `/api/calendar/ical/team/{token}.ics` | Token | Team ICS feed |
|
||||
| GET | `/api/calendar/ical/{token}.ics` | Token | Personal ICS feed |
|
||||
| GET | `/app` | None | Static Mini App (HTML/JS/CSS) |
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Docker:** Multi-stage build (`Dockerfile`), `docker-compose.prod.yml` for production.
|
||||
- **Entrypoint:** `entrypoint.sh` runs `alembic -c pyproject.toml upgrade head`, then
|
||||
starts `python main.py` as `botuser`.
|
||||
- **Data:** SQLite DB persisted via Docker volume at `/app/data`.
|
||||
- **Health:** `curl -f http://localhost:8080/health` every 30 s.
|
||||
|
||||
## CI/CD (Gitea Actions)
|
||||
|
||||
### Lint & test (`.gitea/workflows/ci.yml`)
|
||||
|
||||
Triggered on push/PR to `main` and `develop`:
|
||||
|
||||
1. **Ruff:** `ruff check duty_teller tests`
|
||||
2. **Pytest:** `pytest tests/ -v` (80% coverage gate)
|
||||
3. **Bandit:** `bandit -r duty_teller -ll` (security scan)
|
||||
|
||||
### Docker build & release (`.gitea/workflows/docker-build.yml`)
|
||||
|
||||
Triggered on `v*` tags:
|
||||
|
||||
1. Build and push image to Gitea registry.
|
||||
2. Create release with auto-generated notes.
|
||||
|
||||
## Key environment variables
|
||||
|
||||
| Variable | Default | Required | Purpose |
|
||||
|----------|---------|----------|---------|
|
||||
| `BOT_TOKEN` | — | Yes | Telegram bot API token |
|
||||
| `DATABASE_URL` | `sqlite:///data/duty_teller.db` | No | SQLAlchemy database URL |
|
||||
| `MINI_APP_BASE_URL` | — | Yes (prod) | Public URL for the Mini App |
|
||||
| `HTTP_HOST` | `127.0.0.1` | No | FastAPI bind host |
|
||||
| `HTTP_PORT` | `8080` | No | FastAPI bind port |
|
||||
| `ADMIN_USERNAMES` | — | No | Comma-separated admin Telegram usernames |
|
||||
| `ALLOWED_USERNAMES` | — | No | Comma-separated allowed usernames |
|
||||
| `DUTY_DISPLAY_TZ` | `Europe/Moscow` | No | Timezone for duty display |
|
||||
| `DEFAULT_LANGUAGE` | `en` | No | Default UI language (`en` or `ru`) |
|
||||
| `EXTERNAL_CALENDAR_ICS_URL` | — | No | External ICS URL for holidays |
|
||||
| `CORS_ORIGINS` | `*` | No | Comma-separated CORS origins |
|
||||
|
||||
## Languages
|
||||
|
||||
- **Backend:** Python 3.12+
|
||||
- **Frontend:** Vanilla JavaScript (ES modules, no bundler)
|
||||
- **i18n:** Russian (default) and English
|
||||
|
||||
## Version control
|
||||
|
||||
- Git with Gitea Flow branching strategy.
|
||||
- Conventional Commits for commit messages.
|
||||
- PRs reviewed via Gitea Pull Requests.
|
||||
103
.cursor/rules/testing.mdc
Normal file
103
.cursor/rules/testing.mdc
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
description: Rules for writing and running tests
|
||||
globs:
|
||||
- tests/**
|
||||
- webapp/js/**/*.test.js
|
||||
---
|
||||
|
||||
# Testing
|
||||
|
||||
## Python tests (pytest)
|
||||
|
||||
### Configuration
|
||||
|
||||
- Config in `pyproject.toml` under `[tool.pytest.ini_options]`.
|
||||
- `asyncio_mode = "auto"` — async test functions are detected automatically, no decorator needed.
|
||||
- Coverage: `--cov=duty_teller --cov-fail-under=80`.
|
||||
- Run: `pytest tests/ -v` from project root.
|
||||
|
||||
### File layout
|
||||
|
||||
```
|
||||
tests/
|
||||
├── conftest.py # Shared fixtures (in-memory DB, sessions, bot app)
|
||||
├── helpers.py # Test helper functions
|
||||
└── test_*.py # Test modules
|
||||
```
|
||||
|
||||
### Key fixtures (conftest.py)
|
||||
|
||||
- `conftest.py` sets `BOT_TOKEN=test-token-for-pytest` if the env var is missing,
|
||||
so tests run without a real token.
|
||||
- Database fixtures use in-memory SQLite for isolation.
|
||||
|
||||
### Writing Python tests
|
||||
|
||||
- File naming: `tests/test_<module>.py` (e.g. `tests/test_import_service.py`).
|
||||
- Function naming: `test_<what>_<scenario>` or `test_<what>_<expected_result>`.
|
||||
- Use `session_scope` with the test database URL for DB tests.
|
||||
- Async tests: just use `async def test_...` — `asyncio_mode = "auto"` handles it.
|
||||
- Mock external dependencies (Telegram API, HTTP calls) with `unittest.mock` or `pytest-mock`.
|
||||
|
||||
### Example structure
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from duty_teller.db.session import session_scope
|
||||
|
||||
def test_get_or_create_user_creates_new(test_db_url):
|
||||
with session_scope(test_db_url) as session:
|
||||
user = get_or_create_user(session, telegram_user_id=123, full_name="Test")
|
||||
assert user.telegram_user_id == 123
|
||||
```
|
||||
|
||||
## JavaScript tests (Vitest)
|
||||
|
||||
### Configuration
|
||||
|
||||
- Config: `webapp/vitest.config.js`.
|
||||
- Environment: `happy-dom` (lightweight DOM implementation).
|
||||
- Test files: `webapp/js/**/*.test.js`.
|
||||
- Run: `npm run test` (from `webapp/`).
|
||||
|
||||
### DOM setup
|
||||
|
||||
Modules that import from `dom.js` expect DOM elements to exist at import time.
|
||||
Use `beforeAll` to set up the required HTML structure before the first import:
|
||||
|
||||
```js
|
||||
import { beforeAll, describe, it, expect } from "vitest";
|
||||
|
||||
beforeAll(() => {
|
||||
document.body.innerHTML = `
|
||||
<div id="calendar"></div>
|
||||
<h2 id="monthTitle"></h2>
|
||||
<button id="prevBtn"></button>
|
||||
<button id="nextBtn"></button>
|
||||
<div id="dutyList"></div>
|
||||
<div id="dayDetail"></div>
|
||||
<div id="accessDenied" class="hidden"></div>
|
||||
<div id="errorBanner" class="hidden"></div>
|
||||
`;
|
||||
});
|
||||
```
|
||||
|
||||
### Writing JS tests
|
||||
|
||||
- File naming: `webapp/js/<module>.test.js` (co-located with the source module).
|
||||
- Test pure functions first (`dateUtils`, `i18n`, `utils`); mock DOM for render tests.
|
||||
- Use `describe` blocks to group by function, `it` blocks for individual cases.
|
||||
|
||||
### Example structure
|
||||
|
||||
```js
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { localDateString } from "./dateUtils.js";
|
||||
|
||||
describe("localDateString", () => {
|
||||
it("formats date as YYYY-MM-DD", () => {
|
||||
const d = new Date(2025, 0, 15);
|
||||
expect(localDateString(d)).toBe("2025-01-15");
|
||||
});
|
||||
});
|
||||
```
|
||||
5
.cursor/worktrees.json
Normal file
5
.cursor/worktrees.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"setup-worktree": [
|
||||
"npm install"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user