feat: observability, per-receiver Telegram options, oversized-video fallback
Operability: - Correlation IDs end-to-end: shared dispatch_id between log lines and EventLog rows (event/watcher/scheduled/deferred/action/HA/command paths) and a new X-Request-Id middleware that normalizes inbound ids and binds request_id into log context. - dispatch_summary block merged into EventLog.details: per-target success/failure counts plus Telegram media delivered/skipped/failed and truncated error lists, so partial outcomes surface in the UI. - Diagnostic mode: admin can flip one module to DEBUG for a bounded window with auto-revert (in-memory only; setup_logging() resets on boot, lifespan reverts on shutdown). New /diagnostic-mode endpoints plus DiagnosticsCassette UI on the settings page. Telegram: - Per-receiver options: disable_notification (silent send) and message_thread_id (forum-topic routing), wired through the dispatcher via a ContextVar so all four send sites (sendMessage / sendPhoto-Video- Document / sendMediaGroup / cache-hit POST) pick them up. - send_large_videos_as_documents target setting: bypass the 50 MB sendVideo cap by falling back to sendDocument for oversized videos. - sendMediaGroup byte-budget enforcement (TELEGRAM_MAX_GROUP_TOTAL_BYTES, 45 MB) with per-item fallback on chunk failure so a stale file_id no longer silently drops a cached asset. Tests: - New: diagnostic_mode, dispatch_summary, request_correlation, telegram_media_group_partial, telegram_per_send_options. Docs: - .claude/reviews/: six-axis production-readiness review of v0.8.1. - .claude/docs/functional-review-2026-05-28.md: focused review of Telegram/Immich/logging subsystems.
This commit is contained in:
@@ -166,7 +166,7 @@
|
||||
const defaultForm = () => ({
|
||||
name: '', icon: '', bot_id: 0, bot_token: '',
|
||||
max_media_to_send: 50, max_media_per_group: 10, media_delay: 500, max_asset_size: 50,
|
||||
disable_url_preview: true, send_large_photos_as_documents: false, ai_captions: false, chat_action: 'typing',
|
||||
disable_url_preview: true, send_large_photos_as_documents: false, send_large_videos_as_documents: false, ai_captions: false, chat_action: 'typing',
|
||||
// Discord/Slack shared settings
|
||||
username: '',
|
||||
// ntfy shared settings
|
||||
@@ -407,7 +407,7 @@
|
||||
bot_id: c.bot_id || 0, bot_token: '',
|
||||
max_media_to_send: c.max_media_to_send ?? 50, max_media_per_group: c.max_media_per_group ?? 10,
|
||||
media_delay: c.media_delay ?? 500, max_asset_size: c.max_asset_size ?? 50,
|
||||
disable_url_preview: c.disable_url_preview ?? false, send_large_photos_as_documents: c.send_large_photos_as_documents ?? false,
|
||||
disable_url_preview: c.disable_url_preview ?? false, send_large_photos_as_documents: c.send_large_photos_as_documents ?? false, send_large_videos_as_documents: c.send_large_videos_as_documents ?? false,
|
||||
ai_captions: c.ai_captions ?? false, chat_action: tgt.chat_action ?? c.chat_action ?? 'typing',
|
||||
// discord/slack
|
||||
username: c.username || '',
|
||||
@@ -448,6 +448,7 @@
|
||||
max_media_to_send: form.max_media_to_send, max_media_per_group: form.max_media_per_group,
|
||||
media_delay: form.media_delay, max_asset_size: form.max_asset_size,
|
||||
disable_url_preview: form.disable_url_preview, send_large_photos_as_documents: form.send_large_photos_as_documents,
|
||||
send_large_videos_as_documents: form.send_large_videos_as_documents,
|
||||
ai_captions: form.ai_captions,
|
||||
};
|
||||
} else if (formType === 'webhook') {
|
||||
@@ -603,6 +604,63 @@
|
||||
} catch (err: unknown) { snackError(errMsg(err)); }
|
||||
}
|
||||
|
||||
// Per-Telegram-receiver options panel: silent send + forum thread id.
|
||||
// Edits the receiver's config dict in place via PUT.
|
||||
let editingReceiverId = $state<number | null>(null);
|
||||
// ``<input type="number">`` binds either a ``number`` or empty string
|
||||
// when the field is blank — model both so TS strict mode and the save
|
||||
// path's ``Number(raw)`` coercion agree.
|
||||
let editingReceiverOptions = $state<{ disable_notification: boolean; message_thread_id: number | '' }>({
|
||||
disable_notification: false,
|
||||
message_thread_id: '',
|
||||
});
|
||||
|
||||
function openEditReceiver(_targetId: number, receiver: TargetReceiver) {
|
||||
editingReceiverId = receiver.id;
|
||||
// Empty string maps to "no thread" — the form's <input type=number>
|
||||
// produces '' for an empty field, which we normalize to null on save.
|
||||
const raw = receiver.config?.message_thread_id;
|
||||
const parsed = raw == null || raw === '' ? '' : Number(raw);
|
||||
editingReceiverOptions = {
|
||||
disable_notification: Boolean(receiver.config?.disable_notification),
|
||||
message_thread_id: typeof parsed === 'number' && Number.isFinite(parsed) ? parsed : '',
|
||||
};
|
||||
}
|
||||
|
||||
function cancelEditReceiver() {
|
||||
editingReceiverId = null;
|
||||
}
|
||||
|
||||
async function saveEditReceiver(targetId: number, receiverId: number) {
|
||||
const target = allTargets.find(t => t.id === targetId);
|
||||
const receiver = target?.receivers?.find(r => r.id === receiverId);
|
||||
if (!receiver) return;
|
||||
// Merge new options into the existing config so we don't lose the chat_id
|
||||
// or any other receiver-specific keys (language_code on Telegram).
|
||||
const newConfig: Record<string, any> = { ...receiver.config };
|
||||
newConfig.disable_notification = editingReceiverOptions.disable_notification;
|
||||
const raw = editingReceiverOptions.message_thread_id;
|
||||
if (raw === '' || raw == null) {
|
||||
delete newConfig.message_thread_id;
|
||||
} else {
|
||||
const parsed = Number(raw);
|
||||
if (Number.isFinite(parsed) && parsed > 0) {
|
||||
newConfig.message_thread_id = Math.trunc(parsed);
|
||||
} else {
|
||||
delete newConfig.message_thread_id;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await api(`/targets/${targetId}/receivers/${receiverId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ config: newConfig }),
|
||||
});
|
||||
editingReceiverId = null;
|
||||
await load();
|
||||
snackSuccess(t('targets.telegramOptionsSaved'));
|
||||
} catch (err: unknown) { snackError(errMsg(err)); }
|
||||
}
|
||||
|
||||
async function toggleBroadcastChild(targetId: number, childId: number) {
|
||||
const tgt = allTargets.find(t => t.id === targetId);
|
||||
if (!tgt) return;
|
||||
@@ -753,6 +811,8 @@
|
||||
{receiverBotChats}
|
||||
{receiverTesting}
|
||||
{receiverLabel}
|
||||
{editingReceiverId}
|
||||
bind:editingReceiverOptions
|
||||
onopenReceiverForm={openReceiverForm}
|
||||
onsaveReceiver={saveReceiver}
|
||||
oncancelReceiver={() => addingReceiverForTarget = null}
|
||||
@@ -762,6 +822,9 @@
|
||||
onloadBotChats={loadReceiverBotChats}
|
||||
onchangeReceiverForm={(f) => receiverForm = f}
|
||||
ontoggleBroadcastChild={toggleBroadcastChild}
|
||||
onopenEditReceiver={openEditReceiver}
|
||||
oncancelEditReceiver={cancelEditReceiver}
|
||||
onsaveEditReceiver={saveEditReceiver}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user