diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..46aab2a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,16 @@
+# Agent Instructions
+
+## Standard Practice
+- **Always use subagents** (`Task` tool) to implement code changes unless explicitly told otherwise by the user. This optimizes token usage and enables parallel work.
+- Each subagent should handle a single, well-scoped task with clear acceptance criteria.
+- After subagents complete, verify syntax, commit, and deploy.
+
+## Deployment Pattern
+```bash
+ssh tizona "cd /mnt/user/appdata/willhaben-tracker && docker compose down worker && git pull origin main && docker compose build worker --no-cache && docker compose up -d"
+```
+
+## Key Context
+- Bot token: `8653489932:AAG_Ins2_z3sNHX8ZlGI4mhyzmUhWAWCZlg` (in `.env`)
+- Admin TG ID: `298181113`
+- PTB v21, asyncpg with Postgres 15
diff --git a/worker/src/bot.py b/worker/src/bot.py
index 7c4adf3..eb0fe3c 100644
--- a/worker/src/bot.py
+++ b/worker/src/bot.py
@@ -180,7 +180,6 @@ def _admin_menu_keyboard() -> InlineKeyboardMarkup:
[InlineKeyboardButton("➕ Add User", callback_data="admin_add"),
InlineKeyboardButton("👥 List Users", callback_data="admin_list")],
[InlineKeyboardButton("🗑 Remove User", callback_data="admin_remove")],
- [InlineKeyboardButton("↩ Back", callback_data="menu:")],
])
@@ -384,6 +383,8 @@ async def text_input_handler(update: Update, context: ContextTypes.DEFAULT_TYPE)
context.user_data.clear()
logger.info("Admin added user %d", tg_id)
+ await update.message.reply_text( # type: ignore[union-attr]
+ "⚙️ Admin Panel", parse_mode="HTML", reply_markup=_admin_menu_keyboard())
# ── admin: awaiting TG ID to remove user ──────────────────────────
elif state == "admin_awaiting_tg_id_remove":
@@ -428,6 +429,8 @@ async def text_input_handler(update: Update, context: ContextTypes.DEFAULT_TYPE)
context.user_data.clear()
logger.info("Admin removed user %d", tg_id)
+ await update.message.reply_text( # type: ignore[union-attr]
+ "⚙️ Admin Panel", parse_mode="HTML", reply_markup=_admin_menu_keyboard())
# ── callback router (handles ALL inline button clicks) ────────────────────
@@ -507,12 +510,16 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -
)
name = user["first_name"] or "there"
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"That was all your listings, {name}!\n\nAnything else I can help with?",
- parse_mode="HTML",
- reply_markup=_main_menu_keyboard(),
- )
+ summary_chat_id = update.effective_chat.id # type: ignore[union-attr]
+ try:
+ await context.bot.send_message(
+ chat_id=summary_chat_id,
+ text=f"That was all your listings, {name}!\n\nAnything else I can help with?",
+ parse_mode="HTML",
+ reply_markup=_main_menu_keyboard(),
+ )
+ except Exception:
+ logger.exception("Failed to send summary message for user %s", user["id"])
elif sub == "stats":
total_kw = await pool.fetchval(
@@ -529,7 +536,12 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -
f"Ads indexed: {total_ads}\n"
f"Notifications sent: {total_notifs}"
)
- await query.edit_message_text(text=text, parse_mode="HTML")
+ await query.edit_message_text(
+ text=text, parse_mode="HTML",
+ reply_markup=InlineKeyboardMarkup([
+ [InlineKeyboardButton("↩ Back", callback_data="back:admin")],
+ ]),
+ )
else:
# Empty payload — "Back" button from sub-menus, show main menu
@@ -601,7 +613,10 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -
await _edit_or_send(
context.bot, chat_id, msg_id, # type: ignore[arg-type]
- f'✅ Keyword added!\n\n
"{keyword_text}"{extra}', + f'✅ Keyword added!\n\n"{keyword_text}"{extra}', + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("↩ Back", callback_data="menu:")], + ]), ) logger.info("User %s subscribed to '%s'", update.effective_user.id, keyword_text) @@ -789,13 +804,28 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) - # ADMIN FLOWS # ════════════════════════════════════════════════════════════════════ + elif action == "back": + if payload == "admin": + context.user_data.clear() + try: + await query.edit_message_text( + "⚙️ Admin Panel", parse_mode="HTML", + reply_markup=_admin_menu_keyboard()) + except Exception: + await query.answer(show_alert=True) + elif action == "admin_add": admin_row = await _require_admin(update) if not admin_row: return context.user_data["state"] = "admin_awaiting_tg_id_add" context.user_data["admin_action_msg_id"] = msg_id - await query.edit_message_text("Enter the Telegram user ID to add:", parse_mode="HTML") + await query.edit_message_text( + "Enter the Telegram user ID to add:", parse_mode="HTML", + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("❌ Cancel", callback_data="back:admin")], + ]), + ) elif action == "admin_list": admin_row = await _require_admin(update) @@ -831,4 +861,9 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) - return context.user_data["state"] = "admin_awaiting_tg_id_remove" context.user_data["admin_action_msg_id"] = msg_id - await query.edit_message_text("Enter the Telegram user ID to remove:", parse_mode="HTML") \ No newline at end of file + await query.edit_message_text( + "Enter the Telegram user ID to remove:", parse_mode="HTML", + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("❌ Cancel", callback_data="back:admin")], + ]), + ) \ No newline at end of file diff --git a/worker/src/main.py b/worker/src/main.py index 7de4c60..337a9e8 100644 --- a/worker/src/main.py +++ b/worker/src/main.py @@ -79,7 +79,14 @@ async def scheduler_task(pool: object, bot: ExtBot) -> None: if initial_loaded: notify_fields = {**fields, "keyword": keyword} for tg_id in telegram_ids: - await notify_new_ad(bot, tg_id, notify_fields) + msg_id_val = await notify_new_ad(bot, tg_id, notify_fields) + if msg_id_val: + user_row = await pool.fetchrow("SELECT id FROM users WHERE telegram_id = $1", tg_id) + if user_row: + try: + await log_notification(pool, str(user_row["id"]), ad_uuid, msg_id_val) + except Exception: + logger.exception("Failed to log new ad notification") new_count += 1 else: ad_uuid = str(existing["id"])