feat: UX & notification improvements — icons, events, chat names, link validation, templates

- Show entity icons on all cards with fallback defaults (providers, trackers, targets, bots)
- Enrich EventLog with provider_name, tracker_name, assets_count; add DB migration
- Dashboard events: filtering (type, provider, search), sorting, pagination, dynamic page size
- Friendly chat names on telegram target cards (resolve from TelegramChat table)
- Test message button on bot chat items with locale-aware messages
- Album public link validation on tracker save with auto-create dialog
- Support albums without public links: conditional <a href> in templates
- Fetch shared links during poll, enrich events with public_url/protected_url
- Per-asset public_url in template context ({share_url}/photos/{asset_id})
- Common date/location detection: common_date + common_location context vars
- Dual date formats: date_format (datetime) + date_only_format (date only)
- Template clone button, HTML link rendering in template preview
- Fix Telegram asset download 401: pass x-api-key headers through client
- Fix provider external_url matching for API key scoping
- Fix event timestamp timezone (append Z suffix for UTC)
- Localize event filter controls, test messages (EN/RU)
- Template variable UI helpers updated with all new fields
- CLAUDE.md: template system sync rules documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 16:18:03 +03:00
parent 91e5cd58e9
commit 03c5c66eed
41 changed files with 1424 additions and 132 deletions
@@ -1,6 +1,6 @@
"""Telegram bot management API routes."""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -23,7 +23,7 @@ class BotCreate(BaseModel):
class BotUpdate(BaseModel):
name: str | None = None
commands_config: dict | None = None
icon: str | None = None
@router.get("")
@@ -69,12 +69,12 @@ async def update_bot(
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
"""Update a bot's display name and/or commands config."""
"""Update a bot's display name and icon."""
bot = await _get_user_bot(session, bot_id, user.id)
if body.name is not None:
bot.name = body.name
if body.commands_config is not None:
bot.commands_config = body.commands_config
if body.icon is not None:
bot.icon = body.icon
session.add(bot)
await session.commit()
await session.refresh(bot)
@@ -173,6 +173,37 @@ async def discover_chats(
return [_chat_response(c) for c in result.all()]
@router.post("/{bot_id}/chats/{chat_id}/test")
async def test_chat(
bot_id: int,
chat_id: str,
locale: str = Query("en"),
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
"""Send a test message to a chat via the bot."""
from ..services.notifier import _get_test_message
bot = await _get_user_bot(session, bot_id, user.id)
message = _get_test_message(locale, "telegram")
try:
async with aiohttp.ClientSession() as http:
async with http.post(
f"{TELEGRAM_API_BASE_URL}{bot.token}/sendMessage",
json={
"chat_id": chat_id,
"text": message,
"parse_mode": "HTML",
},
) as resp:
data = await resp.json()
if data.get("ok"):
return {"success": True}
return {"success": False, "error": data.get("description", "Unknown error")}
except aiohttp.ClientError as e:
return {"success": False, "error": str(e)}
@router.delete("/{bot_id}/chats/{chat_db_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_chat(
bot_id: int,
@@ -247,10 +278,10 @@ def _bot_response(b: TelegramBot) -> dict:
return {
"id": b.id,
"name": b.name,
"icon": b.icon,
"bot_username": b.bot_username,
"bot_id": b.bot_id,
"token_preview": f"{b.token[:8]}...{b.token[-4:]}" if len(b.token) > 12 else "***",
"commands_config": b.commands_config,
"created_at": b.created_at.isoformat(),
}