From 82e400ddcd60c51a28075f4ce14b1cb9292efb12 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sun, 22 Mar 2026 23:39:52 +0300 Subject: [PATCH] feat: chat language display, disabled EntitySelect items, dev scripts Chat language: - Added language_code field to TelegramChat model + migration - Saved from message.from.language_code on webhook/polling - Displayed as badge on bot chat cards and target receiver items - Resolved from DB in target API response (works for existing receivers) - Shown in chat picker dropdown (desc includes language) EntitySelect improvements: - Tracker-target link selector shows all targets, already-linked ones appear disabled with "Already linked" hint - Receiver chat picker shows already-added chats as disabled Dev scripts: - scripts/restart-backend.sh and restart-frontend.sh - Updated .claude/docs/dev-servers.md to reference scripts --- .claude/docs/dev-servers.md | 10 +- frontend/src/lib/i18n/en.json | 1 + frontend/src/lib/i18n/ru.json | 1 + .../src/routes/bots/TelegramBotTab.svelte | 1 + .../routes/notification-trackers/+page.svelte | 2 +- .../LinkedTargetsSection.svelte | 3 + frontend/src/routes/targets/+page.svelte | 21 +++- .../src/notify_bridge_server/api/targets.py | 11 ++- .../notify_bridge_server/api/telegram_bots.py | 97 +++++++------------ .../notify_bridge_server/commands/webhook.py | 41 +++----- .../database/migrations.py | 8 ++ .../notify_bridge_server/database/models.py | 1 + .../notify_bridge_server/services/telegram.py | 6 +- .../services/telegram_poller.py | 37 +++---- scripts/restart-backend.sh | 22 +++++ scripts/restart-frontend.sh | 21 ++++ 16 files changed, 161 insertions(+), 122 deletions(-) create mode 100644 scripts/restart-backend.sh create mode 100644 scripts/restart-frontend.sh diff --git a/.claude/docs/dev-servers.md b/.claude/docs/dev-servers.md index 82cb9d3..5fb9af3 100644 --- a/.claude/docs/dev-servers.md +++ b/.claude/docs/dev-servers.md @@ -1,13 +1,15 @@ # Development Servers -**MANDATORY**: You MUST restart the backend server IMMEDIATELY after ANY backend code change (files in `packages/server/` or `packages/core/`). Do NOT wait for the user to ask — restart automatically every time. Failure to restart means the user will test against stale code and encounter bugs that don't exist. Use this one-liner: +**MANDATORY**: You MUST restart the backend server IMMEDIATELY after ANY backend code change (files in `packages/server/` or `packages/core/`). Do NOT wait for the user to ask — restart automatically every time. Failure to restart means the user will test against stale code and encounter bugs that don't exist. + ```bash -PID=$(netstat -ano 2>/dev/null | grep ':8420.*LISTENING' | awk '{print $5}' | head -1) && [ -n "$PID" ] && taskkill //F //PID $PID 2>/dev/null; sleep 1 && cd packages/server && pip install -e . 2>&1 | tail -1 && cd ../.. && NOTIFY_BRIDGE_DATA_DIR=./test-data NOTIFY_BRIDGE_SECRET_KEY=test-secret-key-minimum-32chars nohup python -m uvicorn notify_bridge_server.main:app --host 0.0.0.0 --port 8420 > /dev/null 2>&1 & sleep 3 && curl -s http://localhost:8420/api/health +bash scripts/restart-backend.sh ``` -**IMPORTANT**: When the user requests it, restart the frontend dev server using this one-liner: +**IMPORTANT**: When the user requests it, restart the frontend dev server: + ```bash -PID=$(netstat -ano 2>/dev/null | grep ':5173.*LISTENING' | awk '{print $5}' | head -1) && [ -n "$PID" ] && taskkill //F //PID $PID 2>/dev/null; sleep 1 && cd frontend && npx vite dev --port 5173 --host > /dev/null 2>&1 & sleep 4 && curl -s -o /dev/null -w "Frontend: %{http_code}" http://localhost:5173/ +bash scripts/restart-frontend.sh ``` ## Test Credentials diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 08a3843..90cc957 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -176,6 +176,7 @@ "linkedTargets": "targets", "noLinkedTargets": "No targets linked. Add a target below.", "addTarget": "Add target", + "alreadyLinked": "Already linked", "testBasic": "Send test message", "testPeriodic": "Test periodic summary", "testScheduled": "Test scheduled assets", diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index e4e9f1c..b7e2715 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -176,6 +176,7 @@ "linkedTargets": "получатели", "noLinkedTargets": "Нет привязанных получателей. Добавьте получателя ниже.", "addTarget": "Добавить получателя", + "alreadyLinked": "Уже привязан", "testBasic": "Отправить тестовое сообщение", "testPeriodic": "Тест периодической сводки", "testScheduled": "Тест запланированных фото", diff --git a/frontend/src/routes/bots/TelegramBotTab.svelte b/frontend/src/routes/bots/TelegramBotTab.svelte index 11b458b..28f7aa4 100644 --- a/frontend/src/routes/bots/TelegramBotTab.svelte +++ b/frontend/src/routes/bots/TelegramBotTab.svelte @@ -306,6 +306,7 @@
{chat.title || chat.username || 'Unknown'} {chatTypeLabel(chat.type)} + {#if chat.language_code}{chat.language_code.toUpperCase()}{/if} {chat.chat_id}
diff --git a/frontend/src/routes/notification-trackers/+page.svelte b/frontend/src/routes/notification-trackers/+page.svelte index ab9d659..93259c3 100644 --- a/frontend/src/routes/notification-trackers/+page.svelte +++ b/frontend/src/routes/notification-trackers/+page.svelte @@ -429,7 +429,7 @@ {tracker} {trackingConfigs} {templateConfigs} - unlinkedTargets={getUnlinkedTargets(tracker)} + unlinkedTargets={targets} newLinkTargetId={newLinkTargetId[tracker.id] || 0} newLinkTrackingConfigId={newLinkTrackingConfigId[tracker.id] || 0} newLinkTemplateConfigId={newLinkTemplateConfigId[tracker.id] || 0} diff --git a/frontend/src/routes/notification-trackers/LinkedTargetsSection.svelte b/frontend/src/routes/notification-trackers/LinkedTargetsSection.svelte index b845eb9..06c15d1 100644 --- a/frontend/src/routes/notification-trackers/LinkedTargetsSection.svelte +++ b/frontend/src/routes/notification-trackers/LinkedTargetsSection.svelte @@ -57,11 +57,14 @@ const trackingConfigItems = $derived(toItems(trackingConfigs)); const templateConfigItems = $derived(toItems(templateConfigs)); + const linkedTargetIds = $derived(new Set((tracker.tracker_targets || []).map((tt: any) => tt.target_id))); const targetItems = $derived(unlinkedTargets.map(tgt => ({ value: tgt.id, label: tgt.name, icon: tgt.icon || (tgt.type === 'telegram' ? 'mdiSend' : 'mdiWebhook'), desc: tgt.type, + disabled: linkedTargetIds.has(tgt.id), + disabledHint: linkedTargetIds.has(tgt.id) ? t('notificationTracker.alreadyLinked') : undefined, }))); diff --git a/frontend/src/routes/targets/+page.svelte b/frontend/src/routes/targets/+page.svelte index 906196f..958521b 100644 --- a/frontend/src/routes/targets/+page.svelte +++ b/frontend/src/routes/targets/+page.svelte @@ -311,7 +311,19 @@ receiverSubmitting = true; receiverHeadersError = ''; try { - const config = { ...receiverForm }; + const config: Record = { ...receiverForm }; + // Enrich Telegram receiver with chat metadata + if (config.chat_id && addingReceiverForTarget) { + const target = allTargets.find(t => t.id === addingReceiverForTarget); + const botId = target?.config?.bot_id || target?.config?.telegram_bot_id; + if (botId && receiverBotChats[botId]) { + const chat = receiverBotChats[botId].find((c: any) => String(c.chat_id) === String(config.chat_id)); + if (chat) { + config.chat_name = chat.title || chat.username || ''; + if (chat.language_code) config.language_code = chat.language_code; + } + } + } // Parse headers JSON for webhook if ('headers' in config && typeof config.headers === 'string') { if (config.headers) { @@ -535,6 +547,9 @@
{receiverLabel(target, recv)} + {#if (recv as any).language_code || recv.config?.language_code} + {((recv as any).language_code || recv.config.language_code).toUpperCase()} + {/if}
r.receiver_key))} - {@const chatItems = (receiverBotChats[botId] || []).map((c: TelegramChat) => ({ + {@const chatItems = (receiverBotChats[botId] || []).map((c: any) => ({ value: c.chat_id, label: c.title || c.username || c.chat_id, icon: c.type === 'private' ? 'mdiAccount' : c.type === 'channel' ? 'mdiBullhorn' : 'mdiAccountGroup', - desc: `${c.type} · ${c.chat_id}`, + desc: `${c.type}${c.language_code ? ' · ' + c.language_code.toUpperCase() : ''} · ${c.chat_id}`, disabled: existingKeys.has(c.chat_id), disabledHint: existingKeys.has(c.chat_id) ? t('targets.alreadyAdded') : undefined, }))} diff --git a/packages/server/src/notify_bridge_server/api/targets.py b/packages/server/src/notify_bridge_server/api/targets.py index 2680760..24301d5 100644 --- a/packages/server/src/notify_bridge_server/api/targets.py +++ b/packages/server/src/notify_bridge_server/api/targets.py @@ -87,8 +87,9 @@ async def list_targets( ) target_receivers[tgt.id] = list(recv_result.all()) - # Resolve chat names from receivers for telegram targets + # Resolve chat names and languages from receivers for telegram targets chat_names: dict[str, str] = {} + chat_languages: dict[str, str] = {} for tgt in targets: if tgt.type == "telegram": bot_id = tgt.config.get("bot_id") @@ -106,9 +107,12 @@ async def list_targets( chat = chat_result.first() if chat: chat_names[f"{bot_id}_{chat_id}"] = chat.title or chat.username or "" + lang = getattr(chat, 'language_code', '') or '' + if lang: + chat_languages[f"{bot_id}_{chat_id}"] = lang return [ - _target_response(t, chat_names, target_receivers.get(t.id, [])) + _target_response(t, chat_names, target_receivers.get(t.id, []), chat_languages) for t in targets ] @@ -257,6 +261,7 @@ def _target_response( target: NotificationTarget, chat_names: dict[str, str] | None = None, receivers: list[TargetReceiver] | None = None, + chat_languages: dict[str, str] | None = None, ) -> dict: recv_list = receivers or [] resp = { @@ -287,6 +292,8 @@ def _target_response( key = f"{bot_id}_{chat_id}" if key in chat_names: recv_resp["chat_name"] = chat_names[key] + if chat_languages and key in chat_languages: + recv_resp["language_code"] = chat_languages[key] return resp diff --git a/packages/server/src/notify_bridge_server/api/telegram_bots.py b/packages/server/src/notify_bridge_server/api/telegram_bots.py index 2ab3173..5069992 100644 --- a/packages/server/src/notify_bridge_server/api/telegram_bots.py +++ b/packages/server/src/notify_bridge_server/api/telegram_bots.py @@ -9,7 +9,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession import aiohttp -from notify_bridge_core.notifications.telegram.media import TELEGRAM_API_BASE_URL +from notify_bridge_core.notifications.telegram.client import TelegramClient from ..auth.dependencies import get_current_user from ..commands.handler import register_commands_with_telegram @@ -291,22 +291,9 @@ async def test_chat( """Send a test message to a chat via the bot.""" 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)} + async with aiohttp.ClientSession() as http: + client = TelegramClient(http, bot.token) + return await client.send_message(chat_id, message) @router.delete("/{bot_id}/chats/{chat_db_id}", status_code=status.HTTP_204_NO_CONTENT) @@ -328,57 +315,42 @@ async def delete_chat( # --- Helpers --- async def _get_webhook_info(token: str) -> dict | None: - """Call Telegram getWebhookInfo to check current webhook state.""" - try: - async with aiohttp.ClientSession() as http: - async with http.get(f"{TELEGRAM_API_BASE_URL}{token}/getWebhookInfo") as resp: - data = await resp.json() - if data.get("ok"): - return data.get("result", {}) - except aiohttp.ClientError: - pass - return None + """Call Telegram getWebhookInfo via TelegramClient.""" + async with aiohttp.ClientSession() as http: + client = TelegramClient(http, token) + result = await client.get_webhook_info() + return result.get("result") if result.get("success") else None async def _get_me(token: str) -> dict | None: - """Call Telegram getMe to validate token and get bot info.""" - try: - async with aiohttp.ClientSession() as http: - async with http.get(f"{TELEGRAM_API_BASE_URL}{token}/getMe") as resp: - data = await resp.json() - if data.get("ok"): - return data.get("result", {}) - except aiohttp.ClientError: - pass - return None + """Call Telegram getMe via TelegramClient.""" + async with aiohttp.ClientSession() as http: + client = TelegramClient(http, token) + result = await client.get_me() + return result.get("result") if result.get("success") else None async def _fetch_chats_from_telegram(token: str) -> list[dict]: - """Fetch chats from Telegram getUpdates API.""" - seen: dict[int, dict] = {} - try: - async with aiohttp.ClientSession() as http: - async with http.get( - f"{TELEGRAM_API_BASE_URL}{token}/getUpdates", - params={"limit": 100, "allowed_updates": '["message"]'}, - ) as resp: - data = await resp.json() - if not data.get("ok"): - return [] - for update in data.get("result", []): - msg = update.get("message", {}) - chat = msg.get("chat", {}) - chat_id = chat.get("id") - if chat_id and chat_id not in seen: - seen[chat_id] = { - "id": chat_id, - "title": chat.get("title") or (chat.get("first_name", "") + (" " + chat.get("last_name", "")).strip()), - "type": chat.get("type", "private"), - "username": chat.get("username", ""), - } - except aiohttp.ClientError: - pass - return list(seen.values()) + """Fetch chats from Telegram getUpdates via TelegramClient.""" + async with aiohttp.ClientSession() as http: + client = TelegramClient(http, token) + result = await client.get_updates(limit=100) + if not result.get("success"): + return [] + + seen: dict[int, dict] = {} + for update in result.get("result", []): + msg = update.get("message", {}) + chat = msg.get("chat", {}) + chat_id = chat.get("id") + if chat_id and chat_id not in seen: + seen[chat_id] = { + "id": chat_id, + "title": chat.get("title") or (chat.get("first_name", "") + (" " + chat.get("last_name", "")).strip()), + "type": chat.get("type", "private"), + "username": chat.get("username", ""), + } + return list(seen.values()) def _chat_response(c: TelegramChat) -> dict: @@ -388,6 +360,7 @@ def _chat_response(c: TelegramChat) -> dict: "title": c.title, "type": c.chat_type, "username": c.username, + "language_code": getattr(c, 'language_code', '') or '', "discovered_at": c.discovered_at.isoformat(), } diff --git a/packages/server/src/notify_bridge_server/commands/webhook.py b/packages/server/src/notify_bridge_server/commands/webhook.py index 99833da..66f2f62 100644 --- a/packages/server/src/notify_bridge_server/commands/webhook.py +++ b/packages/server/src/notify_bridge_server/commands/webhook.py @@ -10,7 +10,7 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Request from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession -from notify_bridge_core.notifications.telegram.media import TELEGRAM_API_BASE_URL +from notify_bridge_core.notifications.telegram.client import TelegramClient from ..database.engine import get_session from ..database.models import TelegramBot @@ -68,50 +68,37 @@ async def telegram_webhook( return {"ok": True, "skipped": "empty"} # Auto-persist chat from incoming message + from_user = message.get("from", {}) + msg_language = from_user.get("language_code", "") try: - await save_chat_from_webhook(session, bot.id, chat_info) + await save_chat_from_webhook(session, bot.id, chat_info, language_code=msg_language) await session.commit() except Exception: _LOGGER.warning("Failed to auto-save chat %s", chat_id, exc_info=True) # Handle commands if text.startswith("/"): - language_code = message.get("from", {}).get("language_code", "") - cmd_response = await handle_command(bot, chat_id, text, language_code=language_code) + message_id = message.get("message_id") + cmd_response = await handle_command(bot, chat_id, text, language_code=msg_language) if cmd_response is not None: if isinstance(cmd_response, list): - await send_media_group(bot.token, chat_id, cmd_response) + await send_media_group(bot.token, chat_id, cmd_response, reply_to_message_id=message_id) else: - await send_reply(bot.token, chat_id, cmd_response) + await send_reply(bot.token, chat_id, cmd_response, reply_to_message_id=message_id) return {"ok": True} return {"ok": True, "skipped": "not_a_command"} async def register_webhook(bot_token: str, webhook_url: str, secret: str | None = None) -> dict: - """Register webhook URL with Telegram Bot API.""" + """Register webhook URL with Telegram Bot API via TelegramClient.""" async with aiohttp.ClientSession() as http: - url = f"{TELEGRAM_API_BASE_URL}{bot_token}/setWebhook" - payload: dict[str, Any] = {"url": webhook_url} - if secret: - payload["secret_token"] = secret - try: - async with http.post(url, json=payload) as resp: - result = await resp.json() - if result.get("ok"): - return {"success": True} - return {"success": False, "error": result.get("description")} - except aiohttp.ClientError as err: - return {"success": False, "error": str(err)} + client = TelegramClient(http, bot_token) + return await client.set_webhook(webhook_url, secret=secret) async def unregister_webhook(bot_token: str) -> dict: - """Remove webhook from Telegram Bot API.""" + """Remove webhook from Telegram Bot API via TelegramClient.""" async with aiohttp.ClientSession() as http: - url = f"{TELEGRAM_API_BASE_URL}{bot_token}/deleteWebhook" - try: - async with http.post(url) as resp: - result = await resp.json() - return {"success": result.get("ok", False)} - except aiohttp.ClientError as err: - return {"success": False, "error": str(err)} + client = TelegramClient(http, bot_token) + return await client.delete_webhook() diff --git a/packages/server/src/notify_bridge_server/database/migrations.py b/packages/server/src/notify_bridge_server/database/migrations.py index 1ce0715..a54180c 100644 --- a/packages/server/src/notify_bridge_server/database/migrations.py +++ b/packages/server/src/notify_bridge_server/database/migrations.py @@ -173,6 +173,14 @@ async def migrate_schema(engine: AsyncEngine) -> None: ) logger.info("Added shared column to %s table", state_table) + # Add language_code to telegram_chat if missing + if await _has_table(conn, "telegram_chat"): + if not await _has_column(conn, "telegram_chat", "language_code"): + await conn.execute( + text("ALTER TABLE telegram_chat ADD COLUMN language_code TEXT DEFAULT ''") + ) + logger.info("Added language_code column to telegram_chat table") + # --------------------------------------------------------------------------- # Legacy tracker_target migration (pre-Phase 1) diff --git a/packages/server/src/notify_bridge_server/database/models.py b/packages/server/src/notify_bridge_server/database/models.py index 03a77be..df35b98 100644 --- a/packages/server/src/notify_bridge_server/database/models.py +++ b/packages/server/src/notify_bridge_server/database/models.py @@ -95,6 +95,7 @@ class TelegramChat(SQLModel, table=True): title: str = Field(default="") chat_type: str = Field(default="private") username: str = Field(default="") + language_code: str = Field(default="") discovered_at: datetime = Field(default_factory=_utcnow) diff --git a/packages/server/src/notify_bridge_server/services/telegram.py b/packages/server/src/notify_bridge_server/services/telegram.py index 2943cf9..8216640 100644 --- a/packages/server/src/notify_bridge_server/services/telegram.py +++ b/packages/server/src/notify_bridge_server/services/telegram.py @@ -7,7 +7,8 @@ from ..database.models import TelegramChat async def save_chat_from_webhook( - session: AsyncSession, bot_id: int, chat_data: dict + session: AsyncSession, bot_id: int, chat_data: dict, + language_code: str = "", ) -> None: """Save or update a chat entry from an incoming webhook message. @@ -32,6 +33,8 @@ async def save_chat_from_webhook( if existing: existing.title = title existing.username = chat_data.get("username", existing.username) + if language_code: + existing.language_code = language_code session.add(existing) else: session.add(TelegramChat( @@ -40,4 +43,5 @@ async def save_chat_from_webhook( title=title, chat_type=chat_data.get("type", "private"), username=chat_data.get("username", ""), + language_code=language_code, )) diff --git a/packages/server/src/notify_bridge_server/services/telegram_poller.py b/packages/server/src/notify_bridge_server/services/telegram_poller.py index 1203d51..d0ea1a4 100644 --- a/packages/server/src/notify_bridge_server/services/telegram_poller.py +++ b/packages/server/src/notify_bridge_server/services/telegram_poller.py @@ -17,7 +17,7 @@ import aiohttp from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession -from notify_bridge_core.notifications.telegram.media import TELEGRAM_API_BASE_URL +from notify_bridge_core.notifications.telegram.client import TelegramClient from ..database.engine import get_engine from ..database.models import CommandTracker, CommandTrackerListener, TelegramBot @@ -150,25 +150,16 @@ async def _poll_bot(bot_id: int) -> None: bot_obj = bot offset = _last_update_id.get(bot_id, 0) - params: dict[str, Any] = { - "timeout": 0, - "limit": 50, - "allowed_updates": '["message"]', - } - if offset: - params["offset"] = offset + 1 try: async with aiohttp.ClientSession() as http: - async with http.get( - f"{TELEGRAM_API_BASE_URL}{bot_token}/getUpdates", - params=params, - timeout=aiohttp.ClientTimeout(total=10), - ) as resp: - data = await resp.json() - if not data.get("ok"): - return - updates = data.get("result", []) + client = TelegramClient(http, bot_token) + result = await client.get_updates( + offset=offset + 1 if offset else None, limit=50, + ) + if not result.get("success"): + return + updates = result.get("result", []) except Exception as e: _LOGGER.debug("Polling error for bot %d: %s", bot_id, e) return @@ -190,6 +181,8 @@ async def _poll_bot(bot_id: int) -> None: chat_info = message.get("chat", {}) chat_id = str(chat_info.get("id", "")) text = message.get("text", "") + from_user = message.get("from", {}) + msg_language = from_user.get("language_code", "") if not chat_id: continue @@ -197,7 +190,7 @@ async def _poll_bot(bot_id: int) -> None: # Auto-persist chat (fresh session per save) try: async with AsyncSession(engine) as save_session: - await save_chat_from_webhook(save_session, bot_obj.id, chat_info) + await save_chat_from_webhook(save_session, bot_obj.id, chat_info, language_code=msg_language) await save_session.commit() except Exception: _LOGGER.debug("Failed to auto-save chat %s", chat_id, exc_info=True) @@ -205,13 +198,13 @@ async def _poll_bot(bot_id: int) -> None: # Dispatch commands if text and text.startswith("/"): try: - language_code = message.get("from", {}).get("language_code", "") - cmd_response = await handle_command(bot_obj, chat_id, text, language_code=language_code) + message_id = message.get("message_id") + cmd_response = await handle_command(bot_obj, chat_id, text, language_code=msg_language) if cmd_response is not None: if isinstance(cmd_response, list): - await send_media_group(bot_token, chat_id, cmd_response) + await send_media_group(bot_token, chat_id, cmd_response, reply_to_message_id=message_id) else: - await send_reply(bot_token, chat_id, cmd_response) + await send_reply(bot_token, chat_id, cmd_response, reply_to_message_id=message_id) except Exception: _LOGGER.error("Error handling command from bot %d", bot_id, exc_info=True) diff --git a/scripts/restart-backend.sh b/scripts/restart-backend.sh new file mode 100644 index 0000000..7add044 --- /dev/null +++ b/scripts/restart-backend.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Restart the backend dev server. +# Usage: bash scripts/restart-backend.sh + +set -e +cd "$(dirname "$0")/.." + +# Kill existing backend +PID=$(netstat -ano 2>/dev/null | grep ':8420.*LISTENING' | awk '{print $5}' | head -1) +if [ -n "$PID" ]; then + taskkill //F //PID "$PID" 2>/dev/null || true + sleep 1 +fi + +# Start backend +NOTIFY_BRIDGE_DATA_DIR=./test-data \ +NOTIFY_BRIDGE_SECRET_KEY=test-secret-key-minimum-32chars \ +nohup python -m uvicorn notify_bridge_server.main:app \ + --host 0.0.0.0 --port 8420 > /dev/null 2>&1 & + +sleep 3 +curl -s http://localhost:8420/api/health diff --git a/scripts/restart-frontend.sh b/scripts/restart-frontend.sh new file mode 100644 index 0000000..016c5d1 --- /dev/null +++ b/scripts/restart-frontend.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Restart the frontend dev server. +# Usage: bash scripts/restart-frontend.sh + +set -e +cd "$(dirname "$0")/.." + +# Kill existing frontend +PID=$(netstat -ano 2>/dev/null | grep ':5173.*LISTENING' | awk '{print $5}' | head -1) +if [ -n "$PID" ]; then + taskkill //F //PID "$PID" 2>/dev/null || true + sleep 1 +fi + +# Start frontend +cd frontend +npx vite dev --port 5173 --host > /dev/null 2>&1 & +cd .. + +sleep 4 +curl -s -o /dev/null -w "Frontend: %{http_code}\n" http://localhost:5173/