feat: entity relationship refactor — notification trackers, command system, chat actions

Rework entity schema: rename Tracker→NotificationTracker, add CommandConfig/
CommandTracker/CommandTrackerListener entities for decoupled command handling.
Commands now resolve through CommandTracker→CommandConfig instead of
TelegramBot.commands_config. Smart ref-counted bot polling based on active
listeners. Add chat_action to telegram targets. Full frontend CRUD pages
for command configs and command trackers. Idempotent SQLite migrations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 01:27:20 +03:00
parent 0dcca2fbe6
commit 1d445f3980
34 changed files with 2777 additions and 582 deletions
@@ -19,11 +19,11 @@ from ..database.engine import get_engine
from ..database.models import (
EventLog,
NotificationTarget,
NotificationTracker,
NotificationTrackerState,
NotificationTrackerTarget,
ServiceProvider,
TemplateConfig,
Tracker,
TrackerState,
TrackerTarget,
TrackingConfig,
)
@@ -89,7 +89,7 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
# Load all DB data eagerly before entering aiohttp context
async with AsyncSession(engine) as session:
tracker = await session.get(Tracker, tracker_id)
tracker = await session.get(NotificationTracker, tracker_id)
if not tracker or not tracker.enabled:
return {"status": "skipped", "reason": "disabled or not found"}
@@ -99,7 +99,7 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
# Load tracker state
result = await session.exec(
select(TrackerState).where(TrackerState.tracker_id == tracker_id)
select(NotificationTrackerState).where(NotificationTrackerState.tracker_id == tracker_id)
)
states = result.all()
state_dict: dict[str, Any] = {}
@@ -113,7 +113,7 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
# Load tracker-target links (replaces old target_ids JSON array)
tt_result = await session.exec(
select(TrackerTarget).where(TrackerTarget.tracker_id == tracker_id)
select(NotificationTrackerTarget).where(NotificationTrackerTarget.tracker_id == tracker_id)
)
tracker_targets = tt_result.all()
@@ -188,7 +188,7 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
existing.shared = cstate.get("shared", False)
session.add(existing)
else:
new_ts = TrackerState(
new_ts = NotificationTrackerState(
tracker_id=tracker_id,
collection_id=cid,
collection_name=cstate.get("name", ""),