feat: locale-aware command templates, debounced auto-sync, entity pickers

- Locale-aware templates: CommandTemplateSlot now has a locale column,
  allowing each slot to have per-language variants (EN/RU). Templates
  are resolved at runtime from the Telegram user's language_code.

- Merged system configs: "Default Commands (EN)" and "(RU)" merged
  into a single "Default Commands" config with locale-aware slots.
  Migration handles existing data automatically.

- Configurable command descriptions: hardcoded COMMAND_DESCRIPTIONS
  replaced with desc_* template slots (desc_status, desc_help, etc.)
  that users can customize per locale. setMyCommands registers all
  locales explicitly.

- Removed locale from CommandConfig: no longer needed since locale
  is derived from the Telegram user's language at runtime.

- Debounced command auto-sync: after command config/tracker changes,
  affected bots are marked dirty and synced after a 30s debounce
  window. Manual "Sync with Telegram" button still works.

- Entity pickers in LinkedTargetsSection: replaced 6 plain <select>
  elements with EntitySelect components (search, icons, keyboard nav).
  Added onselect callback and size="sm" props to EntitySelect.
This commit is contained in:
2026-03-22 03:14:51 +03:00
parent 751097b347
commit 1167d138a3
47 changed files with 604 additions and 230 deletions
@@ -850,3 +850,95 @@ async def migrate_template_locale(engine: AsyncEngine) -> None:
await conn.execute(text(
f"UPDATE {table} SET locale = 'en' WHERE user_id = 0 AND locale = ''"
))
async def migrate_command_slot_locale(engine: AsyncEngine) -> None:
"""Add locale column to command_template_slot and merge system EN/RU configs.
1. Recreate command_template_slot with locale column and new unique constraint
2. Backfill locale from parent config's locale (or 'en')
3. Merge "Default Commands (RU)" slots into "Default Commands (EN)" with locale='ru'
4. Rename merged config, update references, delete orphan RU config
"""
async with engine.begin() as conn:
if not await _has_table(conn, "command_template_slot"):
return
# Skip if locale column already exists (idempotent)
if await _has_column(conn, "command_template_slot", "locale"):
return
logger.info("Adding locale column to command_template_slot and merging system configs")
# Step 1: Recreate table with locale column and new unique constraint
await conn.execute(text(
"CREATE TABLE command_template_slot_new ("
" id INTEGER PRIMARY KEY,"
" config_id INTEGER NOT NULL REFERENCES command_template_config(id),"
" slot_name TEXT NOT NULL,"
" locale TEXT NOT NULL DEFAULT 'en',"
" template TEXT DEFAULT '',"
" UNIQUE(config_id, slot_name, locale)"
")"
))
# Step 2: Copy existing data, deriving locale from parent config
await conn.execute(text(
"INSERT INTO command_template_slot_new (id, config_id, slot_name, locale, template) "
"SELECT s.id, s.config_id, s.slot_name, "
" CASE WHEN c.locale != '' THEN c.locale ELSE 'en' END, "
" s.template "
"FROM command_template_slot s "
"LEFT JOIN command_template_config c ON s.config_id = c.id"
))
await conn.execute(text("DROP TABLE command_template_slot"))
await conn.execute(text(
"ALTER TABLE command_template_slot_new RENAME TO command_template_slot"
))
# Step 3: Merge system EN/RU configs into one
# Find the system EN and RU config IDs
en_row = (await conn.execute(text(
"SELECT id FROM command_template_config "
"WHERE user_id = 0 AND (locale = 'en' OR name LIKE '%(EN)%') "
"LIMIT 1"
))).fetchone()
ru_row = (await conn.execute(text(
"SELECT id FROM command_template_config "
"WHERE user_id = 0 AND (locale = 'ru' OR name LIKE '%(RU)%') "
"LIMIT 1"
))).fetchone()
if en_row and ru_row and en_row[0] != ru_row[0]:
en_id, ru_id = en_row[0], ru_row[0]
# Move RU slots to the EN config (they already have locale='ru')
await conn.execute(text(
"UPDATE command_template_slot SET config_id = :en_id "
"WHERE config_id = :ru_id"
), {"en_id": en_id, "ru_id": ru_id})
# Update any command_config references from RU to EN
if await _has_table(conn, "command_config"):
await conn.execute(text(
"UPDATE command_config SET command_template_config_id = :en_id "
"WHERE command_template_config_id = :ru_id"
), {"en_id": en_id, "ru_id": ru_id})
# Delete the orphan RU config
await conn.execute(text(
"DELETE FROM command_template_config WHERE id = :ru_id"
), {"ru_id": ru_id})
# Rename the merged config
await conn.execute(text(
"UPDATE command_template_config SET name = 'Default Commands', "
"description = 'Default Immich command templates', locale = '' "
"WHERE id = :en_id"
), {"en_id": en_id})
logger.info(
"Merged system command template configs (EN=%d, RU=%d) into single config %d",
en_id, ru_id, en_id,
)