Namespace quest definition icon keys with legacy back-compat

Add core/icons.py as the one place icon keys are validated and
normalized. A definition icon is now stored as lucide:<name>,
fa:<name> or emoji:<payload> (at most 8 code points, no whitespace,
control characters or markup). Create and edit validate strictly and
store the canonical form; a legacy bare name is stored as
lucide:<name>, and None still clears the icon.

Reads normalize leniently so existing bare-name rows need no data
migration: the admin quest-definition response and the panel snapshot's
per-instance icon both return the namespaced form.

Task 707994b7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 07:18:49 -04:00
co-authored by Claude Opus 5.5
parent fb9907ae7f
commit 7af0b5a6cc
9 changed files with 411 additions and 15 deletions
+5
View File
@@ -152,6 +152,10 @@ core_quest_definitions = importlib.import_module(
f"{_CORE_MODULE_NAME}.quest_definitions"
)
#: The icon-key module (``core.icons.normalize_icon_for_read``) the
#: admin plane's quest-definition responses normalize ``icon`` through.
core_icons = importlib.import_module(f"{_CORE_MODULE_NAME}.icons")
#: The recurrence model (``core.recurrence.ScheduleRule`` and
#: ``RuleValidationError``) the quest-definition routes build request
#: rules with — the model validates at construction, the routes never
@@ -215,6 +219,7 @@ __all__ = [
"core_db",
"core_events",
"core_history",
"core_icons",
"core_materialize",
"core_migrations",
"core_presence",
+2 -1
View File
@@ -336,6 +336,7 @@ from api.nestquest_core import (
core_dao_children,
core_dao_rules,
core_history,
core_icons,
core_materialize,
core_presence_management,
core_quest_definitions,
@@ -1205,7 +1206,7 @@ def _quest_definition_response(
id=bundle.definition.id,
title=bundle.definition.title,
description=bundle.definition.description,
icon=bundle.definition.icon,
icon=core_icons.normalize_icon_for_read(bundle.definition.icon),
is_active=bundle.definition.is_active,
skip_on_away=bundle.definition.skip_on_away,
rule=_rule_response(bundle.rule),