refactor: comprehensive consistency review — UI/UX, code quality, functional parity
Fix 19 issues across 3 priority tiers found during full-codebase review: CRITICAL: - Fix undefined --color-secondary CSS variable causing invisible UI elements - Fix Google Photos command templates using nonexistent asset.originalFileName - Fix scheduler template variable docs (tracker_name → schedule_name) - Add missing admin guard on notification template update endpoint HIGH: - Fix 5 hardcoded English strings missing i18n (MultiEntitySelect, actions, settings, TelegramBotTab, users) - Replace 17 raw <button> elements with shared <Button> component - Replace 5 raw error divs with shared <ErrorBanner> component - Refactor webhook handler duplication into shared _dispatch_webhook_event() - Add 30+ provider-specific fields to TrackingConfig TypeScript type - Add default TrackingConfig seeds for immich and google_photos - Add provider-specific command variable docs (Gitea, Planka, NUT, GP, Webhook) MEDIUM: - Replace hardcoded hex colors and Tailwind classes with CSS variable tokens - Remove dead code (unused imports, orphaned check_notification_tracker) - Fix Svelte 5 patterns ($state for _prevProviderId, remove unnecessary as any) - Fix inconsistent POST response shape (targets now returns full response) - Fix Google Photos template dead asset.year branches, clarify album_url docs
This commit is contained in:
@@ -112,7 +112,8 @@ async def get_command_variables(
|
||||
"asset_fields": asset_fields,
|
||||
}
|
||||
|
||||
return {
|
||||
# --- Shared slots (all providers) ---
|
||||
shared = {
|
||||
"start": {
|
||||
"description": "/start greeting message",
|
||||
"variables": {**common_vars, "bot_name": "Bot display name"},
|
||||
@@ -122,6 +123,20 @@ async def get_command_variables(
|
||||
"variables": {**common_vars, "commands": "List of command dicts (use {% for cmd in commands %})"},
|
||||
"command_fields": command_fields,
|
||||
},
|
||||
"rate_limited": {
|
||||
"description": "Rate limit warning message",
|
||||
"variables": {**common_vars, "wait": "Seconds to wait before retry"},
|
||||
},
|
||||
"no_results": {
|
||||
"description": "Empty results fallback",
|
||||
"variables": {**common_vars, "command": "Command name", "query": "Search query (empty for non-search commands)"},
|
||||
},
|
||||
"desc_help": {"description": "Description for /help command", "variables": common_vars},
|
||||
"desc_status": {"description": "Description for /status command", "variables": common_vars},
|
||||
}
|
||||
|
||||
# --- Immich-specific ---
|
||||
immich = {
|
||||
"status": {
|
||||
"description": "/status tracker summary",
|
||||
"variables": {
|
||||
@@ -163,17 +178,6 @@ async def get_command_variables(
|
||||
"variables": {**common_vars, "assets": "List of asset dicts with year field (use {% for asset in assets %})", "count": "Number of results"},
|
||||
"asset_fields": asset_fields,
|
||||
},
|
||||
"rate_limited": {
|
||||
"description": "Rate limit warning message",
|
||||
"variables": {**common_vars, "wait": "Seconds to wait before retry"},
|
||||
},
|
||||
"no_results": {
|
||||
"description": "Empty results fallback",
|
||||
"variables": {**common_vars, "command": "Command name", "query": "Search query (empty for non-search commands)"},
|
||||
},
|
||||
# --- Description slots (shown in /help listing) ---
|
||||
"desc_help": {"description": "Description for /help command", "variables": common_vars},
|
||||
"desc_status": {"description": "Description for /status command", "variables": common_vars},
|
||||
"desc_albums": {"description": "Description for /albums command", "variables": common_vars},
|
||||
"desc_events": {"description": "Description for /events command", "variables": common_vars},
|
||||
"desc_summary": {"description": "Description for /summary command", "variables": common_vars},
|
||||
@@ -186,7 +190,6 @@ async def get_command_variables(
|
||||
"desc_place": {"description": "Description for /place command", "variables": common_vars},
|
||||
"desc_favorites": {"description": "Description for /favorites command", "variables": common_vars},
|
||||
"desc_people": {"description": "Description for /people command", "variables": common_vars},
|
||||
# --- Usage example slots (shown in /help listing) ---
|
||||
"usage_search": {"description": "Usage example for /search (e.g. '/search sunset')", "variables": common_vars},
|
||||
"usage_find": {"description": "Usage example for /find", "variables": common_vars},
|
||||
"usage_person": {"description": "Usage example for /person", "variables": common_vars},
|
||||
@@ -198,6 +201,183 @@ async def get_command_variables(
|
||||
"usage_memory": {"description": "Usage example for /memory", "variables": common_vars},
|
||||
}
|
||||
|
||||
# --- Gitea-specific ---
|
||||
repo_fields = {
|
||||
"full_name": "Repository full name (owner/repo)",
|
||||
"description": "Repository description",
|
||||
}
|
||||
issue_fields = {
|
||||
"repo": "Repository name",
|
||||
"number": "Issue number",
|
||||
"title": "Issue title",
|
||||
"url": "Issue URL",
|
||||
"user": "Issue author",
|
||||
}
|
||||
pr_fields = {
|
||||
"repo": "Repository name",
|
||||
"number": "PR number",
|
||||
"title": "PR title",
|
||||
"url": "PR URL",
|
||||
"user": "PR author",
|
||||
}
|
||||
commit_fields = {
|
||||
"repo": "Repository name",
|
||||
"short_id": "Short commit hash",
|
||||
"message": "Commit message",
|
||||
"author": "Commit author",
|
||||
}
|
||||
gitea = {
|
||||
"status": {
|
||||
"description": "/status Gitea server summary",
|
||||
"variables": {**common_vars, "repos_count": "Number of tracked repositories", "server_version": "Gitea server version", "last_event": "Last event timestamp"},
|
||||
},
|
||||
"repos": {
|
||||
"description": "/repos tracked repositories",
|
||||
"variables": {**common_vars, "repos": "List of repo dicts (use {% for repo in repos %})"},
|
||||
"repo_fields": repo_fields,
|
||||
},
|
||||
"issues": {
|
||||
"description": "/issues open issues",
|
||||
"variables": {**common_vars, "issues": "List of issue dicts (use {% for issue in issues %})"},
|
||||
"issue_fields": issue_fields,
|
||||
},
|
||||
"prs": {
|
||||
"description": "/prs open pull requests",
|
||||
"variables": {**common_vars, "prs": "List of PR dicts (use {% for pr in prs %})"},
|
||||
"pr_fields": pr_fields,
|
||||
},
|
||||
"commits": {
|
||||
"description": "/commits recent commits",
|
||||
"variables": {**common_vars, "commits": "List of commit dicts (use {% for c in commits %})"},
|
||||
"commit_fields": commit_fields,
|
||||
},
|
||||
"desc_repos": {"description": "Description for /repos command", "variables": common_vars},
|
||||
"desc_issues": {"description": "Description for /issues command", "variables": common_vars},
|
||||
"desc_prs": {"description": "Description for /prs command", "variables": common_vars},
|
||||
"desc_commits": {"description": "Description for /commits command", "variables": common_vars},
|
||||
}
|
||||
|
||||
# --- Planka-specific ---
|
||||
board_fields = {"name": "Board name"}
|
||||
card_fields_planka = {
|
||||
"name": "Card name",
|
||||
"list_name": "List the card belongs to",
|
||||
"board_name": "Board the card belongs to",
|
||||
}
|
||||
list_fields = {"name": "List name", "board_name": "Board name"}
|
||||
planka = {
|
||||
"status": {
|
||||
"description": "/status Planka board summary",
|
||||
"variables": {**common_vars, "boards_count": "Number of tracked boards", "last_event": "Last event timestamp"},
|
||||
},
|
||||
"boards": {
|
||||
"description": "/boards tracked boards",
|
||||
"variables": {**common_vars, "boards": "List of board dicts (use {% for board in boards %})"},
|
||||
"board_fields": board_fields,
|
||||
},
|
||||
"cards": {
|
||||
"description": "/cards recent cards",
|
||||
"variables": {**common_vars, "cards": "List of card dicts (use {% for card in cards %})"},
|
||||
"card_fields": card_fields_planka,
|
||||
},
|
||||
"lists": {
|
||||
"description": "/lists board lists",
|
||||
"variables": {**common_vars, "lists": "List of list dicts (use {% for lst in lists %})"},
|
||||
"list_fields": list_fields,
|
||||
},
|
||||
"desc_boards": {"description": "Description for /boards command", "variables": common_vars},
|
||||
"desc_cards": {"description": "Description for /cards command", "variables": common_vars},
|
||||
"desc_lists": {"description": "Description for /lists command", "variables": common_vars},
|
||||
}
|
||||
|
||||
# --- NUT-specific ---
|
||||
device_fields = {
|
||||
"name": "UPS device name",
|
||||
"description": "UPS description",
|
||||
"model": "UPS model",
|
||||
"battery_charge": "Battery charge percentage",
|
||||
"battery_runtime": "Estimated runtime (formatted)",
|
||||
"input_voltage": "Input voltage",
|
||||
"output_voltage": "Output voltage",
|
||||
}
|
||||
nut = {
|
||||
"status": {
|
||||
"description": "/status UPS summary",
|
||||
"variables": {**common_vars, "devices_count": "Number of monitored devices", "last_event": "Last event timestamp"},
|
||||
},
|
||||
"devices": {
|
||||
"description": "/devices monitored UPS devices",
|
||||
"variables": {**common_vars, "devices": "List of device dicts (use {% for d in devices %})"},
|
||||
"device_fields": device_fields,
|
||||
},
|
||||
"battery": {
|
||||
"description": "/battery battery report",
|
||||
"variables": {**common_vars, "devices": "List of UPS dicts (use {% for ups in devices %})"},
|
||||
"device_fields": device_fields,
|
||||
},
|
||||
"desc_devices": {"description": "Description for /devices command", "variables": common_vars},
|
||||
"desc_battery": {"description": "Description for /battery command", "variables": common_vars},
|
||||
}
|
||||
|
||||
# --- Google Photos-specific ---
|
||||
gp_asset_fields = {
|
||||
"id": "Asset ID",
|
||||
"filename": "Original filename",
|
||||
"type": "IMAGE or VIDEO",
|
||||
"created_at": "Creation date/time (ISO 8601)",
|
||||
}
|
||||
google_photos = {
|
||||
"status": {
|
||||
"description": "/status Google Photos summary",
|
||||
"variables": {**common_vars, "trackers_active": "Number of active trackers", "trackers_total": "Total tracker count", "total_albums": "Total tracked albums", "last_event": "Last event timestamp"},
|
||||
},
|
||||
"albums": {
|
||||
"description": "/albums tracked albums",
|
||||
"variables": {**common_vars, "albums": "List of album dicts (use {% for album in albums %})"},
|
||||
"album_fields": album_fields,
|
||||
},
|
||||
"latest": {
|
||||
"description": "/latest recent photos",
|
||||
"variables": {**common_vars, "assets": "List of asset dicts (use {% for asset in assets %})", "count": "Number of results"},
|
||||
"asset_fields": gp_asset_fields,
|
||||
},
|
||||
"search": {
|
||||
"description": "/search photo search results",
|
||||
"variables": {**common_vars, "assets": "List of asset dicts (use {% for asset in assets %})", "query": "Search query", "count": "Number of results"},
|
||||
"asset_fields": gp_asset_fields,
|
||||
},
|
||||
"random": {
|
||||
"description": "/random random photos",
|
||||
"variables": {**common_vars, "assets": "List of asset dicts (use {% for asset in assets %})", "count": "Number of results"},
|
||||
"asset_fields": gp_asset_fields,
|
||||
},
|
||||
"desc_albums": {"description": "Description for /albums command", "variables": common_vars},
|
||||
"desc_latest": {"description": "Description for /latest command", "variables": common_vars},
|
||||
"desc_search": {"description": "Description for /search command", "variables": common_vars},
|
||||
"desc_random": {"description": "Description for /random command", "variables": common_vars},
|
||||
"usage_latest": {"description": "Usage example for /latest", "variables": common_vars},
|
||||
"usage_search": {"description": "Usage example for /search", "variables": common_vars},
|
||||
"usage_random": {"description": "Usage example for /random", "variables": common_vars},
|
||||
}
|
||||
|
||||
# --- Webhook-specific ---
|
||||
webhook = {
|
||||
"status": {
|
||||
"description": "/status webhook provider summary",
|
||||
"variables": {**common_vars, "provider_name": "Webhook provider name", "last_event": "Last event timestamp"},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
**shared,
|
||||
"immich": immich,
|
||||
"gitea": gitea,
|
||||
"planka": planka,
|
||||
"nut": nut,
|
||||
"google_photos": google_photos,
|
||||
"webhook": webhook,
|
||||
}
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_configs(
|
||||
|
||||
@@ -15,8 +15,6 @@ from ..database.models import (
|
||||
NotificationTarget,
|
||||
NotificationTracker,
|
||||
NotificationTrackerTarget,
|
||||
TargetReceiver,
|
||||
TelegramChat,
|
||||
)
|
||||
|
||||
|
||||
@@ -164,16 +162,3 @@ async def check_notification_target(session: AsyncSession, target_id: int) -> li
|
||||
return consumers
|
||||
|
||||
|
||||
async def check_notification_tracker(session: AsyncSession, tracker_id: int) -> list[str]:
|
||||
"""Check if a NotificationTracker has any linked targets."""
|
||||
consumers = []
|
||||
result = await session.exec(
|
||||
select(NotificationTrackerTarget).where(
|
||||
NotificationTrackerTarget.tracker_id == tracker_id
|
||||
)
|
||||
)
|
||||
for tt in result.all():
|
||||
target = await session.get(NotificationTarget, tt.target_id)
|
||||
name = target.name if target else f"#{tt.target_id}"
|
||||
consumers.append(f"Linked Target: {name}")
|
||||
return consumers
|
||||
|
||||
@@ -180,7 +180,11 @@ async def create_target(
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(target)
|
||||
return {"id": target.id, "type": target.type, "name": target.name}
|
||||
# Load receivers for a full response consistent with GET
|
||||
recv_result = await session.exec(
|
||||
select(TargetReceiver).where(TargetReceiver.target_id == target.id)
|
||||
)
|
||||
return _target_response(target, receivers=list(recv_result.all()))
|
||||
|
||||
|
||||
@router.get("/{target_id}")
|
||||
|
||||
@@ -134,10 +134,10 @@ async def get_template_variables(
|
||||
"has_oversized_videos": "Whether any video exceeds the target's size limit (boolean)",
|
||||
"max_video_size": "Target video size limit in bytes (null if no limit)",
|
||||
"max_video_size_mb": "Target video size limit in MB (null if no limit)",
|
||||
# Immich aliases
|
||||
# Provider-specific aliases
|
||||
"album_name": "Alias for collection_name",
|
||||
"album_id": "Alias for collection_id",
|
||||
"album_url": "Alias for collection_url",
|
||||
"album_url": "Alias for collection_url (Immich) or album product URL (Google Photos)",
|
||||
}
|
||||
rename_vars = {
|
||||
**event_vars,
|
||||
@@ -236,7 +236,7 @@ async def get_template_variables(
|
||||
"message_scheduled_message": {
|
||||
"description": "Notification for scheduled message events",
|
||||
"variables": {
|
||||
"tracker_name": "Name of the tracker that fired",
|
||||
"schedule_name": "Name of the schedule that fired",
|
||||
"fire_count": "How many times this tracker has fired",
|
||||
"current_date": "Current date (formatted)",
|
||||
"current_time": "Current time (formatted)",
|
||||
@@ -420,6 +420,8 @@ async def update_config(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
config = await _get(session, config_id, user.id)
|
||||
if config.user_id == 0 and user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Cannot modify system default configs")
|
||||
for field, value in body.model_dump(exclude_unset=True, exclude={"slots"}).items():
|
||||
if value is not None:
|
||||
setattr(config, field, value)
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -71,6 +72,98 @@ def _passes_filters(
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared dispatch helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _dispatch_webhook_event(
|
||||
engine: Any,
|
||||
provider_id: int,
|
||||
provider_name: str,
|
||||
provider_config: dict[str, Any],
|
||||
event: ServiceEvent,
|
||||
detail_keys: tuple[str, ...],
|
||||
) -> int:
|
||||
"""Load trackers, filter, create EventLogs, dispatch notifications, and commit.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
engine:
|
||||
SQLAlchemy async engine.
|
||||
provider_id:
|
||||
ID of the ServiceProvider that received the webhook.
|
||||
provider_name:
|
||||
Human-readable name of the provider (for logging).
|
||||
provider_config:
|
||||
The provider's ``config`` dict (passed through to target config builder).
|
||||
event:
|
||||
Parsed :class:`ServiceEvent` to dispatch.
|
||||
detail_keys:
|
||||
Keys from ``event.extra`` to include in the EventLog ``details`` dict.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Number of successfully dispatched notifications.
|
||||
"""
|
||||
dispatched = 0
|
||||
async with AsyncSession(engine) as session:
|
||||
tracker_result = await session.exec(
|
||||
select(NotificationTracker).where(
|
||||
NotificationTracker.provider_id == provider_id,
|
||||
NotificationTracker.enabled == True, # noqa: E712
|
||||
)
|
||||
)
|
||||
trackers = tracker_result.all()
|
||||
|
||||
for tracker in trackers:
|
||||
filters = tracker.filters or {}
|
||||
if not _passes_filters(event, filters):
|
||||
_LOGGER.debug(
|
||||
"Event filtered out for tracker %d (%s)", tracker.id, tracker.name
|
||||
)
|
||||
continue
|
||||
|
||||
link_data = await load_link_data(session, tracker.id)
|
||||
if not link_data:
|
||||
continue
|
||||
|
||||
# Log event
|
||||
extra_details = {k: v for k, v in event.extra.items() if k in detail_keys}
|
||||
session.add(EventLog(
|
||||
tracker_id=tracker.id,
|
||||
tracker_name=tracker.name,
|
||||
provider_id=provider_id,
|
||||
provider_name=provider_name,
|
||||
event_type=event.event_type.value,
|
||||
collection_id=event.collection_id,
|
||||
collection_name=event.collection_name,
|
||||
assets_count=0,
|
||||
details={
|
||||
"provider_type": event.provider_type.value,
|
||||
**extra_details,
|
||||
},
|
||||
))
|
||||
|
||||
# Dispatch to targets
|
||||
dispatcher = NotificationDispatcher()
|
||||
target_configs = _build_target_configs(event, link_data, provider_config)
|
||||
if target_configs:
|
||||
results = await dispatcher.dispatch(event, target_configs)
|
||||
for r in results:
|
||||
if r.get("success"):
|
||||
dispatched += 1
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Notification failed for tracker %d: %s",
|
||||
tracker.id, r.get("error", "unknown"),
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
|
||||
return dispatched
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gitea webhook endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -108,74 +201,27 @@ async def gitea_webhook(provider_id: int, request: Request):
|
||||
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON")
|
||||
|
||||
event = parse_gitea_webhook(event_header, payload, provider.name)
|
||||
if event is None:
|
||||
return {"ok": True, "skipped": "unmapped event"}
|
||||
|
||||
# --- Find trackers for this provider and dispatch ---
|
||||
dispatched = 0
|
||||
async with AsyncSession(engine) as session:
|
||||
tracker_result = await session.exec(
|
||||
select(NotificationTracker).where(
|
||||
NotificationTracker.provider_id == provider_id,
|
||||
NotificationTracker.enabled == True,
|
||||
)
|
||||
)
|
||||
trackers = tracker_result.all()
|
||||
|
||||
for tracker in trackers:
|
||||
# Apply filters
|
||||
filters = tracker.filters or {}
|
||||
if not _passes_filters(event, filters):
|
||||
_LOGGER.debug(
|
||||
"Event filtered out for tracker %d (%s)", tracker.id, tracker.name
|
||||
)
|
||||
continue
|
||||
|
||||
# Load tracker-target links
|
||||
link_data = await load_link_data(session, tracker.id)
|
||||
if not link_data:
|
||||
continue
|
||||
|
||||
# Log event
|
||||
session.add(EventLog(
|
||||
tracker_id=tracker.id,
|
||||
tracker_name=tracker.name,
|
||||
provider_id=provider_id,
|
||||
provider_name=provider.name,
|
||||
event_type=event.event_type.value,
|
||||
collection_id=event.collection_id,
|
||||
collection_name=event.collection_name,
|
||||
assets_count=0,
|
||||
details={
|
||||
"provider_type": event.provider_type.value,
|
||||
**{k: v for k, v in event.extra.items() if k in (
|
||||
"sender", "branch", "commit_count",
|
||||
"issue_number", "issue_title",
|
||||
"pr_number", "pr_title",
|
||||
"release_tag", "release_name",
|
||||
)},
|
||||
},
|
||||
))
|
||||
|
||||
# Dispatch to targets
|
||||
dispatcher = NotificationDispatcher()
|
||||
target_configs = _build_target_configs(event, link_data, provider.config or {})
|
||||
if target_configs:
|
||||
results = await dispatcher.dispatch(event, target_configs)
|
||||
for r in results:
|
||||
if r.get("success"):
|
||||
dispatched += 1
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Notification failed for tracker %d: %s",
|
||||
tracker.id, r.get("error", "unknown"),
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
# --- Dispatch ---
|
||||
dispatched = await _dispatch_webhook_event(
|
||||
engine=engine,
|
||||
provider_id=provider_id,
|
||||
provider_name=provider.name,
|
||||
provider_config=provider.config or {},
|
||||
event=event,
|
||||
detail_keys=(
|
||||
"sender", "branch", "commit_count",
|
||||
"issue_number", "issue_title",
|
||||
"pr_number", "pr_title",
|
||||
"release_tag", "release_name",
|
||||
),
|
||||
)
|
||||
|
||||
return {"ok": True, "dispatched": dispatched}
|
||||
|
||||
@@ -218,7 +264,7 @@ async def planka_webhook(provider_id: int, request: Request):
|
||||
# Parse payload
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON")
|
||||
|
||||
event_type = payload.get("type", "")
|
||||
@@ -230,65 +276,20 @@ async def planka_webhook(provider_id: int, request: Request):
|
||||
if event is None:
|
||||
return {"ok": True, "skipped": "unmapped event"}
|
||||
|
||||
# --- Find trackers for this provider and dispatch ---
|
||||
dispatched = 0
|
||||
async with AsyncSession(engine) as session:
|
||||
tracker_result = await session.exec(
|
||||
select(NotificationTracker).where(
|
||||
NotificationTracker.provider_id == provider_id,
|
||||
NotificationTracker.enabled == True,
|
||||
)
|
||||
)
|
||||
trackers = tracker_result.all()
|
||||
|
||||
for tracker in trackers:
|
||||
filters = tracker.filters or {}
|
||||
if not _passes_filters(event, filters):
|
||||
_LOGGER.debug(
|
||||
"Event filtered out for tracker %d (%s)", tracker.id, tracker.name
|
||||
)
|
||||
continue
|
||||
|
||||
link_data = await load_link_data(session, tracker.id)
|
||||
if not link_data:
|
||||
continue
|
||||
|
||||
# Log event
|
||||
session.add(EventLog(
|
||||
tracker_id=tracker.id,
|
||||
tracker_name=tracker.name,
|
||||
provider_id=provider_id,
|
||||
provider_name=provider.name,
|
||||
event_type=event.event_type.value,
|
||||
collection_id=event.collection_id,
|
||||
collection_name=event.collection_name,
|
||||
assets_count=0,
|
||||
details={
|
||||
"provider_type": event.provider_type.value,
|
||||
**{k: v for k, v in event.extra.items() if k in (
|
||||
"sender", "card_name", "board_name",
|
||||
"list_name", "old_list_name", "new_list_name",
|
||||
"comment_text", "task_name", "attachment_name",
|
||||
"label_name",
|
||||
)},
|
||||
},
|
||||
))
|
||||
|
||||
# Dispatch to targets
|
||||
dispatcher = NotificationDispatcher()
|
||||
target_configs = _build_target_configs(event, link_data, provider.config or {})
|
||||
if target_configs:
|
||||
results = await dispatcher.dispatch(event, target_configs)
|
||||
for r in results:
|
||||
if r.get("success"):
|
||||
dispatched += 1
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Notification failed for tracker %d: %s",
|
||||
tracker.id, r.get("error", "unknown"),
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
# --- Dispatch ---
|
||||
dispatched = await _dispatch_webhook_event(
|
||||
engine=engine,
|
||||
provider_id=provider_id,
|
||||
provider_name=provider.name,
|
||||
provider_config=provider.config or {},
|
||||
event=event,
|
||||
detail_keys=(
|
||||
"sender", "card_name", "board_name",
|
||||
"list_name", "old_list_name", "new_list_name",
|
||||
"comment_text", "task_name", "attachment_name",
|
||||
"label_name",
|
||||
),
|
||||
)
|
||||
|
||||
return {"ok": True, "dispatched": dispatched}
|
||||
|
||||
@@ -424,7 +425,7 @@ async def generic_webhook(provider_id: int, request: Request):
|
||||
# Parse JSON payload
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
if store_payloads:
|
||||
async with AsyncSession(get_engine()) as log_session:
|
||||
await _save_webhook_log(
|
||||
@@ -451,70 +452,28 @@ async def generic_webhook(provider_id: int, request: Request):
|
||||
source_ip = request.client.host if request.client else ""
|
||||
event.extra["source_ip"] = source_ip
|
||||
|
||||
# --- Find trackers for this provider and dispatch ---
|
||||
dispatched = 0
|
||||
async with AsyncSession(engine) as session:
|
||||
tracker_result = await session.exec(
|
||||
select(NotificationTracker).where(
|
||||
NotificationTracker.provider_id == provider_id,
|
||||
NotificationTracker.enabled == True,
|
||||
)
|
||||
)
|
||||
trackers = tracker_result.all()
|
||||
# --- Dispatch ---
|
||||
dispatched = await _dispatch_webhook_event(
|
||||
engine=engine,
|
||||
provider_id=provider_id,
|
||||
provider_name=provider_name,
|
||||
provider_config=provider_config,
|
||||
event=event,
|
||||
detail_keys=(
|
||||
"event_type_raw", "source_ip",
|
||||
),
|
||||
)
|
||||
|
||||
for tracker in trackers:
|
||||
filters = tracker.filters or {}
|
||||
if not _passes_filters(event, filters):
|
||||
_LOGGER.debug(
|
||||
"Event filtered out for tracker %d (%s)", tracker.id, tracker.name
|
||||
)
|
||||
continue
|
||||
|
||||
link_data = await load_link_data(session, tracker.id)
|
||||
if not link_data:
|
||||
continue
|
||||
|
||||
# Log event
|
||||
session.add(EventLog(
|
||||
tracker_id=tracker.id,
|
||||
tracker_name=tracker.name,
|
||||
provider_id=provider_id,
|
||||
provider_name=provider_name,
|
||||
event_type=event.event_type.value,
|
||||
collection_id=event.collection_id,
|
||||
collection_name=event.collection_name,
|
||||
assets_count=0,
|
||||
details={
|
||||
"provider_type": "webhook",
|
||||
"event_type_raw": event.extra.get("event_type_raw", ""),
|
||||
"source_ip": source_ip,
|
||||
},
|
||||
))
|
||||
|
||||
# Dispatch to targets
|
||||
dispatcher = NotificationDispatcher()
|
||||
target_configs = _build_target_configs(event, link_data, provider_config)
|
||||
if target_configs:
|
||||
results = await dispatcher.dispatch(event, target_configs)
|
||||
for r in results:
|
||||
if r.get("success"):
|
||||
dispatched += 1
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Notification failed for tracker %d: %s",
|
||||
tracker.id, r.get("error", "unknown"),
|
||||
)
|
||||
|
||||
# Log matched payload
|
||||
if store_payloads:
|
||||
# Log matched payload (separate session — dispatch already committed)
|
||||
if store_payloads:
|
||||
async with AsyncSession(engine) as log_session:
|
||||
await _save_webhook_log(
|
||||
session, provider_id, request.method, safe_headers,
|
||||
log_session, provider_id, request.method, safe_headers,
|
||||
payload, "matched" if dispatched > 0 else "unmatched",
|
||||
extracted_fields=dict(event.extra),
|
||||
max_count=max_stored,
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
await log_session.commit()
|
||||
|
||||
return {"ok": True, "dispatched": dispatched}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user