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
This commit is contained in:
2026-03-22 23:39:52 +03:00
parent e90c128dca
commit 82e400ddcd
16 changed files with 161 additions and 122 deletions
@@ -306,6 +306,7 @@
<div class="flex items-center gap-2">
<span class="font-medium">{chat.title || chat.username || 'Unknown'}</span>
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{chatTypeLabel(chat.type)}</span>
{#if chat.language_code}<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{chat.language_code.toUpperCase()}</span>{/if}
<span class="text-xs text-[var(--color-muted-foreground)] font-mono">{chat.chat_id}</span>
</div>
<div class="flex items-center gap-1">
@@ -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}
@@ -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<EntityItem[]>(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,
})));
</script>
+18 -3
View File
@@ -311,7 +311,19 @@
receiverSubmitting = true;
receiverHeadersError = '';
try {
const config = { ...receiverForm };
const config: Record<string, any> = { ...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 @@
<div class="flex items-center gap-2 min-w-0">
<MdiIcon name={TYPE_ICONS[target.type] || 'mdiTarget'} size={14} />
<span class="text-sm truncate">{receiverLabel(target, recv)}</span>
{#if (recv as any).language_code || recv.config?.language_code}
<span class="text-xs px-1 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{((recv as any).language_code || recv.config.language_code).toUpperCase()}</span>
{/if}
</div>
<div class="flex items-center gap-1">
<IconButton icon="mdiSend" title={t('targets.test')}
@@ -563,11 +578,11 @@
{#if target.type === 'telegram'}
{@const botId = target.config?.bot_id}
{@const existingKeys = new Set((target.receivers || []).map((r: TargetReceiver) => 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,
}))}