Phase 10: Telegram bot commands + Phase 11: Snackbar notifications
All checks were successful
Validate / Hassfest (push) Successful in 3s

Phase 10 — Telegram Bot Commands:
- Add commands_config JSON field to TelegramBot model (enabled cmds,
  default count, response mode, rate limits, locale)
- Create command handler with 14 commands: /status, /albums, /events,
  /summary, /latest, /memory, /random, /search, /find, /person,
  /place, /favorites, /people, /help
- Add search_smart, search_metadata, search_by_person, get_random,
  download_asset, get_asset_thumbnail to ImmichClient
- Auto-register commands with Telegram setMyCommands API (EN+RU)
- Rate limiting per chat per command category
- Media mode: download thumbnails and send as photos to Telegram
- Webhook handler routes /commands before falling through to AI chat
- Frontend: expandable Commands section per bot with checkboxes,
  count/mode/locale settings, rate limit inputs, sync button

Phase 11 — Snackbar Notifications:
- Create snackbar store (snackbar.svelte.ts) with $state rune
- Create Snackbar component with fly/fade transitions, typed colors
- Mount globally in +layout.svelte
- Replace all alert() calls with typed snackbar notifications
- Add success snacks to all CRUD operations across all pages
- 4 types: success (3s), error (5s), info (3s), warning (4s)
- Max 3 visible, auto-dismiss, manual dismiss via X button

Both: Add ~30 i18n keys (EN+RU) for commands UI and snack messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 21:39:05 +03:00
parent ffce3ee337
commit e6ff0a423a
20 changed files with 1384 additions and 70 deletions

View File

@@ -10,6 +10,7 @@
import MdiIcon from '$lib/components/MdiIcon.svelte';
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
let servers = $state<any[]>([]);
let showForm = $state(false);
@@ -30,6 +31,7 @@
loadError = '';
} catch (err: any) {
loadError = err.message || t('servers.loadError');
snackError(loadError);
} finally { loaded = true; }
// Ping all servers in background
for (const s of servers) {
@@ -58,7 +60,8 @@
await api('/servers', { method: 'POST', body: JSON.stringify(form) });
}
showForm = false; editing = null; await load();
} catch (err: any) { error = err.message; }
snackSuccess(t('snack.serverSaved'));
} catch (err: any) { error = err.message; snackError(err.message); }
submitting = false;
}
@@ -70,7 +73,7 @@
if (!confirmDelete) return;
const id = confirmDelete.id;
confirmDelete = null;
try { await api(`/servers/${id}`, { method: 'DELETE' }); await load(); } catch (err: any) { error = err.message; }
try { await api(`/servers/${id}`, { method: 'DELETE' }); await load(); snackSuccess(t('snack.serverDeleted')); } catch (err: any) { error = err.message; snackError(err.message); }
}
</script>