147 files

This commit is contained in:
Home Assistant Version Control
2026-08-27 20:54:53 +00:00
parent 52cbcb4187
commit 69f4dc3f17
149 changed files with 13415 additions and 1156 deletions
+36 -3
View File
@@ -125,6 +125,8 @@ class TaskMateCalendar(CoordinatorEntity, CalendarEntity):
self._child_id = child.id
self._attr_unique_id = f"{entry.entry_id}_{child.id}_calendar"
self._attr_name = f"TaskMate {child.name}"
self._events_cache: list[CalendarEvent] | None = None
self._events_key: tuple | None = None
@property
def device_info(self) -> DeviceInfo:
@@ -140,17 +142,40 @@ class TaskMateCalendar(CoordinatorEntity, CalendarEntity):
@property
def event(self) -> CalendarEvent | None:
"""The current or next upcoming event (HA shows this as the entity state)."""
"""The current or next upcoming event (HA shows this as the entity state).
Home Assistant reads this twice per state write (once for the state,
once for the state attributes), and writes on every coordinator
refresh — so the horizon projection is memoized against the same key
the sensors use: the data snapshot plus the external-entity version
that covers availability/visibility flips (#823). Only the cheap
"which one is next" filter re-runs.
"""
child = self._child
if not child:
return None
now = dt_util.now()
today = now.date()
events = self._build_events(child, today, today + timedelta(days=_NEXT_EVENT_HORIZON_DAYS))
events = self._cached_horizon(child, today)
upcoming = [e for e in events if _as_local_dt(e.end) > now]
upcoming.sort(key=lambda e: _as_local_dt(e.start))
return upcoming[0] if upcoming else None
def _cached_horizon(self, child: Child, today: date) -> list[CalendarEvent]:
"""The next-up horizon for ``today``, rebuilt only when inputs change."""
key = (
today,
id(self.coordinator.data),
getattr(self.coordinator, "external_state_version", 0),
)
cached = getattr(self, "_events_cache", None)
if cached is not None and getattr(self, "_events_key", None) == key:
return cached
events = self._build_events(child, today, today + timedelta(days=_NEXT_EVENT_HORIZON_DAYS))
self._events_cache = events
self._events_key = key
return events
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
@@ -161,6 +186,14 @@ class TaskMateCalendar(CoordinatorEntity, CalendarEntity):
return self._build_events(child, start_date.date(), end_date.date())
def _build_events(self, child: Child, start_day: date, end_day: date) -> list[CalendarEvent]:
with self.coordinator.availability_build_scope():
return self._build_events_locked(child, start_day, end_day)
def _build_events_locked(self, child: Child, start_day: date, end_day: date) -> list[CalendarEvent]:
"""Project the range. Must run inside ``availability_build_scope`` so the
rotation pool and balanced-mode grouping resolve from the scope cache
instead of rebuilding every stored chore/child per (chore, day) (#823).
"""
coord = self.coordinator
events: list[CalendarEvent] = []
@@ -182,7 +215,7 @@ class TaskMateCalendar(CoordinatorEntity, CalendarEntity):
events.append(_away_event(run_start, end_day + timedelta(days=1), run_label))
# ---- chore occurrences (skipped on away days) ----
chores = coord.storage.get_chores()
chores = coord._cached_chores()
tz = dt_util.DEFAULT_TIME_ZONE
day = start_day
while day <= end_day:
+16 -15
View File
@@ -280,7 +280,7 @@ class AssignmentsMixin:
2. unavailability_entity — neutral when missing; _AVAILABLE_STATES means BUSY.
Final = step1 AND NOT step2.
"""
child = self.storage.get_child(child_id)
child = self._cached_child(child_id)
if not child:
return True
@@ -327,10 +327,11 @@ class AssignmentsMixin:
Prefers the chore's `assigned_to` list. When empty, falls back to every
stored child so "Everyone"-style chores can still alternate/randomize.
"""
pool = [cid for cid in (chore.assigned_to or []) if self.storage.get_child(cid)]
lookup = self._cached_child_lookup()
pool = [cid for cid in (chore.assigned_to or []) if cid in lookup]
if pool:
return pool
return [child.id for child in self.storage.get_children()]
return list(lookup)
def _compute_active_children(self, chore: Chore, today: date | None = None) -> list[str]:
"""Return the child IDs the chore is active for today.
@@ -347,16 +348,18 @@ class AssignmentsMixin:
parent has skipped. Stale skip state (skip_date != today) is ignored at
read time and cleared during the midnight refresh.
"""
# PERF-1: the active set depends only on the chore for "today" (the
# availability matrix calls this per chore × child). Memoize per chore
# within an availability build scope. Skip when an explicit `today` is
# passed (callers that probe other days must not read today's cache).
# PERF-1/#823: the active set depends only on (chore, day) — the
# availability matrix calls this per chore × child and the calendar
# projection per chore × day. Memoize on that pair within a build
# scope; `today=None` ("today") keys separately from an explicit date
# so a probe of another day can never be served today's answer.
cache = getattr(self, "_avail_cache", None)
if cache is not None and today is None:
cached = cache["active"].get(chore.id)
if cache is not None:
key = (chore.id, today)
cached = cache["active"].get(key)
if cached is None:
cached = self._compute_active_children_uncached(chore, None)
cache["active"][chore.id] = cached
cached = self._compute_active_children_uncached(chore, today)
cache["active"][key] = cached
return cached
return self._compute_active_children_uncached(chore, today)
@@ -448,7 +451,7 @@ class AssignmentsMixin:
pool_key = tuple(sorted(pool))
group_ids = sorted(
c.id
for c in self.storage.get_chores()
for c in self._cached_chores()
if getattr(c, "assignment_mode", "everyone") == "balanced"
and tuple(sorted(self._chore_assignment_pool(c))) == pool_key
)
@@ -613,9 +616,7 @@ class AssignmentsMixin:
active_child_id = getattr(chore, "assignment_current_child_id", "") or ""
completions_today = 0
completed_bonus_ids_today: set[str] = set()
for comp in self._cached_completions():
if comp.chore_id != chore.id:
continue
for comp in self._cached_completions_for_chore(chore.id):
comp_dt = comp.completed_at
try:
if hasattr(comp_dt, "astimezone"):
+1 -1
View File
@@ -1330,7 +1330,7 @@ class ChoresMixin:
# Vacation / pause mode: while away, all chores are hidden. Covers the
# global static range, the family vacation calendar, and this child's
# own availability sensor (per-child vacation).
if self._is_child_on_vacation(self.storage.get_child(child_id)):
if self._is_child_on_vacation(self._cached_child(child_id)):
return False
# Check if chore is globally disabled (soft-disabled one-shot chores)
+55 -5
View File
@@ -183,18 +183,26 @@ class TaskMateCoordinator(
"""Memoize chore-only computations for one availability build (PERF-1).
The availability matrix calls ``is_chore_available_for_child`` for every
chore × child. Several inner computations depend only on the chore (the
chore × child, and the calendar projection does the same for every
chore × day. Several inner computations depend only on the chore (the
rotation active-set, the rotation-done check) or are re-fetched from
storage repeatedly (the completions list, which ``get_completions``
rebuilds from dicts on each call). Inside this scope those are computed
once and cached. The build is fully synchronous (no awaits), so storage
cannot mutate while the cache is live. Re-entrant scopes reuse the cache.
storage repeatedly — every ``storage.get_*`` call rebuilds its whole
list of dataclasses from raw dicts, so a per-chore ``get_chores()``
(balanced-mode grouping) or a per-pool-member ``get_child()``
(``_chore_assignment_pool``) turns a projection into hundreds of
thousands of object constructions (#823). Inside this scope those are
computed once and cached. The build is fully synchronous (no awaits),
so storage cannot mutate while the cache is live. Re-entrant scopes
reuse the cache.
"""
if getattr(self, "_avail_cache", None) is not None:
yield # already inside a scope — reuse the existing cache
return
self._avail_cache = {
"completions": self.storage.get_completions(),
"completions_by_chore": None, # built lazily — only rotation modes need it
"chores": None,
"child_lookup": None,
"active": {},
"rotation_done": {},
}
@@ -210,6 +218,48 @@ class TaskMateCoordinator(
return cache["completions"]
return self.storage.get_completions()
def _cached_completions_for_chore(self, chore_id: str) -> list:
"""Completions for one chore, via a shared index when in scope.
Without the index each rotation chore walks the entire completions
list, making the availability matrix O(chores × completions).
"""
cache = getattr(self, "_avail_cache", None)
if cache is None:
return [c for c in self.storage.get_completions() if c.chore_id == chore_id]
index = cache["completions_by_chore"]
if index is None:
index = {}
for comp in cache["completions"]:
index.setdefault(comp.chore_id, []).append(comp)
cache["completions_by_chore"] = index
return index.get(chore_id, [])
def _cached_chores(self) -> list:
"""Chores list, rebuilt from storage once per scope."""
cache = getattr(self, "_avail_cache", None)
if cache is None:
return self.storage.get_chores()
if cache["chores"] is None:
cache["chores"] = self.storage.get_chores()
return cache["chores"]
def _cached_child_lookup(self) -> dict:
"""``{child_id: Child}``, rebuilt from storage once per scope."""
cache = getattr(self, "_avail_cache", None)
if cache is None:
return {c.id: c for c in self.storage.get_children()}
if cache["child_lookup"] is None:
cache["child_lookup"] = {c.id: c for c in self.storage.get_children()}
return cache["child_lookup"]
def _cached_child(self, child_id: str):
"""A single child, served from the scope lookup when in scope."""
cache = getattr(self, "_avail_cache", None)
if cache is None:
return self.storage.get_child(child_id)
return self._cached_child_lookup().get(child_id)
def _is_child_on_vacation(self, child, on: date | None = None) -> bool:
"""True when ``child`` should be treated as away (streak frozen, chores
hidden) on ``on`` (default today). Three stacking sources, OR'd:
+1 -1
View File
@@ -17,5 +17,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/tempus2016/taskmate/issues",
"requirements": [],
"version": "5.3.0"
"version": "5.4.1"
}
+18 -18
View File
@@ -106,7 +106,7 @@ class TimedSession:
segments=list(data.get("segments", [])),
total_seconds_today=data.get("total_seconds_today", 0),
session_date=data.get("session_date", ""),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -136,7 +136,7 @@ class BonusSubTask:
name=data.get("name", ""),
points=data.get("points", 5),
description=data.get("description", ""),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -212,7 +212,7 @@ class Child:
quiet_hours_start=data.get("quiet_hours_start", ""),
quiet_hours_end=data.get("quiet_hours_end", ""),
level=int(data.get("level", 1) or 1),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -422,7 +422,7 @@ class Chore:
timed_rate_points=data.get("timed_rate_points", 10),
timed_rate_minutes=max(1, int(data.get("timed_rate_minutes", 5) or 5)),
timed_max_daily_minutes=max(0, int(data.get("timed_max_daily_minutes", 0) or 0)),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -542,7 +542,7 @@ class Reward:
unlock_minutes=int(data.get("unlock_minutes", 0) or 0),
restock_period=data.get("restock_period", "weekly"),
restock_last=data.get("restock_last", ""),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -598,7 +598,7 @@ class Quest:
assigned_to=list(data.get("assigned_to", [])),
repeatable=bool(data.get("repeatable", False)),
active=bool(data.get("active", True)),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -648,7 +648,7 @@ class Challenge:
bonus_points=int(data.get("bonus_points", 15) or 0),
assigned_to=list(data.get("assigned_to", [])),
active=bool(data.get("active", True)),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -697,7 +697,7 @@ class ChoreCompletion:
bonus_subtask_id=data.get("bonus_subtask_id", ""),
timed_duration_seconds=data.get("timed_duration_seconds", 0),
photo_url=data.get("photo_url", ""),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -747,7 +747,7 @@ class MandatoryMiss:
postpone_count=max(0, int(data.get("postpone_count", 0) or 0)),
escalation_stage=max(0, int(data.get("escalation_stage", 0) or 0)),
created_at=data.get("created_at", "") or dt_util_now_iso(),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -788,7 +788,7 @@ class RewardClaim:
claimed_at=claimed_at or datetime.now(timezone.utc),
approved=data.get("approved", False),
approved_at=approved_at,
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -819,7 +819,7 @@ class PoolAllocation:
child_id=data.get("child_id", ""),
reward_id=data.get("reward_id", ""),
allocated_points=data.get("allocated_points", 0),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -847,7 +847,7 @@ class Penalty:
def from_dict(cls, data: dict[str, Any]) -> "Penalty":
"""Create a Penalty from a dictionary."""
return cls(
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
name=data.get("name", ""),
# Always positive — a negative value would *grant* points on use
points=max(0, int(data.get("points", 0) or 0)),
@@ -883,7 +883,7 @@ class Bonus:
def from_dict(cls, data: dict[str, Any]) -> "Bonus":
"""Create a Bonus from a dictionary."""
return cls(
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
name=data.get("name", ""),
# Always positive — a negative value would *deduct* points on use
points=max(0, int(data.get("points", 0) or 0)),
@@ -946,7 +946,7 @@ class PointsTransaction:
points=data.get("points", 0),
reason=data.get("reason", ""),
created_at=created_at or datetime.now(timezone.utc),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
link_id=data.get("link_id", ""),
)
@@ -984,7 +984,7 @@ class Badge:
def from_dict(cls, data: dict[str, Any]) -> "Badge":
created_at = parse_datetime(data.get("created_at"))
return cls(
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
name=data.get("name", ""),
description=data.get("description", ""),
icon=data.get("icon", "mdi:trophy"),
@@ -1033,7 +1033,7 @@ class AwardedBadge:
def from_dict(cls, data: dict[str, Any]) -> "AwardedBadge":
earned_at = parse_datetime(data.get("earned_at"))
return cls(
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
child_id=data.get("child_id", ""),
badge_id=data.get("badge_id", ""),
earned_at=earned_at or datetime.now(timezone.utc),
@@ -1078,7 +1078,7 @@ class TaskGroup:
name=data.get("name", ""),
policy=data.get("policy", "sticky"),
chore_ids=list(data.get("chore_ids", [])),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
@@ -1122,7 +1122,7 @@ class ScheduledChange:
applied=bool(data.get("applied", False)),
applied_at=data.get("applied_at", ""),
created_at=data.get("created_at", "") or dt_util_now_iso(),
id=data.get("id", generate_id()),
id=data.get("id") or generate_id(),
)
def to_dict(self) -> dict[str, Any]:
+5 -6
View File
@@ -936,12 +936,11 @@ class TaskMateChoreAvailabilitySensor(_CachedAttrsSensor):
@property
def native_value(self) -> int:
common = _compute_common(self.coordinator)
total = 0
availability = _build_chore_availability(self.coordinator, common)
for per_child in availability.values():
total += sum(1 for v in per_child.values() if v)
return total
# Read the matrix back off the attribute cache rather than rebuilding
# it: HA writes the state and the attributes together, so building it
# here too doubled the cost of every write (#823).
availability = self.extra_state_attributes.get("chore_availability", {})
return sum(sum(1 for v in per_child.values() if v) for per_child in availability.values())
@property
def icon(self) -> str:
+8 -1
View File
@@ -524,13 +524,20 @@ async def _ws_list_ha_users(hass, connection, msg, coordinator):
def _image_url_or_blank(value):
"""Accept a blank string (clears the picture) or one of our image URLs.
Returns the *canonical* URL: the panel is handed a signed one for its
<img> and posts it back verbatim, so the signature is stripped here rather
than persisted (#827).
A bare predicate can't be used as a voluptuous validator: voluptuous treats
a callable as a coercer, so returning False would be a *value*, not a
rejection. This raises instead.
"""
text = str(value or "")
if not text or images.is_taskmate_image_url(text):
if not text:
return text
canonical = images.normalize_taskmate_image_url(text)
if canonical:
return canonical
raise vol.Invalid("image_url must be a TaskMate image URL")
@@ -208,6 +208,8 @@
"child.chores_resetting_soon": "Aufgaben werden bald zurückgesetzt!",
"child.click_to_complete": "Klicken Sie hier, um den Vorgang abzuschließen",
"child.click_to_undo": "Klicken Sie zum Rückgängigmachen",
"child.claimed_by_another": "Bereits erledigt von {name}",
"child.claimed_by_another_generic": "Heute bereits beansprucht",
"child.difficulty_easy": "Leicht",
"child.difficulty_medium": "Mittel",
"child.difficulty_hard": "Schwer",
@@ -236,10 +238,16 @@
"child.editor.dependency_show": "Anzeigen – normal zeigen (weiterhin nicht antippbar)",
"child.editor.dependency_helper": "Was angezeigt wird, wenn eine Aufgabe von einer anderen abhängt, die heute noch nicht genehmigt ist",
"child.blocked_by_dependency": "Zuerst {chores} erledigen",
"child.parent": "Eltern",
"child.editor.due_days_dim": "Gedimmt – ausgegraut anzeigen, nicht interaktiv",
"child.editor.due_days_helper": "Gilt für Aufgaben, bei denen due_days festgelegt ist, wenn der heutige Tag nicht geplant ist",
"child.editor.due_days_hide": "Ausblenden – nicht anzeigen",
"child.editor.due_days_show": "Anzeigen – trotzdem normal anzeigen",
"child.editor.first_come_dim": "Gedimmt – für alle außer dem Kind, das die Aufgabe erledigt hat, ausgegraut anzeigen",
"child.editor.first_come_helper": "Was für den Rest der Gruppe angezeigt wird, nachdem eine Wer-zuerst-kommt-Aufgabe von einem Kind beansprucht wurde",
"child.editor.first_come_hide": "Ausblenden – nach Beanspruchung entfernen (Standard, entspricht dem bisherigen Verhalten)",
"child.editor.first_come_show": "Anzeigen – für alle vollständig sichtbar lassen (weiterhin nicht antippbar, sobald beansprucht)",
"child.editor.first_come_when_completed": "Wer-zuerst-kommt-Aufgaben – nach Beanspruchung",
"child.editor.elapsed_dim": "Gedimmt – ausgegraut anzeigen, nicht interaktiv",
"child.editor.elapsed_helper": "Was ist zu zeigen, wenn eine Morgen-/Nachmittags-/Abend-/Nachtaufgabe nicht erledigt wurde und die Zeit abgelaufen ist?",
"child.editor.elapsed_hide": "Ausblenden – nicht anzeigen",
@@ -210,6 +210,8 @@
"child.chores_resetting_soon": "Chores resetting soon!",
"child.click_to_complete": "Click to complete",
"child.click_to_undo": "Click to undo",
"child.claimed_by_another": "Already done by {name}",
"child.claimed_by_another_generic": "Already claimed today",
"child.difficulty_easy": "Easy",
"child.difficulty_medium": "Medium",
"child.difficulty_hard": "Hard",
@@ -238,10 +240,16 @@
"child.editor.dependency_show": "Show — show normally (still not tappable)",
"child.editor.dependency_helper": "What to show when a chore depends on another that isn't approved yet today",
"child.blocked_by_dependency": "Do {chores} first",
"child.parent": "Parent",
"child.editor.due_days_dim": "Dim — show greyed out, non-interactive",
"child.editor.due_days_helper": "Applies to chores with due_days set when today isn't scheduled",
"child.editor.due_days_hide": "Hide — don't show",
"child.editor.due_days_show": "Show — show normally regardless",
"child.editor.first_come_dim": "Dim — show greyed out for everyone except the child who completed it",
"child.editor.first_come_helper": "What to show to the rest of the pool once a first-come chore has been claimed by one child",
"child.editor.first_come_hide": "Hide — remove once claimed (default, matches prior behavior)",
"child.editor.first_come_show": "Show — keep fully visible for everyone (still not tappable once claimed)",
"child.editor.first_come_when_completed": "First-Come Chores — When Claimed",
"child.editor.elapsed_dim": "Dim — show greyed out, non-interactive",
"child.editor.elapsed_helper": "What to show when a morning/afternoon/evening/night chore wasn't completed and that time has passed",
"child.editor.elapsed_hide": "Hide — don't show",
@@ -210,6 +210,8 @@
"child.chores_resetting_soon": "Chores resetting soon!",
"child.click_to_complete": "Click to complete",
"child.click_to_undo": "Click to undo",
"child.claimed_by_another": "Already done by {name}",
"child.claimed_by_another_generic": "Already claimed today",
"child.difficulty_easy": "Easy",
"child.difficulty_medium": "Medium",
"child.difficulty_hard": "Hard",
@@ -238,10 +240,16 @@
"child.editor.dependency_show": "Show — show normally (still not tappable)",
"child.editor.dependency_helper": "What to show when a chore depends on another that isn't approved yet today",
"child.blocked_by_dependency": "Do {chores} first",
"child.parent": "Parent",
"child.editor.due_days_dim": "Dim — show greyed out, non-interactive",
"child.editor.due_days_helper": "Applies to chores with due_days set when today isn't scheduled",
"child.editor.due_days_hide": "Hide — don't show",
"child.editor.due_days_show": "Show — show normally regardless",
"child.editor.first_come_dim": "Dim — show greyed out for everyone except the child who completed it",
"child.editor.first_come_helper": "What to show to the rest of the pool once a first-come chore has been claimed by one child",
"child.editor.first_come_hide": "Hide — remove once claimed (default, matches prior behavior)",
"child.editor.first_come_show": "Show — keep fully visible for everyone (still not tappable once claimed)",
"child.editor.first_come_when_completed": "First-Come Chores — When Claimed",
"child.editor.elapsed_dim": "Dim — show greyed out, non-interactive",
"child.editor.elapsed_helper": "What to show when a morning/afternoon/evening/night chore wasn't completed and that time has passed",
"child.editor.elapsed_hide": "Hide — don't show",
@@ -208,6 +208,8 @@
"child.chores_resetting_soon": "Réinitialisation des tâches bientôt !",
"child.click_to_complete": "Cliquez pour terminer",
"child.click_to_undo": "Cliquez pour annuler",
"child.claimed_by_another": "Déjà fait par {name}",
"child.claimed_by_another_generic": "Déjà prise aujourd'hui",
"child.difficulty_easy": "Facile",
"child.difficulty_medium": "Moyen",
"child.difficulty_hard": "Difficile",
@@ -236,10 +238,16 @@
"child.editor.dependency_show": "Afficher — affichage normal (toujours non cliquable)",
"child.editor.dependency_helper": "Ce qu'il faut afficher quand une tâche dépend d'une autre non encore approuvée aujourd'hui",
"child.blocked_by_dependency": "Fais d'abord {chores}",
"child.parent": "Parent",
"child.editor.due_days_dim": "Estomper — afficher en gris, non interactif",
"child.editor.due_days_helper": "S'applique aux tâches avec des jours de passage définis quand aujourd'hui n'est pas programmé",
"child.editor.due_days_hide": "Masquer — ne pas afficher",
"child.editor.due_days_show": "Afficher — afficher normalement malgré tout",
"child.editor.first_come_dim": "Estomper — afficher en gris pour tout le monde sauf l'enfant qui a terminé la tâche",
"child.editor.first_come_helper": "Ce qu'il faut montrer au reste du groupe une fois qu'une tâche de type premier arrivé a été prise par un enfant",
"child.editor.first_come_hide": "Masquer — retirer une fois prise (par défaut, comportement identique à avant)",
"child.editor.first_come_show": "Afficher — rester entièrement visible pour tous (toujours non cliquable une fois prise)",
"child.editor.first_come_when_completed": "Tâches premier arrivé — une fois prises",
"child.editor.elapsed_dim": "Estomper — afficher en gris, non interactif",
"child.editor.elapsed_helper": "Que montrer lorsqu'une tâche du matin/après-midi/soir/nuit n'a pas été accomplie et que le moment est passé",
"child.editor.elapsed_hide": "Masquer — ne pas afficher",
@@ -208,6 +208,8 @@
"child.chores_resetting_soon": "Oppgavene nullstilles snart!",
"child.click_to_complete": "Klikk for å fullføre",
"child.click_to_undo": "Klikk for å angre",
"child.claimed_by_another": "Allerede gjort av {name}",
"child.claimed_by_another_generic": "Allerede tatt i dag",
"child.difficulty_easy": "Lett",
"child.difficulty_medium": "Middels",
"child.difficulty_hard": "Vanskelig",
@@ -236,10 +238,16 @@
"child.editor.dependency_show": "Vis — vis normalt (fortsatt ikke trykkbar)",
"child.editor.dependency_helper": "Hva som vises når en oppgave avhenger av en annen som ikke er godkjent i dag ennå",
"child.blocked_by_dependency": "Gjør {chores} først",
"child.parent": "Forelder",
"child.editor.due_days_dim": "Dim — vis gråtonet, ikke-interaktiv",
"child.editor.due_days_helper": "Gjelder oppgaver med satte dager når i dag ikke er planlagt",
"child.editor.due_days_hide": "Skjul — ikke vis",
"child.editor.due_days_show": "Vis — vis normalt uansett",
"child.editor.first_come_dim": "Dim — vis gråtonet for alle unntatt barnet som fullførte oppgaven",
"child.editor.first_come_helper": "Hva som vises for resten av gruppen når en førstemann-oppgave er tatt av ett barn",
"child.editor.first_come_hide": "Skjul — fjern når den er tatt (standard, samme som tidligere oppførsel)",
"child.editor.first_come_show": "Vis — hold den fullt synlig for alle (fortsatt ikke trykkbar når den er tatt)",
"child.editor.first_come_when_completed": "Førstemann-oppgaver — når tatt",
"child.editor.elapsed_dim": "Dim — vis gråtonet, ikke-interaktiv",
"child.editor.elapsed_helper": "Hva som vises når en morgen/ettermiddag/kveld/natt-oppgave ikke ble fullført og tiden har gått",
"child.editor.elapsed_hide": "Skjul — ikke vis",
@@ -208,6 +208,8 @@
"child.chores_resetting_soon": "Oppgåvene nullstillast snart!",
"child.click_to_complete": "Klikk for å fullføre",
"child.click_to_undo": "Klikk for å angre",
"child.claimed_by_another": "Allereie gjort av {name}",
"child.claimed_by_another_generic": "Allereie teken i dag",
"child.difficulty_easy": "Lett",
"child.difficulty_medium": "Middels",
"child.difficulty_hard": "Vanskeleg",
@@ -236,10 +238,16 @@
"child.editor.dependency_show": "Vis — vis normalt (framleis ikkje trykkbar)",
"child.editor.dependency_helper": "Kva som blir vist når ei oppgåve avheng av ei anna som ikkje er godkjend i dag enno",
"child.blocked_by_dependency": "Gjer {chores} først",
"child.parent": "Forelder",
"child.editor.due_days_dim": "Dim — vis gråtona, ikkje-interaktiv",
"child.editor.due_days_helper": "Gjeld oppgåver med sette dagar når i dag ikkje er planlagt",
"child.editor.due_days_hide": "Gøym — ikkje vis",
"child.editor.due_days_show": "Vis — vis normalt uansett",
"child.editor.first_come_dim": "Dim — vis gråtona for alle unnateke barnet som fullførte oppgåva",
"child.editor.first_come_helper": "Kva som visast for resten av gruppa når ei førstemann-oppgåve er teken av eitt barn",
"child.editor.first_come_hide": "Gøym — fjern når ho er teken (standard, same som tidlegare åtferd)",
"child.editor.first_come_show": "Vis — hald ho fullt synleg for alle (framleis ikkje trykkbar når ho er teken)",
"child.editor.first_come_when_completed": "Førstemann-oppgåver — når teken",
"child.editor.elapsed_dim": "Dim — vis gråtona, ikkje-interaktiv",
"child.editor.elapsed_helper": "Kva som visast når ei morgon/ettermiddag/kveld/natt-oppgåve ikkje vart fullført og tida har gått",
"child.editor.elapsed_hide": "Gøym — ikkje vis",
@@ -208,6 +208,8 @@
"child.chores_resetting_soon": "As tarefas reiniciam em breve!",
"child.click_to_complete": "Clica para concluir",
"child.click_to_undo": "Clica para desfazer",
"child.claimed_by_another": "Já feita por {name}",
"child.claimed_by_another_generic": "Já reivindicada hoje",
"child.difficulty_easy": "Fácil",
"child.difficulty_medium": "Médio",
"child.difficulty_hard": "Difícil",
@@ -236,10 +238,16 @@
"child.editor.dependency_show": "Mostrar — mostrar normalmente (continua sem toque)",
"child.editor.dependency_helper": "O que mostrar quando uma tarefa depende de outra ainda não aprovada hoje",
"child.blocked_by_dependency": "Faça primeiro {chores}",
"child.parent": "Responsável",
"child.editor.due_days_dim": "Atenuar — mostrar em cinzento, sem interação",
"child.editor.due_days_helper": "Aplica-se a tarefas com due_days definido quando hoje não está agendado",
"child.editor.due_days_hide": "Ocultar — não mostrar",
"child.editor.due_days_show": "Mostrar — mostrar normalmente",
"child.editor.first_come_dim": "Atenuar — mostrar em cinza para todos, exceto a criança que concluiu a tarefa",
"child.editor.first_come_helper": "O que mostrar para o resto do grupo depois que uma tarefa do tipo quem chega primeiro for reivindicada por uma criança",
"child.editor.first_come_hide": "Ocultar — remover assim que reivindicada (padrão, igual ao comportamento anterior)",
"child.editor.first_come_show": "Mostrar — manter totalmente visível para todos (continua sem toque depois de reivindicada)",
"child.editor.first_come_when_completed": "Tarefas Quem Chega Primeiro — Quando Reivindicadas",
"child.editor.elapsed_dim": "Atenuar — mostrar em cinzento, sem interação",
"child.editor.elapsed_helper": "O que mostrar quando uma tarefa da manhã/tarde/noite/madrugada não foi concluída e o horário já passou",
"child.editor.elapsed_hide": "Ocultar — não mostrar",
@@ -208,6 +208,8 @@
"child.chores_resetting_soon": "As tarefas reiniciam em breve!",
"child.click_to_complete": "Clica para concluir",
"child.click_to_undo": "Clica para desfazer",
"child.claimed_by_another": "Já feita por {name}",
"child.claimed_by_another_generic": "Já reclamada hoje",
"child.difficulty_easy": "Fácil",
"child.difficulty_medium": "Médio",
"child.difficulty_hard": "Difícil",
@@ -236,10 +238,16 @@
"child.editor.dependency_show": "Mostrar — mostrar normalmente (continua sem toque)",
"child.editor.dependency_helper": "O que mostrar quando uma tarefa depende de outra ainda não aprovada hoje",
"child.blocked_by_dependency": "Faz primeiro {chores}",
"child.parent": "Responsável",
"child.editor.due_days_dim": "Atenuar — mostrar em cinzento, sem interação",
"child.editor.due_days_helper": "Aplica-se a tarefas com due_days definido quando hoje não está agendado",
"child.editor.due_days_hide": "Ocultar — não mostrar",
"child.editor.due_days_show": "Mostrar — mostrar normalmente",
"child.editor.first_come_dim": "Atenuar — mostrar em cinzento para todos exceto a criança que concluiu a tarefa",
"child.editor.first_come_helper": "O que mostrar ao resto do grupo depois de uma tarefa do tipo quem chega primeiro ser reclamada por uma criança",
"child.editor.first_come_hide": "Ocultar — remover assim que reclamada (predefinição, igual ao comportamento anterior)",
"child.editor.first_come_show": "Mostrar — manter totalmente visível para todos (continua sem toque depois de reclamada)",
"child.editor.first_come_when_completed": "Tarefas Quem Chega Primeiro — Quando Reclamadas",
"child.editor.elapsed_dim": "Atenuar — mostrar em cinzento, sem interação",
"child.editor.elapsed_helper": "O que mostrar quando uma tarefa da manhã/tarde/noite/madrugada não foi concluída e o horário já passou",
"child.editor.elapsed_hide": "Ocultar — não mostrar",
@@ -1118,6 +1118,29 @@ class TaskMateChildCard extends LitElement {
pointer-events: none;
}
/* First-come chore claimed by a sibling today — dimmed/greyed, same
treatment as recurrence-unavailable (first_come_claimed_mode: dim) */
.chore-card.first-come-unavailable {
opacity: 0.45;
filter: grayscale(0.6);
pointer-events: none;
}
.first-come-label {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 0.72rem;
font-weight: 600;
color: var(--secondary-text-color);
background: color-mix(in srgb, var(--primary-text-color, #212121) 8%, transparent);
border-radius: 8px;
padding: 1px 6px;
margin-top: 3px;
}
.first-come-label ha-icon { --mdc-icon-size: 12px; }
/* Locked preview — chore is visible but not yet claimable because its
time-of-day window has not started. Dim with a padlock badge. */
.chore-card.chore-locked {
@@ -2593,9 +2616,18 @@ class TaskMateChildCard extends LitElement {
/** Mirror of _renderChoreCard's "completed today" detection, designed branch only. */
_isChoreDone(chore, child, todaysCompletions) {
// async_parent_complete_chore writes child_id="__parent__" to dismiss a
// chore for the WHOLE pool with zero points — it never credits a specific
// child. Every other assignment mode treats that as "done for whoever is
// looking", which is the intended behavior. first_come is exclusive: a
// pool-wide dismissal means nobody actually completed it, so it must not
// read as this child's own done state (green row + live undo chip) for
// anyone — it should fall through to the locked/dimmed row instead
// (chore._isFirstComeLocked, set in _filterAndSortChores).
const isFirstCome = chore.assignment_mode === "first_come";
const childCompletionsToday = (todaysCompletions || []).filter(
(comp) => comp.chore_id === chore.id
&& (comp.child_id === child.id || comp.child_id === "__parent__")
&& (comp.child_id === child.id || (!isFirstCome && comp.child_id === "__parent__"))
&& !comp.bonus_subtask_id
);
let count = childCompletionsToday.length;
@@ -2608,7 +2640,11 @@ class TaskMateChildCard extends LitElement {
if (!actualTs.some(a => Math.abs(a - t) < 2000)) count += 1;
}
}
const done = count >= (chore.daily_limit || 1);
// first_come is a single-winner race whatever quota is stored on the chore —
// the filter and the backend both clamp it to 1 (coord_assignments). Reading
// the raw daily_limit here would leave the winner's own row looking incomplete
// and tappable, and complete_chore would then silently no-op the tap (#805).
const done = count >= (isFirstCome ? 1 : (chore.daily_limit || 1));
return { done, completions: childCompletionsToday };
}
@@ -2651,6 +2687,7 @@ class TaskMateChildCard extends LitElement {
const elapsedTimeMode = this.config.elapsed_time_mode || "dim";
const dependencyMode = this.config.dependency_mode || "hide";
const recurrenceDoneMode = this.config.recurrence_done_mode || "dim";
const firstComeClaimedMode = this.config.first_come_claimed_mode || "hide";
// Annotate each chore with done-state + handlers, preserving the classic path's logic.
const rows = childChores.map((chore, i) => {
@@ -2662,8 +2699,12 @@ class TaskMateChildCard extends LitElement {
// Recurring chore still inside its cooldown. Excluded when done so the
// undo chip stays live, exactly as on the classic row.
const recLocked = !!chore._isRecurrenceLocked && !done;
// First-come chore claimed by a different pool member (first_come_claimed_mode
// dim/show only — hide is filtered upstream same as recurrence). Never true
// for the winner's own row: _isFirstComeLocked is only set on siblings.
const firstComeLocked = !!chore._isFirstComeLocked && !done;
const onAct = () => {
if (loading || blocked || recLocked) return;
if (loading || blocked || recLocked || firstComeLocked) return;
if (done) this._handleUndo(chore, child, completions);
else this._handleComplete(chore, child);
};
@@ -2675,10 +2716,11 @@ class TaskMateChildCard extends LitElement {
(chore._isTimeElapsed && elapsedTimeMode === "dim") ||
(blocked && dependencyMode === "dim") ||
(chore._isRecurrenceLocked && recurrenceDoneMode === "dim") ||
(chore._isFirstComeLocked && firstComeClaimedMode === "dim") ||
chore._isLockedPreview === true
);
return {
chore, child, done, loading, onAct, index: i, dimmed, blocked, recLocked,
chore, child, done, loading, onAct, index: i, dimmed, blocked, recLocked, firstComeLocked,
tone: this._designTone(i),
glyph: this._choreGlyph(chore),
points: chore.effective_points ?? chore.points,
@@ -2816,13 +2858,21 @@ class TaskMateChildCard extends LitElement {
const recLabel = r.recLocked
? (r.chore.recurrence ? r.chore.recurrence.replace(/_/g, " ") : this._t("child.recurring"))
: "";
// Same idea as recLabel, but for a first-come chore a sibling already
// claimed today — name it when we know who, otherwise a generic label.
const firstComeLabel = r.firstComeLocked
? (r.chore._firstComeCompletedByName
? this._t("child.claimed_by_another", { name: r.chore._firstComeCompletedByName })
: this._t("child.claimed_by_another_generic"))
: "";
return html`
${r.mandatory || r.photo || depNames.length || recLabel ? html`
${r.mandatory || r.photo || depNames.length || recLabel || firstComeLabel ? html`
<div class="tmd-meta">
${r.mandatory ? html`<span class="tmd-tag mandatory">⚠ ${this._t("child.mandatory")}</span>` : ""}
${r.photo ? html`<span class="tmd-tag photo">📷 ${this._t("child.photo_needed")}</span>` : ""}
${depNames.length ? html`<span class="tmd-tag">🔒 ${this._t("child.blocked_by_dependency", { chores: depNames.join(", ") })}</span>` : ""}
${recLabel ? html`<span class="tmd-tag">🕒 ${recLabel}</span>` : ""}
${firstComeLabel ? html`<span class="tmd-tag">✅ ${firstComeLabel}</span>` : ""}
</div>` : ""}
${showDesc ? html`<div class="tmd-desc">${r.chore.description}</div>` : ""}`;
}
@@ -2838,7 +2888,7 @@ class TaskMateChildCard extends LitElement {
_designDoneBtn(r, label, cls) {
return html`<button class="btn ${cls || ""}"
?disabled=${r.loading || r.blocked || r.recLocked}
?disabled=${r.loading || r.blocked || r.recLocked || r.firstComeLocked}
@click=${(e) => { e.stopPropagation(); r.onAct(); }}>${label}</button>`;
}
@@ -3009,6 +3059,7 @@ class TaskMateChildCard extends LitElement {
// as unavailable, so hiding is what matches it (#793).
const dependencyMode = this.config.dependency_mode || 'hide';
const recurrenceDoneMode = this.config.recurrence_done_mode || 'dim';
const firstComeClaimedMode = this.config.first_come_claimed_mode || 'hide';
const allCompletionsToday = attrs.todays_completions || [];
// Availability comes from the backend's own matrix (#803). The card cannot
// work a recurrence window out for itself: last_completed is not exposed,
@@ -3083,7 +3134,35 @@ class TaskMateChildCard extends LitElement {
);
const hasPendingBonus = bonusSubtasks.some(b => b && b.id && !completedBonusIds.has(b.id));
if (!hasPendingBonus) {
isAssignedToChild = false;
// first_come_claimed_mode (#first-come-visibility): by default a
// claimed first-come chore disappears for the whole pool,
// winner included — unchanged from prior behaviour. Under
// dim/show it stays in the list instead: the winner's own row
// still renders as done via _isChoreDone below, and every
// other pool member is flagged locked so the render layer can
// grey it out and refuse taps (the backend rejects the
// completion anyway).
if (assignmentMode === 'first_come' && firstComeClaimedMode !== 'hide') {
const completedByThisChild = allTodayCompletions.some(
(comp) => comp.chore_id === chore.id
&& String(comp.child_id) === childId
&& !comp.bonus_subtask_id
);
chore._isFirstComeLocked = !completedByThisChild;
if (chore._isFirstComeLocked) {
const winningCompletion = allTodayCompletions.find(
(comp) => comp.chore_id === chore.id && !comp.bonus_subtask_id
);
const winner = winningCompletion
? (attrs.children || []).find(c => String(c.id) === String(winningCompletion.child_id))
: null;
chore._firstComeCompletedByName = winner
? winner.name
: (winningCompletion && winningCompletion.child_id === '__parent__' ? this._t('child.parent') : '');
}
} else {
isAssignedToChild = false;
}
}
}
}
@@ -3678,12 +3757,23 @@ class TaskMateChildCard extends LitElement {
const isCelebrating = this._celebrating === chore.id;
// Check how many times this chore was completed today by this child
// Both pending (awaiting approval) AND approved completions count toward the daily limit
// Both pending (awaiting approval) AND approved completions count toward the daily limit.
// Exception: async_parent_complete_chore's child_id="__parent__" dismisses a
// first_come chore for the whole pool with zero points and no winner — it must
// not read as THIS child's own done state (green row + live undo chip) for
// anyone, or every sibling gets a completed chip for a chore none of them did.
// It should fall through to the locked/dimmed row instead (chore._isFirstComeLocked
// below). Every other assignment mode keeps the existing __parent__-counts-for-
// everyone behavior, which is intentional there.
const isFirstCome = chore.assignment_mode === "first_come";
const childCompletionsToday = todaysCompletions.filter(
(comp) => comp.chore_id === chore.id && (comp.child_id === child.id || comp.child_id === "__parent__") && !comp.bonus_subtask_id
(comp) => comp.chore_id === chore.id && (comp.child_id === child.id || (!isFirstCome && comp.child_id === "__parent__")) && !comp.bonus_subtask_id
);
let completionsToday = childCompletionsToday.length;
const dailyLimit = chore.daily_limit || 1;
// Clamped for first_come the same way the filter and the backend clamp it: a
// stored daily_limit > 1 must not make the winner's finished row read as still
// outstanding, or it invites a tap that complete_chore silently swallows (#805).
const dailyLimit = isFirstCome ? 1 : (chore.daily_limit || 1);
// Check for optimistic completions (chores just completed but not yet confirmed by HA)
const optimisticKey = `${chore.id}_${child.id}`;
@@ -3730,6 +3820,12 @@ class TaskMateChildCard extends LitElement {
// mode-gated. Same split dependency_mode uses.
const recurrenceLocked = !!chore._isRecurrenceLocked && !isCompletedForToday;
const notAvailableRecurrence = recurrenceLocked && recurrenceDoneMode === 'dim';
// First-come chore claimed by a sibling today (first_come_claimed_mode
// dim/show — hide is filtered upstream). Never true on the winner's own
// row: _isFirstComeLocked is only set for the other pool members.
const firstComeClaimedMode = this.config.first_come_claimed_mode || 'hide';
const firstComeLocked = !!chore._isFirstComeLocked && !isCompletedForToday;
const notAvailableFirstCome = firstComeLocked && firstComeClaimedMode === 'dim';
// Elapsed: only dim incomplete chores — completed ones keep their green "done" style
const elapsedTimeMode = this.config.elapsed_time_mode || 'dim';
const timeElapsed = chore._isTimeElapsed && !isCompletedForToday && elapsedTimeMode === 'dim';
@@ -3752,13 +3848,14 @@ class TaskMateChildCard extends LitElement {
if (isLockedPreview) return; // Next-period preview — not yet claimable
if (depBlocked) return; // Prerequisite chore not approved yet
if (timeElapsed) return; // Time period passed — not interactive
if (firstComeLocked) return; // Claimed by another child today — not interactive
if (isCompletedForToday) {
this._handleUndo(chore, child, childCompletionsToday);
} else {
this._handleComplete(chore, child);
}
};
const isInteractive = !(isLoading || notDueToday || recurrenceLocked || isLockedPreview || depBlocked || timeElapsed);
const isInteractive = !(isLoading || notDueToday || recurrenceLocked || isLockedPreview || depBlocked || timeElapsed || firstComeLocked);
const handleRowKeyDown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
@@ -3774,13 +3871,17 @@ class TaskMateChildCard extends LitElement {
? this._t('child.not_scheduled_for_today')
: timeElapsed
? this._t('child.time_has_passed')
: firstComeLocked
? (chore._firstComeCompletedByName
? this._t('child.claimed_by_another', { name: chore._firstComeCompletedByName })
: this._t('child.claimed_by_another_generic'))
: isCompletedForToday
? this._t('child.click_to_undo')
: this._t('child.click_to_complete');
return html`
<div
class="chore-card ${chore.mandatory ? "mandatory" : ""} ${isLoading ? "loading" : ""} ${isCelebrating ? "celebrating" : ""} ${isCompletedForToday ? "completed" : ""} ${notDueToday ? "not-due-today" : ""} ${notAvailableRecurrence ? "recurrence-unavailable" : ""} ${timeElapsed ? "time-elapsed" : ""} ${isLockedPreview ? "chore-locked" : ""} ${depDimmed ? "dependency-blocked" : ""}"
class="chore-card ${chore.mandatory ? "mandatory" : ""} ${isLoading ? "loading" : ""} ${isCelebrating ? "celebrating" : ""} ${isCompletedForToday ? "completed" : ""} ${notDueToday ? "not-due-today" : ""} ${notAvailableRecurrence ? "recurrence-unavailable" : ""} ${notAvailableFirstCome ? "first-come-unavailable" : ""} ${timeElapsed ? "time-elapsed" : ""} ${isLockedPreview ? "chore-locked" : ""} ${depDimmed ? "dependency-blocked" : ""}"
role="button"
tabindex="${isInteractive ? '0' : '-1'}"
aria-disabled="${isInteractive ? 'false' : 'true'}"
@@ -3812,6 +3913,14 @@ class TaskMateChildCard extends LitElement {
${chore.recurrence ? chore.recurrence.replace(/_/g, ' ') : this._t('child.recurring')}
</div>
` : ''}
${firstComeLocked ? html`
<div class="first-come-label">
<ha-icon icon="mdi:check-circle-outline"></ha-icon>
${chore._firstComeCompletedByName
? this._t('child.claimed_by_another', { name: chore._firstComeCompletedByName })
: this._t('child.claimed_by_another_generic')}
</div>
` : ''}
<div class="chore-points">
<ha-icon icon="${pointsIcon}"></ha-icon>
+${chore.effective_points ?? chore.points}
@@ -4829,6 +4938,19 @@ class TaskMateChildCardEditor extends LitElement {
},
},
},
{
name: 'first_come_claimed_mode',
selector: {
select: {
options: [
{ value: 'hide', label: this._t('child.editor.first_come_hide') },
{ value: 'dim', label: this._t('child.editor.first_come_dim') },
{ value: 'show', label: this._t('child.editor.first_come_show') },
],
mode: 'dropdown',
},
},
},
{
name: 'dependency_mode',
selector: {
@@ -4872,6 +4994,7 @@ class TaskMateChildCardEditor extends LitElement {
card_design: this._t('common.design.field_label'),
due_days_mode: this._t('child.editor.chores_not_due_today'),
recurrence_done_mode: this._t('child.editor.recurring_when_completed'),
first_come_claimed_mode: this._t('child.editor.first_come_when_completed'),
dependency_mode: this._t('child.editor.dependency_mode'),
elapsed_time_mode: this._t('child.editor.missed_time_chores'),
show_countdown: this._t('child.editor.show_countdown'),
@@ -4891,6 +5014,7 @@ class TaskMateChildCardEditor extends LitElement {
time_category: this._t('child.editor.time_category_helper'),
due_days_mode: this._t('child.editor.due_days_helper'),
recurrence_done_mode: this._t('child.editor.recurrence_helper'),
first_come_claimed_mode: this._t('child.editor.first_come_helper'),
dependency_mode: this._t('child.editor.dependency_helper'),
elapsed_time_mode: this._t('child.editor.elapsed_helper'),
};
@@ -4907,6 +5031,7 @@ class TaskMateChildCardEditor extends LitElement {
card_design: this.config.card_design || 'global',
due_days_mode: this.config.due_days_mode || 'hide',
recurrence_done_mode: this.config.recurrence_done_mode || 'dim',
first_come_claimed_mode: this.config.first_come_claimed_mode || 'hide',
dependency_mode: this.config.dependency_mode || 'hide',
elapsed_time_mode: this.config.elapsed_time_mode || 'dim',
show_countdown: this.config.show_countdown !== false,
@@ -4953,6 +5078,7 @@ class TaskMateChildCardEditor extends LitElement {
show_countdown: true, show_description: false, debug: false,
time_category: 'morning', due_days_mode: 'hide',
recurrence_done_mode: 'dim', elapsed_time_mode: 'dim',
first_come_claimed_mode: 'hide',
};
if (value === '' || value === null || value === undefined) {
delete newConfig[key];