feat: /help command, atm.bat launcher, tzdata fix pentru Windows

- Telegram /h /help — listă comenzi în română
- atm.bat — pornire cu venv local automat, pip install la primul run
- tzdata adăugat în deps principale cu marker sys_platform==win32
- README: secțiuni dev, instalare Windows, flow-uri calibrare

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 13:11:44 +03:00
parent 7b91cb0cd0
commit 42a1a0e7fd
6 changed files with 85 additions and 6 deletions

View File

@@ -43,20 +43,60 @@ atm/
Python 3.11+. Python 3.11+.
```bash ### Windows (producție)
pip install -e ".[windows]" # Windows: capture live + focus fereastră
pip install -e ".[dev]" # Linux/macOS/WSL: doar dev + teste (fără capture) ```powershell
python -m venv .venv
.venv\Scripts\activate
pip install -e ".[windows]"
# → creează .venv\Scripts\atm.exe
atm --help atm --help
``` ```
**WSL/Linux:** recomandat să folosești un virtualenv local: `[windows]` aduce `mss`, `pygetwindow`, `pywin32`. Fără venv, `pip install -e ".[windows]"` direct în Python-ul global funcționează la fel.
Pornire rapidă cu scriptul inclus — instalează automat la primul run:
```powershell
atm.bat # prima rulare: pip install + atm run
atm.bat run --stop-at 23:00
atm.bat debug
```
### WSL / Linux (dev + teste)
```bash ```bash
python3 -m venv .venv python3 -m venv .venv
source .venv/bin/activate source .venv/bin/activate
pip install -e ".[dev]" pip install -e ".[dev]"
``` ```
`[windows]` aduce `mss`, `pygetwindow`, `pywin32` (nu le pune pe WSL). `[dev]` aduce `pytest`, `pytest-cov`, `pytest-asyncio`. Nu include dependențele Windows (`mss`, `pygetwindow`, `pywin32`) — nu rulează capture live.
---
## Dev
```bash
pytest -q # toate testele (184+)
pytest tests/test_commands.py # un modul specific
pytest -q --cov=atm --cov-report=term-missing # cu coverage
```
Smoke-test fără Windows (stub de captură din `samples/`):
```bash
atm run --capture-stub --duration 0.05
```
Structura testelor:
| Fișier | Ce acoperă |
|---|---|
| `test_commands.py` | parsing comenzi Telegram |
| `test_config.py` | loader TOML, attach_screenshots |
| `test_handle_tick.py` | loop principal, snapshot, FSM |
| `test_main.py` | lifecycle, operating hours, canary, dispatcher |
| `test_validate.py` | gate offline clasificare culori |
| `test_canary.py` | drift + callback pauză |
--- ---
@@ -299,6 +339,7 @@ Trimiți în chat-ul bot-ului:
| `/resume force` | Elimină și drift-pause-ul canary (după recalibrare) | | `/resume force` | Elimină și drift-pause-ul canary (după recalibrare) |
| `/3` sau `/interval 3` | Interval auto-screenshot = 3 min | | `/3` sau `/interval 3` | Interval auto-screenshot = 3 min |
| `/stop` | Oprește scheduler-ul de screenshot | | `/stop` | Oprește scheduler-ul de screenshot |
| `/h` sau `/help` | Listă scurtă a tuturor comenzilor disponibile |
Doar `allowed_chat_ids` sunt acceptate. După 3 `401` consecutive, poller-ul intră în mod degradat. Doar `allowed_chat_ids` sunt acceptate. După 3 `401` consecutive, poller-ul intră în mod degradat.

15
atm.bat Normal file
View File

@@ -0,0 +1,15 @@
@echo off
cd /d "%~dp0"
if not exist ".venv\Scripts\atm.exe" (
echo Instalez atm in venv local...
python -m venv .venv
call .venv\Scripts\activate.bat
pip install -e ".[windows]"
)
if "%~1"=="" (
.venv\Scripts\atm.exe run
) else (
.venv\Scripts\atm.exe %*
)

View File

@@ -14,6 +14,7 @@ dependencies = [
"requests>=2.31", "requests>=2.31",
"rich>=13.0", "rich>=13.0",
"httpx>=0.27", "httpx>=0.27",
"tzdata>=2024.1; sys_platform == 'win32'",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@@ -17,7 +17,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CommandAction = Literal["set_interval", "stop", "status", "ss", "pause", "resume"] CommandAction = Literal["set_interval", "stop", "status", "ss", "pause", "resume", "help"]
_BASE = "https://api.telegram.org/bot{token}/{method}" _BASE = "https://api.telegram.org/bot{token}/{method}"
@@ -148,6 +148,8 @@ class TelegramPoller:
t = text.lstrip("/").strip() t = text.lstrip("/").strip()
if not t: if not t:
return None return None
if t in ("h", "help"):
return Command(action="help")
if t == "stop": if t == "stop":
return Command(action="stop") return Command(action="stop")
if t == "status": if t == "status":

View File

@@ -1065,6 +1065,18 @@ async def _dispatch_command(ctx: RunContext, cmd) -> None:
title = "Monitorizare reluată" title = "Monitorizare reluată"
body = "" body = ""
ctx.notifier.send(Alert(kind="status", title=title, body=body)) ctx.notifier.send(Alert(kind="status", title=title, body=body))
elif cmd.action == "help":
body = (
"/status — stare FSM, uptime, ultima detecție\n"
"/ss — screenshot acum\n"
"/pause — oprește detecția (heartbeat continuă)\n"
"/resume — reia detecția (doar pauza user)\n"
"/resume force — reia + anulează drift-pause canary\n"
"/3 — screenshot automat la fiecare 3 min (sau orice număr)\n"
"/stop — oprește screenshot-urile automate\n"
"/h — acest mesaj"
)
ctx.notifier.send(Alert(kind="status", title="Comenzi ATM", body=body))
async def _drain_cmd_queue(ctx: RunContext) -> None: async def _drain_cmd_queue(ctx: RunContext) -> None:

View File

@@ -36,6 +36,14 @@ def test_parse_resume_force():
assert cmd.value == 1 assert cmd.value == 1
def test_parse_help():
p = _make_poller()
assert p._parse_command("h") == Command(action="help")
assert p._parse_command("/h") == Command(action="help")
assert p._parse_command("help") == Command(action="help")
assert p._parse_command("/help") == Command(action="help")
def test_parse_existing_commands_still_work(): def test_parse_existing_commands_still_work():
"""Regression: adding pause/resume must not break stop/status/ss/interval.""" """Regression: adding pause/resume must not break stop/status/ss/interval."""
p = _make_poller() p = _make_poller()