331 files
This commit is contained in:
@@ -298,6 +298,8 @@ def _build_task_summary(
|
||||
"trigger_baseline_value": ct.get("_trigger_baseline_value"),
|
||||
"times_performed": ct.get("_times_performed", 0),
|
||||
"total_cost": ct.get("_total_cost", 0.0),
|
||||
# #200: per completion, credits left out (total_cost nets them).
|
||||
"average_cost": ct.get("_average_cost"),
|
||||
"average_duration": ct.get("_average_duration"),
|
||||
# Adaptive scheduling
|
||||
"adaptive_config": task_data.get("adaptive_config"),
|
||||
@@ -826,6 +828,7 @@ def async_register_commands(hass: HomeAssistant) -> None:
|
||||
ws_update_history_entry,
|
||||
ws_update_task,
|
||||
)
|
||||
from .tasks_bulk import ws_update_many_tasks
|
||||
from .users import ws_assign_user, ws_list_users, ws_tasks_by_user
|
||||
from .vacation import (
|
||||
ws_vacation_end_now,
|
||||
@@ -857,6 +860,7 @@ def async_register_commands(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, ws_unarchive_task)
|
||||
websocket_api.async_register_command(hass, ws_pause_task)
|
||||
websocket_api.async_register_command(hass, ws_resume_task)
|
||||
websocket_api.async_register_command(hass, ws_update_many_tasks)
|
||||
websocket_api.async_register_command(hass, ws_list_tasks)
|
||||
websocket_api.async_register_command(hass, ws_task_history)
|
||||
websocket_api.async_register_command(hass, ws_complete_task)
|
||||
|
||||
@@ -26,11 +26,13 @@ from ..const import (
|
||||
CONF_OBJECT_MODEL,
|
||||
CONF_TASKS,
|
||||
DOMAIN,
|
||||
MAX_COST,
|
||||
MAX_ENTITY_SLUG_LENGTH,
|
||||
MAX_ID_LENGTH,
|
||||
MAX_IMPORT_PAYLOAD_BYTES,
|
||||
MAX_JSON_IMPORT_PAYLOAD_BYTES,
|
||||
MAX_VACATION_EXEMPT_TASKS,
|
||||
MIN_COST,
|
||||
)
|
||||
from ..helpers.aggregate import object_name
|
||||
from ..helpers.dates import normalize_hhmm, parse_iso_date
|
||||
@@ -92,7 +94,8 @@ _TASK_FIELD_TYPES: dict[str, type | tuple[type, ...]] = {
|
||||
|
||||
|
||||
def _sanitize_history(history: Any) -> list[dict[str, Any]]:
|
||||
"""Scrub imported history entries: drop a non-finite/negative ``cost``.
|
||||
"""Scrub imported history entries: drop a non-finite or out-of-range ``cost``
|
||||
(a credit — #200 — is negative and stays).
|
||||
|
||||
Every live write path range-guards cost, but import copied history verbatim
|
||||
and ``json.loads``/``yaml.safe_load`` both accept ``NaN``/``Infinity``. Such
|
||||
@@ -132,7 +135,9 @@ def _sanitize_history(history: Any) -> list[dict[str, Any]]:
|
||||
if "completed_by" in clean and not isinstance(clean["completed_by"], str):
|
||||
clean.pop("completed_by")
|
||||
cost = clean.get("cost")
|
||||
if isinstance(cost, bool) or not isinstance(cost, (int, float)) or not math.isfinite(cost) or cost < 0:
|
||||
if "cost" in clean and (
|
||||
isinstance(cost, bool) or not isinstance(cost, (int, float)) or not math.isfinite(cost) or not MIN_COST <= cost <= MAX_COST
|
||||
):
|
||||
clean.pop("cost", None)
|
||||
# #104 bookkeeping: the parts value is money too (same NaN hole), the
|
||||
# two markers are exact values or nothing.
|
||||
|
||||
@@ -19,6 +19,7 @@ from ..const import (
|
||||
MAX_DURATION_MINUTES,
|
||||
MAX_TEXT_LENGTH,
|
||||
MAX_TIMESTAMP_LENGTH,
|
||||
MIN_COST,
|
||||
)
|
||||
from ..helpers.completion_photos import MAX_COMPLETION_PHOTOS, normalize_photo_doc_ids
|
||||
from ..helpers.parts import MAX_PART_STOCK
|
||||
@@ -71,7 +72,7 @@ def _refuse_too_early(connection: websocket_api.ActiveConnection, msg: dict[str,
|
||||
vol.Required("entry_id"): ID_FIELD,
|
||||
vol.Required("task_id"): ID_FIELD,
|
||||
vol.Optional("notes"): vol.Any(vol.All(str, vol.Length(max=MAX_TEXT_LENGTH)), None),
|
||||
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=0, max=MAX_COST)), None),
|
||||
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=MIN_COST, max=MAX_COST)), None),
|
||||
vol.Optional("duration"): vol.Any(vol.All(vol.Coerce(int), vol.Range(min=0, max=MAX_DURATION_MINUTES)), None),
|
||||
# Restrict checklist_state to {string-key (≤500): bool, ...} with
|
||||
# a hard cap on entries. Without this, attackers (or bad clients)
|
||||
|
||||
@@ -16,6 +16,7 @@ from ..const import (
|
||||
MAX_META_LENGTH,
|
||||
MAX_TEXT_LENGTH,
|
||||
MAX_TIMESTAMP_LENGTH,
|
||||
MIN_COST,
|
||||
)
|
||||
from ..helpers.completion_photos import (
|
||||
MAX_COMPLETION_PHOTOS,
|
||||
@@ -60,7 +61,7 @@ from . import (
|
||||
# Patch fields — all optional; absent fields stay unchanged.
|
||||
vol.Optional("timestamp"): vol.All(str, vol.Length(max=MAX_TIMESTAMP_LENGTH)),
|
||||
vol.Optional("notes"): vol.Any(vol.All(str, vol.Length(max=MAX_TEXT_LENGTH)), None),
|
||||
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=0, max=MAX_COST)), None),
|
||||
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=MIN_COST, max=MAX_COST)), None),
|
||||
vol.Optional("duration"): vol.Any(vol.All(vol.Coerce(int), vol.Range(min=0, max=MAX_DURATION_MINUTES)), None),
|
||||
vol.Optional("completed_by"): vol.Any(vol.All(str, vol.Length(max=MAX_META_LENGTH)), None),
|
||||
# Recorded readings (#161 phase 2): a typo among nine meter values is
|
||||
|
||||
Reference in New Issue
Block a user