This commit is contained in:
Home Assistant Version Control
2026-08-31 14:54:46 +00:00
parent 38964507bd
commit 1770e33ddc
28 changed files with 692 additions and 232 deletions
+27 -27
View File
@@ -16,7 +16,7 @@
"activity.claimed": "behauptet",
"activity.completed": "vollendet",
"activity.completed_chores_will_appear": "Abgeschlossene Aufgaben werden hier angezeigt",
"activity.days_ago": "{count}d ago",
"activity.days_ago": "vor {count} T.",
"activity.default_title": "Aktivität",
"activity.recent_events": "Letzte Ereignisse",
"activity.undo": "Rückgängig",
@@ -25,30 +25,30 @@
"activity.undo_confirm_txn": "„{detail}“ für {child} rückgängig machen? Dies kehrt {points} Punkte um.",
"activity.undo_error_title": "TaskMate — Rückgängig fehlgeschlagen",
"activity.undo_error_body": "Konnte nicht rückgängig gemacht werden: {message}",
"activity.editor.accent_stripes_helper": "Show the 4px coloured stripe at the left of each row indicating event type",
"activity.editor.accent_stripes": "Coloured event stripes",
"activity.editor.accent_stripes_helper": "Zeigt links an jeder Zeile den 4 px breiten Farbstreifen, der den Ereignistyp angibt",
"activity.editor.accent_stripes": "Farbige Ereignisstreifen",
"activity.editor.filter_child_helper": "Nur Aktivität für dieses Kind anzeigen",
"activity.editor.max_items": "Maximale Anzahl an Artikeln",
"activity.editor.max_items_helper": "Maximale Anzahl anzuzeigender Ereignisse (Standard: 30)",
"activity.editor.show_relative_time_helper": "Show 'just now' / '5m ago' alongside the absolute time on each row",
"activity.editor.show_relative_time": "Relative time labels",
"activity.editor.show_filter_chips_helper": "Show the All / Chores / Rewards / Adjustments chip bar below the header",
"activity.editor.show_filter_chips": "Show filter chips",
"activity.editor.show_relative_time_helper": "Zeigt „gerade eben“ / „vor 5 Min.“ neben der genauen Uhrzeit in jeder Zeile",
"activity.editor.show_relative_time": "Relative Zeitangaben",
"activity.editor.show_filter_chips_helper": "Zeigt die Leiste Alle / Hausarbeiten / Belohnungen / Anpassungen unter der Kopfzeile",
"activity.editor.show_filter_chips": "Filterleiste anzeigen",
"activity.editor.show_undo": "Rückgängig-Schaltflächen anzeigen",
"activity.editor.show_undo_helper": "Zeigt die Rückgängig-Schaltfläche in jeder Zeile. Für ein kindgerechtes Dashboard deaktivieren, damit Kinder ihre eigene Aktivität nicht rückgängig machen können.",
"activity.events_count": "{count} Ereignisse",
"activity.just_now": "just now",
"activity.hours_ago": "{count}h ago",
"activity.filter_rewards": "Rewards",
"activity.filter_chores": "Chores",
"activity.filter_aria_label": "Filter activity by type",
"activity.filter_all": "All",
"activity.filter_adjustments": "Adjustments",
"activity.feed_end": "— end of feed —",
"activity.just_now": "gerade eben",
"activity.hours_ago": "vor {count} Std.",
"activity.filter_rewards": "Belohnungen",
"activity.filter_chores": "Hausarbeiten",
"activity.filter_aria_label": "Aktivität nach Typ filtern",
"activity.filter_all": "Alle",
"activity.filter_adjustments": "Anpassungen",
"activity.feed_end": "— Ende des Verlaufs —",
"activity.lost": "verloren",
"activity.minutes_ago": "{count}m ago",
"activity.minutes_ago": "vor {count} Min.",
"activity.no_activity_yet": "Noch keine Aktivität",
"activity.no_events_for_filter": "No matching events",
"activity.no_events_for_filter": "Keine passenden Ereignisse",
"activity.points_manually": "Punkte manuell eingeben",
"activity.reason_allocated_to_pool": "Dem Pool zugewiesen: {name}",
"activity.reason_admin_adjustment": "Manuelle Anpassung",
@@ -64,7 +64,7 @@
"activity.received": "erhalten",
"activity.redeemed": "eingelöst",
"activity.spent": "ausgegeben",
"activity.try_different_filter": "Try a different filter",
"activity.try_different_filter": "Probieren Sie einen anderen Filter",
"activity.unknown_child": "Unbekannt",
"approvals.all_caught_up": "Alles aufgeholt!",
"approvals.approve": "Genehmigen",
@@ -559,7 +559,7 @@
"panel.btn_save_order": "Reihenfolge speichern",
"panel.btn_save_settings": "Einstellungen speichern",
"panel.btn_save_template": "Vorlage speichern",
"panel.btn_skip_chore": "Skip rotation (advance to next child)",
"panel.btn_skip_chore": "Rotation überspringen (zum nächsten Kind wechseln)",
"panel.bulk_assigned_to": "Zugewiesen an",
"panel.bulk_chore_names_hint": "Alle Aufgaben teilen sich die folgenden Einstellungen.",
"panel.bulk_chore_names_label": "Aufgabennamen (einer pro Zeile oder kommagetrennt)",
@@ -839,7 +839,7 @@
"panel.settings_title": "Einstellungen",
"panel.settings_weekend_multiplier_hint": "Bonus an Sa/So (1,0 = aus)",
"panel.settings_weekend_multiplier_label": "Wochenend-Multiplikator",
"panel.skip_chore_confirm": "Skip this chore's current assignee and advance the rotation?",
"panel.skip_chore_confirm": "Das aktuell zugewiesene Kind für diese Hausarbeit überspringen und die Rotation weiterschalten?",
"panel.streak_pause": "Pause — Serie wird bis zur nächsten Erledigung beibehalten",
"panel.streak_reset": "Zurücksetzen — Serie geht bei ausgelassenem Tag auf 0",
"panel.tab_activity": "Aktivität",
@@ -984,8 +984,8 @@
"panel.toast_reward_updated": "Belohnung aktualisiert",
"panel.toast_save_failed": "Speichern fehlgeschlagen: {error}",
"panel.toast_settings_saved": "Gespeichert ({count} Feld(er))",
"panel.toast_skip_done": "Rotation skipped — next child assigned",
"panel.toast_skip_failed": "Skip failed: {error}",
"panel.toast_skip_done": "Rotation übersprungen — nächstes Kind zugewiesen",
"panel.toast_skip_failed": "Überspringen fehlgeschlagen: {error}",
"panel.toast_template_created": "Vorlage \"{name}\" erstellt",
"panel.toast_template_deleted": "Vorlage gelöscht",
"panel.toast_template_failed_apply": "Vorlage konnte nicht angewendet werden",
@@ -1094,7 +1094,7 @@
"points_display.editor.mode_cumulative": "∑ Kombiniert",
"points_display.editor.mode_label": "Modus",
"points_display.editor.mode_multi": "👥 Alle Kinder",
"points_display.editor.mode_single": "⭐ Single",
"points_display.editor.mode_single": "⭐ Einzeln",
"points_display.editor.primary_career_score": "🏆 Karrierepunkte",
"points_display.editor.primary_current_points": "💰 Aktuelle Punkte",
"points_display.editor.primary_display": "Primäranzeige",
@@ -1267,10 +1267,10 @@
"weekly.day_fri": "Fr",
"weekly.day_mon": "Mo",
"weekly.day_sat": "Sa",
"weekly.day_sun": "Sonne",
"weekly.day_sun": "So",
"weekly.day_thu": "Do",
"weekly.day_tue": "Di",
"weekly.day_wed": "Heiraten",
"weekly.day_wed": "Mi",
"weekly.default_title": "Diese Woche",
"weekly.editor.child_helper": "Wöchentliche Zusammenfassung nur für ein bestimmtes Kind anzeigen",
"weekly.editor.title": "Titel",
@@ -1504,8 +1504,8 @@
"child.avatar_change": "Avatar ändern",
"child.avatar_locked": "Gesperrt",
"child.avatar_change_failed": "Avatar konnte nicht geändert werden.",
"panel.tab_challenges": "Challenges",
"panel.challenge_title": "Challenges",
"panel.tab_challenges": "Herausforderungen",
"panel.challenge_title": "Herausforderungen",
"panel.search_challenges": "Challenges suchen …",
"panel.btn_add_challenge": "Challenge hinzufügen",
"panel.challenge_add_tile": "Challenge hinzufügen",
+22 -22
View File
@@ -16,7 +16,7 @@
"activity.claimed": "a réclamé",
"activity.completed": "a terminé",
"activity.completed_chores_will_appear": "Les tâches terminées apparaîtront ici",
"activity.days_ago": "{count}d ago",
"activity.days_ago": "il y a {count}j",
"activity.default_title": "Activité",
"activity.recent_events": "Événements récents",
"activity.undo": "Annuler",
@@ -25,30 +25,30 @@
"activity.undo_confirm_txn": "Annuler « {detail} » pour {child} ? Cela inverse {points} points.",
"activity.undo_error_title": "TaskMate — échec de l'annulation",
"activity.undo_error_body": "Impossible d'annuler : {message}",
"activity.editor.accent_stripes_helper": "Show the 4px coloured stripe at the left of each row indicating event type",
"activity.editor.accent_stripes": "Coloured event stripes",
"activity.editor.accent_stripes_helper": "Afficher la bande de couleur de 4 px à gauche de chaque ligne, qui indique le type d'événement",
"activity.editor.accent_stripes": "Bandes de couleur par événement",
"activity.editor.filter_child_helper": "Afficher uniquement l'activité de cet enfant",
"activity.editor.max_items": "Nombre max d'éléments",
"activity.editor.max_items_helper": "Nombre maximum d'événements à afficher (par défaut : 30)",
"activity.editor.show_relative_time_helper": "Show 'just now' / '5m ago' alongside the absolute time on each row",
"activity.editor.show_relative_time": "Relative time labels",
"activity.editor.show_filter_chips_helper": "Show the All / Chores / Rewards / Adjustments chip bar below the header",
"activity.editor.show_filter_chips": "Show filter chips",
"activity.editor.show_relative_time_helper": "Afficher « à l'instant » / « il y a 5 min » à côté de l'heure exacte sur chaque ligne",
"activity.editor.show_relative_time": "Horodatage relatif",
"activity.editor.show_filter_chips_helper": "Afficher la barre Tout / Tâches / Récompenses / Ajustements sous l'en-tête",
"activity.editor.show_filter_chips": "Afficher les puces de filtre",
"activity.editor.show_undo": "Afficher les boutons d'annulation",
"activity.editor.show_undo_helper": "Affiche le bouton d'annulation sur chaque ligne. Désactivez-le pour un tableau de bord adapté aux enfants, afin qu'ils ne puissent pas annuler leur propre activité.",
"activity.events_count": "{count} événements",
"activity.just_now": "just now",
"activity.hours_ago": "{count}h ago",
"activity.filter_rewards": "Rewards",
"activity.filter_chores": "Chores",
"activity.filter_aria_label": "Filter activity by type",
"activity.filter_all": "All",
"activity.filter_adjustments": "Adjustments",
"activity.feed_end": "— end of feed —",
"activity.just_now": "à l'instant",
"activity.hours_ago": "il y a {count} h",
"activity.filter_rewards": "Récompenses",
"activity.filter_chores": "Tâches",
"activity.filter_aria_label": "Filtrer l'activité par type",
"activity.filter_all": "Tout",
"activity.filter_adjustments": "Ajustements",
"activity.feed_end": "— fin du flux —",
"activity.lost": "a perdu",
"activity.minutes_ago": "{count}m ago",
"activity.minutes_ago": "il y a {count} min",
"activity.no_activity_yet": "Aucune activité pour le moment",
"activity.no_events_for_filter": "No matching events",
"activity.no_events_for_filter": "Aucun événement correspondant",
"activity.points_manually": "points manuellement",
"activity.reason_allocated_to_pool": "Alloué à la cagnotte : {name}",
"activity.reason_admin_adjustment": "Ajustement manuel",
@@ -64,7 +64,7 @@
"activity.received": "a reçu",
"activity.redeemed": "a échangé",
"activity.spent": "a dépensé",
"activity.try_different_filter": "Try a different filter",
"activity.try_different_filter": "Essayez un autre filtre",
"activity.unknown_child": "Inconnu",
"approvals.all_caught_up": "Tout est à jour !",
"approvals.approve": "Approuver",
@@ -559,7 +559,7 @@
"panel.btn_save_order": "Enregistrer l'ordre",
"panel.btn_save_settings": "Enregistrer les paramètres",
"panel.btn_save_template": "Enregistrer le modèle",
"panel.btn_skip_chore": "Skip rotation (advance to next child)",
"panel.btn_skip_chore": "Passer la rotation (enfant suivant)",
"panel.bulk_assigned_to": "Assigné à",
"panel.bulk_chore_names_hint": "Toutes les tâches partageront les paramètres ci-dessous.",
"panel.bulk_chore_names_label": "Noms des tâches (un par ligne ou séparés par des virgules)",
@@ -839,7 +839,7 @@
"panel.settings_title": "Paramètres",
"panel.settings_weekend_multiplier_hint": "Bonus le Sam/Dim (1.0 = désactivé)",
"panel.settings_weekend_multiplier_label": "Multiplicateur weekend",
"panel.skip_chore_confirm": "Skip this chore's current assignee and advance the rotation?",
"panel.skip_chore_confirm": "Passer l'enfant actuellement assigné à cette tâche et faire avancer la rotation ?",
"panel.streak_pause": "Pause — série préservée jusqu'à la prochaine complétion",
"panel.streak_reset": "Réinitialisation — la série retombe à 0 si un jour est manqué",
"panel.tab_activity": "Activité",
@@ -984,8 +984,8 @@
"panel.toast_reward_updated": "Récompense mise à jour",
"panel.toast_save_failed": "Échec de l'enregistrement : {error}",
"panel.toast_settings_saved": "Enregistré ({count} champ(s))",
"panel.toast_skip_done": "Rotation skipped — next child assigned",
"panel.toast_skip_failed": "Skip failed: {error}",
"panel.toast_skip_done": "Rotation passée — enfant suivant assigné",
"panel.toast_skip_failed": "Échec du passage : {error}",
"panel.toast_template_created": "Modèle « {name} » créé",
"panel.toast_template_deleted": "Modèle supprimé",
"panel.toast_template_failed_apply": "Échec de l'application du modèle",
+22 -22
View File
@@ -16,7 +16,7 @@
"activity.claimed": "krevde",
"activity.completed": "fullførte",
"activity.completed_chores_will_appear": "Fullførte oppgaver vises her",
"activity.days_ago": "{count}d ago",
"activity.days_ago": "{count}d siden",
"activity.default_title": "Aktivitet",
"activity.recent_events": "Nylige hendelser",
"activity.undo": "Angre",
@@ -25,30 +25,30 @@
"activity.undo_confirm_txn": "Angre «{detail}» for {child}? Dette reverserer {points} poeng.",
"activity.undo_error_title": "TaskMate — angring mislyktes",
"activity.undo_error_body": "Kunne ikke angre: {message}",
"activity.editor.accent_stripes_helper": "Show the 4px coloured stripe at the left of each row indicating event type",
"activity.editor.accent_stripes": "Coloured event stripes",
"activity.editor.accent_stripes_helper": "Vis den 4 px brede fargestripen til venstre i hver rad som viser hendelsestypen",
"activity.editor.accent_stripes": "Fargede hendelsesstriper",
"activity.editor.filter_child_helper": "Vis kun aktivitet for dette barnet",
"activity.editor.max_items": "Maks antall",
"activity.editor.max_items_helper": "Maksimalt antall hendelser å vise (standard: 30)",
"activity.editor.show_relative_time_helper": "Show 'just now' / '5m ago' alongside the absolute time on each row",
"activity.editor.show_relative_time": "Relative time labels",
"activity.editor.show_filter_chips_helper": "Show the All / Chores / Rewards / Adjustments chip bar below the header",
"activity.editor.show_filter_chips": "Show filter chips",
"activity.editor.show_relative_time_helper": "Vis «nettopp» / «5m siden» ved siden av det nøyaktige klokkeslettet i hver rad",
"activity.editor.show_relative_time": "Relative tidsetiketter",
"activity.editor.show_filter_chips_helper": "Vis raden Alle / Oppgaver / Belønninger / Justeringer under overskriften",
"activity.editor.show_filter_chips": "Vis filterknapper",
"activity.editor.show_undo": "Vis angre-knapper",
"activity.editor.show_undo_helper": "Viser angre-knappen på hver rad. Slå av for et barnevennlig dashbord slik at barn ikke kan angre sin egen aktivitet.",
"activity.events_count": "{count} hendelser",
"activity.just_now": "just now",
"activity.hours_ago": "{count}h ago",
"activity.filter_rewards": "Rewards",
"activity.filter_chores": "Chores",
"activity.filter_aria_label": "Filter activity by type",
"activity.filter_all": "All",
"activity.filter_adjustments": "Adjustments",
"activity.feed_end": "— end of feed —",
"activity.just_now": "nettopp",
"activity.hours_ago": "{count}t siden",
"activity.filter_rewards": "Belønninger",
"activity.filter_chores": "Oppgaver",
"activity.filter_aria_label": "Filtrer aktivitet etter type",
"activity.filter_all": "Alle",
"activity.filter_adjustments": "Justeringer",
"activity.feed_end": "— slutten av feeden —",
"activity.lost": "mistet",
"activity.minutes_ago": "{count}m ago",
"activity.minutes_ago": "{count}m siden",
"activity.no_activity_yet": "Ingen aktivitet ennå",
"activity.no_events_for_filter": "No matching events",
"activity.no_events_for_filter": "Ingen samsvarende hendelser",
"activity.points_manually": "poeng manuelt",
"activity.reason_allocated_to_pool": "Tildelt til sparegris: {name}",
"activity.reason_admin_adjustment": "Manuell justering",
@@ -64,7 +64,7 @@
"activity.received": "mottatt",
"activity.redeemed": "innløste",
"activity.spent": "brukte",
"activity.try_different_filter": "Try a different filter",
"activity.try_different_filter": "Prøv et annet filter",
"activity.unknown_child": "Ukjent",
"approvals.all_caught_up": "Alt er oppdatert!",
"approvals.approve": "Godkjenn",
@@ -559,7 +559,7 @@
"panel.btn_save_order": "Lagre rekkefølge",
"panel.btn_save_settings": "Lagre innstillinger",
"panel.btn_save_template": "Lagre mal",
"panel.btn_skip_chore": "Skip rotation (advance to next child)",
"panel.btn_skip_chore": "Hopp over rotasjonen (gå til neste barn)",
"panel.bulk_assigned_to": "Tildelt til",
"panel.bulk_chore_names_hint": "Alle oppgaver deler innstillingene nedenfor.",
"panel.bulk_chore_names_label": "Oppgavenavn (ett per linje, eller kommaseparert)",
@@ -839,7 +839,7 @@
"panel.settings_title": "Innstillinger",
"panel.settings_weekend_multiplier_hint": "Bonus lør/søn (1.0 = av)",
"panel.settings_weekend_multiplier_label": "Helgemultiplikator",
"panel.skip_chore_confirm": "Skip this chore's current assignee and advance the rotation?",
"panel.skip_chore_confirm": "Hoppe over barnet som er tildelt denne oppgaven, og gå videre i rotasjonen?",
"panel.streak_pause": "Pause — serien bevares til neste fullføring",
"panel.streak_reset": "Tilbakestill — serien går til 0 ved uteglemt dag",
"panel.tab_activity": "Aktivitet",
@@ -984,8 +984,8 @@
"panel.toast_reward_updated": "Belønning oppdatert",
"panel.toast_save_failed": "Lagring mislyktes: {error}",
"panel.toast_settings_saved": "Lagret ({count} felt)",
"panel.toast_skip_done": "Rotation skipped — next child assigned",
"panel.toast_skip_failed": "Skip failed: {error}",
"panel.toast_skip_done": "Rotasjonen ble hoppet over — neste barn er tildelt",
"panel.toast_skip_failed": "Kunne ikke hoppe over: {error}",
"panel.toast_template_created": "Malen «{name}» opprettet",
"panel.toast_template_deleted": "Mal slettet",
"panel.toast_template_failed_apply": "Kunne ikke bruke mal",
+23 -23
View File
@@ -16,7 +16,7 @@
"activity.claimed": "kravde",
"activity.completed": "fullførte",
"activity.completed_chores_will_appear": "Fullførte oppgåver kjem her",
"activity.days_ago": "{count}d ago",
"activity.days_ago": "{count}d sidan",
"activity.default_title": "Aktivitet",
"activity.recent_events": "Nylege hendingar",
"activity.undo": "Angre",
@@ -25,30 +25,30 @@
"activity.undo_confirm_txn": "Angre «{detail}» for {child}? Dette reverserer {points} poeng.",
"activity.undo_error_title": "TaskMate — angring mislukkast",
"activity.undo_error_body": "Kunne ikkje angre: {message}",
"activity.editor.accent_stripes_helper": "Show the 4px coloured stripe at the left of each row indicating event type",
"activity.editor.accent_stripes": "Coloured event stripes",
"activity.editor.accent_stripes_helper": "Vis den 4 px breie fargestripa til venstre i kvar rad som viser hendingstypen",
"activity.editor.accent_stripes": "Farga hendingsstriper",
"activity.editor.filter_child_helper": "Vis berre aktivitet for dette bornet",
"activity.editor.max_items": "Maks tal",
"activity.editor.max_items_helper": "Maks tal hendingar å vise (standard: 30)",
"activity.editor.show_relative_time_helper": "Show 'just now' / '5m ago' alongside the absolute time on each row",
"activity.editor.show_relative_time": "Relative time labels",
"activity.editor.show_filter_chips_helper": "Show the All / Chores / Rewards / Adjustments chip bar below the header",
"activity.editor.show_filter_chips": "Show filter chips",
"activity.editor.show_relative_time_helper": "Vis «nettopp» / «5m sidan» ved sida av det nøyaktige klokkeslettet i kvar rad",
"activity.editor.show_relative_time": "Relative tidsetikettar",
"activity.editor.show_filter_chips_helper": "Vis rada Alle / Oppgåver / Premiar / Justeringar under overskrifta",
"activity.editor.show_filter_chips": "Vis filterknappar",
"activity.editor.show_undo": "Vis angre-knappar",
"activity.editor.show_undo_helper": "Viser angre-knappen på kvar rad. Slå av for eit barnevenleg dashbord slik at barn ikkje kan angre sin eigen aktivitet.",
"activity.events_count": "{count} hendingar",
"activity.just_now": "just now",
"activity.hours_ago": "{count}h ago",
"activity.filter_rewards": "Rewards",
"activity.filter_chores": "Chores",
"activity.filter_aria_label": "Filter activity by type",
"activity.filter_all": "All",
"activity.filter_adjustments": "Adjustments",
"activity.feed_end": "— end of feed —",
"activity.just_now": "nettopp",
"activity.hours_ago": "{count}t sidan",
"activity.filter_rewards": "Premiar",
"activity.filter_chores": "Oppgåver",
"activity.filter_aria_label": "Filtrer aktivitet etter type",
"activity.filter_all": "Alle",
"activity.filter_adjustments": "Justeringar",
"activity.feed_end": "— slutten av feeden —",
"activity.lost": "mista",
"activity.minutes_ago": "{count}m ago",
"activity.minutes_ago": "{count}m sidan",
"activity.no_activity_yet": "Ingen aktivitet enno",
"activity.no_events_for_filter": "No matching events",
"activity.no_events_for_filter": "Ingen samsvarande hendingar",
"activity.points_manually": "poeng manuelt",
"activity.reason_allocated_to_pool": "Tildelt til sparegris: {name}",
"activity.reason_admin_adjustment": "Manuell justering",
@@ -64,7 +64,7 @@
"activity.received": "motteke",
"activity.redeemed": "løyste inn",
"activity.spent": "brukte",
"activity.try_different_filter": "Try a different filter",
"activity.try_different_filter": "Prøv eit anna filter",
"activity.unknown_child": "Ukjend",
"approvals.all_caught_up": "Alt er oppdatert!",
"approvals.approve": "Godkjenn",
@@ -559,7 +559,7 @@
"panel.btn_save_order": "Lagre rekkjefølgje",
"panel.btn_save_settings": "Lagre innstillingar",
"panel.btn_save_template": "Lagre mal",
"panel.btn_skip_chore": "Skip rotation (advance to next child)",
"panel.btn_skip_chore": "Hopp over rotasjonen (gå til neste barn)",
"panel.bulk_assigned_to": "Tildelt til",
"panel.bulk_chore_names_hint": "Alle oppgåver vil dele innstillingane nedanfor.",
"panel.bulk_chore_names_label": "Oppgåvenamn (eitt per linje, eller kommaskilde)",
@@ -839,7 +839,7 @@
"panel.settings_title": "Innstillingar",
"panel.settings_weekend_multiplier_hint": "Bonus på lau/sun (1.0 = av)",
"panel.settings_weekend_multiplier_label": "Helgemultiplikator",
"panel.skip_chore_confirm": "Skip this chore's current assignee and advance the rotation?",
"panel.skip_chore_confirm": "Hoppe over barnet som er tildelt denne oppgåva, og gå vidare i rotasjonen?",
"panel.streak_pause": "Pause — rekkja vert bevart til neste fullføring",
"panel.streak_reset": "Nullstill — rekkja går til 0 ved missa dag",
"panel.tab_activity": "Aktivitet",
@@ -861,7 +861,7 @@
"panel.notif_custom_message_placeholder": "Melding (du kan bruke {child_name})",
"panel.notif_custom_name_placeholder": "Namn på påminning",
"panel.notif_day_fri": "Fre",
"panel.notif_day_mon": "Man",
"panel.notif_day_mon": "Mån",
"panel.notif_day_sat": "Lau",
"panel.notif_day_sun": "Sun",
"panel.notif_day_thu": "Tor",
@@ -984,8 +984,8 @@
"panel.toast_reward_updated": "Premie oppdatert",
"panel.toast_save_failed": "Lagring feila: {error}",
"panel.toast_settings_saved": "Lagra ({count} felt)",
"panel.toast_skip_done": "Rotation skipped — next child assigned",
"panel.toast_skip_failed": "Skip failed: {error}",
"panel.toast_skip_done": "Rotasjonen vart hoppa over — neste barn er tildelt",
"panel.toast_skip_failed": "Kunne ikkje hoppe over: {error}",
"panel.toast_template_created": "Malen «{name}» oppretta",
"panel.toast_template_deleted": "Mal sletta",
"panel.toast_template_failed_apply": "Klarte ikkje å bruke mal",
@@ -16,7 +16,7 @@
"activity.claimed": "reclamou",
"activity.completed": "concluiu",
"activity.completed_chores_will_appear": "As tarefas concluídas aparecerão aqui",
"activity.days_ago": "{count}d ago",
"activity.days_ago": "{count}d atrás",
"activity.default_title": "Atividade",
"activity.recent_events": "Eventos recentes",
"activity.undo": "Desfazer",
@@ -25,30 +25,30 @@
"activity.undo_confirm_txn": "Desfazer “{detail}” para {child}? Isso reverte {points} pontos.",
"activity.undo_error_title": "TaskMate — falha ao desfazer",
"activity.undo_error_body": "Não foi possível desfazer: {message}",
"activity.editor.accent_stripes_helper": "Show the 4px coloured stripe at the left of each row indicating event type",
"activity.editor.accent_stripes": "Coloured event stripes",
"activity.editor.accent_stripes_helper": "Mostra a barra colorida de 4 px à esquerda de cada linha, que indica o tipo de evento",
"activity.editor.accent_stripes": "Barras coloridas de evento",
"activity.editor.filter_child_helper": "Mostrar apenas atividade desta criança",
"activity.editor.max_items": "Máximo de Itens",
"activity.editor.max_items_helper": "Número máximo de eventos a mostrar (padrão: 30)",
"activity.editor.show_relative_time_helper": "Show 'just now' / '5m ago' alongside the absolute time on each row",
"activity.editor.show_relative_time": "Relative time labels",
"activity.editor.show_filter_chips_helper": "Show the All / Chores / Rewards / Adjustments chip bar below the header",
"activity.editor.show_filter_chips": "Show filter chips",
"activity.editor.show_relative_time_helper": "Mostra \"agora mesmo\" / \"5min atrás\" ao lado da hora exata em cada linha",
"activity.editor.show_relative_time": "Etiquetas de tempo relativo",
"activity.editor.show_filter_chips_helper": "Mostra a barra Todos / Tarefas / Recompensas / Ajustes abaixo do cabeçalho",
"activity.editor.show_filter_chips": "Mostrar filtros",
"activity.editor.show_undo": "Mostrar botões de desfazer",
"activity.editor.show_undo_helper": "Mostra o botão de desfazer em cada linha. Desative para um painel adequado para crianças, para que elas não possam desfazer a própria atividade.",
"activity.events_count": "{count} eventos",
"activity.just_now": "just now",
"activity.hours_ago": "{count}h ago",
"activity.filter_rewards": "Rewards",
"activity.filter_chores": "Chores",
"activity.filter_aria_label": "Filter activity by type",
"activity.filter_all": "All",
"activity.filter_adjustments": "Adjustments",
"activity.feed_end": "— end of feed —",
"activity.just_now": "agora mesmo",
"activity.hours_ago": "{count}h atrás",
"activity.filter_rewards": "Recompensas",
"activity.filter_chores": "Tarefas",
"activity.filter_aria_label": "Filtrar atividade por tipo",
"activity.filter_all": "Todos",
"activity.filter_adjustments": "Ajustes",
"activity.feed_end": "— fim do histórico —",
"activity.lost": "perdeu",
"activity.minutes_ago": "{count}m ago",
"activity.minutes_ago": "{count}min atrás",
"activity.no_activity_yet": "Ainda sem atividade",
"activity.no_events_for_filter": "No matching events",
"activity.no_events_for_filter": "Nenhum evento correspondente",
"activity.points_manually": "pontos manualmente",
"activity.reason_allocated_to_pool": "Alocado ao cofrinho: {name}",
"activity.reason_admin_adjustment": "Ajuste manual",
@@ -64,7 +64,7 @@
"activity.received": "recebeu",
"activity.redeemed": "resgatou",
"activity.spent": "gastou",
"activity.try_different_filter": "Try a different filter",
"activity.try_different_filter": "Tente outro filtro",
"activity.unknown_child": "Desconhecido",
"approvals.all_caught_up": "Está tudo em dia!",
"approvals.approve": "Aprovar",
@@ -559,7 +559,7 @@
"panel.btn_save_order": "Salvar ordem",
"panel.btn_save_settings": "Salvar configurações",
"panel.btn_save_template": "Salvar modelo",
"panel.btn_skip_chore": "Skip rotation (advance to next child)",
"panel.btn_skip_chore": "Pular a rotação (avançar para a próxima criança)",
"panel.bulk_assigned_to": "Atribuído a",
"panel.bulk_chore_names_hint": "Todas as tarefas compartilharão as configurações abaixo.",
"panel.bulk_chore_names_label": "Nomes das tarefas (um por linha ou separados por vírgula)",
@@ -839,7 +839,7 @@
"panel.settings_title": "Configurações",
"panel.settings_weekend_multiplier_hint": "Bônus no sáb/dom (1.0 = desligado)",
"panel.settings_weekend_multiplier_label": "Multiplicador de fim de semana",
"panel.skip_chore_confirm": "Skip this chore's current assignee and advance the rotation?",
"panel.skip_chore_confirm": "Pular a criança atualmente atribuída a esta tarefa e avançar a rotação?",
"panel.streak_pause": "Pausar — sequência preservada até a próxima conclusão",
"panel.streak_reset": "Reiniciar — sequência vai a 0 no dia perdido",
"panel.tab_activity": "Atividade",
@@ -984,8 +984,8 @@
"panel.toast_reward_updated": "Recompensa atualizada",
"panel.toast_save_failed": "Falha ao salvar: {error}",
"panel.toast_settings_saved": "Salvo ({count} campo(s))",
"panel.toast_skip_done": "Rotation skipped — next child assigned",
"panel.toast_skip_failed": "Skip failed: {error}",
"panel.toast_skip_done": "Rotação pulada — próxima criança atribuída",
"panel.toast_skip_failed": "Falha ao pular: {error}",
"panel.toast_template_created": "Modelo \"{name}\" criado",
"panel.toast_template_deleted": "Modelo excluído",
"panel.toast_template_failed_apply": "Falha ao aplicar modelo",
+22 -22
View File
@@ -16,7 +16,7 @@
"activity.claimed": "reclamou",
"activity.completed": "concluiu",
"activity.completed_chores_will_appear": "As tarefas concluídas aparecerão aqui",
"activity.days_ago": "{count}d ago",
"activity.days_ago": "há {count}d",
"activity.default_title": "Atividade",
"activity.recent_events": "Eventos recentes",
"activity.undo": "Anular",
@@ -25,30 +25,30 @@
"activity.undo_confirm_txn": "Anular “{detail}” para {child}? Isto reverte {points} pontos.",
"activity.undo_error_title": "TaskMate — falha ao anular",
"activity.undo_error_body": "Não foi possível anular: {message}",
"activity.editor.accent_stripes_helper": "Show the 4px coloured stripe at the left of each row indicating event type",
"activity.editor.accent_stripes": "Coloured event stripes",
"activity.editor.accent_stripes_helper": "Mostra a barra colorida de 4 px à esquerda de cada linha, que indica o tipo de evento",
"activity.editor.accent_stripes": "Barras coloridas de evento",
"activity.editor.filter_child_helper": "Mostrar apenas atividade desta criança",
"activity.editor.max_items": "Máximo de Itens",
"activity.editor.max_items_helper": "Número máximo de eventos a mostrar (padrão: 30)",
"activity.editor.show_relative_time_helper": "Show 'just now' / '5m ago' alongside the absolute time on each row",
"activity.editor.show_relative_time": "Relative time labels",
"activity.editor.show_filter_chips_helper": "Show the All / Chores / Rewards / Adjustments chip bar below the header",
"activity.editor.show_filter_chips": "Show filter chips",
"activity.editor.show_relative_time_helper": "Mostra «agora mesmo» / «há 5min» ao lado da hora exata em cada linha",
"activity.editor.show_relative_time": "Etiquetas de tempo relativo",
"activity.editor.show_filter_chips_helper": "Mostra a barra Todos / Tarefas / Recompensas / Ajustes por baixo do cabeçalho",
"activity.editor.show_filter_chips": "Mostrar filtros",
"activity.editor.show_undo": "Mostrar botões de desfazer",
"activity.editor.show_undo_helper": "Mostra o botão de desfazer em cada linha. Desative para um painel adequado a crianças, para que não possam desfazer a própria atividade.",
"activity.events_count": "{count} eventos",
"activity.just_now": "just now",
"activity.hours_ago": "{count}h ago",
"activity.filter_rewards": "Rewards",
"activity.filter_chores": "Chores",
"activity.filter_aria_label": "Filter activity by type",
"activity.filter_all": "All",
"activity.filter_adjustments": "Adjustments",
"activity.feed_end": "— end of feed —",
"activity.just_now": "agora mesmo",
"activity.hours_ago": "há {count}h",
"activity.filter_rewards": "Recompensas",
"activity.filter_chores": "Tarefas",
"activity.filter_aria_label": "Filtrar atividade por tipo",
"activity.filter_all": "Todos",
"activity.filter_adjustments": "Ajustes",
"activity.feed_end": "— fim do histórico —",
"activity.lost": "perdeu",
"activity.minutes_ago": "{count}m ago",
"activity.minutes_ago": "há {count}min",
"activity.no_activity_yet": "Ainda sem atividade",
"activity.no_events_for_filter": "No matching events",
"activity.no_events_for_filter": "Nenhum evento correspondente",
"activity.points_manually": "pontos manualmente",
"activity.reason_allocated_to_pool": "Alocado ao mealheiro: {name}",
"activity.reason_admin_adjustment": "Ajuste manual",
@@ -64,7 +64,7 @@
"activity.received": "recebeu",
"activity.redeemed": "resgatou",
"activity.spent": "gastou",
"activity.try_different_filter": "Try a different filter",
"activity.try_different_filter": "Experimenta outro filtro",
"activity.unknown_child": "Desconhecido",
"approvals.all_caught_up": "Está tudo em dia!",
"approvals.approve": "Aprovar",
@@ -564,7 +564,7 @@
"panel.btn_save_order": "Guardar ordem",
"panel.btn_save_settings": "Guardar definições",
"panel.btn_save_template": "Guardar modelo",
"panel.btn_skip_chore": "Skip rotation (advance to next child)",
"panel.btn_skip_chore": "Saltar a rotação (avançar para a criança seguinte)",
"panel.bulk_assigned_to": "Atribuída a",
"panel.bulk_chore_names_hint": "Todas as tarefas partilharão as definições abaixo.",
"panel.bulk_chore_names_label": "Nomes das tarefas (um por linha ou separados por vírgula)",
@@ -844,7 +844,7 @@
"panel.settings_title": "Definições",
"panel.settings_weekend_multiplier_hint": "Bónus ao Sáb/Dom (1.0 = desligado)",
"panel.settings_weekend_multiplier_label": "Multiplicador de fim de semana",
"panel.skip_chore_confirm": "Skip this chore's current assignee and advance the rotation?",
"panel.skip_chore_confirm": "Saltar a criança atualmente atribuída a esta tarefa e avançar a rotação?",
"panel.streak_pause": "Pausa — sequência preservada até à próxima conclusão",
"panel.streak_reset": "Reiniciar — sequência volta a 0 no dia falhado",
"panel.tab_activity": "Atividade",
@@ -989,8 +989,8 @@
"panel.toast_reward_updated": "Recompensa atualizada",
"panel.toast_save_failed": "Falha ao guardar: {error}",
"panel.toast_settings_saved": "Guardado ({count} campo(s))",
"panel.toast_skip_done": "Rotation skipped — next child assigned",
"panel.toast_skip_failed": "Skip failed: {error}",
"panel.toast_skip_done": "Rotação saltada — criança seguinte atribuída",
"panel.toast_skip_failed": "Falha ao saltar: {error}",
"panel.toast_template_created": "Modelo \"{name}\" criado",
"panel.toast_template_deleted": "Modelo eliminado",
"panel.toast_template_failed_apply": "Falha ao aplicar modelo",
@@ -17,6 +17,13 @@ const css = LitElement.prototype.css;
const tmSafePhotoUrl = (u) =>
typeof u === "string" && u.startsWith("/api/taskmate/photo/") ? u : "";
// A pending-claims attribute is only ever usable as a list. The resolver used
// to hand back the pending-approvals sensor's scalar COUNT under this name,
// which turned .filter()/.some() into a TypeError and blanked the whole card
// (#834). The name collision is fixed in the resolver; this keeps a stray
// value from taking the card down again.
const tmClaimList = (v) => (Array.isArray(v) ? v : []);
const _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d);
class TaskMateApprovalsCard extends LitElement {
@@ -25,6 +32,7 @@ class TaskMateApprovalsCard extends LitElement {
hass: { type: Object },
config: { type: Object },
_loading: { type: Object },
_signed: { state: true },
};
}
@@ -40,6 +48,8 @@ class TaskMateApprovalsCard extends LitElement {
constructor() {
super();
this._loading = {};
this._signed = {}; // safe photo path -> per-viewer signed path
this._inflight = new Set();
}
_t(key, params) {
@@ -47,6 +57,56 @@ class TaskMateApprovalsCard extends LitElement {
return fn ? fn(this.hass, key, params) : key;
}
// Evidence photos are served from an auth-gated view; an <img> carries no
// bearer token, so each path is signed per-viewer via auth/sign_path. The
// sensor now publishes bare paths (no self-authenticating URL in shared
// state), so this binds photo access to the actual viewer.
_photoHref(raw) {
const safe = tmSafePhotoUrl(raw);
return (safe && this._signed[safe]) || "";
}
_collectPhotoUrls() {
const out = [];
const st = this.hass && this.hass.states;
if (!st) return out;
for (const eid in st) {
if (eid.indexOf("sensor.taskmate") !== 0) continue;
const attrs = (st[eid] && st[eid].attributes) || {};
for (const k in attrs) {
const v = attrs[k];
if (!Array.isArray(v)) continue;
for (const item of v) {
if (item && typeof item === "object" && typeof item.photo_url === "string" && item.photo_url) {
out.push(item.photo_url);
}
}
}
}
return out;
}
async _ensureSignedPhotos(rawUrls) {
for (const raw of rawUrls) {
const url = tmSafePhotoUrl(raw);
if (!url || this._signed[url] || this._inflight.has(url)) continue;
this._inflight.add(url);
try {
const res = await this.hass.callWS({ type: "auth/sign_path", path: url, expires: 3600 });
if (res && res.path) this._signed = { ...this._signed, [url]: res.path };
} catch (e) {
console.warn("taskmate: sign_path failed", e);
} finally {
this._inflight.delete(url);
}
}
}
updated() {
const urls = this._collectPhotoUrls();
if (urls.length) this._ensureSignedPhotos(urls);
}
static get styles() {
const base = css`
:host {
@@ -530,10 +590,9 @@ class TaskMateApprovalsCard extends LitElement {
// Pending reward claims: supported via either the pending_approvals sensor
// (reward_claims attribute) or the rewards sensor (pending_reward_claims).
let rewardClaims =
entity.attributes.reward_claims ||
attrs.pending_reward_claims ||
[];
let rewardClaims = tmClaimList(
entity.attributes.reward_claims || attrs.pending_reward_claims,
);
const filteredClaims = this._filterClaimsByChild(rewardClaims);
// Missed mandatory chores awaiting parent review (#532)
@@ -611,7 +670,7 @@ class TaskMateApprovalsCard extends LitElement {
if (!completions) completions = (attrs.todays_completions || []).filter(c => !c.approved);
const filteredCompletions = this._filterByChild(completions);
const rewardClaims = entity.attributes.reward_claims || attrs.pending_reward_claims || [];
const rewardClaims = tmClaimList(entity.attributes.reward_claims || attrs.pending_reward_claims);
const filteredClaims = this._filterClaimsByChild(rewardClaims);
const misses = this._filterMissesByChild(attrs.mandatory_misses || []);
@@ -805,7 +864,7 @@ class TaskMateApprovalsCard extends LitElement {
}
_apPhotoDesigned(it, cls) {
const photoUrl = tmSafePhotoUrl(it.photo);
const photoUrl = this._photoHref(it.photo);
if (it.kind !== "completion" || !photoUrl) {
return it.kind === "claim" && it.icon
? html`<div class="${cls}"><ha-icon icon="${it.icon}"></ha-icon></div>`
@@ -1332,11 +1391,11 @@ class TaskMateApprovalsCard extends LitElement {
${completion.points}
</span>
</div>
${tmSafePhotoUrl(completion.photo_url) ? html`
<a class="approval-photo" href="${tmSafePhotoUrl(completion.photo_url)}" target="_blank" rel="noopener"
@click="${(e) => this._openPhoto(e, tmSafePhotoUrl(completion.photo_url), this._photoCaption(completion.chore_name, completion.child_name, completion.completed_at))}"
${this._photoHref(completion.photo_url) ? html`
<a class="approval-photo" href="${this._photoHref(completion.photo_url)}" target="_blank" rel="noopener"
@click="${(e) => this._openPhoto(e, this._photoHref(completion.photo_url), this._photoCaption(completion.chore_name, completion.child_name, completion.completed_at))}"
title="${this._t('approvals.view_photo') || 'View photo'}">
<img src="${tmSafePhotoUrl(completion.photo_url)}" alt="" loading="lazy">
<img src="${this._photoHref(completion.photo_url)}" alt="" loading="lazy">
</a>
` : ''}
</div>
@@ -37,6 +37,30 @@
"sensor.taskmate_pending_approvals",
];
// Attributes a companion must NOT contribute to the merge, because another
// sensor already owns that name for different data (#834).
//
// sensor.taskmate_pending_approvals publishes both the full lists and a
// scalar count of each. Its `pending_reward_claims` count collides with
// sensor.taskmate_rewards' `pending_reward_claims` LIST, and the approvals
// sensor merges last, so the number won — every card that did
// `claims.filter(...)` / `claims.some(...)` threw as soon as a claim was
// pending, blanking the card. Zero pending claims hid it: the cards' own
// `|| []` fallback catches a count of 0 because it is falsy, so the crash
// only appeared once a child actually claimed something.
//
// Nothing reads these counts through the resolver — the admin panel takes
// its badge counts from the taskmate/get_state WebSocket call and the cards
// derive theirs from the lists' length. They stay readable directly off
// sensor.taskmate_pending_approvals for templates and automations.
const COMPANION_SKIP_KEYS = {
"sensor.taskmate_pending_approvals": [
"pending_chore_completions",
"pending_reward_claims",
"pending_mandatory_misses",
],
};
function mergedAttributes(hass, primaryEntityId) {
if (!hass || !hass.states) return {};
const merged = {};
@@ -46,8 +70,14 @@
}
for (const id of COMPANIONS) {
const s = hass.states[id];
if (s && s.attributes) {
if (!s || !s.attributes) continue;
const skip = COMPANION_SKIP_KEYS[id];
if (!skip) {
Object.assign(merged, s.attributes);
continue;
}
for (const [key, value] of Object.entries(s.attributes)) {
if (!skip.includes(key)) merged[key] = value;
}
}
return merged;
@@ -84,7 +84,11 @@ function localize(hass, key, params) {
str = _cache[_FALLBACK][key];
}
if (str === undefined) {
str = key;
// Missing translation: only echo the key back when it's a plain identifier.
// A key carrying HTML-significant characters is not a real key — it's caller
// data concatenated into the lookup (e.g. `panel.assign_${value}_short`) —
// so refuse to reflect it, since some call sites render _t() output as HTML.
str = /^[\w.-]+$/.test(key) ? key : "";
}
// Trigger background loads for any language not yet in cache
@@ -93,10 +97,12 @@ function localize(hass, key, params) {
}
if (!(_FALLBACK in _cache)) _loadLocale(_FALLBACK);
// Replace {placeholder} tokens
// Replace {placeholder} tokens. Use a replacer function so a value containing
// "$&", "$1" etc. is inserted literally rather than interpreted as a
// replacement pattern.
if (params && typeof str === 'string') {
for (const [k, v] of Object.entries(params)) {
str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), () => String(v));
}
}
@@ -3349,7 +3349,7 @@ class TaskMatePanel extends HTMLElement {
: ((c.due_days || []).length === 0 ? this._t("panel.common_daily") : (c.due_days || []).map(d => this._labelOf(DAYS, d)).join(" · "));
const schedClass = c.schedule_mode === "recurring" ? "tm-pill-accent" : c.schedule_mode === "one_shot" ? "tm-pill-warn" : "tm-pill-success";
const modeBadge = c.assignment_mode && c.assignment_mode !== "everyone"
? `<span class="tm-pill tm-pill-${c.assignment_mode}">${this._t(`panel.assign_${c.assignment_mode}_short`)}</span>` : "";
? `<span class="tm-pill tm-pill-${this._esc(c.assignment_mode)}">${this._t(`panel.assign_${c.assignment_mode}_short`)}</span>` : "";
const nameCell = renaming
? `<input class="tm-inline-input" type="text" data-field="_inlineRename" value="${this._esc(this._inlineRename.value)}" autofocus>
<button type="button" class="tm-icon-btn" data-act="rename-chore-commit" title="${this._t("panel.tooltip_save")}">✓</button>
@@ -3571,7 +3571,7 @@ class TaskMatePanel extends HTMLElement {
}
_tierClass(tier) {
return `tm-badge-tier-${(tier || "bronze").toLowerCase()}`;
return `tm-badge-tier-${this._esc((tier || "bronze").toLowerCase())}`;
}
_criteriaLabel(criteria, combinator = "AND") {
@@ -3973,7 +3973,7 @@ class TaskMatePanel extends HTMLElement {
<div class="tm-child-name">
<h3>${this._esc(g.name)}</h3>
${this._idBadge(g.id)}
<div class="tm-meta"><span class="tm-pill tm-pill-${g.policy}">${this._t(`panel.group_policy_${g.policy}_short`)}</span> · ${this._t("panel.group_chore_count", {count: (g.chore_ids || []).length})}</div>
<div class="tm-meta"><span class="tm-pill tm-pill-${this._esc(g.policy)}">${this._t(`panel.group_policy_${g.policy}_short`)}</span> · ${this._t("panel.group_chore_count", {count: (g.chore_ids || []).length})}</div>
</div>
</div>
<ul class="tm-group-list">
@@ -4103,7 +4103,7 @@ class TaskMatePanel extends HTMLElement {
_renderTemplateChoreCard(chore, idx) {
const expanded = chore._expanded;
const schedLabel = (chore.due_days || []).length === 0 ? this._t("panel.common_daily") : (chore.due_days || []).map(d => this._labelOf(DAYS, d)).join(", ");
const schedLabel = (chore.due_days || []).length === 0 ? this._t("panel.common_daily") : (chore.due_days || []).map(d => this._esc(this._labelOf(DAYS, d))).join(", ");
return `
<div class="tm-tpl-preview-card">
<div class="tm-tpl-preview-header" data-act="tpl-toggle-expand" data-idx="${idx}">
@@ -5387,7 +5387,7 @@ class TaskMatePanel extends HTMLElement {
const value = f === "assigned_to"
? (v || []).map(id => this._esc((children.find(c => c.id === id) || {}).name || id)).join(", ")
|| this._t("panel.sched_value_nobody")
: Array.isArray(v) ? v.join(", ")
: Array.isArray(v) ? this._esc(v.join(", "))
: typeof v === "boolean" ? this._t(v ? "panel.sched_value_yes" : "panel.sched_value_no")
: this._esc(String(v));
return `${label}: <strong>${value}</strong>`;
@@ -19,6 +19,13 @@ const css = LitElement.prototype.css;
const tmSafePhotoUrl = (u) =>
typeof u === "string" && u.startsWith("/api/taskmate/photo/") ? u : "";
// A pending-claims attribute is only ever usable as a list. The resolver used
// to hand back the pending-approvals sensor's scalar COUNT under this name,
// which turned .filter()/.some() into a TypeError and blanked the whole card
// (#834). The name collision is fixed in the resolver; this keeps a stray
// value from taking the card down again.
const tmClaimList = (v) => (Array.isArray(v) ? v : []);
const _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d);
class TaskMateParentDashboardCard extends LitElement {
@@ -29,6 +36,7 @@ class TaskMateParentDashboardCard extends LitElement {
_loading: { type: Object },
_activeSection: { type: String },
_expanded: { type: Object },
_signed: { state: true },
};
}
@@ -69,6 +77,58 @@ class TaskMateParentDashboardCard extends LitElement {
this._loading = {};
this._activeSection = "overview";
this._expanded = {};
this._signed = {}; // safe photo path -> per-viewer signed path
this._inflight = new Set();
}
// Evidence photos come from an auth-gated view; an <img> sends no bearer
// token, so sign each path per-viewer via auth/sign_path. The sensor now
// publishes bare paths, keeping any self-authenticating URL out of shared
// state and binding photo access to the actual viewer.
_photoHref(raw) {
const safe = tmSafePhotoUrl(raw);
return (safe && this._signed[safe]) || "";
}
_collectPhotoUrls() {
const out = [];
const st = this.hass && this.hass.states;
if (!st) return out;
for (const eid in st) {
if (eid.indexOf("sensor.taskmate") !== 0) continue;
const attrs = (st[eid] && st[eid].attributes) || {};
for (const k in attrs) {
const v = attrs[k];
if (!Array.isArray(v)) continue;
for (const item of v) {
if (item && typeof item === "object" && typeof item.photo_url === "string" && item.photo_url) {
out.push(item.photo_url);
}
}
}
}
return out;
}
async _ensureSignedPhotos(rawUrls) {
for (const raw of rawUrls) {
const url = tmSafePhotoUrl(raw);
if (!url || this._signed[url] || this._inflight.has(url)) continue;
this._inflight.add(url);
try {
const res = await this.hass.callWS({ type: "auth/sign_path", path: url, expires: 3600 });
if (res && res.path) this._signed = { ...this._signed, [url]: res.path };
} catch (e) {
console.warn("taskmate: sign_path failed", e);
} finally {
this._inflight.delete(url);
}
}
}
updated() {
const urls = this._collectPhotoUrls();
if (urls.length) this._ensureSignedPhotos(urls);
}
static get styles() {
@@ -527,7 +587,7 @@ class TaskMateParentDashboardCard extends LitElement {
// approve button after a recurring chore resets. `completions` stays
// today-only for the overview tab's activity display.
const pendingCompletions = attrs.chore_completions || completions.filter(c => !c.approved);
const pendingRewardClaims = attrs.pending_reward_claims || [];
const pendingRewardClaims = tmClaimList(attrs.pending_reward_claims);
const pointsIcon = attrs.points_icon || "mdi:star";
const pointsName = attrs.points_name || this._t('common.points');
const totalPending = pendingCompletions.length + pendingRewardClaims.length;
@@ -662,7 +722,7 @@ class TaskMateParentDashboardCard extends LitElement {
const chores = attrs.chores || [];
const completions = attrs.todays_completions || [];
const pendingCompletions = attrs.chore_completions || completions.filter(c => !c.approved);
const pendingRewardClaims = attrs.pending_reward_claims || [];
const pendingRewardClaims = tmClaimList(attrs.pending_reward_claims);
const pointsIcon = attrs.points_icon || "mdi:star";
const pointsName = attrs.points_name || this._t('common.points');
const totalPending = pendingCompletions.length + pendingRewardClaims.length;
@@ -816,7 +876,7 @@ class TaskMateParentDashboardCard extends LitElement {
const time = comp.completed_at
? new Date(comp.completed_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
const pts = comp.points || choreMap[comp.chore_id]?.points || 0;
const photoUrl = tmSafePhotoUrl(comp.photo_url);
const photoUrl = this._photoHref(comp.photo_url);
const photoCap = this._photoCaption(comp.chore_name, child && child.name, comp.completed_at);
const photo = photoUrl
? html`<a class="pd-photo-link" href="${photoUrl}" target="_blank" rel="noopener"
@@ -1149,11 +1209,11 @@ class TaskMateParentDashboardCard extends LitElement {
+${comp.points || chore?.points || 0}
</span>
</div>
${tmSafePhotoUrl(comp.photo_url) ? html`
<a class="approval-photo" href="${tmSafePhotoUrl(comp.photo_url)}" target="_blank" rel="noopener"
@click="${(e) => this._openPhoto(e, tmSafePhotoUrl(comp.photo_url), this._photoCaption(comp.chore_name || chore?.name, child && child.name, comp.completed_at))}"
${this._photoHref(comp.photo_url) ? html`
<a class="approval-photo" href="${this._photoHref(comp.photo_url)}" target="_blank" rel="noopener"
@click="${(e) => this._openPhoto(e, this._photoHref(comp.photo_url), this._photoCaption(comp.chore_name || chore?.name, child && child.name, comp.completed_at))}"
title="${this._t('approvals.view_photo') || 'View photo'}">
<img src="${tmSafePhotoUrl(comp.photo_url)}" alt="" loading="lazy">
<img src="${this._photoHref(comp.photo_url)}" alt="" loading="lazy">
</a>
` : ''}
</div>
@@ -87,7 +87,10 @@ class TaskMatePhotoGalleryCard extends LitElement {
async _ensureSigned(items) {
for (const it of items) {
const url = it.photo_url;
if (!url || this._signed[url] || this._inflight.has(url)) continue;
// Only sign our own photo paths; never hand auth/sign_path a foreign or
// javascript:/data: value that could round-trip into a clickable href.
if (!url || typeof url !== "string" || !url.startsWith("/api/taskmate/photo/")) continue;
if (this._signed[url] || this._inflight.has(url)) continue;
this._inflight.add(url);
try {
const res = await this.hass.callWS({ type: "auth/sign_path", path: url, expires: 3600 });
@@ -15,6 +15,13 @@ const LitElement = customElements.get("hui-masonry-view")
const html = LitElement.prototype.html;
const css = LitElement.prototype.css;
// A pending-claims attribute is only ever usable as a list. The resolver used
// to hand back the pending-approvals sensor's scalar COUNT under this name,
// which turned .filter()/.some() into a TypeError and blanked the whole card
// (#834). The name collision is fixed in the resolver; this keeps a stray
// value from taking the card down again.
const tmClaimList = (v) => (Array.isArray(v) ? v : []);
const _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d);
class TaskMateRewardsCard extends LitElement {
@@ -1363,7 +1370,8 @@ class TaskMateRewardsCard extends LitElement {
// Pending claim check
const pcAttrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config?.entity)) || this.hass?.states?.[this.config?.entity]?.attributes || {};
const pendingClaims = pcAttrs.pending_reward_claims || [];
// Always a list, even if the attribute arrives as something else (#834).
const pendingClaims = tmClaimList(pcAttrs.pending_reward_claims);
const hasPendingClaim = pendingClaims.some(c =>
c.reward_id === reward.id && (!childId || c.child_id === childId)
);
@@ -1842,7 +1850,8 @@ class TaskMateRewardsCard extends LitElement {
const pcAttrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config?.entity))
|| this.hass?.states?.[this.config?.entity]?.attributes || {};
const pendingClaims = pcAttrs.pending_reward_claims || [];
// Always a list, even if the attribute arrives as something else (#834).
const pendingClaims = tmClaimList(pcAttrs.pending_reward_claims);
const hasPendingClaim = pendingClaims.some(c =>
c.reward_id === reward.id && (!childId || c.child_id === childId)
);