import html import urllib.parse from pathlib import Path from .i18n import available_languages, normalize_language, text_for PACKAGE_DIR = Path(__file__).resolve().parent TEMPLATE_PATH = PACKAGE_DIR / "templates" / "index.html" STATIC_DIR = PACKAGE_DIR / "static" def read_static_file(name): # The HTTP layer serves only explicitly named static files from this package # directory. This avoids exposing arbitrary local paths. path = STATIC_DIR / name if not path.is_file() or path.parent != STATIC_DIR: return None return path.read_bytes() def action_visual(action, lang): # Keep GUI visuals tied to action semantics, not to local camera names. New # action families can add another name_key/kind branch without changing MQTT # topics or camera command handling. name_key = action.cfg.get("name_key") if name_key == "action.siren": return { "kind": "siren", "label": text_for(lang, "visual.siren"), "svg": ( '" ), } if name_key == "action.red_blue": return { "kind": "red-blue", "label": text_for(lang, "visual.red_blue"), "svg": ( '" ), } if name_key == "action.manual_white": return { "kind": "white-light", "label": text_for(lang, "visual.white_light"), "svg": ( '" ), } if action.kind == "all_off": return { "kind": "all-off", "label": text_for(lang, "visual.all_off"), "svg": ( '" ), } return { "kind": "generic", "label": text_for(lang, "visual.generic"), "svg": ( '" ), } def visual_markup(action, lang): visual = action_visual(action, lang) return ( f'
' f'{visual["svg"]}
' ) def render_card(action, lang, token_query): display_name = action.display_name(lang) icon = visual_markup(action, lang) snapshot_markup = ( '
' f"{icon}" f'{html.escape(text_for(lang, "no_preview"))}' "
" ) if action.cfg.get("snapshot"): snapshot_url = ( f"/snapshot/{urllib.parse.quote(action.id)}.jpg" f"?lang={urllib.parse.quote(lang)}{token_query}" ) snapshot_markup = ( '
' f'' f"{icon}" "
" ) buttons = [] for command in action.command_list(): label = text_for(lang, f"button.{command}") action_url = ( f"/action/{urllib.parse.quote(action.id)}/{urllib.parse.quote(command)}" f"?lang={urllib.parse.quote(lang)}{token_query}" ) ack_attrs = "" if command == "on" and action.cfg.get("requires_ack"): ack_attrs = ( ' data-requires-ack="true"' f' data-ack-message="{html.escape(text_for(lang, "ack.siren_on"))}"' ) buttons.append( f'
' f'
" ) state_value = (action.state or "unknown").lower() state_label = text_for(lang, f"state.{state_value}") if state_label == f"state.{state_value}": state_label = state_value.upper() platform_label = text_for(lang, f"platform.{action.platform}") if platform_label == f"platform.{action.platform}": platform_label = action.platform timer_markup = "" if action.auto_off_seconds > 0: if action.auto_off_at: timer_markup = ( f'' f'{html.escape(text_for(lang, "timer.auto_off"))}' ) else: timer_markup = ( f'' f'{html.escape(text_for(lang, "timer.auto_off_configured", seconds=action.auto_off_seconds))}' "" ) return ( '
' f"{snapshot_markup}" '
' '
' f"

{html.escape(display_name)}

" f'{html.escape(action.id)}' "
" '
' f'{html.escape(platform_label)}' f'' f"{html.escape(state_label)}" "
" f"{timer_markup}" f'
{"".join(buttons)}
' "
" "
" ) def render_language_options(lang): options = [] for code, label in available_languages().items(): selected = " selected" if code == lang else "" options.append(f'') return "".join(options) def render_page(actions, lang, token_param=None): lang = normalize_language(lang) token_query = f"&token={urllib.parse.quote(token_param)}" if token_param else "" cards = [render_card(action, lang, token_query) for action in actions] cards_markup = "".join(cards) if not cards_markup: cards_markup = f'
{html.escape(text_for(lang, "no_actions"))}
' token_input = ( f'' if token_param else "" ) template = TEMPLATE_PATH.read_text(encoding="utf-8") return template.format( lang=html.escape(lang), title=html.escape(text_for(lang, "title")), action_count=len(cards), actions_label=html.escape(text_for(lang, "actions")), language_label=html.escape(text_for(lang, "language")), language_options=render_language_options(lang), token_input=token_input, cards=cards_markup, )