feat: remove hardcoded command templates, enforce template system exclusively

- Remove ALL hardcoded EN/RU fallback strings from handler.py — every
  command response now renders through CommandTemplateSlot templates
- _render_cmd_template now returns error placeholder instead of None
  when template is missing, ensuring no silent failures
- Fix register_commands_with_telegram tuple unpacking bug (was ignoring
  cmd_template_slots from _resolve_command_context)
- Auto-assign system default template (matching locale) on command
  config creation when none specified
- Add command_template_config_id to CommandConfigCreate model
- Remove "no template" option from frontend dropdown — template is
  now required for command configs
- Auto-select first matching template when creating new command config
- Fix || vs ?? for command_template_config_id, default_count, and
  rate_limits in frontend edit function (0 was treated as falsy)
This commit is contained in:
2026-03-21 22:53:07 +03:00
parent 3e3a6f0777
commit ddcbfdaa0b
3 changed files with 102 additions and 156 deletions
@@ -10,7 +10,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
from ..auth.dependencies import get_current_user
from ..database.engine import get_session
from ..database.models import CommandConfig, CommandTracker, User
from ..database.models import CommandConfig, CommandTemplateConfig, CommandTracker, User
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +26,7 @@ class CommandConfigCreate(BaseModel):
response_mode: str = "media"
default_count: int = 5
rate_limits: dict[str, Any] = {}
command_template_config_id: int | None = None
class CommandConfigUpdate(BaseModel):
@@ -36,6 +37,7 @@ class CommandConfigUpdate(BaseModel):
response_mode: str | None = None
default_count: int | None = None
rate_limits: dict[str, Any] | None = None
command_template_config_id: int | None = None
@router.get("")
@@ -65,7 +67,18 @@ async def create_command_config(
detail=f"Invalid provider_type. Must be one of: {', '.join(valid_types)}",
)
config = CommandConfig(user_id=user.id, **body.model_dump())
data = body.model_dump()
# Auto-assign system default template if none specified
if not data.get("command_template_config_id"):
locale = data.get("locale", "en")
provider_type = data.get("provider_type", "immich")
default_tpl = await _find_system_default_template(
session, provider_type, locale
)
if default_tpl:
data["command_template_config_id"] = default_tpl.id
config = CommandConfig(user_id=user.id, **data)
session.add(config)
await session.commit()
await session.refresh(config)
@@ -91,9 +104,13 @@ async def update_command_config(
session: AsyncSession = Depends(get_session),
):
"""Update a command config."""
from sqlalchemy.orm.attributes import flag_modified
config = await _get_user_config(session, config_id, user.id)
for field, value in body.model_dump(exclude_unset=True).items():
setattr(config, field, value)
# JSON columns need explicit dirty flag for SQLAlchemy change detection
if field in ("rate_limits", "enabled_commands"):
flag_modified(config, field)
session.add(config)
await session.commit()
await session.refresh(config)
@@ -129,6 +146,7 @@ def _config_response(c: CommandConfig) -> dict:
"response_mode": c.response_mode,
"default_count": c.default_count,
"rate_limits": c.rate_limits or {},
"command_template_config_id": c.command_template_config_id,
"created_at": c.created_at.isoformat(),
}
@@ -140,3 +158,24 @@ async def _get_user_config(
if not config or config.user_id != user_id:
raise HTTPException(status_code=404, detail="Command config not found")
return config
async def _find_system_default_template(
session: AsyncSession, provider_type: str, locale: str
) -> CommandTemplateConfig | None:
"""Find a system default (user_id=0) command template matching provider + locale."""
# Try exact locale match first (e.g. "Default Commands (EN)" for locale "en")
locale_upper = locale.upper()
result = await session.exec(
select(CommandTemplateConfig).where(
CommandTemplateConfig.user_id == 0,
CommandTemplateConfig.provider_type == provider_type,
)
)
templates = result.all()
# Match by locale suffix in name, e.g. "(EN)" or "(RU)"
for tpl in templates:
if f"({locale_upper})" in tpl.name:
return tpl
# Fallback: return first system template for this provider
return templates[0] if templates else None