/*! * WashData - Home Assistant integration for appliance cycle monitoring via smart plugs. * Copyright (C) 2026 Lukas Bandura * SPDX-License-Identifier: AGPL-3.0-or-later * * This file is a MINIFIED BUILD. The corresponding readable source ships in the * same directory and is the licensed, preferred form for modification. * This program comes with ABSOLUTELY NO WARRANTY. See the GNU Affero General * Public License for details: . */ const w="ha_washdata",Zt=(()=>{try{return new URL(import.meta.url).searchParams.get("v")||""}catch{return""}})(),ve=2e4,xe=6e3,zt=25,St=50,$e="https://github.com/iluebbe/maintenance_supporter",Mt=["running","starting","paused","user_paused","ending","anti_wrinkle"],ke=[{key:"show_contributor",def:!0,labelKey:"lbl.show_contributor",labelFb:"Show contributor names",docKey:"setting.show_contributor.doc",docFb:'Show the "by " attribution on community appliances and reference cycles.'},{key:"share_name",def:!1,labelKey:"lbl.share_name",labelFb:"Show my name on what I share",docKey:"setting.share_name.doc",docFb:"Show your account name as the uploader of cycles you share. Off: shared without a name."}],Bt=34,Qt=.5,Se=200,Te=5,Tt=22,Ft=[0,5,10,30,60],Ee=10,Ce=320,Q=["#e6194B","#3cb44b","#4363d8","#f58231","#911eb4","#42d4f4","#f032e6","#bfef45","#fabed4","#469990","#dcbeff","#9A6324","#800000","#aaffc3","#808000","#ffd8b1","#000075","#a9a9a9"],Pe="{device}, {duration}, {minutes}, {program}, {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}",yt=[{id:"basic",label:"Basic",intro:"Core identity and the essentials most setups need.",groups:[{sub:"Device info",fields:[{key:"name",label:"Device Name",basic:!0,type:"text",doc:"Name shown in Home Assistant for this device."},{key:"device_type",label:"Device Type",basic:!0,type:"devicetype",doc:"Sets detection defaults (thresholds, off delay, end handling) for this kind of appliance. Change it only if the device was set up as the wrong type."},{key:"store_brand",label:"Appliance Brand",type:"storebrand",optional:!0,basic:!0,doc:"Optional. Your appliance brand from the community catalog, used to find and share reference recordings. Leave blank if you do not use online features."},{key:"store_model",label:"Appliance Model",type:"storemodel",optional:!0,basic:!0,doc:"Optional. Your appliance model from the community catalog (pick a brand first). If it is not listed, you can add it."}]},{sub:"Basic configuration",fields:[{key:"power_sensor",label:"Power Sensor",basic:!0,type:"entity",domain:"sensor",doc:"The sensor that reports this appliance's live power in watts (e.g. sensor.washer_power). All detection is based on it."},{key:"min_power",label:"Minimum Power",unit:"W",type:"number",step:.1,min:0,def:2,basic:!0,doc:"Readings below this count as 0 W (standby). Filters out the idle draw of smart plugs and standby LEDs."},{key:"off_delay",label:"Off Delay",unit:"s",type:"number",min:0,def:180,basic:!0,doc:"How long power must stay low before the cycle is finished. Power coming back within this time continues the cycle. Dishwashers can be idle 20-60 min while drying, so set it longer than that."},{key:"linked_device",label:"Group Under Device",type:"device",doc:'Optional. Show this WashData device under another device (e.g. the smart plug) in Home Assistant, as "Connected via ...".'}]},{sub:"Energy",fields:[{key:"energy_sensor",label:"Energy Meter Entity",type:"entity",domain:"sensor",optional:!0,doc:"Optional kWh or Wh counter (total_increasing, e.g. the plug's own meter). Cycle energy is then read from it, which is more accurate than computing it from a slow power sensor. If its reading is unusable, the computed value is used."},{key:"energy_price_entity",label:"Energy Price Entity",type:"entity",domain:"sensor",basic:!0,notPrice:!0,doc:"Sensor with the current price per kWh (e.g. a dynamic tariff). It overrides the static price below, so it must be a price, not the plug's power or energy sensor."},{key:"energy_price_static",label:"Static Energy Price (per kWh)",type:"number",step:.001,min:0,basic:!0,doc:"Fixed price per kWh, used when no price entity is set."},{key:"energy_price_dynamic",label:"Time-Weighted Cost",type:"checkbox",def:!0,basic:!0,doc:"Charge each cycle at the price at each moment it ran, instead of the price when it finished. Needs a price entity. Process History (Diagnostics) recosts older cycles."}]}]},{id:"detection",label:"Detection",intro:"How a cycle is detected as starting, running and finishing.",groups:[{sub:"Thresholds & Gap",fields:[{key:"start_threshold_w",label:"Start Threshold",unit:"W",type:"number",step:1,min:0,doc:"Power must rise above this to start a cycle. Too low: standby power causes false starts. Too high: slow-starting programs (cold fill) are missed."},{key:"stop_threshold_w",label:"Stop Threshold",unit:"W",type:"number",step:.1,min:0,doc:"Power must fall below this before the off delay starts counting. Keep it below the Start Threshold. Too high, and low-power phases (rinse holds, anti-crease) start the end countdown by mistake."},{key:"min_off_gap",label:"Min Off Gap",unit:"s",type:"number",min:0,doc:"An off period shorter than this does not split a cycle, so soak pauses stay in one wash. Keep it shorter than the gap between back-to-back loads, or those merge into one cycle."}]},{sub:"Cycle Start",fields:[{key:"start_duration_threshold",label:"Start Duration",internal:!0,unit:"s",type:"number",min:0,def:5,doc:"Power must stay above the Start Threshold this long before a cycle starts, so brief on/off blips are ignored."},{key:"start_energy_threshold",label:"Start Energy",internal:!0,unit:"Wh",type:"number",step:.01,min:0,def:.2,doc:"Energy the appliance must use before the cycle counts as running. A short power spike uses little energy, so it does not start a cycle."},{key:"completion_min_seconds",label:"Min Cycle Duration",unit:"s",type:"number",min:0,def:600,doc:"Cycles shorter than this (test runs, opening the door to add a sock) are saved as Interrupted and get no finish notification."},{key:"curve_preroll_seconds",label:"Curve Pre-roll",unit:"s",type:"number",step:10,min:0,max:600,def:0,doc:"Adds readings from failed start attempts to the front of a cycle, for machines that probe before starting (door lock, first fill). 300 s covers most; 0 = off. Recorded starts move earlier, so learned durations drift for a while."}]},{sub:"Cycle End",fields:[{key:"end_energy_threshold",label:"End Energy",unit:"Wh",type:"number",step:.001,min:0,def:.05,doc:"Energy allowed during the off delay. More than this and the cycle keeps going, so tumbles and drying tails stay attached. Lower it if cycles end during a tail; raise it if finished cycles keep waiting."},{key:"smart_termination_duration_ratio",label:"Smart Termination Ratio",type:"number",step:.01,min:.5,max:1,doc:"Fraction of the program's average duration a cycle must reach before Smart Termination may end it early. If run times vary (water temperature, sensor-dry, load size), lower it (e.g. 0.85). Leave empty for 0.98 (0.99 on dishwashers)."}]},{sub:"Power Off",fields:[{key:"power_off_threshold_w",label:"Power Off Threshold",unit:"W",type:"number",step:.1,min:0,def:0,doc:"After a cycle, power below this for the Power Off Delay sets the state to Off, instead of the Progress Reset Delay doing it. Set it above the switched-off draw, below standby and the Stop Threshold. 0 disables it (default)."},{key:"power_off_delay",label:"Power Off Delay",unit:"s",type:"number",min:0,def:30,doc:"How long power must stay below the Power Off Threshold after a cycle before the state returns to Off. Only used when that threshold is above 0. Checked periodically, so the real wait can be slightly longer."}]},{sub:"Signal Processing",fields:[{key:"sampling_interval",label:"Sampling Interval",internal:!0,unit:"s",type:"number",min:1,def:30,doc:"Minimum time between processed readings. Readings that arrive sooner are skipped (low readings during a cycle are always kept). Set it to your plug's reporting interval or lower; a higher value only throws readings away."}]}]},{id:"matching",label:"Matching",intro:"How finished cycles are matched to learned profiles and labelled.",notDeviceTypes:["other"],groups:[{sub:"Match Scoring",fields:[{key:"profile_match_threshold",label:"Match Threshold",type:"number",step:.01,min:0,max:1,def:.4,doc:"Live match score a cycle needs before Smart Termination or the anti-crease finish may end it early. It does not change the shown program. Raise it if weak matches end cycles early; lower it if clear matches wait the off delay."},{key:"profile_unmatch_threshold",label:"Unmatch Threshold",internal:!0,type:"number",step:.01,min:0,max:1,def:.35,doc:"If the live match score drops below this, the current program guess is cleared. Keep it a little below the Match Threshold so a brief dip does not reset it."},{key:"profile_match_interval",label:"Match Interval",internal:!0,unit:"s",type:"number",min:0,doc:"How often to match a running cycle against profiles. Default 300 s (5 min)."}]},{sub:"Duration Gates",fields:[{key:"profile_match_min_duration_ratio",label:"Min Duration Ratio",internal:!0,type:"number",step:.01,min:0,max:1,def:.1,doc:"A program is only considered after the cycle has run this fraction of its typical duration. 0.10: a 2-hour program becomes a candidate after 12 minutes. Not applied in a cycle's first 15 minutes."},{key:"profile_match_max_duration_ratio",label:"Max Duration Ratio",internal:!0,type:"number",step:.01,min:0,def:1.8,doc:"A program is dropped once the cycle has run longer than this multiple of its typical duration. 1.8: a cycle 80% longer than the program no longer matches it."},{key:"duration_tolerance",label:"Estimate Tolerance",internal:!0,type:"number",step:.01,min:0,max:1,def:.1,doc:"Learning feedback only: a cycle within +/-X% of its estimated duration counts as a good estimate. Does not affect matching."}]},{sub:"Profile Evidence",fields:[{key:"profile_evidence_sources",label:"Cycles that shape a program",type:"checkboxlist",def:["real_cycles","reference_cycles","backfill_cycles"],choices:[["real_cycles","Cycles this machine ran"],["reference_cycles","Downloaded from the community store"],["backfill_cycles","Found in imported power history"]],doc:"Which kinds of cycles shape each program's curve and matching. Unticking a kind deletes nothing; useful if you do not trust imported data. Statistics always count only this machine's cycles. Unticking all is ignored."}]},{sub:"Auto-Labeling",fields:[{key:"auto_label_confidence",label:"Auto-Label Confidence",type:"number",step:.01,min:0,max:1,def:.9,doc:"Match score at cycle end at or above which the program is labelled without asking. Raise it for more certainty; lower it to label more cycles automatically. Keep it above Learning Confidence."},{key:"learning_confidence",label:"Learning Confidence",internal:!0,type:"number",step:.01,min:0,max:1,def:.6,doc:"Lowest match score for labelling a finished cycle. A clear winner is labelled; a close call goes to the review queue in Cycles. Below this, nothing is labelled or asked. Keep it below Auto-Label Confidence."}]}]},{id:"timing",label:"Timing & Watchdog",intro:"Background cadence, the offline watchdog and housekeeping.",groups:[{sub:"Watchdog",fields:[{key:"watchdog_interval",label:"Watchdog Interval",internal:!0,unit:"s",type:"number",min:1,def:30,doc:"How often the background check looks for stalled sensors and timeouts. Default 30 s."},{key:"no_update_active_timeout",label:"No-Update Timeout",internal:!0,unit:"s",type:"number",min:0,def:600,doc:"If no power update arrives for this long during a cycle, assume the plug went offline and force-stop the cycle. The default 600 s allows for cloud or mesh delays."}]},{sub:"Housekeeping",fields:[{key:"progress_reset_delay",label:"Progress Reset Delay",unit:"s",type:"number",min:0,def:1800,doc:"After a cycle, keep progress at 100% this long before resetting to Idle."},{key:"auto_maintenance",label:"Auto Maintenance (nightly cleanup)",type:"checkbox",def:!0,doc:"Nightly cleanup: label old cycles that now clearly match a program, rebuild program curves, refresh anomaly marks and cycle health, and remove debug traces if Save Debug Traces is off. Cycles and their power traces are always kept."},{key:"power_profile_interval_min",label:"Power Profile Interval",unit:"min",type:"number",min:1,def:15,doc:"Bucket size of the power_profile attribute (average watts per slot) used by planners such as EMHASS and tibber_prices. Smaller keeps short spikes; larger smooths the curve. Default 15 min. Does not affect detection."}]},{sub:"Debug",fields:[{key:"expose_debug_entities",label:"Expose Debug Entities",internal:!0,type:"checkbox",doc:"Add diagnostic entities (match confidence, ambiguity, internal state). Off keeps the entity list clean."},{key:"save_debug_traces",label:"Save Debug Traces",type:"checkbox",doc:"Also store the matcher's debug data (every candidate's scores) with each cycle, for troubleshooting. Adds about 20 KB per cycle."}]}]},{id:"anti_wrinkle",label:"Anti-Wrinkle",intro:"Keeps a dryer's low-power tumble pulses after the main phase attached to the finished cycle.",onlyDeviceTypes:["washing_machine","dryer","washer_dryer"],fields:[{key:"anti_wrinkle_enabled",label:"Enable Anti-Wrinkle Detection",basic:!0,type:"checkbox",doc:"Keep a dryer's short low-power tumble pulses after the main heat phase with the finished cycle, instead of reading them as new cycles."},{key:"anti_wrinkle_max_power",label:"Max Anti-Wrinkle Power",unit:"W",type:"number",step:10,min:0,def:400,doc:"A pulse above this power counts as a real new cycle, not a tumble. Set it just above your tumble-pulse power."},{key:"anti_wrinkle_max_duration",label:"Max Duration",unit:"s",type:"number",min:0,def:60,doc:"A pulse longer than this counts as a real cycle, not an anti-wrinkle tumble."},{key:"anti_wrinkle_exit_power",label:"Exit Power Threshold",internal:!0,unit:"W",type:"number",step:.1,min:0,def:.8,doc:"In anti-wrinkle mode, power below this counts as quiet between tumble pulses, and quiet for longer than the Max Pulse Gap ends the mode. The Stop Threshold is used when it is higher, so this only changes anything when set above the Stop Threshold."},{key:"anti_wrinkle_idle_timeout",label:"Max Pulse Gap",unit:"s",type:"number",step:30,min:0,def:120,doc:"How long the dryer may stay quiet between tumble pulses before anti-wrinkle mode ends. Set it above your dryer's longest gap, or later pulses are read as false starts."},{key:"anti_crease_finalize_ratio",label:"Anti-Crease Finalize Ratio",type:"number",step:.01,min:.5,max:1,def:.98,doc:"Fraction of the program's average duration a cycle must reach before its low-power tumbling counts as the anti-crease tail. Lower it (e.g. 0.75) on load-sensing dryers. Keep washers at the default (0.98), or a wash can end halfway."}]},{id:"dishwasher",label:"Dishwasher",intro:"End handling for dishwashers, which often dry quietly for a long time before a short final drain.",onlyDeviceTypes:["dishwasher"],fields:[{key:"dishwasher_end_spike_quiet_release",label:"Passive-Dry Quiet Release",unit:"s",type:"number",step:60,min:0,def:600,doc:"After the expected duration, how long the dishwasher must stay below the Stop Threshold before WashData stops waiting for the final drain. Raise it if a late drain after a long dry is missed. The wait never exceeds 30 minutes."}]},{id:"delay",label:"Delay Start",intro:"Detects an appliance that is on and waiting for a delayed start.",fields:[{key:"delay_start_detect_enabled",label:"Enable Delay-Start Detection",type:"checkbox",doc:"Detect an appliance that is on and waiting for a delayed start, so its standby draw is not taken for a running cycle."},{key:"delay_confirm_seconds",label:"Confirm Seconds",unit:"s",type:"number",min:0,def:60,doc:"Power must stay in the standby band this long before the appliance counts as waiting to start."},{key:"delay_timeout_hours",label:"Timeout Hours",unit:"h",type:"number",step:.5,min:0,def:8,doc:"Give up waiting for a delayed start after this many hours and return to idle."}]},{id:"triggers",label:"Triggers & Door",intro:"Optional extras: end trigger, door sensor, pause switch and unload reminder.",groups:[{sub:"External End Trigger",fields:[{key:"external_end_trigger_enabled",label:"Enable External End Trigger",type:"checkbox",doc:"Let a binary sensor signal the end of a cycle, in addition to power-based detection."},{key:"external_end_trigger",label:"External Trigger Entity",type:"entity",domain:"binary_sensor",doc:'Binary sensor whose change marks the cycle end (e.g. a "finished" contact or another integration).'},{key:"external_end_trigger_inverted",label:"Invert External Trigger (trigger on OFF)",type:"checkbox",doc:"Treat the sensor turning OFF (not ON) as the end signal."}]},{sub:"Door & Pause",fields:[{key:"door_sensor_entity",label:"Door Sensor Entity",basic:!0,type:"entity",domain:"binary_sensor",doc:"Optional door sensor. Shows when the appliance was opened to unload after a cycle."},{key:"door_opens_at_end",label:"Door Opens Automatically At End",type:"checkbox",doc:"For dishwashers that open the door at the end to dry (e.g. AirDry). A door left open for the dwell time below ends the cycle instead of pausing it; a brief open is ignored. Needs a Door Sensor Entity."},{key:"door_end_dwell_seconds",label:"Door-Open End Dwell",unit:"s",type:"number",min:1,def:60,doc:'With "Door Opens Automatically At End" on: how long the door must stay open before the cycle ends. Long enough to ignore adding a dish (default 60 s).'},{key:"pause_cuts_power",label:"Pause Also Cuts Power (via switch)",type:"checkbox",doc:"When a cycle is paused, also switch off the Switch Entity below. Only for appliances that can safely lose power mid-cycle."},{key:"switch_entity",label:"Switch Entity",type:"entity",domain:"switch",doc:'Optional switch turned off on pause and back on when resuming, with "Pause Also Cuts Power".'}]},{sub:"Unload Reminder",fields:[{key:"notify_unload_delay_minutes",label:"Unload Nag Delay",unit:"min",type:"number",min:0,def:60,basic:!0,doc:'Minutes after a cycle ends before the "unload the machine" reminder. 0 disables it.'},{key:"notify_unload_repeat",label:"Repeat Until Unloaded",type:"checkbox",doc:'Resend the unload reminder every "Unload Nag Delay" minutes until the unload is confirmed or you tap "Stop reminding" (mobile app). Needs a Door Sensor Entity, an Unload Confirmation Entity, or "Confirm Unload Manually".'},{key:"unload_confirm_entity",label:"Unload Confirmation Entity",type:"entity",domains:["event","button","input_button","binary_sensor","sensor","input_boolean","switch","scene","tag"],doc:"Optional. Any entity that confirms the load was taken out, for machines without a door sensor (a button, input_button, motion sensor, scene). Every state change counts except turning off or unavailable. Also enables the unload reminder."},{key:"unload_track_without_door",label:"Confirm Unload Manually",type:"checkbox",doc:'Run the unload reminder without a door sensor or confirmation entity. Confirm the unload from your own automation with the "Mark Unloaded" button or the ha_washdata.mark_unloaded service. Ignored when a Door Sensor Entity is set.'},{key:"pump_stuck_duration",label:"Pump Stuck Duration",unit:"s",type:"number",min:0,def:1800,onlyDeviceType:"pump",doc:"How long a pump may run nonstop before the stuck-pump event fires."}]}]},{id:"notifications",label:"Notifications",groups:[{sub:"Services",fields:[{key:"notify_start_services",label:"Start Services",type:"entitylist",domain:"notify",placeholder:"add a notify service\u2026",basic:!0,doc:"notify.* services to call when a cycle starts (phone, dashboard, ...). Leave empty for none."},{key:"notify_finish_services",label:"Finish Services",type:"entitylist",domain:"notify",placeholder:"add a notify service\u2026",basic:!0,doc:"notify.* services to call when a cycle finishes. Leave empty for none."},{key:"notify_live_services",label:"Live Progress Services",basic:!0,type:"entitylist",domain:"notify",placeholder:"add a notify service\u2026",doc:"notify.* services for live progress updates while a cycle runs. Leave empty to turn them off."},{key:"notify_people",label:"People (for Only When Home)",type:"entitylist",domain:"person",placeholder:"add a person\u2026",doc:'person.* entities that "Notify Only When Home" checks.'},{key:"notify_only_when_home",label:"Notify Only When Home",type:"checkbox",doc:"Only notify when at least one of the people above is home."},{key:"notify_fire_events",label:"Fire HA Events for Notifications",type:"checkbox",def:!0,doc:"Also fire ha_washdata_* events on cycle start and finish for your own automations."}]},{sub:"Timing",fields:[{key:"notify_before_end_minutes",label:"Pre-End Alert",unit:"min",type:"number",min:0,def:0,doc:"Send an Almost Done alert when estimated time remaining drops below this. 0 disables it."},{key:"notify_live_interval_seconds",label:"Live Update Interval",unit:"s",type:"number",min:30,def:300,doc:"How often live-progress notifications are refreshed while a cycle runs."},{key:"notify_live_overrun_percent",label:"Live Overrun % Before Alert",unit:"%",type:"number",min:0,def:20,doc:"Live updates stop once a cycle runs this percent past its expected duration (20 = up to 1.2x the expected time)."},{key:"notify_live_chronometer",label:"Use Live Chronometer",type:"checkbox",doc:"Show a live countdown timer in the notification (where supported) instead of a fixed estimate."},{key:"notify_live_silent",label:"Silent Live Updates",type:"checkbox",def:!0,doc:"iOS only. Update live progress silently, at a battery-saving priority that iOS may batch. The first update and the start/finish notifications still alert. Off: every update alerts."},{key:"notify_live_sticky",label:"Keep Live Notification On Tap",type:"checkbox",doc:"Android only. Keep the live progress notification when tapped. Off: a tap dismisses it."},{key:"notify_live_click_action",label:"Notification Tap Target",type:"text",optional:!0,doc:`What a tap on this appliance's notification opens. Blank: the WashData panel on this appliance. Enter a dashboard path (e.g. /lovelace/laundry) or URL, or "none" for no tap action.`},{key:"notify_timeout_seconds",label:"Auto-Dismiss After",unit:"s",type:"number",min:0,def:0,doc:"Dismiss the notification after this many seconds (where supported). 0 keeps it until you dismiss it."}]},{sub:"Messages",fields:[{key:"notify_title",label:"Notification Title",type:"text",def:"WashData: {device}",doc:"Notification title. Only {device} is substituted; any other variable falls back to the default title."},{key:"notify_icon",label:"Notification Icon",type:"text",def:"",doc:"Small icon on the notification (e.g. mdi:washing-machine). Sent only to mobile_app targets, not notify groups. On iOS it replaces the app icon and needs companion app 2026.8 or newer. Blank: platform default."},{key:"notify_icon_color",label:"Notification Colour",type:"color",def:"",placeholder:"#03A9F4",doc:"Accent colour for this appliance's notifications, so washer, dryer and dishwasher are easy to tell apart. On iOS it fills the circle behind the icon and colours the Live Activity bar. Many Android phones (Samsung especially) ignore it. Use hex, e.g. #03A9F4. Blank: platform default."},{key:"notify_start_message",label:"Start Message",type:"textarea",def:"{device} started.",doc:"Body sent when a cycle starts. Template variables: {device}, {program}. Any other variable falls back to the default text."},{key:"notify_finish_message",label:"Finish Message",type:"textarea",def:"{device} finished. Duration: {duration} min.",basic:!0,doc:"Body sent when a cycle finishes. Variables: {device}, {program}, {duration} (minutes), {duration_hm} (1 h 05 min), {energy_kwh}, {cost}, {time_finished}, {vs_typical}, {cycle_count}, {status} (completed or force_stopped; interrupted cycles send nothing). Any other variable falls back to the default text."},{key:"notify_pre_complete_message",label:"Pre-Complete Message",type:"textarea",def:"{device}: Less than {minutes} minutes remaining.",doc:"Text of each live progress update. Template variables: {device}, {program}, {minutes} (minutes left). Any other variable falls back to the default text."},{key:"notify_reminder_message",label:"Reminder Message",type:"textarea",def:"",doc:"Text of the one-time Pre-End Alert. Template variables: {device}, {program}, {minutes} (the Pre-End Alert setting). Any other variable falls back to the default text."},{key:"notify_channel",label:"Android Channel (start/live)",type:"text",def:"",placeholder:"e.g. WashData",suggestions:["WashData","WashData Status","Appliance Status"],doc:"Android channel for start and live messages; the channel sets sound and priority. Blank: app default."},{key:"notify_finish_channel",label:"Android Channel (finish)",type:"text",def:"",placeholder:"e.g. WashData Finished",suggestions:["WashData Finished","WashData Alerts","Appliance Finished"],doc:"Android channel for the finish message. Blank reuses the start/live channel."},{key:"peak_rate_threshold",label:"Peak-Rate Threshold (per kWh)",type:"number",step:.001,min:0,def:0,clearable:!0,doc:"If the price per kWh is at or above this when a cycle starts, add a peak-rate tip to the start notification. 0 or blank disables it."},{key:"peak_rate_message",label:"Peak-Rate Message",type:"text",def:"",placeholder:"Running at peak rate ({price}/kWh).",doc:"Custom text for the peak-rate tip. Template variables: {device}, {price}. Blank uses the default."}]},{sub:"Cycle Timers",fields:[{key:"notify_cycle_timers",label:"Cycle Timers",type:"timerlist",doc:"Notifications at set minutes into a cycle (e.g. to add softener). Message supports {device}, {program}, {minutes}. With Auto-pause, the cycle also pauses there and the notification gets a Resume button."}]},{sub:"Quiet Hours & Milestones",fields:[{key:"notify_quiet_start_hour",label:"Quiet Hours Start",basic:!0,unit:"h",type:"number",min:0,max:23,clearable:!0,doc:"Start of a do-not-disturb window (0-23). Finish, reminder and clean-laundry notifications in this window are held until it ends. Can cross midnight (e.g. 22 to 7). Blank disables it."},{key:"notify_quiet_end_hour",label:"Quiet Hours End",basic:!0,unit:"h",type:"number",min:0,max:23,clearable:!0,doc:"End of the do-not-disturb window (0-23); held notifications are sent then. Blank disables it."},{key:"notify_milestones",label:"Cycle Milestones",type:"intlist",def:"50, 100, 500, 1000",placeholder:"50, 100, 500, 1000",doc:"Cycle counts that trigger a one-off celebration notification, comma-separated (e.g. 50, 100, 500, 1000). Blank disables them."},{key:"notify_milestone_message",label:"Milestone Message",type:"textarea",def:"{device} has completed {cycle_count} cycles!",doc:"Message for the milestone notification. Template variables: {device}, {cycle_count}."}]}]},{id:"ml_training",label:"ML Training",fields:[{key:"enable_ml_models",label:"Apply smart models during a cycle",type:"checkbox",def:!1,doc:"Use a model trained on this machine to predict a running cycle's energy and cost, and let a well-known program that stops unusually early finish without the full off delay. Off: power-based logic only."},{key:"ml_training_enabled",label:"Learn from this machine",type:"checkbox",def:!1,doc:"Retrain the energy model on this machine's finished cycles from time to time. A new model is kept only if it beats the simple estimate on test cycles."},{key:"ml_training_hour",label:"Learn at hour",unit:"h",type:"number",min:0,max:23,def:2,doc:"Hour of day (0-23) for overnight training. Pick a quiet hour, e.g. 2."},{key:"ml_training_min_cycles",label:"Cycles needed first",type:"number",min:5,def:30,doc:"Wait for at least this many recorded cycles before training."},{key:"ml_training_interval_days",label:"Check at most every",unit:"days",type:"number",min:1,def:7,doc:"Retrain at most once every this many days."}]}],wt={};for(const h of yt){const t=h.groups||[{fields:h.fields}];for(const e of t)for(const s of e.fields||[])wt[s.key]=s}const te={profile_match_min_duration_ratio:.1,profile_match_max_duration_ratio:1.8},Ae=[{keys:["off_delay","min_off_gap"],check:h=>h.off_delay!=null&&h.min_off_gap!=null&&h.min_off_gap>h.off_delay,fieldNotes:h=>({off_delay:{msgKey:"note.off_delay.effective",msgVars:{eff:h.min_off_gap,gap:h.min_off_gap,delay:h.off_delay},msgFb:`A finished cycle actually waits ${h.min_off_gap} s, not ${h.off_delay} s: Min Off Gap (${h.min_off_gap} s) is longer and takes precedence.`}})}],Me=new Set(["energy","energy_storage","power","gas","water","current","voltage"]),Le=new Set(["w","kw","mw","wh","kwh","mwh","va","kva","varh","a","ma","v","mv"]);function Nt(h,t,e){if(!h)return null;if(h===t.power_sensor)return{msgKey:"conflict.price_entity.power_sensor",msgVars:{},msgFb:"This is the device's Power Sensor, not a price. Cycles would be costed in watts per kWh; clear this field."};if(h===t.energy_sensor)return{msgKey:"conflict.price_entity.energy_sensor",msgVars:{},msgFb:"This is the device's Energy Meter, not a price. Cycles would be costed at the meter reading per kWh; clear this field."};const s=e&&e.stateOf?e.stateOf(h):null;if(!s)return null;const a=s.attributes||{},r=String(a.device_class==null?"":a.device_class).trim().toLowerCase();if(Me.has(r))return{msgKey:"conflict.price_entity.device_class",msgVars:{dc:r},msgFb:`This sensor measures ${r}, not a price per kWh. Clear this field or pick a tariff sensor.`};const i=String(a.unit_of_measurement==null?"":a.unit_of_measurement).trim();return Le.has(i.toLowerCase())?{msgKey:"conflict.price_entity.unit",msgVars:{unit:i},msgFb:`This sensor reads in ${i}, not a price per kWh. Clear this field or pick a tariff sensor.`}:null}const ee=[{keys:["energy_price_entity","energy_sensor","power_sensor"],check:(h,t)=>Nt(h.energy_price_entity,h,t)!=null,fieldErrors:(h,t)=>({energy_price_entity:Nt(h.energy_price_entity,h,t)})},{keys:["start_threshold_w","stop_threshold_w"],check:h=>h.start_threshold_w!=null&&h.stop_threshold_w!=null&&h.start_threshold_w<=h.stop_threshold_w,fieldErrors:h=>({start_threshold_w:{msgKey:"conflict.hysteresis.start",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be above Stop Threshold (${h.stop_threshold_w} W)`,fixVal:+Math.max(h.stop_threshold_w+.5,h.stop_threshold_w*1.25).toFixed(1)},stop_threshold_w:{msgKey:"conflict.hysteresis.stop",msgVars:{start:h.start_threshold_w},msgFb:`Must be below Start Threshold (${h.start_threshold_w} W)`,fixVal:+Math.min(h.start_threshold_w-.5,h.start_threshold_w*.8).toFixed(1)}})},{keys:["min_power","stop_threshold_w"],check:h=>h.min_power!=null&&h.stop_threshold_w!=null&&h.min_power>h.stop_threshold_w,fieldErrors:h=>({min_power:{msgKey:"conflict.min_power.min_power",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be at or below Stop Threshold (${h.stop_threshold_w} W)`,fixVal:+(h.stop_threshold_w*.8).toFixed(1)},stop_threshold_w:{msgKey:"conflict.min_power.stop",msgVars:{min:h.min_power},msgFb:`Must be at or above Min Power (${h.min_power} W)`,fixVal:+(h.min_power*1.25).toFixed(1)}})},{keys:["power_off_threshold_w","stop_threshold_w"],check:h=>h.power_off_threshold_w!=null&&h.power_off_threshold_w>0&&h.stop_threshold_w!=null&&h.power_off_threshold_w>=h.stop_threshold_w,fieldErrors:h=>({power_off_threshold_w:{msgKey:"conflict.power_off.threshold",msgVars:{stop:h.stop_threshold_w},msgFb:`Must be below Stop Threshold (${h.stop_threshold_w} W) to take effect`,fixVal:+(h.stop_threshold_w*.6).toFixed(1)},stop_threshold_w:{msgKey:"conflict.power_off.stop",msgVars:{pot:h.power_off_threshold_w},msgFb:`Must be above Power Off Threshold (${h.power_off_threshold_w} W)`,fixVal:+(h.power_off_threshold_w*1.67).toFixed(1)}})},{keys:["off_delay","min_off_gap"],check:h=>h.off_delay!=null&&h.min_off_gap!=null&&h.off_delay>h.min_off_gap,fieldErrors:h=>({off_delay:{msgKey:"conflict.off_delay.off_delay",msgVars:{gap:h.min_off_gap},msgFb:`Off Delay (${h.off_delay} s) overrides Min Off Gap (${h.min_off_gap} s); cycles within the gap may merge`,fixVal:h.min_off_gap},min_off_gap:{msgKey:"conflict.off_delay.gap",msgVars:{delay:h.off_delay},msgFb:`Min Off Gap should be at least Off Delay (${h.off_delay} s)`,fixVal:h.off_delay}})},{keys:["watchdog_interval","sampling_interval"],check:h=>h.watchdog_interval!=null&&h.sampling_interval!=null&&h.watchdog_interval<2*h.sampling_interval,fieldErrors:h=>({watchdog_interval:{msgKey:"conflict.watchdog.interval",msgVars:{si:h.sampling_interval},msgFb:`Should be at least 2\xD7 Sampling Interval (${h.sampling_interval} s)`,fixVal:+(2*h.sampling_interval+1)},sampling_interval:{msgKey:"conflict.watchdog.sampling",msgVars:{wi:h.watchdog_interval},msgFb:`Sampling Interval should be at most half of Watchdog Interval (${h.watchdog_interval} s)`,fixVal:+Math.floor(h.watchdog_interval/2)}})},{keys:["no_update_active_timeout","watchdog_interval"],check:h=>h.no_update_active_timeout!=null&&h.watchdog_interval!=null&&h.no_update_active_timeout<=h.watchdog_interval,fieldErrors:h=>({no_update_active_timeout:{msgKey:"conflict.no_update_timeout.timeout",msgVars:{wi:h.watchdog_interval},msgFb:`Must be greater than Watchdog Interval (${h.watchdog_interval} s)`,fixVal:h.watchdog_interval*2},watchdog_interval:{msgKey:"conflict.no_update_timeout.watchdog",msgVars:{to:h.no_update_active_timeout},msgFb:`Must be less than No-Update Timeout (${h.no_update_active_timeout} s)`,fixVal:+Math.floor(h.no_update_active_timeout/2)}})},{keys:["start_duration_threshold","sampling_interval"],check:h=>h.start_duration_threshold!=null&&h.sampling_interval!=null&&h.start_duration_threshold({start_duration_threshold:{msgKey:"conflict.start_dur.threshold",msgVars:{si:h.sampling_interval},msgFb:`Should be at least one Sampling Interval (${h.sampling_interval} s) to prevent single-sample false starts`,fixVal:h.sampling_interval},sampling_interval:{msgKey:"conflict.start_dur.sampling",msgVars:{sdt:h.start_duration_threshold},msgFb:`Sampling Interval exceeds Start Duration (${h.start_duration_threshold} s); single-sample spikes can open a cycle`,fixVal:h.start_duration_threshold}})},{keys:["learning_confidence","profile_match_threshold"],check:h=>h.learning_confidence!=null&&h.profile_match_threshold!=null&&h.learning_confidence({learning_confidence:{msgKey:"conflict.confidence.learning",msgVars:{match:h.profile_match_threshold},msgFb:`Must be at or above Match Threshold (${h.profile_match_threshold})`,fixVal:+h.profile_match_threshold.toFixed(2)},profile_match_threshold:{msgKey:"conflict.confidence.match_for_learning",msgVars:{lc:h.learning_confidence},msgFb:`Must be at or below Learning Confidence (${h.learning_confidence})`,fixVal:+h.learning_confidence.toFixed(2)}})},{keys:["profile_match_threshold","auto_label_confidence"],check:h=>h.profile_match_threshold!=null&&h.auto_label_confidence!=null&&h.profile_match_threshold>h.auto_label_confidence,fieldErrors:h=>({profile_match_threshold:{msgKey:"conflict.confidence.match_for_auto",msgVars:{alc:h.auto_label_confidence},msgFb:`Must be at or below Auto-Label Confidence (${h.auto_label_confidence})`,fixVal:+h.auto_label_confidence.toFixed(2)},auto_label_confidence:{msgKey:"conflict.confidence.auto",msgVars:{match:h.profile_match_threshold},msgFb:`Must be at or above Match Threshold (${h.profile_match_threshold})`,fixVal:+h.profile_match_threshold.toFixed(2)}})},{keys:["profile_unmatch_threshold","profile_match_threshold"],check:h=>h.profile_unmatch_threshold!=null&&h.profile_match_threshold!=null&&h.profile_unmatch_threshold>=h.profile_match_threshold,fieldErrors:h=>({profile_unmatch_threshold:{msgKey:"conflict.unmatch.unmatch",msgVars:{match:h.profile_match_threshold},msgFb:`Must be below Match Threshold (${h.profile_match_threshold}); otherwise a committed match un-matches instantly`,fixVal:+(h.profile_match_threshold-.05).toFixed(2)},profile_match_threshold:{msgKey:"conflict.unmatch.match",msgVars:{un:h.profile_unmatch_threshold},msgFb:`Must be above Unmatch Threshold (${h.profile_unmatch_threshold})`,fixVal:+(h.profile_unmatch_threshold+.05).toFixed(2)}})},{keys:["anti_wrinkle_max_power","start_threshold_w"],check:h=>["washing_machine","dryer","washer_dryer"].includes(h.device_type)&&h.anti_wrinkle_max_power!=null&&h.start_threshold_w!=null&&h.anti_wrinkle_max_power<=h.start_threshold_w,fieldErrors:h=>({anti_wrinkle_max_power:{msgKey:"conflict.anti_wrinkle_max.max",msgVars:{start:h.start_threshold_w},msgFb:`Must be above Start Threshold (${h.start_threshold_w} W); otherwise anti-wrinkle duration limit is bypassed`,fixVal:+(h.start_threshold_w*2).toFixed(0)},start_threshold_w:{msgKey:"conflict.anti_wrinkle_max.start",msgVars:{max:h.anti_wrinkle_max_power},msgFb:`Must be below Anti-Wrinkle Max Power (${h.anti_wrinkle_max_power} W)`,fixVal:+(h.anti_wrinkle_max_power*.5).toFixed(1)}})},{keys:["pump_stuck_duration","no_update_active_timeout"],check:h=>h.device_type==="pump"&&h.pump_stuck_duration!=null&&h.no_update_active_timeout!=null&&h.no_update_active_timeout<=h.pump_stuck_duration,fieldErrors:h=>({pump_stuck_duration:{msgKey:"conflict.pump_stuck.duration",msgVars:{to:h.no_update_active_timeout},msgFb:`Must be below No-Update Timeout (${h.no_update_active_timeout} s) so the stuck alarm can fire first`,fixVal:h.no_update_active_timeout-60},no_update_active_timeout:{msgKey:"conflict.pump_stuck.timeout",msgVars:{ps:h.pump_stuck_duration},msgFb:`Must exceed Pump Stuck Duration (${h.pump_stuck_duration} s) so the stuck alarm can fire first`,fixVal:h.pump_stuck_duration+60}})},{keys:["profile_match_min_duration_ratio","profile_match_max_duration_ratio"],check:h=>h.profile_match_min_duration_ratio!=null&&h.profile_match_max_duration_ratio!=null&&h.profile_match_min_duration_ratio>=h.profile_match_max_duration_ratio,fieldErrors:h=>({profile_match_min_duration_ratio:{msgKey:"conflict.duration_ratio.min",msgVars:{max:h.profile_match_max_duration_ratio},msgFb:`Must be less than Max Duration Ratio (${h.profile_match_max_duration_ratio})`,fixVal:+(h.profile_match_max_duration_ratio*.5).toFixed(2)},profile_match_max_duration_ratio:{msgKey:"conflict.duration_ratio.max",msgVars:{min:h.profile_match_min_duration_ratio},msgFb:`Must be greater than Min Duration Ratio (${h.profile_match_min_duration_ratio})`,fixVal:+(h.profile_match_min_duration_ratio*2).toFixed(2)}})},{keys:["end_energy_threshold","stop_threshold_w","off_delay"],check:h=>h.end_energy_threshold!=null&&h.stop_threshold_w!=null&&h.off_delay!=null&&h.off_delay>0&&h.end_energy_threshold({end_energy_threshold:{msgKey:"conflict.end_energy.energy",msgVars:{w:h.stop_threshold_w,d:h.off_delay},msgFb:`Too strict for Stop Threshold (${h.stop_threshold_w} W) over Off Delay (${h.off_delay} s); the cycle can only end through a fallback path`,fixVal:Math.ceil(h.stop_threshold_w*h.off_delay/3600*1e3)/1e3},stop_threshold_w:{msgKey:"conflict.end_energy.stop",msgVars:{e:h.end_energy_threshold,d:h.off_delay,p:+(h.end_energy_threshold*3600/h.off_delay).toFixed(2)},msgFb:`End Energy Threshold (${h.end_energy_threshold} Wh over ${h.off_delay} s) only permits ${+(h.end_energy_threshold*3600/h.off_delay).toFixed(2)} W`,fixVal:Math.floor(h.end_energy_threshold*3600/h.off_delay*10)/10}})}],De=` :host { /* The panel owns the full height it is given and scrolls INTERNALLY (.wd-main). HA renders a non-iframe custom panel into a plain container that sets only display/background and the safe-area padding, and no height at all (frontend src/panels/custom/ha-panel-custom.ts), so the old min-height: 100% resolved against an auto-height parent and the panel simply grew with its content: the DOCUMENT scrolled, .wd-main's overflow never engaged, and a dropdown opened near the bottom was cut off by the window edge with no way to reach the rest of it (scrolling the page moves the anchor and closes it). The insets are subtracted because that same container adds them as padding around us, so 100dvh alone would overflow by exactly that much. */ /* NOT display:flex, and the layout must not depend on it: a rule in the embedding document targeting our tag beats any :host rule regardless of specificity, and both HA and the E2E fixture set display:block on the panel element. The height survives that because _syncPanelHeight writes it inline. .wd-root below takes height:100% of it and is the flex column everything else resolves against. */ display: block; height: calc(100dvh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px)); overflow: hidden; background: var(--primary-background-color); color: var(--primary-text-color); font-family: var(--paper-font-body1_-_font-family, Roboto, sans-serif); --wd-radius-sm: 4px; --wd-radius-md: 8px; --wd-radius-lg: 12px; --wd-space-xs: 4px; --wd-space-sm: 6px; --wd-space-md: 10px; --wd-space-lg: 16px; --wd-space-xl: 24px; --wd-font-sm: 0.75em; --wd-font-xs: 0.7em; --wd-white: #fff; --wd-tint-xs: rgba(0,0,0,0.04); --wd-tint-sm: rgba(0,0,0,0.08); --wd-tint-md: rgba(0,0,0,0.12); } .wd-header { display: flex; align-items: center; gap: 12px; padding: 14px 24px; background: var(--app-header-background-color, var(--primary-color)); color: var(--app-header-text-color, #fff); position: sticky; top: 0; z-index: 20; box-shadow: 0 2px 6px rgba(0,0,0,.25); } .wd-header h1 { margin: 0; font-size: 1.25em; font-weight: 600; letter-spacing: .01em; } .wd-logo { flex-shrink: 0; opacity: .95; } .wd-burger { display: none; align-items: center; justify-content: center; background: transparent; border: none; color: inherit; cursor: pointer; padding: 5px; margin: -2px 2px -2px -4px; border-radius: var(--wd-radius-md); flex-shrink: 0; } .wd-burger:hover { background: rgba(255,255,255,.16); } .wd-gear-btn { background: transparent; border: none; color: inherit; cursor: pointer; padding: 5px; margin-left: 4px; border-radius: var(--wd-radius-md); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; opacity: .8; } .wd-gear-btn:hover { background: rgba(255,255,255,.16); opacity: 1; } @media (max-width: 870px) { .wd-burger { display: inline-flex; } } .wd-burger.wd-burger--force { display: inline-flex; } .wd-header .wd-sub { font-size: .72em; opacity: .75; margin-top: 2px; } .wd-header .wd-ts { margin-left: auto; font-size: .7em; opacity: .65; white-space: nowrap; } .wd-body { max-width: 1160px; margin: 0 auto; padding: 20px 16px 60px; } .wd-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; } .wd-chip { padding: 5px 16px; border-radius: 16px; border: 1px solid var(--divider-color, rgba(0,0,0,.12)); background: var(--card-background-color); color: var(--primary-text-color); cursor: pointer; font-size: .85em; transition: background .15s, color .15s; } .wd-chip:hover { background: var(--secondary-background-color); } .wd-chip.active { background: var(--primary-color); color: var(--wd-white); border-color: var(--primary-color); } .wd-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--divider-color, rgba(0,0,0,.1)); margin-bottom: 20px; overflow-x: auto; } /* The device list and the tab bar are the app's navigation: they stay put while only the pane below them scrolls. Sticky rather than hoisted out of .wd-main, so the DOM order and the tablist/tabpanel relationship are untouched. The negative margins bleed the opaque background over .wd-body's own padding, so a card scrolling past cannot show through above or beside the nav; the padding puts that space back inside the sticky box. */ .wd-nav { position: sticky; top: 0; z-index: 40; background: var(--primary-background-color); margin: -20px -16px 20px; padding: 20px 16px 0; } .wd-nav .wd-tabs { margin-bottom: 0; } .wd-tab { padding: 10px 22px; border: none; background: transparent; color: var(--secondary-text-color); font-size: .8em; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; cursor: pointer; border-bottom: 2px solid transparent; transition: color .15s, border-color .15s; white-space: nowrap; } .wd-tab:hover { color: var(--primary-text-color); } .wd-tab.active { color: var(--primary-color); border-bottom-color: var(--primary-color); } .wd-pane { display: none; } .wd-pane.active { display: block; } .wd-card { background: var(--card-background-color); border-radius: var(--wd-radius-lg); padding: 20px 22px; margin-bottom: 16px; box-shadow: var(--ha-card-box-shadow, 0 2px 6px rgba(0,0,0,.08)); } .wd-card-title { margin: 0 0 14px; font-size: .72em; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--secondary-text-color); display: flex; align-items: center; gap: 8px; } .wd-card-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; align-items: center; } .wd-badge { display: inline-flex; align-items: center; gap: 7px; padding: 5px 14px; border-radius: 20px; font-size: .85em; font-weight: 500; margin-bottom: 18px; } .wd-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex-shrink: 0; } .wd-running .wd-dot { animation: wd-pulse 1.4s ease-in-out infinite; } @keyframes wd-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } } .wd-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; margin-bottom: 18px; } .wd-stat { background: var(--secondary-background-color); border-radius: var(--wd-radius-md); padding: 14px 10px; text-align: center; } .wd-stat-val { font-size: 1.5em; font-weight: 600; line-height: 1.1; } .wd-stat-lbl { margin-top: 5px; font-size: .72em; color: var(--secondary-text-color); } .wd-prog-bg { background: var(--secondary-background-color); border-radius: 6px; height: 10px; overflow: hidden; } .wd-prog-fill { height: 100%; background: var(--primary-color); border-radius: 6px; transition: width .6s ease; } .wd-prog-row { display: flex; justify-content: space-between; margin-top: 6px; font-size: .78em; color: var(--secondary-text-color); } /* D1: compact phase timeline below the progress bar */ .wd-ptl-wrap { margin-top: 8px; } .wd-ptl { position: relative; height: 12px; border-radius: 6px; overflow: hidden; background: var(--secondary-background-color); } .wd-ptl-seg { position: absolute; top: 0; bottom: 0; } .wd-ptl-seg-lbl { position: absolute; left: 4px; top: 50%; transform: translateY(-50%); font-size: 8px; line-height: 1; color: var(--wd-white); white-space: nowrap; overflow: hidden; max-width: calc(100% - 6px); text-shadow: 0 0 2px rgba(0,0,0,.55); pointer-events: none; } .wd-ptl-cursor { position: absolute; top: -2px; bottom: -2px; width: 2px; background: var(--primary-text-color, #111); box-shadow: 0 0 0 1px rgba(255,255,255,.6); } .wd-ptl-cur { margin-top: 5px; font-size: .74em; color: var(--secondary-text-color); } .wd-cycle-ctrl { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; } .wd-table { width: 100%; border-collapse: collapse; font-size: .875em; } .wd-table th { text-align: left; padding: 8px 12px; color: var(--secondary-text-color); font-weight: 600; font-size: .72em; letter-spacing: .07em; text-transform: uppercase; border-bottom: 1px solid var(--divider-color); } .wd-table td { padding: 10px 12px; border-bottom: 1px solid var(--divider-color, rgba(0,0,0,.05)); vertical-align: middle; } .wd-table tr:last-child td { border-bottom: none; } .wd-table tbody tr:hover td { background: var(--secondary-background-color); } .wd-table-wrap { overflow-x: auto; } /* Audit UI-20: a horizontal scroller that hides content past an edge gets a shadow on that edge, so on a phone the tab strip and wide tables visibly continue instead of looking cut off. A shadow rather than a fade: past a table's last visible column there is usually only whitespace, where a fade shows nothing. _syncScrollAffordances sets the classes only while there really is more content on that side; backgrounds do not affect layout, and nothing overflows at desktop widths, so the desktop view is unchanged. The background of a scroll container does not scroll with it, so the shadow stays on the visible edge. Mid-grey reads on light and dark themes alike. */ .wd-ovf-l, .wd-ovf-r { background-repeat: no-repeat; background-size: 22px 100%; } .wd-ovf-r { background-image: linear-gradient(to left, rgba(127,127,127,.42), rgba(127,127,127,0)); background-position: right center; } .wd-ovf-l { background-image: linear-gradient(to right, rgba(127,127,127,.42), rgba(127,127,127,0)); background-position: left center; } .wd-ovf-l.wd-ovf-r { background-image: linear-gradient(to right, rgba(127,127,127,.42), rgba(127,127,127,0)), linear-gradient(to left, rgba(127,127,127,.42), rgba(127,127,127,0)); background-position: left center, right center; } .wd-th-sort { cursor: pointer; user-select: none; white-space: nowrap; } .wd-th-sort:hover { color: var(--primary-color); } .wd-tc-date { white-space: nowrap; color: var(--secondary-text-color); font-size: .82em; } .wd-tc-num { white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; } /* Dedicated flags/icons column: keep every badge on one line so review/anomaly/source icons never overwrite each other or spill into the profile name. */ .wd-tc-flags { white-space: nowrap; font-size: .9em; } th.wd-tc-flags { color: var(--secondary-text-color); font-weight: 500; } .wd-filter-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; } .wd-filter-input { flex: 1; min-width: 120px; padding: 5px 10px; border-radius: 6px; border: 1px solid var(--divider-color); background: var(--card-background-color); color: var(--primary-text-color); font-size: .84em; } .wd-filter-select { padding: 5px 8px; border-radius: 6px; border: 1px solid var(--divider-color); background: var(--card-background-color); color: var(--primary-text-color); font-size: .84em; } .wd-row-link { cursor: pointer; } .wd-pill { display: inline-block; padding: 2px 9px; border-radius: var(--wd-radius-sm); background: var(--secondary-background-color); color: var(--secondary-text-color); font-size: .78em; } .wd-tag { display: inline-flex; align-items: center; padding: 1px 6px; border-radius: 10px; font-size: .72em; font-weight: 600; vertical-align: middle; background: var(--secondary-background-color); color: var(--secondary-text-color); margin-left: 4px; } .wd-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-size: .85em; font-weight: 500; transition: opacity .15s; white-space: nowrap; } .wd-btn:hover { opacity: .85; } .wd-btn:disabled { opacity: .55; cursor: default; } .wd-btn-primary { background: var(--primary-color); color: var(--wd-white); } .wd-btn-secondary { background: var(--secondary-background-color); color: var(--primary-text-color); border: 1px solid var(--divider-color); } .wd-btn-danger { background: var(--error-color, #f44336); color: var(--wd-white); } .wd-btn-sm { padding: 4px 10px; font-size: .78em; } .wd-btn-xs { padding: 2px 8px; font-size: .72em; } .wd-spin { display: inline-block; width: 13px; height: 13px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: wd-rot .7s linear infinite; vertical-align: -2px; } @keyframes wd-rot { to { transform: rotate(360deg); } } .wd-field { margin-bottom: 16px; } .wd-field label { display: block; font-size: .82em; font-weight: 600; margin-bottom: 5px; color: var(--secondary-text-color); letter-spacing: .04em; text-transform: uppercase; } .wd-field input[type=text], .wd-field input[type=number], .wd-field select, .wd-field textarea { width: 100%; box-sizing: border-box; padding: 8px 10px; border-radius: 6px; border: 1px solid var(--divider-color, rgba(0,0,0,.2)); background: var(--secondary-background-color); color: var(--primary-text-color); font-size: .9em; font-family: inherit; } .wd-field textarea { min-height: 64px; resize: vertical; } .wd-field input[type=checkbox] { width: auto; margin-right: 8px; } /* Switch-style boolean settings (replaces the old plain checkbox). Scoped under .wd-field-switch so the switch label wins over the generic ".wd-field label" (display:block, higher specificity) rule and stays a centered flex row. */ .wd-field-switch label { margin: 0; } .wd-field-switch .wd-switch-row { display: flex; align-items: center; gap: 10px; min-height: 22px; } /* Switch label: a flex row [toggle][text], vertically centred. The .wd-field .wd-switch-lbl selector is needed to beat .wd-field label (display:block + .82em + uppercase, specificity 0,1,1) which otherwise leaks in and both breaks the vertical centring (align-items is a no-op on a block) and shrinks the label. The bare .wd-switch-lbl covers inline use outside a .wd-field (e.g. adopt). */ .wd-switch-lbl, .wd-field .wd-switch-lbl { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; min-width: 0; margin: 0; font-size: 1rem; font-weight: 400; letter-spacing: normal; text-transform: none; } /* Match the switch label to every other setting name (see .wd-field label). */ .wd-switch-text { font-size: .82em; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--secondary-text-color); } #wd-settings-form .wd-switch-text, #wd-ml-form .wd-switch-text { color: var(--primary-text-color); } /* Normal-case, readable label for switches outside the Settings form (store / panel prefs / access control), whose labels are descriptive sentences. */ .wd-switch-text--plain { font-size: .9em; font-weight: 600; line-height: 1.3; letter-spacing: normal; text-transform: none; color: var(--primary-text-color); } .wd-switch { position: relative; display: inline-flex; flex: 0 0 auto; width: 40px; height: 22px; } .wd-switch input { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; } .wd-switch-slider { position: absolute; inset: 0; border-radius: 22px; background: var(--switch-unchecked-track-color, rgba(120,120,120,.5)); transition: background .2s; } .wd-switch-slider::before { content: ""; position: absolute; height: 16px; width: 16px; left: 3px; top: 3px; border-radius: 50%; background: var(--switch-unchecked-button-color, #fafafa); box-shadow: 0 1px 2px rgba(0,0,0,.3); transition: transform .2s; } .wd-switch input:checked + .wd-switch-slider { background: var(--switch-checked-track-color, var(--primary-color, #03a9f4)); } /* Force a light thumb: some themes set --switch-checked-button-color to the accent, which made the knob vanish into the (also-accent) track. A white thumb reads on any track colour. */ .wd-switch input:checked + .wd-switch-slider::before { transform: translateX(18px); background: #fff; } .wd-switch input:focus-visible + .wd-switch-slider { outline: 2px solid var(--primary-color, #03a9f4); outline-offset: 2px; } /* A11y: a shared keyboard focus ring for all interactive controls (many HA themes suppress the UA default outline). */ .wd-tab:focus-visible, .wd-btn:focus-visible, .wd-chip:focus-visible, .wd-sec-btn:focus-visible, .wd-subtab:focus-visible, .wd-mini-tab:focus-visible, .wd-devcard:focus-visible, [tabindex]:focus-visible, a:focus-visible, select:focus-visible { outline: 2px solid var(--primary-color, #03a9f4); outline-offset: 2px; border-radius: var(--wd-radius-sm); } /* A11y: honor the user's reduced-motion preference \u2014 drop non-essential animation. */ @media (prefers-reduced-motion: reduce) { .wd-dot, .wd-devdot, .wd-rec-active, .wd-spin, .wd-toast { animation: none !important; } * { scroll-behavior: auto !important; } } /* Notifications > Automations: split "New" dropdown + pills. */ .wd-auto-dd summary { cursor: pointer; list-style: none; } .wd-auto-dd summary::-webkit-details-marker { display: none; } .wd-auto-dd summary::marker { content: ''; } .wd-auto-pill { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; background: var(--secondary-background-color); border: 1px solid var(--divider-color); border-radius: 16px; padding: 3px 4px 3px 12px; } .wd-auto-pill-link { text-decoration: none; color: var(--primary-text-color); font-size: .92em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .wd-auto-pill-link:hover { text-decoration: underline; } .wd-auto-pill-x { flex: 0 0 auto; border: none; background: transparent; color: var(--secondary-text-color); cursor: pointer; font-size: 1.15em; line-height: 1; padding: 0 5px; border-radius: 50%; } .wd-auto-pill-x:hover { background: var(--error-color, #f44336); color: var(--wd-white); } .wd-field-hint { font-size: .78em; color: var(--secondary-text-color); margin-top: 4px; } /* Derived-value note (#445): informational, never an error - no red, no fix button. */ .wd-setting-note { font-size: .78em; color: var(--secondary-text-color); margin-top: 4px; padding: 5px 9px; border-radius: var(--wd-radius-sm); background: var(--secondary-background-color); border-left: 3px solid var(--primary-color); } /* Entity-pill multi-picker (compact chips + inline add input) */ .wd-pillbox { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; padding: 5px 6px; min-height: 34px; border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); background: var(--card-background-color); } .wd-pillbox:focus-within { border-color: var(--primary-color); } .wd-pillbox .wd-pill { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; padding: 2px 4px 2px 9px; font-size: .82em; line-height: 1.4; border-radius: var(--wd-radius-lg); background: var(--primary-color); color: var(--wd-white); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* The label shrinks and ellipsizes; the remove button never does (#467: a long notify service pushed it out of the clipped pill). */ .wd-pill-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .wd-pill-x { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: rgba(255,255,255,.25); color: var(--wd-white); font-size: 13px; line-height: 1; cursor: pointer; flex: none; } .wd-pill-x:hover { background: rgba(255,255,255,.45); } .wd-pill-add { flex: 1; min-width: 90px; border: 0 !important; background: transparent !important; padding: 3px 4px !important; font-size: .88em; color: var(--primary-text-color); outline: none; } .wd-form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0 20px; } /* Cycle timer list */ .wd-timerlist { display: flex; flex-direction: column; gap: 8px; } .wd-timer-row { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); background: var(--card-background-color); } .wd-timer-top { display: flex; align-items: center; gap: 8px; } .wd-timer-top input[type="number"] { width: 70px; flex: 0 0 auto; } .wd-timer-top textarea { flex: 1 1 auto; min-width: 0; box-sizing: border-box; resize: vertical; min-height: 32px; height: 34px; } .wd-timer-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .wd-timer-footer .wd-switch-lbl { display: flex; align-items: center; gap: 8px; cursor: pointer; } .wd-timer-add { align-self: flex-start; margin-top: 4px; } /* Roomier Settings layout (scoped so modals keep their compact spacing) */ #wd-settings-form .wd-form-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 18px 28px; align-items: start; } #wd-settings-form .wd-field { margin-bottom: 0; background: var(--secondary-background-color); border-radius: 10px; padding: 12px 14px; } #wd-settings-form .wd-field label { color: var(--primary-text-color); } #wd-settings-form .wd-field input[type=text], #wd-settings-form .wd-field input[type=number], #wd-settings-form .wd-field select, #wd-settings-form .wd-field textarea { background: var(--card-background-color); padding: 9px 11px; } #wd-settings-form .wd-subhead { margin: 22px 0 12px; padding-bottom: 6px; border-bottom: 1px solid var(--divider-color); } .wd-sec-intro { font-size: .85em; color: var(--secondary-text-color); margin: 0 0 16px; line-height: 1.5; } .wd-label-row { display: flex; align-items: center; } .wd-tip { display: inline-flex; width: 15px; height: 15px; border-radius: 50%; align-items: center; justify-content: center; font-size: 10px; font-style: italic; background: var(--secondary-background-color); color: var(--secondary-text-color); cursor: help; margin-left: 6px; position: relative; border: 1px solid var(--divider-color); font-weight: 700; text-transform: none; letter-spacing: normal; } .wd-tip-pop { display: none; position: absolute; bottom: 150%; left: 50%; transform: translateX(-50%); width: 264px; background: var(--card-background-color); color: var(--primary-text-color); border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); padding: 10px 12px; box-shadow: 0 4px 18px rgba(0,0,0,.35); z-index: 60; text-align: left; font-weight: 400; text-transform: none; letter-spacing: normal; } .wd-tip:hover .wd-tip-pop, .wd-tip:focus .wd-tip-pop { display: block; } .wd-tip:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 1px; } .wd-tip-txt { font-size: 12px; line-height: 1.5; display: block; } .wd-dg { display: block; width: 100%; height: auto; margin-bottom: 8px; background: var(--secondary-background-color); border-radius: 6px; } .wd-dg .ln { fill: none; stroke: var(--primary-color); stroke-width: 2.5; } .wd-dg .ln2 { fill: none; stroke: var(--secondary-text-color); stroke-width: 1.5; opacity: .7; } .wd-dg .ok { fill: none; stroke: var(--success-color, #4caf50); stroke-width: 2; } .wd-dg .bad { fill: none; stroke: var(--error-color, #f44336); stroke-width: 2; } .wd-dg .dash { stroke-dasharray: 4 3; } .wd-dg .fz { fill: var(--primary-color); opacity: .18; } .wd-dg .fw { fill: var(--warning-color, #ff9800); opacity: .2; } .wd-dg .fb { fill: var(--error-color, #f44336); opacity: .14; } .wd-dg text { fill: var(--secondary-text-color); font-size: 9px; } .wd-dg .ax { stroke: var(--divider-color); stroke-width: 1; } .wd-sug { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 6px 10px; border-radius: var(--wd-radius-md); font-size: .82em; background: rgba(255,152,0,.10); border: 1px solid rgba(255,152,0,.40); box-sizing: border-box; flex-wrap: wrap; } .wd-sug.wd-sug-split { flex-direction: column; align-items: stretch; gap: 0; padding: 0; overflow: hidden; } .wd-sug-opt { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 8px 10px; } .wd-sug-opt:not(:last-child) { border-bottom: 1px solid rgba(255,152,0,.28); } .wd-sug-chip { display: inline-flex; align-items: center; gap: 3px; flex-shrink: 0; font-size: .75em; font-weight: 700; letter-spacing: .04em; padding: 2px 7px; border-radius: 10px; white-space: nowrap; } .wd-sug-chip-obs { background: rgba(255,152,0,.22); } .wd-sug-val { font-weight: 700; flex-shrink: 0; } .wd-sug-impact-line { flex-basis: 100%; font-size: .86em; opacity: .70; font-style: italic; margin-top: 2px; } .wd-confirm-list { margin: 0 0 12px; padding-left: 18px; max-height: 50vh; overflow-y: auto; } .wd-confirm-list li { margin: 4px 0; } .wd-sug-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .wd-sug-sep { display: none; } .wd-sug-impact { display: none; } .wd-sug-use { border: none; background: var(--warning-color, #ff9800); color: var(--wd-white); border-radius: var(--wd-radius-sm); padding: 2px 8px; font-size: .92em; cursor: pointer; flex-shrink: 0; } .wd-sug-lock { border: none; background: transparent; color: var(--secondary-text-color); border-radius: var(--wd-radius-sm); padding: 2px 6px; font-size: .92em; cursor: pointer; flex-shrink: 0; opacity: .65; } .wd-sug-lock:hover { opacity: 1; background: rgba(255,152,0,.15); } .wd-conflict-err { display: flex; flex-direction: column; gap: 4px; margin-top: 5px; } .wd-conflict-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: .8em; color: var(--error-color, #b71c1c); padding: 5px 9px; border-left: 3px solid var(--error-color, #b71c1c); background: rgba(183,28,28,.07); border-radius: 0 5px 5px 0; } .wd-conflict-fix { border: 1px solid var(--error-color, #b71c1c); background: none; color: var(--error-color, #b71c1c); border-radius: var(--wd-radius-sm); padding: 1px 7px; font-size: .92em; cursor: pointer; white-space: nowrap; flex: none; } .wd-conflict-fix:hover { background: var(--error-color, #b71c1c); color: var(--wd-white); } .wd-conflict-sug-note { font-style: italic; opacity: 0.85; flex: none; } #wd-settings-form .wd-field.wd-has-conflict { outline: 2px solid var(--error-color, #b71c1c); outline-offset: -1px; } .wd-rev-sub { display: flex; align-items: center; gap: 6px; margin: 14px 0 6px; font-size: .85em; font-weight: 600; color: var(--primary-text-color); } .wd-rev-tags { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; } .wd-rev-tag { display: flex; align-items: center; gap: 7px; padding: 7px 10px; border-radius: var(--wd-radius-md); background: var(--secondary-background-color); border: 1px solid var(--divider-color); font-size: .85em; cursor: pointer; } .wd-rev-tag input { margin: 0; } .wd-rev-notes { width: 100%; box-sizing: border-box; background: var(--card-background-color); color: var(--primary-text-color); border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); padding: 9px 11px; font: inherit; resize: vertical; } .wd-sug-banner { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px 16px; border-radius: 10px; margin-bottom: 16px; background: rgba(255,152,0,.12); border: 1px solid rgba(255,152,0,.4); } .wd-subhead { font-size: .76em; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--primary-color); margin: 8px 0 10px; grid-column: 1 / -1; } .wd-section-nav { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 0; } .wd-sec-btn { padding: 5px 14px; border-radius: 14px; border: 1px solid var(--divider-color); background: transparent; color: var(--secondary-text-color); font-size: .8em; cursor: pointer; transition: background .15s; } .wd-sec-btn.active { background: var(--primary-color); color: var(--wd-white); border-color: var(--primary-color); } .wd-level-toggle { display: inline-flex; gap: 4px; } .wd-sec-btn { position: relative; } /* Basic/Advanced slide toggle */ .wd-mode-switch { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; font-size: .82em; user-select: none; white-space: nowrap; } .wd-mode-switch-label { color: var(--secondary-text-color); transition: color .15s; } .wd-mode-switch-label.active { color: var(--primary-color); font-weight: 600; } .wd-toggle-track { position: relative; display: inline-block; width: 36px; height: 20px; flex-shrink: 0; } .wd-toggle-track input { opacity: 0; width: 0; height: 0; position: absolute; } .wd-toggle-knob { position: absolute; inset: 0; border-radius: 20px; background: var(--divider-color); transition: background .2s; } .wd-toggle-knob::after { content: ''; position: absolute; top: 3px; left: 3px; width: 14px; height: 14px; border-radius: 50%; background: #fff; transition: transform .2s; } .wd-toggle-track input:checked + .wd-toggle-knob { background: var(--primary-color); } .wd-toggle-track input:checked + .wd-toggle-knob::after { transform: translateX(16px); } .wd-sec-sug-dot { position: absolute; top: 2px; right: 3px; width: 6px; height: 6px; border-radius: 50%; background: var(--warning-color, #ff9800); display: inline-block; pointer-events: none; } .wd-sec-conf-dot { position: absolute; top: 2px; right: 3px; width: 6px; height: 6px; border-radius: 50%; background: var(--error-color, #b71c1c); display: inline-block; pointer-events: none; } .wd-subtabs { display: flex; gap: 2px; border-bottom: 1px solid var(--divider-color); margin-bottom: 18px; flex-wrap: wrap; } .wd-subtab { padding: 8px 18px; border: none; background: transparent; color: var(--secondary-text-color); font-size: .8em; font-weight: 500; cursor: pointer; border-bottom: 2px solid transparent; transition: color .15s; } .wd-subtab.active { color: var(--primary-color); border-bottom-color: var(--primary-color); } .wd-profiles-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; } .wd-profile-card { background: var(--card-background-color); border-radius: 10px; padding: 16px; border: 1px solid var(--divider-color, rgba(0,0,0,.08)); cursor: pointer; transition: border-color .15s, transform .1s; } .wd-profile-card:hover { border-color: var(--primary-color); transform: translateY(-1px); } /* #444: these are the only cards rendered as a button element, and resetting font without color left them on the UA buttontext system color - which follows the BROWSER/OS color scheme, not the HA theme. On a light HA theme served to a dark-mode browser (Catppuccin Auto Latte Macchiato, the reporter's setup) the profile titles came out near-white on a light card. Everything beside them was readable because it names a theme token explicitly; only the inherited color was wrong. (No backticks in this block: it lives inside a JS template literal.) */ button.wd-attn-card, button.wd-profile-card { appearance: none; font: inherit; color: inherit; text-align: left; width: 100%; } button.wd-profile-card { display: block; } .wd-prof-wrap { position: relative; } .wd-profile-name { font-weight: 600; font-size: 1em; margin-bottom: 6px; } .wd-profile-meta { font-size: .8em; color: var(--secondary-text-color); } /* Profile-card header: name on the left, mini power-signature sparkline on the right, both on one line and vertically aligned regardless of badge count. */ .wd-profile-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } .wd-profile-head .wd-profile-name { margin: 0; flex: 1 1 auto; min-width: 0; } /* Status-pill row: wraps cleanly on its own line below the name, and every pill (health / trend / warm-up / imported) shares one uniform compact pill shape. */ .wd-profile-badges { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 8px; } .wd-profile-badges .wd-badge { margin: 0; padding: 2px 9px; border-radius: 999px; font-size: .72em; font-weight: 600; gap: 4px; } /* D2: mini duration sparkline on profile cards */ .wd-prof-spark { width: 64px; height: 20px; display: block; flex-shrink: 0; } /* Community Store */ .wd-store-crumbs { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; font-size: .85em; } .wd-crumb { background: none; border: none; color: var(--primary-color); cursor: pointer; padding: 2px 4px; font: inherit; } .wd-crumb:hover { text-decoration: underline; } .wd-crumb.active { color: var(--primary-text-color); font-weight: 700; cursor: default; } .wd-crumb-sep { color: var(--secondary-text-color); } .wd-store-search { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; } .wd-store-search input { flex: 1; min-width: 180px; padding: 8px 11px; border-radius: 6px; border: 1px solid var(--divider-color); background: var(--secondary-background-color); color: var(--primary-text-color); font-size: .9em; } /* The box is a combobox (#416), so the flex child is the wrapper, not the input. */ .wd-store-search .wd-combo { flex: 1 1 auto; min-width: 180px; } .wd-store-list { display: flex; flex-direction: column; gap: 8px; } /* Browse rows (appliances / programs): tappable list rows with a hover affordance and a chevron, instead of flat cards. */ .wd-store-rows { display: flex; flex-direction: column; gap: 6px; } .wd-store-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; text-align: left; width: 100%; cursor: pointer; padding: 11px 14px; background: var(--secondary-background-color); border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); color: var(--primary-text-color); transition: border-color .12s ease, background .12s ease; } .wd-store-row:hover { border-color: var(--primary-color); background: var(--card-background-color); } .wd-store-row-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; } .wd-store-row-title { font-weight: 600; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .wd-store-row-sub { display: flex; align-items: center; gap: 10px; font-size: .8em; color: var(--secondary-text-color); } .wd-store-chip { background: var(--accent-dim, rgba(0,180,216,.14)); color: var(--primary-color); border-radius: 999px; padding: 1px 8px; font-size: .92em; text-transform: capitalize; } .wd-store-fav { color: var(--warning-color, #f0b429); } .wd-store-row-arrow { color: var(--secondary-text-color); font-size: 1.3em; flex-shrink: 0; } .wd-store-cycle-top { display: flex; align-items: center; gap: 12px; } .wd-store-cycle-stats { flex: 1; min-width: 0; } .wd-store-spark { width: 120px; height: 36px; flex-shrink: 0; display: block; background: var(--secondary-background-color); border-radius: 6px; } .wd-store-conn { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; } .wd-store-actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; } .wd-tag-pending { background: rgba(56,139,253,.18); color: var(--info-color, #58a6ff); } .wd-tag-approved { background: rgba(63,185,80,.18); color: var(--success-color, #3fb950); } .wd-store-picker-detail { margin-top: 6px; font-size: .85em; color: var(--secondary-text-color); display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; } .wd-store-picker-actions { display: flex; align-items: center; gap: 8px; flex-basis: 100%; margin-top: 4px; flex-wrap: wrap; } .wd-star-row { display: inline-flex; gap: 2px; } .wd-star-btn { background: none; border: none; cursor: pointer; color: var(--warning-color, #f0b429); font-size: 1.1em; padding: 0 1px; line-height: 1; } .wd-star-btn:hover { transform: scale(1.15); } /* Share-device selection tree (profile -> its reference cycles). */ .wd-sd-tree { display: flex; flex-direction: column; gap: 8px; max-height: 44vh; overflow-y: auto; margin-bottom: 16px; } /* #460: a group's overflow:hidden drops its automatic min-height to 0, so past the max-height above the groups shrank and clipped their own rows instead of the tree scrolling (a phone hits the cap at once). Rows keep their height; the tree scrolls. */ .wd-sd-tree > * { flex-shrink: 0; } .wd-sd-group { border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); background: var(--secondary-background-color); overflow: hidden; } .wd-sd-prof { display: flex; align-items: center; gap: 8px; padding: 9px 12px; cursor: pointer; font-weight: 600; } .wd-sd-prof-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .wd-sd-count { font-size: .8em; font-weight: 400; color: var(--secondary-text-color); } .wd-sd-cycles { display: flex; flex-direction: column; border-top: 1px solid var(--divider-color); } .wd-sd-cyc { display: flex; align-items: center; gap: 8px; padding: 6px 12px 6px 28px; cursor: pointer; font-size: .9em; } .wd-sd-cyc:hover, .wd-sd-prof:hover { background: var(--card-background-color); } .wd-sd-cyc-meta { color: var(--secondary-text-color); } .wd-sd-phase { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-top: 1px solid var(--divider-color); cursor: pointer; font-size: .85em; color: var(--secondary-text-color); } .wd-sd-phase:hover { background: var(--card-background-color); } .wd-sd-settings { display: flex; align-items: center; gap: 8px; padding: 10px 2px 2px; cursor: pointer; font-size: .9em; } .wd-sd-consent { display: flex; align-items: flex-start; gap: 8px; padding: 10px 2px 2px; cursor: pointer; font-size: .9em; } .wd-sd-group-nocyc { opacity: .65; } .wd-sd-prof-disabled { display: flex; align-items: baseline; gap: 8px; padding: 9px 12px; font-weight: 600; flex-wrap: wrap; } .wd-sd-nocyc-note { font-size: .8em; font-weight: 400; color: var(--secondary-text-color); } .wd-share-guide { margin-bottom: 10px; border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); overflow: hidden; } .wd-share-guide > summary { padding: 8px 12px; cursor: pointer; font-size: .85em; font-weight: 600; color: var(--secondary-text-color); list-style: none; } .wd-share-guide > summary::-webkit-details-marker { display: none; } .wd-share-guide > summary::before { content: '\u25B6 '; font-size: .7em; } .wd-share-guide[open] > summary::before { content: '\u25BC '; } .wd-share-guide-list { margin: 0; padding: 4px 12px 10px 28px; font-size: .85em; color: var(--secondary-text-color); line-height: 1.5; } .wd-share-guide-list li { margin-bottom: 4px; } .wd-linkbtn { background: none; border: none; padding: 0; color: var(--primary-color); cursor: pointer; font: inherit; text-decoration: underline; } .wd-gear-body { margin-top: 12px; } .wd-empty { text-align: center; padding: 48px 24px; color: var(--secondary-text-color); } .wd-empty .wd-icon { font-size: 3em; margin-bottom: 10px; } .wd-stale { margin: 6px 0 0; padding: 6px 12px; border-radius: var(--wd-radius-md); background: var(--secondary-background-color); border: 1px solid var(--warning-color, #ffa600); color: var(--primary-text-color); font-size: .85em; } .wd-error-state { display: flex; align-items: center; gap: 10px; padding: 10px 14px; margin-bottom: 10px; border-radius: var(--wd-radius-md); background: var(--secondary-background-color); border: 1px solid var(--divider-color); color: var(--error-color, #b71c1c); font-size: .9em; } .wd-info { font-size: .9em; color: var(--secondary-text-color); line-height: 1.6; margin: 0; } .wd-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 100; display: flex; align-items: center; justify-content: center; } .wd-modal { background: var(--card-background-color); border-radius: var(--wd-radius-lg); padding: 24px; max-width: 480px; width: calc(100% - 32px); max-height: 90vh; overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,.3); } .wd-modal-lg { max-width: 880px; } .wd-modal h2 { margin: 0 0 16px; font-size: 1.1em; display: flex; align-items: center; gap: 10px; } .wd-modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; flex-wrap: wrap; } .wd-canvas-wrap { position: relative; margin: 10px 0; background: var(--secondary-background-color); border-radius: var(--wd-radius-md); padding: 6px; } /* touch-action: pan-y is load-bearing, not cosmetic. With "none" (the pre-0.5.6 value) a one-finger swipe that started on a graph was swallowed, so every chart was a 160-240px band the user could not scroll past on a phone (issue #413). With pan-y the browser keeps the vertical swipe (page scrolls) while still handing us the horizontal drag AND both pointers of a two-finger pinch, which is what makes touch zoom possible at all. Mirrors what Home Assistant's own ha-chart-base does via moveOnMouseMove/preventDefaultMouseMove. */ .wd-canvas-wrap canvas { width: 100%; height: 240px; display: block; touch-action: pan-y; cursor: crosshair; } /* Zoom reset affordance, mirroring HA's mdiRestart chart button: only rendered while the canvas actually has a zoom viewport. */ .wd-zoom-reset { position: absolute; top: 10px; right: 12px; z-index: 2; display: none; align-items: center; justify-content: center; width: 30px; height: 30px; padding: 0; border-radius: 50%; border: 1px solid var(--divider-color); background: var(--card-background-color); color: var(--primary-text-color); cursor: pointer; font-size: 15px; line-height: 1; opacity: .92; box-shadow: 0 1px 4px rgba(0,0,0,.25); } .wd-zoom-reset.wd-zoom-reset--on { display: inline-flex; } .wd-zoom-reset:hover { opacity: 1; border-color: var(--primary-color); } .wd-mode-bar { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; } .wd-mini-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--divider-color); margin-bottom: 16px; flex-wrap: wrap; } .wd-mini-tab { padding: 7px 16px; border: none; background: transparent; color: var(--secondary-text-color); font-size: .82em; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent; } .wd-mini-tab.active { color: var(--primary-color); border-bottom-color: var(--primary-color); } .wd-kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 10px; margin: 4px 0 14px; } .wd-kv-item { background: var(--secondary-background-color); border-radius: var(--wd-radius-md); padding: 10px; text-align: center; } .wd-kv-val { font-size: 1.25em; font-weight: 700; } .wd-kv-lbl { font-size: .7em; color: var(--secondary-text-color); margin-top: 3px; } .wd-seg-row, .wd-phase-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; } .wd-swatch { width: 12px; height: 12px; border-radius: 3px; flex-shrink: 0; display: inline-block; } .wd-toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: 200; padding: 10px 20px; border-radius: var(--wd-radius-md); font-size: .9em; font-weight: 500; box-shadow: 0 4px 12px rgba(0,0,0,.25); animation: wd-toast-in .2s ease; } @keyframes wd-toast-in { from { opacity: 0; transform: translateX(-50%) translateY(10px); } } .wd-toast-success { background: var(--success-color, #4caf50); color: var(--wd-white); } .wd-toast-error { background: var(--error-color, #f44336); color: var(--wd-white); } .wd-toast-info { background: var(--info-color, #2196f3); color: var(--wd-white); } /* D4: undo toast \u2014 action button + row layout */ .wd-toast { display: flex; align-items: center; gap: 14px; } .wd-toast-action { background: rgba(255,255,255,.22); color: inherit; border: none; border-radius: 6px; padding: 5px 12px; font: inherit; font-weight: 700; cursor: pointer; text-transform: uppercase; letter-spacing: .04em; font-size: .85em; } .wd-toast-action:hover { background: rgba(255,255,255,.34); } /* D7: "changed since last save" marker beside a settings field label */ .wd-chg-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--info-color, #2196f3); margin-left: 6px; flex-shrink: 0; cursor: help; } .wd-diag-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 16px; } .wd-diag-stat { background: var(--secondary-background-color); border-radius: var(--wd-radius-md); padding: 12px; text-align: center; } .wd-diag-val { font-size: 1.6em; font-weight: 700; } .wd-diag-lbl { font-size: .72em; color: var(--secondary-text-color); margin-top: 4px; } .wd-feedback-item { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--divider-color); } .wd-feedback-item:last-child { border-bottom: none; } .wd-feedback-body { flex: 1; } .wd-feedback-profile { font-weight: 600; } .wd-feedback-meta { font-size: .78em; color: var(--secondary-text-color); } .wd-rec-status { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; } .wd-rec-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; } .wd-rec-active { background: var(--error-color, #f44336); animation: wd-pulse 1s ease-in-out infinite; } .wd-rec-ready { background: var(--success-color, #4caf50); } .wd-rec-idle { background: var(--disabled-color, #bdbdbd); } /* Graph hover tooltip (follows the cursor) */ .wd-gtip { position: fixed; z-index: 300; display: none; pointer-events: none; background: var(--card-background-color); color: var(--primary-text-color); border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); padding: 7px 10px; font-size: 12px; line-height: 1.5; box-shadow: 0 4px 16px rgba(0,0,0,.4); white-space: nowrap; } .wd-gtip b { font-weight: 700; } .wd-gtip--pinned { border-color: var(--primary-color); } .wd-gtip-dismiss { display: block; margin-top: 4px; opacity: .55; font-size: 11px; } /* Status chart legend + toggles */ .wd-leg { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px; font-size: .8em; color: var(--secondary-text-color); } .wd-leg-i { display: inline-flex; align-items: center; gap: 6px; } .wd-leg-i input { margin: 0 2px 0 0; width: auto; } .wd-leg-sw { width: 16px; height: 3px; border-radius: 2px; display: inline-block; } /* Status program selector */ .wd-prog-ctl { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; } .wd-prog-ctl label { font-size: .72em; text-transform: uppercase; letter-spacing: .08em; color: var(--secondary-text-color); margin: 0; } .wd-prog-ctl select { padding: 8px 11px; border-radius: 6px; border: 1px solid var(--divider-color); background: var(--secondary-background-color); color: var(--primary-text-color); min-width: 200px; font-size: .9em; } .wd-prog-tag { font-size: .78em; padding: 3px 9px; border-radius: 10px; } .wd-prog-tag.auto { background: rgba(76,175,80,.18); color: var(--success-color, #4caf50); } .wd-prog-tag.manual { background: rgba(255,152,0,.2); color: var(--warning-color, #ff9800); } /* Status-rich device selector */ .wd-devbar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; } .wd-devcard { display: flex; align-items: center; gap: 9px; padding: 9px 13px; border-radius: var(--wd-radius-lg); border: 1px solid var(--divider-color); background: var(--card-background-color); color: var(--primary-text-color); cursor: pointer; font-size: .9em; } .wd-devcard.active { border-color: var(--primary-color); box-shadow: 0 0 0 1px var(--primary-color); } .wd-devadd { border-style: dashed; color: var(--secondary-text-color); } .wd-devadd:hover { border-color: var(--primary-color); color: var(--primary-color); } .wd-devdot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } .wd-devdot.run { animation: wd-pulse 1.4s ease-in-out infinite; } .wd-devname { font-weight: 600; } .wd-devsub { font-size: .72em; color: var(--secondary-text-color); } .wd-dbadge { font-size: .72em; padding: 1px 7px; border-radius: 10px; background: var(--secondary-background-color); } .wd-dbadge.rec { background: var(--error-color, #f44336); color: var(--wd-white); } .wd-dbadge.fb { background: rgba(33,150,243,.22); } .wd-dbadge.conf { background: rgba(183,28,28,.18); color: var(--error-color, #b71c1c); } /* Attention cards (status dashboard) */ .wd-attn { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; margin-bottom: 16px; } .wd-attn-card { display: flex; align-items: center; gap: 11px; padding: 12px 14px; border-radius: 10px; background: var(--card-background-color); border: 1px solid var(--divider-color); cursor: pointer; transition: border-color .15s; } .wd-attn-card:hover { border-color: var(--primary-color); } .wd-attn-icon { font-size: 1.5em; line-height: 1; } .wd-attn-body { flex: 1; min-width: 0; } .wd-attn-title { font-weight: 600; } .wd-attn-sub { font-size: .76em; color: var(--secondary-text-color); } /* F1 first-run onboarding card (Status power-chart area) */ .wd-onboard { margin-top: 12px; padding: 16px; border-radius: 10px; border: 1px dashed var(--divider-color); background: var(--secondary-background-color); } .wd-onboard .wd-card-title { margin-top: 0; } .wd-onboard-skip { font-size: .8em; color: var(--secondary-text-color); text-decoration: underline; cursor: pointer; } .wd-onboard-skip:hover { color: var(--primary-color); } /* Setup card (replaces getting-started card, phases 0-3) and phase-4 chip */ .wd-setup-card { margin-top: 12px; padding: 16px; border-radius: 10px; border: 1px solid var(--primary-color, #03a9f4); background: color-mix(in srgb, var(--primary-color, #03a9f4) 8%, var(--card-background-color, var(--ha-card-background))); } .wd-setup-card .wd-card-title { margin-top: 0; } .wd-setup-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 20px; font-size: .82em; font-weight: 600; cursor: pointer; border: 1px solid var(--divider-color); background: var(--secondary-background-color); margin-top: 12px; } .wd-setup-chip--healthy { border-color: var(--success-color, #4caf50); background: color-mix(in srgb, var(--success-color, #4caf50) 10%, var(--card-background-color, transparent)); } .wd-setup-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } .wd-setup-dot--green { background: var(--success-color, #4caf50); } .wd-link { background: none; border: none; padding: 0; color: var(--primary-color); cursor: pointer; font: inherit; text-decoration: underline; display: inline; } .wd-link:hover { opacity: .8; } /* Logs page */ .wd-logbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; } .wd-logs { font-family: monospace; font-size: .76em; background: var(--secondary-background-color); border-radius: var(--wd-radius-md); padding: 10px; height: 56vh; min-height: 140px; overflow: auto; resize: vertical; } #wd-log-lines-page { height: auto; min-height: 200px; resize: none; } /* Grouped stat blocks (profile overview) */ .wd-sg-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin: 4px 0 16px; } .wd-sg { background: var(--secondary-background-color); border-radius: 10px; padding: 14px; } .wd-sg-h { font-size: .7em; text-transform: uppercase; letter-spacing: .08em; color: var(--secondary-text-color); margin-bottom: 6px; } .wd-sg-main { font-size: 1.55em; font-weight: 700; line-height: 1.1; } .wd-sg-main span { font-size: .5em; font-weight: 400; color: var(--secondary-text-color); margin-left: 4px; } .wd-sg-sub { font-size: .78em; color: var(--secondary-text-color); margin-top: 5px; line-height: 1.5; } .wd-logline { padding: 2px 0; border-bottom: 1px solid var(--divider-color); white-space: pre-wrap; word-break: break-word; } .wd-logline:last-child { border-bottom: none; } /* Entity combobox */ .wd-combo { position: relative; width: 100%; } .wd-combo-drop { position: absolute; top: 100%; left: 0; right: 0; z-index: 60; background: var(--card-background-color,#fff); border: 1px solid var(--divider-color); border-radius: 6px; box-shadow: 0 4px 14px rgba(0,0,0,.18); max-height: 220px; overflow-y: auto; margin-top: 3px; } /* Flipped above the input when there is more room there (_positionComboDrop). */ .wd-combo-drop.wd-drop-up { top: auto; bottom: 100%; margin-top: 0; margin-bottom: 3px; } .wd-combo-item { padding: 7px 12px; cursor: pointer; font-size: .86em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .wd-combo-item:hover, .wd-combo-item.kbd { background: var(--secondary-background-color); } .wd-combo-row { display: flex; gap: 6px; align-items: center; } .wd-combo-row .wd-combo { flex: 1 1 auto; } /* Colour field: swatch + hex text box + clear (#454) */ .wd-colorfield { display: flex; gap: 6px; align-items: center; width: 100%; } .wd-colorfield input[type="text"] { flex: 1 1 auto; min-width: 0; } .wd-color-sw { flex: 0 0 auto; width: 34px; height: 34px; padding: 2px; cursor: pointer; border: 1px solid var(--divider-color); border-radius: var(--wd-radius-md); background: var(--secondary-background-color); } .wd-color-sw::-webkit-color-swatch-wrapper { padding: 0; } .wd-color-sw::-webkit-color-swatch { border: none; border-radius: 4px; } .wd-color-sw::-moz-color-swatch { border: none; border-radius: 4px; } .wd-color-clear { flex: 0 0 auto; width: 30px; height: 34px; border-radius: var(--wd-radius-md); border: 1px solid var(--divider-color); background: var(--secondary-background-color); color: var(--secondary-text-color); font-size: 1.1em; line-height: 1; cursor: pointer; } .wd-color-clear:hover { color: var(--primary-text-color); } .wd-addbtn { flex: 0 0 auto; width: 34px; height: 34px; border-radius: var(--wd-radius-md); border: 1px solid var(--divider-color); background: var(--secondary-background-color); color: var(--primary-text-color); font-size: 1.25em; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; } .wd-addbtn:hover { background: var(--primary-color); color: #fff; border-color: var(--primary-color); } .wd-loglvl { font-weight: 700; margin-right: 6px; } .wd-logcomp { display: inline-block; font-size: .72em; color: var(--secondary-text-color); background: var(--secondary-background-color); border-radius: 4px; padding: 0 5px; margin-right: 6px; } .wd-logdev { display: inline-block; font-size: .72em; color: var(--primary-color); margin-right: 6px; } .wd-logts { color: var(--secondary-text-color); margin-right: 6px; } .wd-lvl-ERROR, .wd-lvl-CRITICAL { color: var(--error-color, #f44336); } .wd-lvl-WARNING { color: var(--warning-color, #ff9800); } .wd-lvl-INFO { color: var(--info-color, #2196f3); } .wd-lvl-DEBUG { color: var(--secondary-text-color); } /* Compact cycle list */ .wd-clist { display: flex; flex-direction: column; } .wd-crow { display: flex; align-items: center; gap: 10px; padding: 9px 6px; border-bottom: 1px solid var(--divider-color); cursor: pointer; } .wd-crow:hover { background: var(--secondary-background-color); } .wd-crow:last-child { border-bottom: none; } .wd-cmain { flex: 1; min-width: 0; overflow: hidden; } .wd-cprog { font-weight: 600; } .wd-cdate { font-size: .74em; color: var(--secondary-text-color); } .wd-cmeta { text-align: right; font-size: .76em; color: var(--secondary-text-color); white-space: nowrap; } /* Responsive / touch (portrait, phones, side panel) */ @media (max-width: 680px) { .wd-body { padding: 12px 10px 64px; } .wd-nav { margin: -12px -10px 12px; padding: 12px 10px 0; } .wd-card { padding: 14px; margin-bottom: 12px; } .wd-form-grid { grid-template-columns: 1fr; } .wd-stats { grid-template-columns: repeat(2, 1fr); } .wd-kv { grid-template-columns: repeat(2, 1fr); } .wd-tab { padding: 9px 13px; } .wd-modal { padding: 16px; width: calc(100% - 18px); } .wd-modal-lg { max-width: 100%; } .wd-sd-prof-name { white-space: normal; overflow-wrap: anywhere; } /* #460: wrap, never ellipsise a choice */ .wd-canvas-wrap canvas { height: 200px; } .wd-zoom-reset { width: 36px; height: 36px; top: 8px; right: 8px; font-size: 17px; } .wd-gtip { white-space: normal; max-width: calc(100vw - 24px); } .wd-header { padding: 12px 14px; } .wd-btn { padding: 9px 15px; } /* larger touch targets */ .wd-tip-pop { width: 210px; } .wd-pg-lane-lbl { flex: 0 1 120px; max-width: 120px; } #wd-settings-form .wd-form-grid { grid-template-columns: 1fr; gap: 12px 0; } } /* Log drawer */ /* The render target between :host and .wd-shell (created in _boot). It must be a full-height flex column of its own: an unstyled wrapper is a block box sized by its content, so .wd-shell's flex:1 had no flex container to resolve against and the whole chain down to .wd-main fell back to content height. .wd-main then never overflowed (scrollHeight == clientHeight, so no scrollbar) and instead spilled out of the clipped host, stranding the bottom of every long tab where no scrolling reaches it. */ .wd-root { height: 100%; display: flex; flex-direction: column; } /* min-height:0 so the row below can actually shrink and hand its overflow to .wd-main, instead of the shell growing to fit and pushing past the host. */ .wd-shell { display: flex; flex-direction: column; flex: 1; min-height: 0; } .wd-content-row { display: flex; flex: 1; overflow: hidden; min-height: 0; } .wd-main { flex: 1; overflow-y: auto; min-width: 0; } .wd-log-drawer { position: relative; width: 0; overflow: hidden; transition: width .28s cubic-bezier(.4,0,.2,1); border-left: 1px solid var(--divider-color); display: flex; flex-direction: column; background: var(--primary-background-color); } .wd-log-drawer.open { width: 380px; } .wd-log-resize { position: absolute; left: 0; top: 0; bottom: 0; width: 6px; cursor: ew-resize; z-index: 2; transition: background .15s; } .wd-log-resize:hover, .wd-log-resize.dragging { background: var(--primary-color, #03a9f4); opacity: .35; } .wd-log-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--divider-color); font-weight: 600; font-size: .9em; flex-shrink: 0; white-space: nowrap; } .wd-log-drawer-body { flex: 1; overflow-y: auto; padding: 10px 14px; min-width: 0; } .wd-log-close-btn { background: none; border: none; cursor: pointer; color: inherit; opacity: .65; padding: 3px 6px; border-radius: var(--wd-radius-sm); font-size: 1.1em; line-height: 1; } .wd-log-close-btn:hover { opacity: 1; background: var(--secondary-background-color); } .wd-gear-btn.log-active { background: rgba(255,255,255,.22); } @media (max-width: 680px) { .wd-log-drawer.open { width: 100vw !important; position: fixed; top: 0; right: 0; bottom: 0; z-index: 30; border-left: none; } .wd-log-resize { display: none; } } .wd-pg-delta-up { color: var(--success-color, #4caf50); font-weight: 700; } .wd-pg-delta-down { color: var(--error-color, #f44336); font-weight: 700; } .wd-pg-delta-flat { color: var(--secondary-text-color); } /* F3: Unified Playground */ .wd-pg-canvas-wrap { position: relative; width: 100%; } /* Same pan-y contract as .wd-canvas-wrap canvas above: page keeps the vertical swipe, we keep horizontal drags and pinch. This canvas previously had no touch-action at all (default auto), so on touch the browser cancelled every gesture and none of the pan / threshold-drag / hover features worked. */ #wd-pg-canvas { display: block; width: 100%; height: 330px; cursor: crosshair; touch-action: pan-y; border-radius: 6px; background: var(--secondary-background-color); margin: 10px 0 0; } .wd-pg-strip { display: flex; align-items: center; gap: 10px; padding: 8px 2px; font-size: .88em; font-variant-numeric: tabular-nums; flex-wrap: wrap; border-bottom: 1px solid var(--divider-color, rgba(127,127,127,.2)); margin-bottom: 12px; } .wd-pg-strip-state { padding: 2px 10px; border-radius: 20px; font-weight: 700; font-size: .83em; white-space: nowrap; } .wd-pg-strip-pbar { display: inline-flex; align-items: center; gap: 5px; } .wd-pg-strip-track { width: 60px; height: 6px; background: var(--secondary-background-color); border-radius: 3px; overflow: hidden; display: inline-block; vertical-align: middle; } .wd-pg-strip-fill { height: 100%; background: var(--primary-color); border-radius: 3px; transition: width .15s; } .wd-pg-params { display: flex; flex-direction: column; gap: 2px; } .wd-pg-param-row { display: flex; align-items: center; gap: 6px; } .wd-pg-param-lbl { flex: 1; font-size: .83em; color: var(--secondary-text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .wd-pg-param-inp { width: 76px; flex: 0 0 76px; } .wd-pg-param-drag { font-size: .75em; color: var(--primary-color); cursor: default; flex: 0 0 12px; } .wd-pg-cand-row { display: flex; align-items: center; gap: 6px; font-size: .82em; margin: 3px 0; } .wd-pg-cand-name { flex: 0 0 110px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .wd-pg-cand-track { flex: 1; height: 7px; background: var(--secondary-background-color); border-radius: var(--wd-radius-sm); overflow: hidden; } .wd-pg-cand-fill { height: 100%; border-radius: var(--wd-radius-sm); } .wd-pg-cand-pct { flex: 0 0 34px; text-align: right; color: var(--secondary-text-color); } /* Playground: settings control panel (live-settings source + named presets) */ .wd-pg-ctrl { border: 1px solid var(--divider-color, rgba(127,127,127,.25)); border-radius: var(--wd-radius-md, 10px); padding: 8px 10px; margin: 0 0 10px; background: var(--secondary-background-color); } .wd-pg-ctrl .wd-btn { white-space: nowrap; } .wd-pg-preset-sel { min-width: 150px; max-width: 220px; font-size: .82em; } .wd-pg-preset-name { flex: 1 1 150px; min-width: 120px; max-width: 220px; font-size: .82em; } /* Per-setting publish arrow. The empty slot keeps every row's value column aligned whether or not that row currently has a publishable change. */ .wd-pg-pub { width: 20px; height: 20px; flex: 0 0 20px; padding: 0; border: none; border-radius: 5px; cursor: pointer; background: var(--primary-color); color: var(--text-primary-color, #fff); font-size: .8em; line-height: 1; } .wd-pg-pub:hover { filter: brightness(1.15); } .wd-pg-pub-slot { width: 20px; flex: 0 0 20px; display: inline-block; } /* Playground: unified workbench (graph+settings always on top) + "Across your cycles" drawer with History/Optimize sub-tabs. */ .wd-pg-drawer { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--divider-color, rgba(127,127,127,.25)); } .wd-pg-drawer-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 0 0 12px; } .wd-pg-subtabs { display: inline-flex; gap: 2px; padding: 3px; border-radius: 10px; background: var(--secondary-background-color); } .wd-pg-subtab { border: none; background: transparent; color: var(--secondary-text-color); font: inherit; font-size: .84em; font-weight: 600; padding: 5px 13px; border-radius: 8px; cursor: pointer; } .wd-pg-subtab:hover { color: var(--primary-text-color); } .wd-pg-subtab.active { background: var(--card-background-color, var(--primary-background-color)); color: var(--primary-color); box-shadow: 0 1px 3px rgba(0,0,0,.12); } .wd-pg-hrow { cursor: pointer; } .wd-pg-hrow:hover td { background: var(--secondary-background-color); } .wd-pg-hrow.selected td { background: color-mix(in srgb, var(--primary-color) 14%, transparent); box-shadow: inset 2px 0 0 var(--primary-color); } .wd-pg-sim-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 16px; margin-top: 4px; } .wd-pg-sim-main, .wd-pg-sim-side { display: flex; flex-direction: column; gap: 4px; min-width: 0; } .wd-pg-simbar { height: 6px; border-radius: 3px; background: var(--secondary-background-color); overflow: hidden; margin: 6px 0 0; } .wd-pg-simbar-fill { height: 100%; width: 40%; border-radius: 3px; background: var(--primary-color); animation: wd-pg-indeterminate 1.1s ease-in-out infinite; } @keyframes wd-pg-indeterminate { 0% { margin-left: -40%; } 100% { margin-left: 100%; } } .wd-pg-batchbar-fill { height: 100%; width: 0%; border-radius: 3px; background: var(--primary-color); transition: width .18s ease; } /* Header activity pills (background-task registry) */ .wd-task-pills { display: inline-flex; gap: 6px; align-items: center; flex-wrap: wrap; margin: 0 0 0 12px; } .wd-task-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 4px 3px 9px; border-radius: 12px; background: rgba(255,255,255,.16); color: var(--app-header-text-color, #fff); font-size: .78em; line-height: 1; } .wd-task-pill-lbl { font-weight: 600; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .wd-task-pill-pct { font-variant-numeric: tabular-nums; opacity: .95; } .wd-task-pill-eta { opacity: .75; } .wd-task-pill-x { border: none; background: rgba(0,0,0,.18); color: inherit; width: 16px; height: 16px; border-radius: 50%; cursor: pointer; font-size: .9em; line-height: 1; display: inline-flex; align-items: center; justify-content: center; padding: 0; } .wd-task-pill-x:hover { background: rgba(0,0,0,.32); } .wd-task-pill--cancelling { background: rgba(255,160,0,.28); } .wd-task-pill-cancelling { opacity: .9; font-style: italic; } .wd-task-pill-x--cancelling { opacity: .4; cursor: default; pointer-events: none; } .wd-task-spin { width: 10px; height: 10px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: wd-spin-kf .8s linear infinite; opacity: .9; } @keyframes wd-spin-kf { to { transform: rotate(360deg); } } .wd-pg-batchrow { display: flex; align-items: center; gap: 10px; margin: 6px 0 8px; } .wd-pg-batchrow .wd-pg-simbar { flex: 1; margin: 0; } #wd-pg-canvas.wd-pg-panning { cursor: grabbing; } .wd-pg-alerts-card { background: var(--secondary-background-color); border-radius: 10px; padding: 12px; } .wd-pg-outcome-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .wd-pg-outcome-item { text-align: center; background: var(--card-background-color, var(--primary-background-color)); border-radius: 8px; padding: 8px 4px; } .wd-pg-outcome-val { font-size: 1.05em; font-weight: 700; } .wd-pg-outcome-lbl { font-size: .68em; color: var(--secondary-text-color); text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; } .wd-pg-alert { border-left: 3px solid var(--info-color, #2196f3); padding: 4px 0 4px 10px; } /* History table */ .wd-pg-htable { width: 100%; border-collapse: collapse; font-size: .84em; } .wd-pg-htable th { text-align: left; font-weight: 600; color: var(--secondary-text-color); padding: 6px 8px; border-bottom: 1px solid var(--divider-color, rgba(127,127,127,.2)); font-size: .82em; } .wd-pg-htable td { padding: 6px 8px; border-bottom: 1px solid var(--divider-color, rgba(127,127,127,.12)); } .wd-pg-htable tr[data-action] { cursor: pointer; } .wd-pg-htable tr[data-action]:hover { background: var(--secondary-background-color); } .wd-pg-diffbadge { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; font-size: .82em; font-weight: 600; margin: 0 6px 6px 0; } /* Sweep heatmap */ @media (max-width: 720px) { .wd-pg-sim-grid { grid-template-columns: 1fr; } } @media (max-width: 640px) { .wd-pg-strip { gap: 7px; font-size: .82em; } } `;function ht(h){const t=Number(h);if(h==null||!isFinite(t)||t<0)return"-";h=t;const e=Math.floor(h/3600),s=Math.floor(h%3600/60),a=Math.floor(h%60);return e>0?`${e}h ${s}m`:s>0?`${s}m ${a}s`:`${a}s`}function Lt(h){const t=Number(h);return h==null||!isFinite(t)?"-":t>=100?`${it(Math.round(t),0)} W`:`${it(t,1)} W`}function Ht(h,t){const e=Math.abs(t||h||0),s=e>0?Math.min(4,Math.max(0,Math.ceil(-Math.log10(e))+1)):0;return`${Number(h.toFixed(s))}W`}function Ie(h,t){const e=Math.abs(h)/Math.max(1,t);if(!(e>0)||!isFinite(e))return 1;const s=Math.pow(10,Math.floor(Math.log10(e))),a=e/s;return(a<=1?1:a<=2?2:a<=2.5?2.5:a<=5?5:10)*s}function gt(h){return h==null||!isFinite(Number(h))?"-":`${it(h,2)} kWh`}let se="relative",Et,jt,Wt=!0,qt;const ie=new Map;function Re(h,t){const e=t||{};Et=h||void 0;const s=e.number_format;jt=s==="comma_decimal"?["en-US","en"]:s==="decimal_comma"?["de","es","it"]:s==="space_comma"?["fr","sv","cs"]:s==="quote_decimal"?["de-CH"]:s==="system"?void 0:h||void 0,Wt=s!=="none",qt=e.time_format==="12"?!0:e.time_format==="24"?!1:void 0}function ae(h){const t=JSON.stringify([jt,Wt,h]);let e=ie.get(t);if(!e){const s=Object.assign({useGrouping:Wt},h);try{e=new Intl.NumberFormat(jt,s)}catch{e=new Intl.NumberFormat(void 0,s)}ie.set(t,e)}return e}function it(h,t=0){const e=Number(h);return isFinite(e)?ae({minimumFractionDigits:t,maximumFractionDigits:t}).format(e):"-"}function Dt(h){const t=Number(h);return isFinite(t)&&t>0?Math.floor(t):0}function It(h,t){const e=Number(h);if(h==null||!isFinite(e))return"-";if(t)try{return ae({style:"currency",currency:t}).format(e)}catch{}return`${it(e,2)}${t?" "+t:""}`}function ne(h){return qt===void 0?h:Object.assign({hour12:qt},h)}function re(h){const t=ne({hour:"2-digit",minute:"2-digit",second:"2-digit"});try{return h.toLocaleTimeString(Et,t)}catch{return h.toLocaleTimeString(void 0,t)}}function Oe(h){const t=new Date(h);try{return t.toLocaleDateString(Et)}catch{return t.toLocaleDateString()}}const ze=3700;function oe(h){const t=e=>String(e).padStart(2,"0");return`${h.getFullYear()}-${t(h.getMonth()+1)}-${t(h.getDate())}`}function le(h){const t=new Date;return t.setDate(t.getDate()-h),oe(t)}function Be(){return oe(new Date)}function Ut(){return le(10)}function Fe(){return le(ze-1)}function Ne(h){const t=Math.round((h-Date.now())/1e3);let e;try{e=new Intl.RelativeTimeFormat(Et,{numeric:"auto"})}catch{return de(h)}const s=Math.abs(t),a=[["year",31536e3],["month",2592e3],["week",604800],["day",86400],["hour",3600],["minute",60]];for(const[r,i]of a)if(s>=i)return e.format(Math.round(t/i),r);return e.format(t,"second")}function de(h){const t=ne({month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});try{return new Date(h).toLocaleString(Et,t)}catch{return new Date(h).toLocaleString(void 0,t)}}function et(h,t){if(!h)return"-";let e;if(typeof h=="number")e=h>=1e12?h:h*1e3;else{const s=String(h),a=/^(\d{4})-(\d{2})-(\d{2})$/.exec(s);e=a?new Date(+a[1],+a[2]-1,+a[3]).getTime():new Date(s).getTime()}return isNaN(e)?"-":(t||se)==="relative"?Ne(e):de(e)}function Ct(h){if(h==null)return"unknown error";if(h instanceof Error)return`${h.name}: ${h.message}`;const t=h.code!=null?String(h.code):"",e=h.message!=null?String(h.message):"";if(!t&&!e)try{return JSON.stringify(h)}catch{return String(h)}const s=t==="unknown_command"?" (the integration is probably still starting up; this should stop on its own)":"";return`${t||"error"}${e?": "+e:""}${s}`}function d(h){return String(h??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}class ce{constructor(t){this.html=String(t??"")}toString(){return this.html}}function vt(h){return new ce(h)}const he=new Map;function He(h,t){let e=he.get(h);if(e===void 0){try{e=new Intl.PluralRules(h||"en")}catch{e=null}he.set(h,e)}return e?e.select(t):t===1?"one":"other"}function je(h){const t=String(h??"").trim();return/^https?:\/\//i.test(t)?t:""}function Pt(h,t){const e=String(h??"").trim();let s=e.match(/^#([0-9a-fA-F]{3,8})$/);if(s){let a=s[1];if((a.length===3||a.length===4)&&(a=a.split("").map(r=>r+r).join("")),a.length===6||a.length===8){const r=parseInt(a.slice(0,2),16),i=parseInt(a.slice(2,4),16),o=parseInt(a.slice(4,6),16);return`rgba(${r}, ${i}, ${o}, ${t})`}}if(s=e.match(/^rgba?\(([^)]+)\)$/i),s){const a=s[1].split(",").map(r=>r.trim());if(a.length>=3)return`rgba(${a[0]}, ${a[1]}, ${a[2]}, ${t})`}return e}function pe(h,t){const e=parseFloat(h);return isNaN(e)?t:e}const We=["data-action","data-cid","data-idx","data-tab","data-sec","data-gtab","data-ptab","data-hsub","data-proftab","data-opt","data-pgkey","data-gname","data-sugkey"];function qe(h){if(!h||!h.getAttribute||h.tagName==="BODY")return null;const t=s=>window.CSS&&CSS.escape?CSS.escape(s):String(s).replace(/["\\]/g,"\\$&");if(h.id)return"#"+t(h.id);const e=We.filter(s=>h.hasAttribute(s)).map(s=>`[${s}="${t(h.getAttribute(s))}"]`);return e.length?h.tagName.toLowerCase()+e.join(""):null}function Vt(h){return h?Array.from(h.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')).filter(e=>e.getClientRects().length>0):[]}function xt(h){return h==null||h===""?"(none)":h===!0?"on":h===!1?"off":Array.isArray(h)?h.join(", ")||"(none)":String(h)}function _e(h,t,e){return h.slice().sort((s,a)=>{const r=t(s),i=t(a);return r==null&&i==null?0:r==null?1:i==null?-1:(ri?1:0)*e})}function mt(h,t,e,s,a,r,i){const o=e?s===1?" \u25B2":" \u25BC":' \u2195',n=r==="right"?"text-align:right;":"",l=i?` title="${d(i)}"`:"";return`${h}${o}`}function $t(h){h=Math.max(0,Math.round(h));const t=Math.floor(h/3600),e=Math.floor(h%3600/60),s=h%60;return t>0?`${t}:${String(e).padStart(2,"0")}:${String(s).padStart(2,"0")}`:`${e}:${String(s).padStart(2,"0")}`}function ue(h,t){const e={pause:["lbl.artifact_interruption","Interruption"],dip:["lbl.artifact_low_power","Low power"],spike:["lbl.artifact_high_power","High power"]},[s,a]=e[h]||["lbl.artifact_anomaly","Anomaly"];return t?t(s,{},a):a}function ge(h){return h.toLowerCase().replace(/[\s&/\-]+/g,"_").replace(/^_+|_+$/g,"").replace(/_+/g,"_")}function fe(h){let t=String(h??"").trim().toLowerCase();try{t=t.normalize("NFKD").replace(/[\u0300-\u036f]/g,"")}catch{}return t.replace(/[^\p{L}\p{N}]+/gu,"-").replace(/^-+|-+$/g,"")}function me(h){const t=new Set;return String(h??"").split(",").forEach(e=>{const s=parseInt(e.trim(),10);Number.isFinite(s)&&s>0&&t.add(s)}),Array.from(t).sort((e,s)=>e-s)}function Ue(h,t){if(!h||!h.length)return null;for(let e=0;e=h[h.length-1][0])return h[h.length-1][1];for(let e=1;e=t){const s=h[e-1],a=h[e],r=a[0]-s[0]||1;return s[1]+(a[1]-s[1])*((t-s[0])/r)}return h[h.length-1][1]}const Kt=/^#[0-9a-fA-F]{6}$/,Ve="#03A9F4";function ye(h){return Kt.test(String(h||""))?String(h):Ve}function Ge(h,t,e){e=e||{};const s=h.key,a=h.unit?`${h.label} (${h.unit})`:h.label,r=h.unit?` ${h.unit}`:"",i=h.doc?ot(h.doc,h.diagram||Ye[s],e.tText):"",o=e.changed?``:"";if(h.type==="checkbox")return`
${o}${i}
${h.hint?`
${d(h.hint)}
`:""}
`;let n="";const l=t??"";if(h.type==="select"||h.type==="devicetype"||h.type==="device"){const b=(e.opts||[]).map(([v,T])=>``).join("");n=``}else if(h.type==="textarea")n=``;else if(h.type==="json"){const u=l===""||l==null?"":typeof l=="string"?l:JSON.stringify(l,null,2);n=``}else if(h.type==="checkboxlist"){const u=Array.isArray(t)&&t.length?t.map(String):Array.isArray(h.def)?h.def.map(String):[];n=`
`+(h.choices||[]).map(([b,v])=>``).join("")+"
"}else if(h.type==="entitylist"){const b=(Array.isArray(t)?t:t?[t]:[]).map(v=>`${d(v)}`).join("");n=`
${b}
`}else if(h.type==="timerlist"){const u=Array.isArray(l)?l:[],b=e.t?e.t("lbl.timer_min",{},"min"):"min",v=e.t?e.t("lbl.timer_msg_placeholder",{},"Message (optional, {device}/{program}/{minutes})"):"Message (optional, {device}/{program}/{minutes})",T=e.t?e.t("lbl.timer_auto_pause",{},"Auto-pause"):"Auto-pause",C=e.t?e.t("btn.remove_timer",{},"Delete"):"Delete",x=e.t?e.t("btn.add_timer",{},"+ Add timer"):"+ Add timer",$=(D,B)=>{const U=D&&D.offset_minutes?String(D.offset_minutes):"",z=D&&D.message?d(D.message):"",M=D&&D.auto_pause?" checked":"";return`
`},L=u.map((D,B)=>$(D,B)).join("");n=`
${L}
`}else if(h.type==="color"){const u=h.placeholder?` placeholder="${d(h.placeholder)}"`:"",b=Kt.test(String(l))?String(l):ye(h.placeholder),v=e.t?e.t("btn.clear",{},"Clear"):"Clear";n=`
`}else if(h.type==="entity"){const u=h.placeholder?` placeholder="${d(h.placeholder)}"`:"";n=`
`}else if(h.type==="list"){const u=Array.isArray(l)?l.join(", "):d(l);n=``}else if(h.type==="intlist"){const u=Array.isArray(l)?l.join(", "):String(l??""),b=h.placeholder?` placeholder="${d(h.placeholder)}"`:"";n=``}else{const u=h.type==="number"?"number":"text",b=e.datalistId?` list="${e.datalistId}"`:"",v=h.step!=null?` step="${h.step}"`:"",T=h.min!=null?` min="${h.min}"`:"",C=h.max!=null?` max="${h.max}"`:"",x=h.placeholder?` placeholder="${d(h.placeholder)}"`:"";n=`${e.datalist||""}`}const c=e.suggestion,p=c&&c.suggested!=null&&!Ke(c.suggested,t)?c.suggested:null,_=e.t;let f="";if(p!=null){const u=e.tText||_,b=c.reason_key?u(c.reason_key,c.reason_params||{},c.reason||""):c.reason||"",v=b?ot(b):"",T=t!=null&&t!==""?` ${_("suggestion.now_value",{v:`${t}${r}`},"(now {v})")}`:"",C=we(_,s,p,t),x=``,$=d(_("btn.mute_suggestion",{},"Stop suggesting this setting")),L=s?``:"";f=`
\u{1F4A1} ${d(_("suggestion.observed_label",{},"Observed"))}${d(p)}${r}${T}${x}${v}${L}${C?`
${d(C)}
`:""}
`}return a&&n&&(n=n.replace(/<(input|select|textarea)\b(?![^>]*\baria-label=)/,`<$1 aria-label="${d(a)}"`)),`
${o}${i}
${n}${h.hint?`
${d(h.hint)}
`:""}${f}
`}function Ke(h,t){if(h==null||t==null)return!1;const e=parseFloat(h),s=parseFloat(t);return!isNaN(e)&&!isNaN(s)?Math.abs(e-s)<1e-6:String(h)===String(t)}function we(h,t,e,s){const a=parseFloat(e),r=parseFloat(s);return isNaN(a)||isNaN(r)||a===r?"":h(`suggestion.impact.${t}.${a>r?"higher":"lower"}`,{},"")}const Ye={min_power:"min_power",off_delay:"off_delay",start_threshold_w:"hysteresis",stop_threshold_w:"hysteresis",start_energy_threshold:"start_energy",running_dead_zone:"dead_zone",profile_match_min_duration_ratio:"match_ratios",profile_match_max_duration_ratio:"match_ratios",progress_reset_delay:"progress_reset",completion_min_seconds:"min_duration",min_off_gap:"min_off_gap",duration_tolerance:"duration_tolerance",start_duration_threshold:"start_duration",end_energy_threshold:"end_energy_thresh",profile_match_threshold:"confidence",profile_unmatch_threshold:"confidence",auto_label_confidence:"confidence",learning_confidence:"confidence",no_update_active_timeout:"watchdog_timeout",anti_wrinkle_enabled:"anti_wrinkle",anti_wrinkle_max_power:"anti_wrinkle",anti_wrinkle_max_duration:"anti_wrinkle",anti_wrinkle_exit_power:"anti_wrinkle",anti_wrinkle_idle_timeout:"anti_wrinkle",anti_crease_finalize_ratio:"anti_wrinkle",sampling_interval:"sampling"};function be(h){let t=0,e=window.innerWidth,s=0,a=window.innerHeight;for(let r=h.parentElement;r;r=r.parentElement){const i=getComputedStyle(r),o=i.overflowX!=="visible",n=i.overflowY!=="visible";if(!o&&!n)continue;const l=r.getBoundingClientRect();o&&(l.left>t&&(t=l.left),l.rights&&(s=l.top),l.bottom=t-1,r=getComputedStyle(h).direction==="rtl";h.classList.toggle(r?"wd-ovf-r":"wd-ovf-l",!s),h.classList.toggle(r?"wd-ovf-l":"wd-ovf-r",!a)}function ot(h,t,e){const s=t?Je(t,e):"";return`i${s}${d(h)}`}function Xt(h,t,e=""){return`${e||""}`}function kt(h,t,e="",s=""){return`
${Xt(h,t,e)}
${s?`
${d(s)}
`:""}
`}function Je(h,t){const e=t||((r,i,o)=>o),s=r=>`${r}`,a='';switch(h){case"min_power":return s(`${a} ${d(e("diagram.min",{},"min"))}${d(e("diagram.below_off",{},"below = off"))}`);case"hysteresis":return s(`${a} ${d(e("diagram.start",{},"start"))}${d(e("diagram.stop",{},"stop"))}`);case"start_energy":return s(`${a} ${d(e("diagram.spike_ignored",{},"spike ignored"))} ${d(e("diagram.energy_counts",{},"energy counts"))}`);case"off_delay":return s(`${a} ${d(e("diagram.off_delay_wait",{},"off-delay wait"))}`);case"duration_tolerance":return s(`${a} ${d(e("diagram.minus_tol",{},"-tol"))}${d(e("diagram.plus_tol",{},"+tol"))}${d(e("diagram.profile",{},"profile"))}`);case"match_ratios":return s(`${a} ${d(e("diagram.min",{},"min"))}${d(e("diagram.max",{},"max"))}`);case"dead_zone":return s(`${a} ${d(e("diagram.ignored",{},"ignored"))}`);case"progress_reset":return s(`${a} ${d(e("diagram.held_at_100",{},"held at 100%"))}`);case"min_duration":return s(`${a} ${d(e("diagram.too_short",{},"too short"))} ${d(e("diagram.kept",{},"kept"))}`);case"min_off_gap":return s(`${a} ${d(e("diagram.off_gap",{},"off-gap"))} ${d(e("diagram.gap_below_min",{},"gap below min = one cycle"))}`);case"start_duration":return s(`${a} ${d(e("diagram.spike_ignored_short",{},"spike: ignored"))} ${d(e("diagram.confirmed",{},"confirmed"))}`);case"end_energy_thresh":return s(`${a} ${d(e("diagram.tail_energy_resets",{},"tail energy above thresh: timer resets"))} ${d(e("diagram.accum",{},"accum"))} ${d(e("diagram.thr",{},"thr"))}`);case"confidence":return s(` ${d(e("diagram.no_match",{},"no match"))} ${d(e("diagram.feedback",{},"feedback"))} ${d(e("diagram.auto",{},"auto"))} 0.0 1.0`);case"watchdog_timeout":return s(`${a} ${d(e("diagram.no_updates",{},"no updates"))} ${d(e("diagram.sensor_offline",{},"sensor offline: force-stop"))}`);case"anti_wrinkle":return s(`${a} ${d(e("diagram.heat_phase",{},"heat phase"))} ${d(e("diagram.tumble_kept",{},"tumble pulses kept"))}`);case"sampling":return s(`${a} ${d(e("diagram.si",{},"SI"))} ${d(e("diagram.si",{},"SI"))} ${d(e("diagram.si",{},"SI"))} ${d(e("diagram.reading_interval",{},"typical reading interval"))}`);default:return""}}class Ze extends HTMLElement{constructor(){super(),this._hass=null,this._initialized=!1,this._pollTimer=null,this._toastTimer=null,this._hassUpdateThrottle=null,this._evtUnsubs=[],this._hoverRafId=null,this._hoverPending=null,this._constants={stateColors:{},deviceTypes:[],mlLabEnabled:!1,mlTrainingAvailable:!1,storeOnlineAvailable:!1,storeOnlineEnabled:!1,storeWebOrigin:"",storePrefs:{},pgMatchDefaults:{},version:"",iconUrl:""},this._constantsLoaded=!1,this._devices=[],this._cycles=[],this._refCycles=[],this._shareableCycles=[],this._sharePhasePrograms=[],this._shareAllPrograms=[],this._dlSettings=!1,this._selectMode=!1,this._cycleSel=new Set,this._profiles=[],this._profileGroups={groups:[],min_cohesion:.85},this._profileEnvCache={},this._suggestions=[],this._lockedSuggestions=[],this._feedbacks=[],this._diag=null,this._importUndo=null,this._phases=[],this._recState=null,this._opts={},this._optDefaults={},this._mlComparison=null,this._mlById={},this._mlLoading=!1,this._mlTrainingStatus=null,this._setupStatus=null,this._selIdx=0,this._deepLinkApplied=null,this._tab="status",this._settingsSec="basic",this._settingsSearch="",this._settingsSugOnly=!1,this._settingsHistoryOpen=!1,this._canvasZoom={},this._gAxisHandle={},this._gtipPinned=!1,this._gtipLines=null,this._gPinchRaf=null,this._gPinchPending=null,this._toolsSubtab="recording",this._loading=!0,this._tabLoading=!1,this._scrollNavKey=null,this._scrollCarry=null,this._lastRefresh=null,this._powerHistory=[],this._powerT0=null,this._statusEnv=null,this._statusEnvName=null,this._powerData={live:[],raw:[],cycle_active:!1,cycle_elapsed_s:0},this._stagedSuggestions=!1,this._pendingSettings={},this._dirtyOptKeys=new Set,this._busy=new Set,this._tasks={},this._cancellingTasks=new Set,this._taskCallbacks={},this._tasksSubscribed=!1,this._pgHistoryTaskId=null,this._pgSweepTaskId=null,this._panelCfg=null,this._panelTrans=null,this._pollMs=ve,this._panelSubtab="diagnostics",this._historySub="cycles",this._gearTab="prefs",this._catalog={brands:void 0,devices:void 0,forBrand:null,approvedOnly:!1,brandsFull:!1,brandPrefixes:[],typeDevices:void 0,typeDevicesFor:null,modelIndex:new Map},this._catalogEntry=null,this._maintenance=null,this._logs=[],this._logLevel="",this._logDevice="",this._logComponent="",this._logSearch="";try{this._logOpen=localStorage.getItem("wd-log-open")==="1",this._logDrawerWidth=Math.max(280,parseInt(localStorage.getItem("wd-log-width")||"380",10)||380)}catch{this._logOpen=!1,this._logDrawerWidth=380}this._tabInitialized=!1,this._modal=null,this._prevModal=null,this._toast=null,this._cycleSort={col:"date",dir:-1},this._cycleFilter={text:"",status:""},this._cleanupSort={col:"date",dir:-1},this._profSubtab="profiles",this._statusPhases=[],this._statusPhasesName=null,this._cycleOffset=0,this._cyclesTotal=0,this._cyclesHasMore=!1,this._importedOffset=0,this._importedHasMore=!1,this._undoBuffer=new Map,this._undoSeq=0,this._kbdHandler=null,this._tipHandler=null,this._settingsChangelog=null,this._settingsChangeByKey={},this._pgCycleId="",this._pgProfileName="",this._pgPowerPts=null,this._pgEnvData=null,this._pgAnalysisTab="history",this._pgDetail=null,this._pgDetailBusy=!1,this._pgHistory=null,this._pgSweepObjective="match_accuracy",this._pgSweepNew=null,this._pgDetailDebounceTimer=null,this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},this._pgEffective=null,this._pgPublishable=null,this._pgPresets=[],this._pgPresetLimit=0,this._pgPresetSel="",this._pgPresetName="",this._pgSuggClassic={},this._pgView=null,this._pgHoverT=null,this._pgMap=null,this._pgPanStart=null,this._pgHoverEvent=null,this._pgBatchProgress=null,this._pgBatchCancel=!1,this._pgLoadSeq=0,this._pgDragging=null,this._pgNeedsRestart=!1,this._pgSimCycles=20,this._pgSweepParam="off_delay",this._pgSweepFrom="",this._pgSweepTo="",this._pgSweepSteps=5,this._pgLoading=!1,this._storeView="brands",this._storeQuery="",this._storeDevices=[],this._storeDevice=null,this._storeProfiles=[],this._storeProfile=null,this._storeCycles=[],this._storeStatus=null,this._storeConnected=!1,this._storeLoading=!1,this._storeConnectListener=null}set hass(t){const e=this._hass;if(this._hass=t,!this._initialized&&t){this._initialized=!0,this._boot();return}e!==t&&this._initialized&&!this._loading&&!this._hassUpdateThrottle&&(this._hassUpdateThrottle=setTimeout(()=>{this._hassUpdateThrottle=null,this._fetchAll()},xe))}set panel(t){this._panel=t}set narrow(t){this._narrow=t}connectedCallback(){this._initialized&&(this._startPoll(),this._setupSubscriptions(),this._fetchAll(),this.shadowRoot&&!this._kbdHandler&&(this._kbdHandler=t=>this._onKeydown(t),this.shadowRoot.addEventListener("keydown",this._kbdHandler)),this.shadowRoot&&!this._tipHandler&&(this._tipHandler=t=>{const e=t.target,s=e&&e.closest?e.closest(".wd-tip"):null;s&&this._positionTip(s)},this.shadowRoot.addEventListener("pointerover",this._tipHandler)),this.shadowRoot&&!this._gtipDismissHandler&&(this._gtipDismissHandler=t=>this._maybeDismissGraphTip(t),this.shadowRoot.addEventListener("pointerdown",this._gtipDismissHandler))),this._onResize=()=>{this._syncPanelHeight(),this._resizeLogsPage()},window.addEventListener("resize",this._onResize),this._onLocChanged=()=>this._onLocationChanged(),window.addEventListener("location-changed",this._onLocChanged),window.addEventListener("popstate",this._onLocChanged),this._onOutboundClick=t=>this._maybeGuardOutboundClick(t),window.addEventListener("click",this._onOutboundClick,!0),this._onBeforeUnload=t=>{this._unsavedSettingsCount()&&(t.preventDefault(),t.returnValue="")},window.addEventListener("beforeunload",this._onBeforeUnload),window.visualViewport&&window.visualViewport.addEventListener("resize",this._onResize),requestAnimationFrame(()=>this._syncPanelHeight())}_syncPanelHeight(){if(!this.isConnected)return;const e=this.getBoundingClientRect().top+(window.scrollY||document.documentElement.scrollTop||0),s=Math.round((window.innerHeight||0)-e);if(!(s>0))return;const a=`${Math.max(Ce,s)}px`;this.style.height!==a&&(this.style.height=a)}disconnectedCallback(){this._ovfRO&&(this._ovfRO.disconnect(),this._ovfRO=null),this._onResize&&(window.removeEventListener("resize",this._onResize),window.visualViewport&&window.visualViewport.removeEventListener("resize",this._onResize),this._onResize=null),this._onLocChanged&&(window.removeEventListener("location-changed",this._onLocChanged),window.removeEventListener("popstate",this._onLocChanged),this._onLocChanged=null),this._onOutboundClick&&(window.removeEventListener("click",this._onOutboundClick,!0),this._onOutboundClick=null),this._onBeforeUnload&&(window.removeEventListener("beforeunload",this._onBeforeUnload),this._onBeforeUnload=null),this._stopPoll(),this._hassUpdateThrottle&&(clearTimeout(this._hassUpdateThrottle),this._hassUpdateThrottle=null),this._pgRestartRetryTimer&&(clearTimeout(this._pgRestartRetryTimer),this._pgRestartRetryTimer=null),this._evtUnsubs.forEach(t=>{try{t()}catch{}}),this._evtUnsubs=[],this._flushPendingDeletes(),this._kbdHandler&&this.shadowRoot&&(this.shadowRoot.removeEventListener("keydown",this._kbdHandler),this._kbdHandler=null),this._tipHandler&&this.shadowRoot&&(this.shadowRoot.removeEventListener("pointerover",this._tipHandler),this._tipHandler=null),this._gtipDismissHandler&&this.shadowRoot&&(this.shadowRoot.removeEventListener("pointerdown",this._gtipDismissHandler),this._gtipDismissHandler=null),this._storeConnectListener&&(window.removeEventListener("message",this._storeConnectListener),this._storeConnectListener=null)}_boot(){const t=this.attachShadow({mode:"open"}),e=document.createElement("style");e.textContent=De,t.appendChild(e),this._container=document.createElement("div"),this._container.className="wd-root",t.appendChild(this._container),this._gtip=document.createElement("div"),this._gtip.className="wd-gtip",t.appendChild(this._gtip),this._kbdHandler=s=>this._onKeydown(s),t.addEventListener("keydown",this._kbdHandler),this._tipHandler=s=>{const a=s.target,r=a&&a.closest?a.closest(".wd-tip"):null;r&&this._positionTip(r)},t.addEventListener("pointerover",this._tipHandler),this._gtipDismissHandler=s=>this._maybeDismissGraphTip(s),t.addEventListener("pointerdown",this._gtipDismissHandler),this._loadPanelTranslations().catch(()=>{}).finally(()=>{this._fetchAll(),this._startPoll()}),this._setupSubscriptions()}_setupSubscriptions(){this._evtUnsubs.forEach(e=>{try{e()}catch{}}),this._evtUnsubs=[],this._tasksSubscribed=!1;const t=this._hass&&this._hass.connection;if(t&&t.subscribeMessage){const e=s=>{const a=s.data||{};if(a.entry_id&&(this._fetchAll(),s.event_type==="ha_washdata_cycle_ended")){const r=this._devices[this._selIdx];r&&r.entry_id===a.entry_id&&this._fetchCycles(a.entry_id).then(()=>{this._tab==="history"&&this._render()})}};for(const s of["ha_washdata_cycle_started","ha_washdata_cycle_ended"])t.subscribeMessage(e,{type:"subscribe_events",event_type:s}).then(a=>{this._evtUnsubs.push(a)}).catch(()=>{});t.subscribeMessage(s=>this._onTaskEvent(s),{type:`${w}/subscribe_tasks`}).then(s=>{this._tasksSubscribed=!0,this._evtUnsubs.push(s)}).catch(()=>{this._tasksSubscribed=!1})}}_onTaskEvent(t){const e=t&&t.task;if(!e||!e.id)return;const s=this._tasks[e.id];s&&(s.updated_at||0)>(e.updated_at||0)||(this._tasks[e.id]=e,e.state!=="running"&&this._cancellingTasks.delete(e.id),this._updateTaskPills(),this._pgAdoptTask(e),this._onTrackedTaskProgress(e),(e.kind==="history_import"||e.kind==="history_import_apply")&&(e.state==="running"?this._modal&&this._modal.type==="history-import"&&this._render():this._histTaskFinished(e)),this._settleTaskCallback(e))}_pgAdoptTask(t){if(t.kind!=="pg_sweep"&&t.kind!=="pg_history")return;const e=this._devices[this._selIdx];if(!e||t.entry_id!==e.entry_id)return;const s=t.kind==="pg_sweep";if(this._pgSweepTaskId||this._pgHistoryTaskId)return;const a=t.state!=="running"&&t.finished_at&&Date.now()/1e3-t.finished_at<30;t.state!=="running"&&!a||(s?this._pgSweepTaskId=t.id:this._pgHistoryTaskId=t.id,this._pgAnalysisTab=s?"sweep":"history",this._busy.add(s?"pg-sweep":"pg-history"),t.state==="running"&&(this._pgBatchProgress={done:t.done||0,total:t.total||0}),this._render())}_pgAdoptExisting(){Object.values(this._tasks||{}).forEach(t=>{this._pgAdoptTask(t),this._onTrackedTaskProgress(t)})}_htmlPgRecentRuns(t){const e=this._devices[this._selIdx];if(!e)return"";const s=Object.values(this._tasks||{}).filter(r=>r.kind===t&&r.entry_id===e.entry_id&&r.state!=="running"&&r.has_result).sort((r,i)=>(i.finished_at||0)-(r.finished_at||0)).slice(0,8);if(!s.length)return"";const a=s.map(r=>{const i=r.finished_at?et(r.finished_at*1e3):"",o=r.state==="cancelled"?" \u26A0":"";return``}).join("");return`
${this._t("lbl.pg_recent_runs",{},"Recent runs")}
${a}
`}_settleTaskCallback(t){if(t.state==="running")return;const e=this._taskCallbacks[t.id];if(e){delete this._taskCallbacks[t.id],e(t);return}this._autoSettleAdopted(t)}_autoSettleAdopted(t){if(t.state!=="done"||t.kind!=="ml_training"&&t.kind!=="reprocess"||!t.finished_at||Date.now()/1e3-t.finished_at>60||(this._autoSettled=this._autoSettled||new Set,this._autoSettled.has(t.id)))return;const e=this._devices[this._selIdx];if(!e||t.entry_id!==e.entry_id)return;this._autoSettled.add(t.id);const s=e.entry_id,a=()=>{this._isActiveEntry(s)&&this._render()};t.kind==="ml_training"?this._loadMlTrainingStatus(s).finally(a):this._fetchToolsData(s).finally(a)}async _kickAndTrack(t,e,s){if(this._busy.has(e))return;this._busy.add(e),this._render();let a;try{const i=await this._ws(t);if(a=i&&i.task_id,!a)throw new Error("no task id");const o=String(t.type||"").endsWith("reprocess_history")?"reprocess":String(t.type||"").endsWith("trigger_ml_training")?"ml_training":String(t.type||"").endsWith("apply_split")?"split":String(t.type||"").endsWith("trim_cycle")?"trim":String(t.type||"").endsWith("apply_merge")?"merge":String(t.type||"").endsWith("rebuild_envelopes")?"rebuild":"task";this._addProvisionalTask(a,o,t.entry_id,0)}catch(i){this._busy.delete(e),this._showToast(this._tText("msg.toast_error",{error:i.message||i},"Error: "+(i.message||i)),"error"),this._render();return}this._taskCallbacks[a]=async i=>{if(this._busy.delete(e),i.state==="error"){this._showToast(this._tText("msg.toast_error",{error:i.error||""},"Error: "+(i.error||"")),"error"),this._render();return}if(i.state==="cancelled"){this._showToast(this._t("toast.task_cancelled",{},"Cancelled."),"info"),this._render();return}let o=null;try{const n=await this._ws({type:`${w}/get_task_result`,task_id:i.id});o=n&&n.result}catch{}if(o==null){this._showToast(this._t("toast.pg_run_gone",{},"That run is no longer available."),"info"),this._render();return}try{await s(o,i.state)}catch{}this._render()};const r=this._tasks[a];if(r&&r.state!=="running"){this._settleTaskCallback(r);return}this._tasksSubscribed||this._pollTaskGeneric(a)}_finalizeTaskError(t,e){const s=Object.assign({},this._tasks[t]||{id:t},{id:t,state:"error",error:e,finished_at:Date.now()/1e3});this._tasks[t]=s,this._updateTaskPills(),this._settleTaskCallback(s)}async _pollTaskGeneric(t){let e=0;for(let s=0;s<3600&&this._taskCallbacks[t];s++){let a;try{a=await this._ws({type:`${w}/get_task_result`,task_id:t}),e=0}catch{if(++e>=5){this._finalizeTaskError(t,"lost connection");return}await new Promise(i=>setTimeout(i,1200));continue}if(!a){this._finalizeTaskError(t,"no result");return}if(this._tasks[t]=a,this._updateTaskPills(),a.state!=="running"){this._settleTaskCallback(a);return}await new Promise(r=>setTimeout(r,1200))}this._taskCallbacks[t]&&this._finalizeTaskError(t,"timed out")}_deviceName(t){const e=(this._devices||[]).find(s=>s.entry_id===t);return e&&(e.name||e.title)||""}_taskActionLabel(t){return{pg_history:this._t("lbl.task_pg_history",{},"Test on history"),pg_sweep:this._t("lbl.task_pg_sweep",{},"Optimize"),pg_detail:this._t("lbl.task_pg_detail",{},"Simulate cycle"),split:this._t("lbl.task_split",{},"Splitting cycle"),trim:this._t("lbl.task_trim",{},"Trimming cycle"),merge:this._t("lbl.task_merge",{},"Merging cycles"),rebuild:this._t("lbl.task_rebuild",{},"Rebuilding envelopes"),reprocess:this._t("lbl.task_reprocess",{},"Reprocessing"),ml_training:this._t("lbl.task_ml_training",{},"Learning"),history_import:this._t("lbl.task_history_import",{},"Scanning power history"),history_import_apply:this._t("lbl.task_history_import_apply",{},"Importing cycles")}[t]||t}_fmtEta(t){return t=Math.round(t),t<60?this._t("lbl.eta_secs",{n:t},`~${t}s left`):this._t("lbl.eta_mins",{n:Math.round(t/60)},`~${Math.round(t/60)}m left`)}_exclNote(t){if(!t||!t.total)return"";const e=(t.items||[]).map(([s,a])=>`${a} ${this._t("suggestion.exclusions.reason."+s,{},String(s).replace(/_/g," "))}`).join(", ");return" "+this._tText("suggestion.exclusions.summary",{total:t.total,parts:e},`Excluded ${t.total} mis-detected cycle(s): ${e}.`)}_htmlTaskPills(){const t=Object.values(this._tasks||{}).filter(s=>s.state==="running"),e=new Set(t.map(s=>s.id));return this._cancellingTasks.forEach(s=>{e.has(s)||this._cancellingTasks.delete(s)}),t.length?t.map(s=>{const a=this._cancellingTasks.has(s.id),r=this._deviceName(s.entry_id),i=s.label_key?this._tText(s.label_key,s.label_params||{},s.label||this._taskActionLabel(s.kind)):this._taskActionLabel(s.kind),o=(r?r+" \xB7 ":"")+i,n=s.progress!=null?Math.round(s.progress*100)+"%":"",l=s.eta_s!=null&&s.eta_s>0?this._fmtEta(s.eta_s):"",c=this._t("task.cancelling",{},"Cancelling\u2026"),p="wd-task-pill"+(a?" wd-task-pill--cancelling":""),_=a?" \xB7 "+c:n?" "+n:"";return`${d(o)}`+(a?`${d(c)}`:(n?`${n}`:"")+(l?`${d(l)}`:""))+``}).join(""):""}_updateTaskPills(){const t=this.shadowRoot;if(!t)return;const e=t.getElementById("wd-task-pills");e&&(e.innerHTML=this._htmlTaskPills())}_addProvisionalTask(t,e,s,a){!t||this._tasks[t]||(this._tasks[t]={id:t,entry_id:s,kind:e,label:this._taskActionLabel(e),state:"running",done:0,total:a||0,progress:a?0:null,eta_s:null,updated_at:0,has_result:!1},this._updateTaskPills())}_onTrackedTaskProgress(t){if(!(t.id!==this._pgHistoryTaskId&&t.id!==this._pgSweepTaskId)){if(t.state==="running"){this._pgBatchProgress={done:t.done||0,total:t.total||0},this._pgUpdateBatchBar(t.done||0,t.total||0);return}this._pgFinishTask(t,t.id===this._pgHistoryTaskId)}}async _pgFinishTask(t,e){let s=null;try{if(t.state==="done"||t.state==="cancelled"){const a=await this._ws({type:`${w}/get_task_result`,task_id:t.id});s=a&&a.result}}catch{}this._isActiveEntry(t.entry_id)&&(t.state==="error"?this._showToast(this._tText("msg.toast_error",{error:t.error||""},"Error: "+(t.error||"")),"error"):s&&(e?this._pgHistory=s:this._pgSweepNew=s&&!s.error?s:null),e?(this._busy.delete("pg-history"),this._pgHistoryTaskId=null):(this._busy.delete("pg-sweep"),this._pgSweepTaskId=null),this._pgBatchProgress=null,this._render())}async _awaitTask(t){for(let e=0;e<7200&&t;e++){const s=(this._tasks||{})[t];let a=s&&s.state!=="running"?s:null;if(a||(a=await this._ws({type:`${w}/get_task_result`,task_id:t})),a&&a.state&&a.state!=="running"){if(a.state==="error")throw new Error(a.error||"error");return a}await new Promise(r=>setTimeout(r,1e3))}return null}async _pgPollTask(t){for(let e=0;e<3600&&(t===this._pgHistoryTaskId||t===this._pgSweepTaskId);e++){let s;try{s=await this._ws({type:`${w}/get_task_result`,task_id:t})}catch{break}if(!s)break;if(this._tasks[t]=s,this._updateTaskPills(),s.state!=="running"){this._onTrackedTaskProgress(s);return}this._pgBatchProgress={done:s.done||0,total:s.total||0},this._pgUpdateBatchBar(s.done||0,s.total||0),await new Promise(a=>setTimeout(a,1200))}}_panelTransUrl(t){const e=`/ha_washdata/panel-translations/${encodeURIComponent(t)}.json`;return Zt?`${e}?v=${encodeURIComponent(Zt)}`:e}async _fetchPanelLang(t){if(!t)return null;const e=[t],s=t.indexOf("-");s>0&&e.push(t.slice(0,s));for(const a of e)try{const r=await fetch(this._panelTransUrl(a));if(r.ok){const i=await r.json();if(i&&typeof i=="object")return i}}catch{}return null}async _loadPanelLang(t){if(!t||this._panelTrans&&this._panelTrans[t])return;const e=await this._fetchPanelLang(t);e&&(this._panelTrans||(this._panelTrans={}),this._panelTrans[t]=e)}async _loadPanelTranslations(){const t=this._hass&&this._hass.locale&&this._hass.locale.language;await Promise.all([this._loadPanelLang("en"),t&&t!=="en"?this._loadPanelLang(t):Promise.resolve()])}_startPoll(){this._stopPoll(),this._pollTimer=setInterval(()=>this._fetchAll(),this._pollMs)}_stopPoll(){this._pollTimer&&(clearInterval(this._pollTimer),this._pollTimer=null)}async _ws(t){try{return await this._hass.connection.sendMessagePromise(t)}catch(e){if(e&&e.code==="invalid_threshold_pair"){const s=/\(([-\d.]+) W\)[^(]*\(([-\d.]+) W\)/.exec(String(e.message||""));e.message=this._tText("msg.invalid_threshold_pair",{stop:s?s[1]:"?",start:s?s[2]:"?"},e.message)}throw e}}async _fetchAll(){if(!this._hass)return;const t=this._loading;let e=!1;try{if(!this._constantsLoaded){const[l,c]=await Promise.all([this._ws({type:`${w}/get_constants`}).catch(()=>null),this._ws({type:`${w}/get_panel_config`}).catch(()=>null)]);if(l&&(this._constants={stateColors:l.state_colors||{},deviceTypes:l.device_types||[],mlLabEnabled:!!l.ml_lab_enabled,mlTrainingAvailable:!!l.ml_training_available,storeOnlineAvailable:!!l.store_online_available,storeOnlineEnabled:!!l.store_online_enabled,storeWebOrigin:l.store_web_origin||"",storePrefs:l.store_prefs||{},pgMatchDefaults:l.pg_match_defaults||{},PROFILE_MIN_WARMUP_CYCLES:l.PROFILE_MIN_WARMUP_CYCLES,version:l.version||"",iconUrl:l.icon_url||""}),c)try{this._panelCfg=c,this._applyPanelConfig()}catch{}this._constantsLoaded=!0}const s=await this._ws({type:`${w}/get_devices`});this._lastFetchErr=null,this._fetchFailed=!1,this._devices=s.devices||[],this._lastRefresh=new Date;let a=!1;const r=this._deepLinkToken();if(r&&r!==this._deepLinkApplied&&this._devices.length){this._deepLinkApplied=r;const l=this._deepLinkIdx(r);l>=0?(this._selIdx=l,a=!0,this._rememberDevice(l)):console.warn(`[WashData panel] ?device=${r} matches no WashData device`)}if(!a&&this._selIdx===0&&this._devices.length>1){const l=localStorage.getItem("wd-last-device");if(l){const c=this._devices.findIndex(p=>p.entry_id===l);c>0&&(this._selIdx=c)}}const i=this._devices[this._selIdx],o=async()=>{if(i&&this._tab==="status"){try{this._powerData=await this._ws({type:`${w}/get_power_history`,entry_id:i.entry_id,with_raw:this._pref("show_raw_active",!1)})}catch{}if(this._pref("show_debug",!1))try{this._matchDebug=await this._ws({type:`${w}/get_match_debug`,entry_id:i.entry_id})}catch{}if(this._canEdit()&&i.recording)try{this._recState=await this._ws({type:`${w}/get_recording_state`,entry_id:i.entry_id})}catch{}}},n=async()=>{if(i&&i.current_program){if(this._statusEnvName!==i.current_program){this._statusEnvName=i.current_program;try{const l=await this._ws({type:`${w}/get_profile_envelope`,entry_id:i.entry_id,profile_name:i.current_program});this._statusEnv=l.envelope||null}catch{this._statusEnv=null}}await this._ensureStatusPhases(i.entry_id,i.current_program)}else this._statusEnv=null,this._statusEnvName=null,this._statusPhases=[],this._statusPhasesName=null};if(await Promise.all([o(),n()]),t&&i){const l=i.entry_id,c=Promise.all([this._fetchCycles(l),this._fetchSuggestions(l),this._fetchProfiles(l),this._tab==="status"?this._ws({type:`${w}/get_setup_status`,entry_id:l}).then(p=>{this._isActiveEntry(l)&&(this._setupStatus=p)},()=>{this._isActiveEntry(l)&&(this._setupStatus=null)}):null,this._constants.storeOnlineAvailable?(async()=>{try{const p=await this._ws({type:`${w}/get_options`,entry_id:l});this._isActiveEntry(l)&&(this._opts=p.options||{})}catch{}this._onlineEnabled()&&await this._loadStoreStatus(l)})():null]);this._tab==="status"&&(this._loading=!1,e=!0),this._render(),await c}this._logOpen&&this._isAdmin()&&this._fetchLogs().then(()=>this._refreshLogDrawer()).catch(()=>{})}catch(s){const a=Ct(s);a!==this._lastFetchErr&&(this._lastFetchErr=a,console.warn("[WashData panel] fetch error -",a,s)),this._fetchFailed=!0}finally{this._loading=!1,this._refreshStaleChip();const s=this.shadowRoot,a=s&&s.activeElement,r=!!(a&&["SELECT","INPUT","TEXTAREA","OPTION"].includes(a.tagName));t&&!e?this._render():t&&!this._modal&&!r?this._render():this._tab==="status"&&!this._modal&&!r?this._render():this._tab==="status"&&!this._modal&&r?(this._drawStatusCurve(),this._refreshDeviceBar()):this._refreshDeviceBar()}}async _fetchCycles(t){this._cyclesError=!1;try{const e=await this._ws({type:`${w}/get_device_cycles`,entry_id:t,limit:zt,offset:0,imported_offset:0});if(!this._isActiveEntry(t))return;this._cycles=e.cycles||[],this._refCycles=[...e.reference_cycles||[],...e.backfill_cycles||[]],this._cycleOffset=this._cycles.length,this._cyclesTotal=e.total!=null?e.total:this._cycles.length,this._cyclesHasMore=e.has_more!=null?!!e.has_more:!1,this._importedOffset=this._refCycles.length,this._importedHasMore=!!e.imported_has_more}catch{if(!this._isActiveEntry(t))return;this._cyclesError=!0,this._cycles=[],this._refCycles=[],this._cycleOffset=0,this._cyclesTotal=0,this._cyclesHasMore=!1,this._importedOffset=0,this._importedHasMore=!1}}async _loadMoreCycles(t){const e=await this._ws({type:`${w}/get_device_cycles`,entry_id:t,limit:zt,offset:this._cycleOffset,imported_offset:this._importedOffset});if(!this._isActiveEntry(t))return;const s=e.cycles||[],a=new Set(this._cycles.map(o=>o.id));for(const o of s)a.has(o.id)||this._cycles.push(o);this._cycleOffset+=s.length,this._cyclesTotal=e.total!=null?e.total:this._cyclesTotal,this._cyclesHasMore=e.has_more!=null?!!e.has_more:s.length>=zt;const r=[...e.reference_cycles||[],...e.backfill_cycles||[]],i=new Set((this._refCycles||[]).map(o=>o.id));this._refCycles=(this._refCycles||[]).concat(r.filter(o=>!i.has(o.id))),this._importedOffset+=r.length,this._importedHasMore=!!e.imported_has_more}async _ensureStatusPhases(t,e){if(!e){this._statusPhases=[],this._statusPhasesName=null;return}if(this._statusPhasesName!==e){this._statusPhasesName=e;try{const s=await this._ws({type:`${w}/get_profile_phases`,entry_id:t,profile_name:e});this._statusPhases=(s.phases||[]).map(a=>({name:a.name,start:a.start,end:a.end}))}catch{this._statusPhases=[]}}}async _fetchSettingsChangelog(t){try{const s=await this._ws({type:`${w}/get_settings_changelog`,entry_id:t});this._settingsChangelog=s.changelog||[]}catch{this._settingsChangelog=this._settingsChangelog||[]}const e={};for(const s of this._settingsChangelog||[])s&&s.key!=null&&!(s.key in e)&&(e[s.key]=s);this._settingsChangeByKey=e}_registerUndo(t){const e="u"+ ++this._undoSeq;return t.timer=setTimeout(()=>this._commitDelete(e),1e4),this._undoBuffer.set(e,t),e}_undoDelete(t){const e=this._undoBuffer.get(t);if(e){this._undoBuffer.delete(t),e.timer&&clearTimeout(e.timer);try{e.restore()}catch{}this._toastTimer&&clearTimeout(this._toastTimer),this._toast=null,this._render()}}async _commitDelete(t){const e=this._undoBuffer.get(t);if(!e)return;this._undoBuffer.delete(t),e.timer&&clearTimeout(e.timer);const s=(a,r)=>{const i=this._devices[this._selIdx],o=i&&i.entry_id;if(e.eid===o){try{e.restore(a)}catch{}this._showToast(r,"error"),this._render()}};try{const a=await e.commit();a&&a.length&&s(a,this._t("toast.delete_partial_failed",{},"Some items could not be deleted and were restored"))}catch(a){s(null,this._tText("toast.delete_failed",{error:a&&a.message||a},"Delete failed: "+(a&&a.message||a)))}}_isActiveEntry(t){const e=this._devices[this._selIdx];return!!e&&e.entry_id===t}_flushPendingDeletes(){return!this._undoBuffer||!this._undoBuffer.size?Promise.resolve():Promise.all(Array.from(this._undoBuffer.keys()).map(t=>this._commitDelete(t)))}_deleteCyclesWithUndo(t,e){const s=new Set(e),a=[];if(this._cycles=(this._cycles||[]).filter((n,l)=>s.has(n.id)?(a.push({idx:l,rec:n,ref:!1}),!1):!0),this._refCycles=(this._refCycles||[]).filter((n,l)=>s.has(n.id)?(a.push({idx:l,rec:n,ref:!0}),!1):!0),!a.length)return;this._cycleSel.clear(),this._selectMode=!1,this._render();const r=n=>{const l=n&&n.length?n:a,c=this._cycles.slice(),p=this._refCycles.slice();l.slice().sort((_,f)=>_.idx-f.idx).forEach(({idx:_,rec:f,ref:u})=>{const b=u?p:c;b.splice(Math.min(_,b.length),0,f)}),this._cycles=c,this._refCycles=p},i=async()=>{const n=[];for(const l of a)try{await this._ws({type:`${w}/delete_cycle`,entry_id:t,cycle_id:l.rec.id}),this._isActiveEntry(t)&&(l.ref?this._importedOffset=Math.max(0,this._importedOffset-1):this._cycleOffset=Math.max(0,this._cycleOffset-1))}catch{n.push(l)}if(n.lengthn.name===e),a=s>=0?this._profiles[s]:{name:e};s>=0&&(this._profiles=this._profiles.filter(n=>n.name!==e)),this._modal=null,this._render();const r=n=>{const l=this._profiles.slice();l.splice(Math.min(s<0?l.length:s,l.length),0,a),this._profiles=l},i=async()=>{try{await this._ws({type:`${w}/delete_profile`,entry_id:t,profile_name:e,unlabel_cycles:!0})}catch{return[{idx:s,rec:a}]}try{await this._fetchProfiles(t),this._tab==="profiles"&&this._render()}catch{}return[]},o=this._registerUndo({eid:t,restore:r,commit:i});this._showToast(this._tText("msg.profile_deleted",{name:e},"Profile deleted"),"success",{actionLabel:this._t("btn.undo",{},"Undo"),actionToken:o,duration:1e4})}_onKeydown(t){if(!t.defaultPrevented){if(t.key==="Escape"){this._modal&&(t.preventDefault(),this._onModalAction("cancel",null));return}if(this._modal&&t.key==="Tab"){const e=this.shadowRoot,s=e&&e.querySelector('.wd-modal[role="dialog"]');if(s){const a=Vt(s);if(a.length){const r=e.activeElement,i=a.indexOf(r);t.shiftKey?i<=0&&(t.preventDefault(),a[a.length-1].focus()):(i===-1||i===a.length-1)&&(t.preventDefault(),a[0].focus())}}return}if(["ArrowLeft","ArrowRight","Home","End"].includes(t.key)){const e=this.shadowRoot,s=e&&e.activeElement;if(s&&s.classList.contains("wd-tab")){const a=[...e.querySelectorAll("button.wd-tab")],r=a.indexOf(s);if(r===-1)return;let i=r;t.key==="ArrowRight"?i=(r+1)%a.length:t.key==="ArrowLeft"?i=(r-1+a.length)%a.length:t.key==="Home"?i=0:t.key==="End"&&(i=a.length-1),t.preventDefault(),a[i].click(),a[i].focus()}}}}async _loadMlIndex(t){if(this._mlById=this._mlById||{},!!this._constants.mlLabEnabled)try{const e=await this._ws({type:`${w}/get_ml_comparison`,entry_id:t});if(!this._isActiveEntry(t))return;this._mlComparison=e;const s={};for(const a of e&&e.cycles||[])s[a.id]=a;this._mlById=s}catch{}}async _loadMlTrainingStatus(t){if(this._constants.mlTrainingAvailable)try{const e=await this._ws({type:`${w}/get_ml_training_status`,entry_id:t});if(!this._isActiveEntry(t))return;this._mlTrainingStatus=e}catch{}}_cycleOverlayPlan(t,e){const s=e&&e.profile_name;if(s)return{name:s,suspected:!1,runnerUp:null};const a=(this._feedbacks||[]).find(o=>o.cycle_id===t),r=a&&typeof a.detected_profile=="string"?a.detected_profile:"";if(!r)return null;const i=(Array.isArray(a.ranking)?a.ranking:[]).find(o=>o&&typeof o.name=="string"&&o.name&&o.name!==r);return{name:r,suspected:!0,runnerUp:i?i.name:null}}_loadCycleOverlays(t,e,s){const a=this._modal;!a||a.type!=="cycle-detail"||a.cycleId!==e||(a.overlayPlan=this._cycleOverlayPlan(e,s),a.overlayPlan&&(this._fetchCycleProfileEnv(t,a.overlayPlan.name),a.overlayPlan.runnerUp&&this._fetchCycleProfileEnv(t,a.overlayPlan.runnerUp,"runnerUpEnv")))}async _fetchCycleProfileEnv(t,e,s="profileEnv"){if(!e)return;const a=this._modal;a&&a.type==="cycle-detail"&&((a.envFor=a.envFor||{})[s]=e);try{const r=await this._ws({type:`${w}/get_profile_envelope`,entry_id:t,profile_name:e}),i=this._modal;i&&i.type==="cycle-detail"&&i.entryId===t&&i.envFor&&i.envFor[s]===e&&(i[s]=r.envelope||null,this._render())}catch{}}async _fetchSuggestions(t){this._suggestionsError=!1;try{const e=await this._ws({type:`${w}/get_suggestions`,entry_id:t});if(!this._isActiveEntry(t))return;this._suggestions=e.suggestions||[],this._lockedSuggestions=e.locked_suggestions||[]}catch{this._isActiveEntry(t)&&(this._suggestionsError=!0,this._suggestions=[])}}async _fetchProfiles(t){this._profilesError=!1;try{const e=await this._ws({type:`${w}/get_profiles`,entry_id:t});if(!this._isActiveEntry(t))return this._profiles;this._profiles=e.profiles||[],this._profileHealth=e.profile_health||{},this._profileTrends=e.profile_trends||{},this._coverageGaps=e.coverage_gaps||{},this._profileAdvisories=e.profile_advisories||[],this._profileTerminal=e.profile_terminal||{},this._profileMatcherCounts=e.profile_matcher_counts||{}}catch{this._profilesError=!0}return this._profiles}async _ensureProfileEnvs(t,e){this._profileEnvCache=this._profileEnvCache||{};const s=[...new Set(e)].filter(a=>a&&!(a in this._profileEnvCache));return s.length?(await Promise.all(s.map(async a=>{try{const r=await this._ws({type:`${w}/get_profile_envelope`,entry_id:t,profile_name:a});this._profileEnvCache[a]=r&&r.envelope||null}catch{this._profileEnvCache[a]=null}})),this._profileEnvCache):this._profileEnvCache}async _fetchProfileGroups(t){this._profileGroupsError=!1;try{const e=await this._ws({type:`${w}/get_profile_groups`,entry_id:t});this._profileGroups={groups:e.groups||[],min_cohesion:e.min_cohesion||.85}}catch{this._profileGroupsError=!0,this._profileGroups={groups:[],min_cohesion:.85}}return this._profileGroups}_deepLinkToken(){try{const t=new URLSearchParams(window.location.search||"").get("device");return t?t.trim():""}catch{return""}}_deepLinkIdx(t){const e=this._devices||[],s=String(t||"").trim();if(!s||!e.length)return-1;const a=s.toLowerCase();let r=e.findIndex(i=>String(i.entry_id||"").toLowerCase()===a);if(r<0&&(r=e.findIndex(i=>String(i.title||"").trim().toLowerCase()===a)),r<0){const i=fe(s);i&&(r=e.findIndex(o=>fe(o.title)===i))}return r}_rememberDevice(t){const e=(this._devices||[])[t];if(e)try{localStorage.setItem("wd-last-device",e.entry_id)}catch{}}_unsavedSettingsCount(){return this._tab!=="settings"||!this.shadowRoot?0:(this._snapshotFormToPending(this.shadowRoot),Object.keys(this._changedOptions(this._pendingSettings||{})).length)}_guardUnsaved(t){const e=this._unsavedSettingsCount();if(!e){t();return}this._modal={type:"confirm",title:this._t("modal.discard_settings_title",{},"Discard unsaved changes?"),message:this._tText("modal.discard_settings_msg",{n:e},`${e} setting change(s) are not saved yet. Leave Settings and discard them?`),okLabel:this._t("btn.discard",{},"Discard"),onOk:()=>{this._pendingSettings={},this._dirtyOptKeys=new Set,t()}},this._render()}_maybeGuardOutboundClick(t){if(t.defaultPrevented||t.button!==0||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)return;const s=(typeof t.composedPath=="function"?t.composedPath():[]).find(i=>i&&i.tagName==="A"&&i.getAttribute&&i.getAttribute("href"));if(!s||s.target&&s.target!=="_self"||s.hasAttribute("download"))return;let a;try{a=new URL(s.getAttribute("href"),window.location.href)}catch{return}if(a.origin!==window.location.origin)return;const r=this._panel&&this._panel.url_path;r&&a.pathname.split("/").filter(Boolean)[0]===r||this._unsavedSettingsCount()&&(t.preventDefault(),t.stopImmediatePropagation(),this._guardUnsaved(()=>this._navigate(a.pathname+a.search+a.hash)))}_onOwnPath(){const t=this._panel&&this._panel.url_path;return t?(window.location.pathname||"").split("/").filter(Boolean)[0]===t:!0}_onLocationChanged(){if(!this._initialized||!this._devices.length||!this._onOwnPath())return;const t=this._deepLinkToken();if(!t)return;this._deepLinkApplied=t;const e=this._deepLinkIdx(t);if(e<0){console.warn(`[WashData panel] ?device=${t} matches no WashData device`);return}e!==this._selIdx&&this._guardUnsaved(()=>this._selectDevice(e))}async _selectDevice(t){if(t===this._selIdx)return;await this._flushPendingDeletes(),this._selIdx=t,this._rememberDevice(t),this._pendingSettings={},this._dirtyOptKeys=new Set,this._prevOpts=null,this._cascadePending={},this._preCascadeOpts=null,this._stagedSuggestions=!1,this._mlComparison=null,this._mlById={},this._profileEnvCache={},this._powerHistory=[],this._powerT0=null,this._statusEnv=null,this._statusEnvName=null,this._statusPhases=[],this._statusPhasesName=null,this._cycleOffset=0,this._cyclesTotal=0,this._cyclesHasMore=!1,this._importedOffset=0,this._importedHasMore=!1,this._settingsChangelog=null,this._settingsChangeByKey={},this._powerData={live:[],raw:[],cycle_active:!1,cycle_elapsed_s:0},this._matchDebug=null,this._profiles=[],this._profileHealth={},this._profileTrends={},this._coverageGaps={},this._profileAdvisories=[],this._profileTerminal={},this._profileMatcherCounts={},this._opts={},this._optDefaults={},this._suggestions=[],this._lockedSuggestions=[],this._cycles=[],this._refCycles=[],this._recState=null,this._diag=null,this._importUndo=null,this._maintenance=null,this._phases=[],this._mlTrainingStatus=null,this._setupStatus=null,this._deviceAutomations=[],this._selectMode=!1,this._cycleSel=new Set,this._cycleFilter={text:"",status:""},this._profSubtab="profiles",this._pgCycleId="",this._pgProfileName="",this._pgPowerPts=null,this._pgEnvData=null,this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},this._pgEffective=null,this._pgPublishable=null,this._pgPresets=[],this._pgPresetSel="",this._pgPresetName="",this._pgPresetLimit=0,this._pgSuggClassic={},this._pgView=null,this._pgHoverT=null,this._pgLoadSeq++,this._pgNeedsRestart=!1,this._pgLoading=!1,this._pgDetail=null,this._pgHistory=null,this._pgSweepNew=null,this._pgHistoryTaskId=null,this._pgSweepTaskId=null,this._busy.delete("pg-history"),this._busy.delete("pg-sweep"),this._pgBatchProgress=null,this._pgDetailDebounceTimer&&(clearTimeout(this._pgDetailDebounceTimer),this._pgDetailDebounceTimer=null),this._storeView="brands",this._storeDevice=null,this._storeProfile=null,this._storeQuery="",this._storeDevices=[],this._storeProfiles=[],this._storeCycles=[],this._storeStatus=null,this._storeConnected=!1,this._storeLoading=!1,clearTimeout(this._brandSearchTimer),this._brandSearchTimer=null,this._catalog={brands:this._catalog.brands,devices:void 0,forBrand:null,approvedOnly:this._catalog.approvedOnly,brandsFull:this._catalog.brandsFull,brandPrefixes:this._catalog.brandPrefixes},this._entityListCache&&delete this._entityListCache.store_model;const e=this._devices[this._selIdx];e&&await this._fetchSuggestions(e.entry_id),this._fetchTabData()}_htmlStaleChip(){const t=this._lastRefresh,e=t&&Date.now()-t.getTime()>2.5*this._pollMs;if(!this._devices.length||!(this._fetchFailed||e))return'';const s=t?re(t):"";return`
${d(this._tText("msg.connection_lost",{time:s},`Connection lost - showing data from ${s}.`))}
`}_refreshStaleChip(){const t=this.shadowRoot&&this.shadowRoot.getElementById("wd-stale");t&&(t.outerHTML=this._htmlStaleChip())}_refreshDeviceBar(){const t=this.shadowRoot;if(!t)return;const e=t.querySelector(".wd-devbar"),s=this._htmlDeviceBar();if(e&&s){const a=document.createElement("div");a.innerHTML=s;const r=a.firstElementChild;r&&(e.replaceWith(r),r.querySelectorAll(".wd-devcard[data-idx]").forEach(i=>i.addEventListener("click",()=>this._guardUnsaved(()=>this._selectDevice(parseInt(i.dataset.idx,10))))))}}_refreshLogDrawer(){this._logOpen&&(this._refreshLogViews(),this._refreshLogFilterOptions())}_refreshLogFilterOptions(){const t=this.shadowRoot;if(!t)return;const e=(r,i,o)=>``+i.map(n=>``).join(""),s=e(this._t("log.all_devices",{},"All devices"),this._logDevices(),this._logDevice),a=e(this._t("log.all_components",{},"All components"),this._logComponents(),this._logComponent);t.querySelectorAll('.wd-log-filter[data-logfilter="device"]').forEach(r=>{r.innerHTML=s,r.value=this._logDevice||""}),t.querySelectorAll('.wd-log-filter[data-logfilter="component"]').forEach(r=>{r.innerHTML=a,r.value=this._logComponent||""})}async _fetchTabData(){const t=this._devices[this._selIdx];if(!t)return;const e=t.entry_id;this._tabLoading=!0,this._render();try{if(this._tab==="status"){if(this._powerData=await this._ws({type:`${w}/get_power_history`,entry_id:e,with_raw:this._pref("show_raw_active",!1)}),this._profiles.length||await this._fetchProfiles(e),!this._profiles.length&&!this._cycles.length&&await this._fetchCycles(e),await this._ensureStatusPhases(e,t.current_program),this._pref("show_debug",!1))try{this._matchDebug=await this._ws({type:`${w}/get_match_debug`,entry_id:e})}catch{}if(this._canEdit())try{this._recState=await this._ws({type:`${w}/get_recording_state`,entry_id:e})}catch{}try{this._setupStatus=await this._ws({type:`${w}/get_setup_status`,entry_id:e})}catch{this._setupStatus=null}}else if(this._tab==="history"&&this._historySub==="maintenance")this._maintenance||await this._fetchMaintenance(e);else if(this._tab==="history"){await this._fetchCycles(e),this._profiles.length||await this._fetchProfiles(e);try{const s=await this._ws({type:`${w}/get_feedbacks`,entry_id:e});this._feedbacks=s.feedbacks||[]}catch{}this._constants.storeOnlineAvailable&&this._onlineEnabled()&&this._loadStoreStatus(e).then(()=>{this._tab==="history"&&this._render()}),this._constants.mlLabEnabled&&(this._mlLoading=!0,this._loadMlIndex(e).finally(()=>{this._mlLoading=!1;const s=this._modal;s&&s.type==="cycle-detail"&&!s.ml&&this._mlById[s.cycleId]&&(s.ml=this._mlById[s.cycleId]),(this._tab==="history"||s&&s.type==="cycle-detail")&&this._render()}))}else if(this._tab==="profiles"){if(await this._fetchProfiles(e),this._fetchProfileGroups(e).then(()=>{this._tab==="profiles"&&this._render()}),this._cycles.length||this._fetchCycles(e).then(()=>{this._tab==="profiles"&&this._render()}),this._profSubtab==="phase-catalog")try{const s=await this._ws({type:`${w}/get_phase_catalog`,entry_id:e});this._phases=s.phases||[]}catch{}}else if(this._tab==="settings"){const s=await this._ws({type:`${w}/get_options`,entry_id:e});if(!this._isActiveEntry(e))return;this._opts=s.options||{},this._optDefaults=s.defaults||{},await this._fetchSuggestions(e),await this._fetchSettingsChangelog(e),this._constants.mlTrainingAvailable&&this._loadMlTrainingStatus(e).finally(()=>{this._tab==="settings"&&this._renderPreservingFormEdits()}),this._autoLoading=!0,this._loadDeviceAutomations(e).finally(()=>{this._autoLoading=!1,this._tab==="settings"&&this._renderPreservingFormEdits()}),this._constants.storeOnlineAvailable&&this._loadStoreStatus(e).finally(()=>{this._tab==="settings"&&this._renderPreservingFormEdits()})}else if(this._tab==="store"){try{const s=await this._ws({type:`${w}/get_options`,entry_id:e});if(!this._isActiveEntry(e))return;this._opts=s.options||{}}catch{}if(this._profiles.length||this._fetchProfiles(e),await this._loadStoreStatus(e),!this._isActiveEntry(e))return;this._ensureStoreConnectListener(),this._onlineEnabled()&&this._storeSearch(this._storeBrandScope())}else if(this._tab==="advanced"&&this._panelSubtab==="ml"){const s=await this._ws({type:`${w}/get_options`,entry_id:e});if(!this._isActiveEntry(e))return;this._opts=s.options||{},this._loadMlTrainingStatus(e).finally(()=>{this._tab==="advanced"&&this._panelSubtab==="ml"&&this._renderPreservingFormEdits()})}else if(this._tab==="playground"){if(await Promise.all([(async()=>{try{const s=await this._ws({type:`${w}/get_options`,entry_id:e});this._isActiveEntry(e)&&(this._opts=s.options||{})}catch{}})(),this._pgFetchSettings(e,!1),this._fetchCycles(e),this._profiles.length?Promise.resolve():this._fetchProfiles(e)]),!this._isActiveEntry(e))return;this._pgFetchSuggestions(e),!this._pgCycleId&&this._cycles?.length&&(this._pgCycleId=this._cycles[0].id,this._pgProfileName=""),this._pgNeedsRestart=!1,this._pgRestartRetries=0,this._pgAdoptExisting()}else this._tab==="advanced"&&this._panelSubtab==="diagnostics"&&!this._diag&&this._fetchToolsData(e).then(()=>{this._tab==="advanced"&&this._render()});this._tabError=null}catch(s){console.warn("[WashData panel] tab data fetch error -",Ct(s),s),this._tabError=this._tab}finally{this._tabLoading=!1,this._render()}}async _fetchToolsData(t){try{const e=await this._ws({type:`${w}/get_diagnostics`,entry_id:t});if(!this._isActiveEntry(t))return;this._diag=e.stats||{},this._importUndo=e.import_undo||null}catch(e){console.warn("[WashData panel] tools fetch error -",Ct(e),e),this._diag={_error:String(e&&e.message||e)}}}async _fetchMaintenance(t){try{const e=await this._ws({type:`${w}/get_maintenance_log`,entry_id:t});this._maintenance=e||{}}catch(e){console.warn("[WashData panel] maintenance fetch error -",Ct(e),e),this._maintenance={_error:String(e&&e.message||e)}}}async _fetchLogs(){try{const t=await this._ws({type:`${w}/get_logs`,level:null,limit:500});this._logs=t.logs||[]}catch(t){console.warn("[WashData panel] logs fetch error -",Ct(t),t)}}_logComponents(){return[...new Set((this._logs||[]).map(t=>t.logger).filter(Boolean))].sort()}_logDevices(){return[...new Set((this._logs||[]).map(t=>t.device).filter(Boolean))].sort()}_filteredLogRecords(){const t={DEBUG:10,INFO:20,WARNING:30,ERROR:40,CRITICAL:50},e=this._logLevel&&t[this._logLevel]||0,s=this._logDevice,a=this._logComponent,r=(this._logSearch||"").trim().toLowerCase();return(this._logs||[]).filter(i=>!(e&&(t[i.level]||0){const s=re(new Date(e.ts*1e3)),a=e.device?`${d(e.device)}`:"";return`
${s}${d(e.level)}${d(e.logger||"")}${a}${d(e.msg)}
`}).join(""):(this._logs||[]).length>0?`

${this._t("msg.no_logs_match",{},"No log records match these filters.")}

`:`

${this._t("msg.no_logs",{},"No log records buffered yet.")}

`}_htmlLogFilters(t){const s=["","DEBUG","INFO","WARNING","ERROR"].map(i=>``).join(""),a=``+this._logDevices().map(i=>``).join(""),r=``+this._logComponents().map(i=>``).join("");return` `}_refreshLogViews(){const t=this.shadowRoot;if(!t)return;const e=this._logLinesHtml();["wd-log-lines-drawer","wd-log-lines-page"].forEach(s=>{const a=t.getElementById(s);a&&(a.innerHTML=e)})}_syncLogFilters(t){const e=this.shadowRoot;e&&e.querySelectorAll(`.wd-log-filter[data-logfilter="${t.dataset.logfilter}"]`).forEach(s=>{s!==t&&s.value!==t.value&&(s.value=t.value)})}async _fetchRecState(t){try{this._recState=await this._ws({type:`${w}/get_recording_state`,entry_id:t})}catch{}}async _fetchFeedbacks(t){try{const e=await this._ws({type:`${w}/get_feedbacks`,entry_id:t});this._feedbacks=e.feedbacks||[]}catch{}}async _fetchPhases(t){try{const e=await this._ws({type:`${w}/get_phase_catalog`,entry_id:t});this._phases=e.phases||[]}catch{}}_localize(t,e){try{const s=this._hass&&this._hass.localize?this._hass.localize(t):"";return s&&s!==t?s:e}catch{return e}}_tLookup(t,e){const s=this._panelTrans&&(this._panelTrans[e]||this._panelTrans.en);if(!s)return null;const a=t.split(".").reduce((r,i)=>r&&r[i]!==void 0?r[i]:null,s);return a&&typeof a=="string"?a:null}_uiLang(){return this._panelCfg&&this._panelCfg.prefs&&this._panelCfg.prefs.lang_override||this._hass&&this._hass.locale&&this._hass.locale.language||""}_tResolve(t,e,s){if(s!=null){const a=He(e,s),r=this._tLookup(`${t}_${a}`,e)||(a!=="other"?this._tLookup(`${t}_other`,e):null);if(r)return r}return this._tLookup(t,e)}_t(t,e={},s=""){return this._tr(t,e,s,!0)}_tText(t,e={},s=""){return this._tr(t,e,s,!1)}_tr(t,e,s,a){let r;e=e||{};const i=this._uiLang(),o=typeof e.count=="number"?e.count:e.n,n=typeof o=="number"&&isFinite(o)?o:null;if(this._panelTrans?r=i&&this._tResolve(t,i,n)||this._tResolve(t,"en",n)||s:r=this._localize(`component.${w}.panel.${t}`,s),typeof r!="string")return r;for(const[l,c]of Object.entries(e)){const p=`{${l}}`;if(!r.includes(p))continue;const _=c instanceof ce?c.html:a?d(String(c)):String(c);r=r.split(p).join(_)}return r}_stateColor(t){const e=this._constants.stateColors||{};return e[t]||e.unknown||"var(--disabled-color, #bdbdbd)"}_cycleStatusLabel(t){return{completed:this._tText("status.completed",{},"Completed"),interrupted:this._tText("status.interrupted",{},"Interrupted"),force_stopped:this._tText("status.force_stopped",{},"Force stopped"),active:this._tText("status.active",{},"Active")}[t]||t}_stateLabel(t){const e=(t||"unknown").replace(/_/g," ").replace(/\b\w/g,s=>s.toUpperCase());return this._localize(`component.${w}.entity.sensor.washer_state.state.${t}`,e)}_deviceTypeLabel(t){const e=(this._constants.deviceTypes||[]).find(a=>a.id===t),s=e?e.label:(t||"").replace(/_/g," ");return this._localize(`component.${w}.selector.device_type.options.${t}`,s)}_deviceTypeOpts(t){const e=(this._constants.deviceTypes||[]).map(s=>[s.id,this._deviceTypeLabel(s.id)]);return t&&!e.some(([s])=>String(s)===String(t))&&e.push([t,this._deviceTypeLabel(t)]),e}_deviceOpts(t){const e=[["","- None -"]],s=this._hass&&this._hass.devices?this._hass.devices:{},a=this._ownDeviceId();return Object.values(s).forEach(r=>{if(a&&String(r.id)===a)return;const i=r.name_by_user||r.name||r.id;e.push([r.id,i])}),t&&String(t)!==a&&!e.some(([r])=>String(r)===String(t))&&e.push([t,this._tText("lbl.device_unresolved",{id:t},`Unavailable device (${t})`)]),e}_ownDeviceId(){const t=this._devices&&this._devices[this._selIdx],e=t&&t.entry_id;if(!e)return"";const s=this._hass&&this._hass.devices?this._hass.devices:{};for(const a of Object.values(s)){if(!a||!a.id)continue;if((Array.isArray(a.identifiers)?a.identifiers:[]).some(i=>Array.isArray(i)&&i[0]===w&&String(i[1])===String(e))||Array.isArray(a.config_entries)&&a.config_entries.map(String).includes(String(e)))return String(a.id)}return""}_applyFontScale(t){let e=parseFloat(t);isFinite(e)||(e=1),e=Math.max(.7,Math.min(2,e)),this.style.fontSize=e===1?"":Math.round(e*1e3)/10+"%"}_applyPanelConfig(){const t=this._panelCfg;if(!t)return;this._applyFontScale(t.prefs&&t.prefs.font_scale||1);const e=t.prefs&&t.prefs.lang_override;e&&!(this._panelTrans&&this._panelTrans[e])&&this._loadPanelLang(e).then(()=>this._render()).catch(()=>{});const s=t.panel||{};if(!this._tabInitialized){const a=t.prefs&&t.prefs.default_tab||s.default_tab;a&&["status","history","profiles","settings","playground"].includes(a)&&(this._tab=a),this._tabInitialized=!0}}_isAdmin(){return!!(this._panelCfg&&this._panelCfg.is_admin)}_curPerm(){const t=this._devices[this._selIdx];return t&&t.perm||"full"}_canEdit(){const t=this._curPerm();return this._isAdmin()||t==="edit"||t==="full"}_canFull(){const t=this._curPerm();return this._isAdmin()||t==="full"}_onlineEnabled(){return!!(this._constants&&this._constants.storeOnlineAvailable&&this._constants.storeOnlineEnabled)}_visibleTabIds(){const t=this._isAdmin(),e=!t&&this._panelCfg&&this._panelCfg.panel&&this._panelCfg.panel.hidden_tabs||[],s=["status","history","profiles"];return this._canEdit()&&s.push("settings"),this._canEdit()&&s.push("playground"),this._canEdit()&&this._onlineEnabled()&&s.push("store"),this._canEdit()&&s.push("advanced"),s.filter(a=>t||!e.includes(a))}async _busyRun(t,e){this._busy.add(t),this._render();try{return await e()}finally{this._busy.delete(t),this._render()}}async _closeCycleDetail(t){const e=this._prevModal;if(this._prevModal=null,e&&e.type==="profile-panel"){if(this._modal=e,this._render(),e.tab==="cleanup")try{const s=await this._ws({type:`${w}/get_profile_cycles`,entry_id:t,profile_name:e.name});this._modal&&this._modal.type==="profile-panel"&&this._modal.name===e.name&&(this._modal.cleanup={cycles:s.cycles||[],selected:new Set},this._render(),this._drawSpaghetti())}catch{}}else this._modal=null,this._render()}_SCROLLERS=[".wd-main",".wd-modal",".wd-logs",".wd-log-drawer-body",".wd-table-wrap",".wd-sd-tree"];_SCROLLERS_CHROME=[".wd-tabs"];_MODAL_KEY_PREFIX="modal:";_eachScroller(t,e){const s=this.shadowRoot;if(s)for(const a of t){const r=Array.from(s.querySelectorAll(a)),i=o=>a!==".wd-modal"&&!!o.closest(".wd-modal");for(const o of[!1,!0]){const n=r.filter(c=>i(c)===o),l=o?this._MODAL_KEY_PREFIX:"";n.forEach((c,p)=>e(c,`${l}${a}|${p}/${n.length}`))}}}_captureScroll(){if(!this.shadowRoot)return null;const t={};return this._eachScroller([...this._SCROLLERS,...this._SCROLLERS_CHROME],(e,s)=>{(e.scrollTop||e.scrollLeft)&&(t[s]={top:e.scrollTop,left:e.scrollLeft})}),t}_scrollChromeOnly(t){if(!t)return t;const e={};for(const[s,a]of Object.entries(t))this._SCROLLERS_CHROME.some(r=>s.startsWith(`${r}|`))&&(e[s]=a);return e}_restoreScroll(t){!t||!this.shadowRoot||this._eachScroller([...this._SCROLLERS,...this._SCROLLERS_CHROME],(e,s)=>{const a=t[s];if(!a)return;const r=Math.max(0,e.scrollHeight-e.clientHeight),i=Math.max(0,e.scrollWidth-e.clientWidth);e.scrollTop=Math.min(a.top,r),e.scrollLeft=Math.min(a.left,i)})}_navKey(){return[this._tab,this._selIdx,this._panelSubtab,this._profSubtab,this._toolsSubtab,this._settingsSec,this._storeView].join("|")}_modalNavKey(){const t=this._modal;if(!t)return"";const e=t.type==="profile-group"?t.orig||"":t.name||t.cycleId||"";return[t.type,e,t.tab||t.mode||t.step||""].join("|")}_render(){if(!this._container)return;se=this._pref("date_format","relative"),Re(this._uiLang(),this._hass&&this._hass.locale);const t=this.shadowRoot,e=t&&(t.activeElement||this.getRootNode()&&this.getRootNode().activeElement)||null,s=!this._modalFocusActive&&t&&t.activeElement?qe(t.activeElement):null;this._hideGraphTip();const a=this._navKey(),r=this._scrollNavKey!=null&&this._scrollNavKey!==a;this._scrollNavKey=a;let i=this._scrollCarry||this._captureScroll();r&&(i=this._scrollChromeOnly(i));const o=this._modalNavKey();if(this._scrollModalKey!==void 0&&o!==this._scrollModalKey&&i&&(i=Object.fromEntries(Object.entries(i).filter(([n])=>!n.startsWith(".wd-modal|")&&!n.startsWith(this._MODAL_KEY_PREFIX)))),this._scrollModalKey=o,this._container.innerHTML=this._buildHtml(),this._wire(),this._tabLoading?(this._scrollCarry=i,this._restoreScroll(this._scrollChromeOnly(i))):(this._scrollCarry=null,this._restoreScroll(i)),this._drawStatusCurve(),this._drawModalCanvas(),this._drawProfileSparklines(),this._drawHistorySparklines(),this._drawPlaygroundCanvases(),["wd-status-canvas","wd-cyc-canvas","wd-compare-canvas","wd-env-canvas","wd-phase-canvas","wd-spag-canvas","wd-pgroup-canvas"].forEach(n=>this._attachGraphGestures(n)),this._syncScrollAffordances(),this._syncModalFocus(e),s&&!this._modalFocusActive){const n=this.shadowRoot.querySelector(s);if(n&&this.shadowRoot.activeElement!==n)try{n.focus({preventScroll:!0})}catch{}}requestAnimationFrame(()=>this._resizeLogsPage())}_syncScrollAffordances(){const t=this.shadowRoot;if(!t)return;const e=t.querySelectorAll(Xe);this._ovfRO?this._ovfRO.disconnect():typeof ResizeObserver=="function"&&(this._ovfRO=new ResizeObserver(s=>s.forEach(a=>Yt(a.target)))),e.forEach(s=>{s.addEventListener("scroll",()=>Yt(s),{passive:!0}),Yt(s),this._ovfRO&&this._ovfRO.observe(s)})}_resizeLogsPage(){const t=this.shadowRoot&&this.shadowRoot.getElementById("wd-log-lines-page");if(!t)return;const e=t.getBoundingClientRect();e.top<=0||(t.style.height=window.innerHeight-e.top-24+"px")}_syncModalFocus(t=null){const e=this.shadowRoot;if(!e)return;const s=e.querySelector('.wd-modal[role="dialog"]');if(s){const a=s.querySelector("h2");if(a&&!a.id&&(a.id="wd-modal-title"),this._modalFocusActive){const r=e.activeElement||this.getRootNode()&&this.getRootNode().activeElement||null;if(!r||!s.contains(r)){const i=Vt(s);try{(i[0]||s).focus({preventScroll:!0})}catch{}}}else{this._modalFocusActive=!0;const r=t||null;r&&!s.contains(r)&&(this._modalReturnFocus=r);const i=Vt(s);try{(i[0]||s).focus()}catch{}}}else if(this._modalFocusActive){this._modalFocusActive=!1;const a=this._modalReturnFocus;if(this._modalReturnFocus=null,a&&a.isConnected&&typeof a.focus=="function")try{a.focus()}catch{}}}_renderPreservingFormEdits(){this._snapshotFormToPending(this.shadowRoot),this._render()}_snapshotCycleReviewForm(t){const e=this._modal;if(!t||!e||e.type!=="cycle-detail"||e.mode!=="review")return;const s=t.getElementById("wd-cyc-rev-quality"),a=t.getElementById("wd-cyc-rev-golden"),r=t.getElementById("wd-cyc-rev-notes"),i=t.getElementById("wd-cyc-rev-label");if(!s&&!a&&!r&&!i)return;e.ml||(e.ml={}),e.ml.ml_review||(e.ml.ml_review={});const o=e.ml.ml_review;s&&(o.quality=s.value||""),a&&(o.golden=!!a.checked),r&&(o.notes=r.value||""),o.tags=Array.from(t.querySelectorAll(".wd-cyc-rev-tag")).filter(n=>n.checked).map(n=>n.value),i&&(e.reviewLabel=i.value||"")}_buildHtml(){const t=this._toast?`
${d(this._toast.msg)}${this._toast.actionLabel?``:""}
`:"";return`
${this._htmlHeader()}
${this._loading?`
\u23F3
${this._t("msg.loading",{},"Loading\u2026")}
`:this._htmlBody()}
${this._logOpen&&this._isAdmin()?this._htmlLogDrawer():'
'}
${this._modal?this._htmlModal():""} ${t} `}_htmlHeader(){const e=Array.from(this._busy).some(n=>!(n==="pg-sweep"||n==="pg-history"||n==="reprocess"||n.startsWith("ml-train-now")))?`${this._t("status.working",{},"Working\u2026")}`:"",s=this._constants.iconUrl,a=s?``:``,r=this._constants.version;return`
${``} ${a}

WashData

${r?`v${d(r)} · `:""}${this._t("msg.appliance_monitor",{},"Appliance monitor")}
${e} ${this._htmlTaskPills()} ${this._isAdmin()?``:""}
`}_htmlBody(){if(!this._devices.length&&this._fetchFailed)return``;if(!this._devices.length)return`
\u{1F9FA}
${this._t("msg.no_devices",{},"No WashData devices configured yet.")}
`;const t=this._conflictKeysFromOpts().size>0?" \u26A0":"",s=this._busy.has("pg-sim")||this._busy.has("pg-sweep")?'':"",a={status:this._t("tab.status",{},"Overview"),history:this._t("tab.history",{},"Cycles"),profiles:this._t("tab.profiles",{},"Profiles"),settings:this._t("tab.settings",{},"Settings")+t,playground:this._t("tab.playground",{},"Playground")+s,store:this._t("tab.store",{},"Store"),advanced:this._t("tab.advanced",{},"Advanced")},r=this._visibleTabIds();r.includes(this._tab)||(this._tab="status");const i=r.map(n=>``).join(""),o=(n,l)=>r.includes(n)?`
${this._tab===n?l():""}
`:"";return`
${this._htmlDeviceBar()}
${i}
${this._htmlStaleChip()}
${this._tabLoading?`
\u23F3
${this._t("msg.loading",{},"Loading\u2026")}
`:""} ${o("status",()=>this._htmlStatus())} ${o("history",()=>this._htmlHistoryTab())} ${o("profiles",()=>this._htmlProfiles())} ${o("settings",()=>this._htmlSettings())} ${o("playground",()=>this._htmlPlayground())} ${o("store",()=>this._htmlStore())} ${o("advanced",()=>this._htmlPanel())} `}_htmlDeviceBar(){const t=this._isAdmin()?``:"";return this._devices.length<=1?t?`
${t}
`:"":`
${this._devices.map((e,s)=>{const a=e.is_user_paused?"user_paused":e.detector_state||"unknown",r=Mt.includes(a),i=!!e.recording,o=i?"var(--error-color, #f44336)":this._stateColor(a),n=i?this._t("status.recording",{},"Recording"):this._stateLabel(a),l=[],c=this._conflictCountForOpts(e.options||{},e.option_defaults||{});return c&&l.push(`\u26A0 ${c}`),e.feedback_count&&l.push(`\u{1F4AC} ${e.feedback_count}`),``}).join("")}${t}
`}_htmlMatchUncertainty(t,e,s){const a=t&&t.match_uncertainty;if(!a||!e||s||!a.top)return"";const r=a.runner_up?this._t("status.match_uncertain",{top:a.top,runner:a.runner_up},"Uncertain: {top} or {runner}"):this._t("status.match_uncertain_single",{top:a.top},"Uncertain: maybe {top}"),i=Number(a.sure_pct),o=a.sure_pct!=null&&isFinite(i)?` \xB7 ${this._t("status.match_sure",{pct:i},"~{pct}% sure")}`:"",n=ot(this._tText("status.match_uncertain_tip",{top:a.top},"WashData has not settled on a program yet. The figure is how often the leading guess, {top}, was right in past cycles when it led by this much. Information only."));return`${r}${o}${n}`}_htmlStatus(){const t=this._devices[this._selIdx];if(!t)return`
${this._t("msg.no_device_selected",{},"No device selected.")}
`;const e=!!t.is_user_paused,s=e?"user_paused":t.detector_state||"unknown",a=!!t.recording,r=a?"var(--error-color, #f44336)":this._stateColor(s),i=a?this._t("status.recording",{},"Recording"):this._stateLabel(s),o=a||Mt.includes(s),n=t.cycle_progress_pct,l=t.time_remaining_s,c=t.current_program,p=!!t.manual_program,_=c?null:t.armed_program||null,f=c||_||"auto_detect",u=(this._profiles||[]).map(P=>P.name);c&&!u.includes(c)&&u.unshift(c),_&&!u.includes(_)&&u.unshift(_);const b=u.map(P=>``).join(""),v=c?p?this._t("badge.manual",{},"(manually selected)"):this._t("badge.auto",{},"(auto-detected)"):_?this._t("badge.armed",{},"(applies to the next cycle)"):"",C=v?`${v}`:"",x=`
${ot(this._t("lbl.program_tip",{},"Choose which program the current cycle is matched to. Auto-detect picks the best match. Pin a program when auto-detect is wrong; pick one before starting and it applies to the next cycle."))} ${C}${this._htmlMatchUncertainty(t,o,p)}
`,$=[];t.recording&&this._canEdit()&&$.push(`
\u25CF
${this._t("msg.recording_in_progress",{},"Recording in progress")}
${this._t("msg.see_recorder",{},"See recorder widget below")}
`),t.feedback_count&&this._canEdit()&&$.push(``);const L=this._conflictKeysFromOpts();if(L.size&&this._canEdit()){const P=L.size,X=P>1?"s":"";$.push(``)}const D=this._sugCountForDevice(t);D&&this._canEdit()&&$.push(``);const B=t.standby_above_stop;B&&this._canEdit()&&$.push(``);const U=$.length?`
${$.join("")}
`:"",z=t.envelope_position,M=z!=null?Math.round(z*100):null,A=M!=null?` ${this._t("lbl.envelope_position",{pct:M},`curve ${M}%`)}`:"",O=o&&n!=null?`
${it(n,1)}%${A}${l!=null?`${this._t("lbl.time_remaining",{v:ht(l)},`~${ht(l)} remaining`)}`:""}
`:"",I=((this._powerData||{}).live||[]).length>1,R=this._pref("show_expected",!0),G=this._pref("show_raw_active",!1),W=`
${this._t("lbl.power",{},"Power")} ${this._statusEnv?``:""} ${this._pref("show_raw",!1)?``:""}
`,J=this._cyclesTotal||0,Y=(this._profiles||[]).length,nt=this._pref("setup_card_dismissed",!1),_t=this._setupStatus,ct=_t&&!o,dt=ct?this._htmlSetupCard(_t,nt):"",ut=I?`
${W}`:"",m=ct?dt+ut:ut||`

${this._t("msg.live_chart_loading",{},"Live power chart appears as readings arrive.")}

`,g=this._pref("show_debug",!1);let k="";if(g){const P=this._matchDebug||{},X=P.confidence!=null?`${(P.confidence*100).toFixed(1)}%`:"-",q=(P.candidates||[]).map(j=>`${d(j.profile_name)}${j.confidence_pct}%${j.mae}${j.correlation}${j.duration_ratio>=0?"+":""}${j.duration_ratio}%`).join("");k=`
${this._t("hdr.live_match_debug",{},"Live Match Debug")} ${ot(this._tText("msg.live_match_debug_tip",{},"Confidence: how closely the current power curve matches the top candidate profile (0-100%). Ambiguous: the two best candidates score within 5% of each other - the label is uncertain until the cycle finishes."))}
${X}
${this._t("lbl.confidence",{},"Confidence")}
${P.ambiguous?this._t("status.ambiguous",{},"Ambiguous"):this._t("status.clear",{},"Clear")}
${this._t("lbl.label",{},"Match")}
${q?`${q}
${this._t("lbl.profile",{},"Profile")}${this._t("col.dbg_conf",{},"Conf")}${this._t("col.dbg_mae",{},"MAE")}${this._t("col.dbg_corr",{},"Corr")}${this._t("lbl.duration",{},"Duration")}
`:`

${this._t("msg.no_match_yet",{},"No match attempt yet - this populates during a running cycle.")}

`}
`}const y=(()=>{if(!this._canEdit())return"";const P=Mt.includes(s),X=P&&!e,q=e,j=P||e;return!X&&!q&&!j?"":`
${q?``:""} ${X?``:""} ${j?``:""}
`})();return` ${U}
${d(t.title)}
${y}
${d(i)} ${!a&&t.sub_state&&t.sub_state.toLowerCase()!==s?`(${d(t.sub_state==="Stalled"?this._tText("status.stalled",{},"Stalled"):t.sub_state)})`:""}
${x}
${Lt(t.current_power_w)}
${this._t("lbl.power",{},"Power")}
${n!=null?it(n,0)+"%":"-"}
${this._t("lbl.progress",{},"Progress")}
${ht(l)}
${this._t("lbl.remaining",{},"Remaining")}
${O} ${this._htmlPhaseTimeline(t,n,o)} ${ct?"":`
${this._t("hdr.live_power",{},"Live Power")}
`} ${m}
${this._canEdit()?this._htmlRecordingWidget():""} ${k} `}_htmlSetupCard(t,e=!1){if(!t)return"";const{phase:s,message_key:a,message_params:r,cta_label_key:i,cta_action:o,secondary_label_key:n,secondary_action:l,skippable:c,dismissible:p,step_key:_}=t;if(s==="phase4")return`
${this._t("setup.hdr.healthy_chip",{},"Setup complete")}
`;if(s==="phase3"&&e)return`
${this._t("setup.hdr.card",{},"Device Setup")}
`;const f=this._tText(a,r||{},""),u=this._t(i,{},"Continue"),b=n?this._t(n,{},""):null,v=c?` ${this._t("setup.cta.skip_step",{},"Skip this step")} ${this._t("setup.cta.skip_forever",{},"Don't show again")} `:"",T=p?` ${this._t("setup.cta.hide_guidance",{},"Hide guidance")} `:"";return`
${this._t("setup.hdr.card",{},"Device Setup")}

${d(f)}

${b?` ${d(b)} `:""}
${v} ${T}
`}_dispatchSetupCta(t,e){if(t){if(t==="open_recorder"){this.shadowRoot.querySelector(".wd-rec-dot")?.closest(".wd-card")?.scrollIntoView({behavior:"smooth"});return}if(t==="open_cycles"||t==="open_cycles_unlabeled"){this._tab="history",this._historySub="cycles",t==="open_cycles_unlabeled"&&(this._cycleFilter={...this._cycleFilter||{},status:"unlabeled"}),this._fetchTabData();return}if(t==="open_profiles"){this._tab="profiles",this._fetchTabData();return}if(t==="open_suggestions"){this._tab="settings",this._settingsSugOnly=!0,this._fetchTabData();return}if(t==="create_profile_from_cluster"){this._tab="profiles",this._fetchTabData();return}if(t&&t.startsWith("open_cycle:")){this._tab="history",this._historySub="cycles",this._fetchTabData();return}}}async _reloadSetupStatus(){const t=this._devices[this._selIdx];if(t){try{this._setupStatus=await this._ws({type:`${w}/get_setup_status`,entry_id:t.entry_id})}catch{this._setupStatus=null}this._render()}}_htmlPhaseTimeline(t,e,s){const a=this._statusPhases||[];if(!s||!a.length||!t.current_program)return"";const r=Math.max(0,...a.map(u=>u.end||0)),i=Number(t.expected_duration_s),o=Math.max(r,Number.isFinite(i)?i:0);if(o<=0)return"";const n=e!=null?Math.min(1,Math.max(0,e/100)):null;let l="",c=!1;const p=a.map((u,b)=>{const v=Math.max(0,Math.min(1,(u.start||0)/o)),T=Math.max(0,Math.min(1,(u.end||0)/o)),C=Math.max(0,(T-v)*100),x=Q[b%Q.length],$=n==null?!0:v<=n;!c&&n!=null&&T>v&&n>=v&&(n=1&&T>=1)&&(c=!0,l=u.name||"");const L=u.name&&C>12?`${d(u.name)}`:"";return`
${L}
`}).join(""),_=n!=null?`
`:"",f=l?`
${this._t("lbl.current_phase",{},"Current phase")}: ${d(l)}
`:"";return`
${f}
`}_htmlRecordingWidget(){const t=this._recState,e=this._devices[this._selIdx],a=!!(e&&e.recording)?"recording":t?t.state:"idle",r=a==="recording"?"wd-rec-active":a==="stopped"?"wd-rec-ready":"wd-rec-idle",i=a==="recording"?this._t("status.recording",{},"Recording\u2026"):a==="stopped"?this._t("status.ready",{},"Ready to process"):this._t("status.idle",{},"Idle");let o="";const n=c=>this._t("lbl.n_samples",{n:c},`${c} samples`);a==="recording"?o=t?`${ht(t.duration_s)} \xB7 ${n(t.sample_count||0)}`:"":a==="stopped"&&(o=`${n(t.sample_count||0)} \xB7 ${ht(t.duration_s)}`);const l=a==="recording"?``:a==="stopped"?` `:``;return`
${this._t("hdr.manual_recording",{},"Manual Recording")}${ot(this._t("hdr.manual_recording_tip",{},"Record a cycle on purpose: press Start just before the appliance starts and Stop when it finishes, then Process to save it as a profile."))}${o?`${o}`:""}
${l}
`}_htmlHistoryTab(){const t=this._historySub==="maintenance"?"maintenance":"cycles";return`
${[["cycles",this._t("tab.history",{},"Cycles")],["maintenance",this._t("tab.maintenance",{},"Maintenance")]].map(([s,a])=>``).join("")}
${t==="maintenance"?this._htmlMaintenance():this._htmlHistory()}`}_htmlHistory(){const t=this._cycles||[],e=this._refCycles||[],s=e.concat(t),a=this._canEdit(),r=this._selectMode&&a,i=this._cycleSel,{col:o,dir:n}=this._cycleSort,{text:l,status:c}=this._cycleFilter,p=this._mlById||{},_=new Set((this._feedbacks||[]).map(y=>y.cycle_id)),f=y=>p[y.id],u=y=>{const P=f(y);return!!(P&&P.ml_review&&P.ml_review.reviewed_at)},b=y=>{const P=f(y);return!!(P&&P.ml_review&&P.ml_review.golden)},v=y=>_.has(y.id),T=s.filter(v).length;let C=s;if(l){const y=l.toLowerCase();C=C.filter(P=>(P.profile_name||P.matched_profile||"").toLowerCase().includes(y))}c==="unlabelled"?C=C.filter(y=>!y.profile_name&&!y.matched_profile):c==="needs_review"?C=C.filter(v):c==="imported"?C=C.filter(y=>y.is_reference):c&&(C=C.filter(y=>(y.status||"completed")===c));const x={date:y=>y.start_time?new Date(y.start_time).getTime():0,confidence:y=>y.match_confidence,duration:y=>y.duration,energy:y=>y.energy_kwh!=null?y.energy_kwh:y.energy_wh!=null?y.energy_wh/1e3:null,cost:y=>y.cost!=null?y.cost:-1,status:y=>y.status||"completed",profile:y=>(y.profile_name||y.matched_profile||"\uFFFF").toLowerCase()};C=_e(C,x[o]||x.date,n);const $=y=>y==="completed"?"var(--success-color, #4caf50)":y==="interrupted"?"var(--error-color, #f44336)":y==="force_stopped"?"var(--warning-color, #ff9800)":"var(--secondary-text-color)",L=y=>{if(!y.is_reference)return"";const P=y.cycle_origin==="backfill",X=P?this._t("badge.backfilled_tip",{},"Detected in imported power history. Shapes program matching only, not counted in stats."):this._t("badge.imported_tip",{},"Imported from the community store. Used for matching only, not counted in stats.");return` ${P?"\u{1F557}":"\u{1F4E5}"}`},D=y=>b(y)?' \u2B50':_.has(y.id)?' \u{1F4AC}':u(y)?' \u2713':v(y)?' \u25CF':"",B=y=>{if(y.anomaly!=="overrun")return"";const P=y.overrun_ratio?" "+this._tText("badge.overrun_ratio",{x:Number(y.overrun_ratio).toFixed(1)},`(${Number(y.overrun_ratio).toFixed(1)}x expected)`):"";return` \u23F1`},U=y=>{if(y.anomaly!=="underrun")return"";const P=y.underrun_ratio?" "+this._tText("badge.underrun_ratio",{pct:Math.round(y.underrun_ratio*100)},`(${Math.round(y.underrun_ratio*100)}% of expected)`):"";return` \u26A1`},z=y=>{if(!y.energy_anomaly||y.energy_anomaly==="none")return"";const P=y.energy_anomaly==="energy_spike",X=y.energy_z_score!=null?` (${y.energy_z_score>0?"+":""}${Number(y.energy_z_score).toFixed(1)}\u03C3)`:"",q=P?"badge.energy_spike":"badge.energy_low",j=P?"Higher energy than usual":"Lower energy than usual",V=P?"\u{1F53A}":"\u{1F53B}",E=P?"var(--error-color,#f44336)":"var(--info-color,#2196f3)";return` ${V}`},M=y=>{const P=Array.isArray(y.artifacts)?y.artifacts.length:0;return P?` 1?"ies":"y"} detected (e.g. door opened mid-cycle) - open to see them on the graph`))}" style="color:var(--warning-color,#ff9800)">\u26A0`:""},A=y=>{const P=Array.isArray(y.restart_gaps)?y.restart_gaps.length:0;return P?` 1?"s":""} during this cycle - power trace has a hole`))}" style="color:var(--info-color,#2196f3)">\u21BB`:""},O=this._hass&&this._hass.config&&this._hass.config.currency||"",K=y=>{if(y.cost==null)return"-";const P=It(y.cost,O);if(y.energy_price_mode==="dynamic"&&y.energy_price!=null){const X=this._tText("col.cost_dynamic_tip",{price:it(y.energy_price,4),cur:O},"Time-weighted: charged at the price in force during the cycle (effective {price} {cur}/kWh).");return`${P}`}return P},I=C.map(y=>{const P=y.profile_name||y.matched_profile,X=y.match_confidence!=null?y.match_confidence*100:null,q=y.status||"completed",j=y.energy_kwh!=null?y.energy_kwh:y.energy_wh!=null?y.energy_wh/1e3:null,V=r,E=V?``:``,S=this._cycleStatusLabel(q),F=`${L(y)}${D(y)}${B(y)}${U(y)}${z(y)}${M(y)}${A(y)}`.trim();return` ${E} ${P?d(P):`${this._t("lbl.unlabelled",{},"Unlabelled")}`} ${F} ${d(S)} ${et(y.start_time)} ${ht(y.duration)} ${j!=null?gt(j):"-"} ${K(y)} ${X!=null?it(X,0)+"%":"-"} `}).join(""),R=` ${mt(this._t("lbl.profile",{},"Profile"),"profile",o==="profile",n,"cycsort","",this._t("col.profile_tip",{},"Matched program name. Unlabelled means no profile matched at end of cycle."))} ${this._t("lbl.flags",{},"Flags")} ${mt(this._t("lbl.status",{},"Status"),"status",o==="status",n,"cycsort","",this._t("col.status_tip",{},"Cycle outcome: Completed (natural end), Interrupted (abrupt power drop), Force Stopped (manual), or Needs Review (feedback pending)."))} ${mt(this._t("lbl.date",{},"Date"),"date",o==="date",n,"cycsort","",this._t("col.date_tip",{},"Date and time the cycle started."))} ${mt(this._t("lbl.duration",{},"Duration"),"duration",o==="duration",n,"cycsort","right",this._t("col.duration_tip",{},"Total cycle run time from start to end."))} ${mt(this._t("lbl.energy",{},"Energy"),"energy",o==="energy",n,"cycsort","right",this._t("col.energy_tip",{},"Total energy used (kWh), computed from power over time."))} ${mt(this._t("lbl.cost",{},"Cost"),"cost",o==="cost",n,"cycsort","right",this._t("col.cost_tip",{},"Energy cost of this cycle, fixed when it finished. With Time-Weighted Cost on, each moment is charged at its own price; otherwise at the price at the end. Set a price in Settings to fill this in."))} ${mt(this._t("lbl.confidence",{},"Confidence"),"confidence",o==="confidence",n,"cycsort","right",this._t("col.confidence_tip",{},"Profile match confidence (0-100%). How closely the cycle power curve matched the identified program."))} `,G=`
`,W=C.length!==s.length?this._t("lbl.n_shown",{n:C.length},`, ${C.length} shown`):"",J=e.length?this._t("lbl.n_imported_note",{n:e.length},`, ${e.length} imported`):"",Y=this._t("lbl.cycles_title",{n:`${t.length}${J}${W}`},`Cycles (${t.length}${J}${W})`),nt=a?`
`:"",_t=new Set(e.map(y=>y.id)),ct=[...i].some(y=>_t.has(y)),dt=r?`
${this._t("lbl.n_selected",{n:i.size},`${i.size} selected`)}
`:"",ut=this._busy.has("cyc-load-more"),m=this._cyclesHasMore||this._importedHasMore?`
`:"",g=`
${Y}
${G} ${nt}${dt} ${C.length===0?`
\u{1F4CB}
${s.length?this._t("msg.no_cycles_match",{},"No cycles match the current filter."):this._t("msg.no_cycles_yet",{},"No cycles recorded yet.")}
`:`
${R}${I}
`} ${m}
`;return(this._cyclesError?``:"")+g}_profileAdvisory(t,e){return(this._profileAdvisories||[]).find(s=>s&&s.profile===t&&s.code===e)}_advisoryText(t){return this._tText(t.message_key,t.message_params||{},t.message||"")}_trendIcon(t){return t==="up"?`\u2191`:t==="down"?`\u2193`:""}_profileCardHtml(t){const e=t.avg_duration?this._t("lbl.duration_avg",{v:Math.round(t.avg_duration/60)},`~${Math.round(t.avg_duration/60)}m avg`):this._t("lbl.no_duration",{},"no duration"),s=t.avg_energy!=null?` \xB7 ${this._t("lbl.energy_per_cycle",{v:gt(t.avg_energy)},"{v}/cycle")}`:"",a=t.avg_energy!=null&&t.cycle_count?` \xB7 ${this._t("lbl.energy_total",{v:vt(`${d(gt(t.avg_energy*t.cycle_count))}`)},"{v} total")}`:"",r=this._hass&&this._hass.config&&this._hass.config.currency||"",i=t.avg_cost!=null?` \xB7 ${this._t("lbl.avg_cost",{},"Avg")} ${It(t.avg_cost,r)}/${this._t("lbl.per_cycle_short",{},"cycle")}`:"",o=(this._profileHealth||{})[t.name],n=(this._profileTrends||{})[t.name],l=this._profileAdvisory(t.name,"poor_health"),c=l?`\u26A0 ${this._t("badge.poor_fit",{},"poor fit")}`:"",p=this._profileAdvisory(t.name,"duration_trend_up")||this._profileAdvisory(t.name,"energy_trend_up"),_=p?`${this._trendIcon("up")}`:"",f=this._constants&&this._constants.PROFILE_MIN_WARMUP_CYCLES||5,u=o&&o.cycle_count||0,v=u${this._t("msg.warmup_badge",{done:u,needed:f},`Still learning (${u}/${f} cycles)`)}`:"",T=t.is_imported?`\u{1F4E5} ${this._t("status.imported",{},"Imported")}`:"",C=t.is_imported?Number((this._profileMatcherCounts||{})[t.name]||0):null,x=C==null?"":`${C>0?this._t("badge.matcher_used",{n:C},`Matched ${C} of your cycles`):this._t("badge.matcher_unused",{},"Not matched to your cycles yet")}`,$=this._profileAdvisory(t.name,"unmatchable"),L=$?`\u26A0 ${this._t("badge.unmatchable",{},"can't be matched")}`:"",D=(this._profileTerminal||{})[t.name];let B="";if(D&&D.quiet_before_s>0&&D.seen_in>=2){const O=Math.max(1,Math.round(D.quiet_before_s/60)),K=Number(D.event_watts||0).toFixed(0),I=Math.round(D.event_seconds||0),R=this._tText("badge.quiet_tail_tip",{mins:O,secs:I,watts:K,seen:D.seen_in,measured:D.measured},`Near the end this program goes quiet for about ${O} min, then draws about ${K} W for ${I} s. Seen in ${D.seen_in} of ${D.measured} measured cycles.`);B=`${this._t("badge.quiet_tail",{mins:O},`~${O}m quiet tail`)}`}const U=this._profileAdvisory(t.name,"duration_outlier"),z=U?`\u26A0 ${this._t("badge.duration_outlier",{n:(U.message_params||{}).n||1},`${(U.message_params||{}).n||1} odd-length cycle(s)`)}`:"",M=[L,z,c,_,B,v,T,x].filter(Boolean).join(" "),A=Array.isArray(t.signature_curve)&&t.signature_curve.length>=3?``:"";return`
`}_paintSparkline(t,e){if(!t||!Array.isArray(e)||e.length<3)return;const s=(getComputedStyle(this).getPropertyValue("--primary-color")||"#03a9f4").trim()||"#03a9f4",a=window.devicePixelRatio||1,r=t.getBoundingClientRect(),i=t.width=Math.max(1,Math.round((r.width||64)*a)),o=t.height=Math.max(1,Math.round((r.height||20)*a)),n=t.getContext("2d");n.clearRect(0,0,i,o);const l=Math.max(...e,1),c=2*a,p=f=>c+(e.length===1?0:f/(e.length-1)*(i-2*c)),_=f=>o-c-Math.max(0,f)/l*(o-2*c);n.beginPath(),e.forEach((f,u)=>{const b=p(u),v=_(f);u===0?n.moveTo(b,v):n.lineTo(b,v)}),n.strokeStyle=s,n.lineWidth=1.5*a,n.lineJoin="round",n.lineCap="round",n.stroke()}_drawProfileSparklines(){const t=this.shadowRoot;if(!t)return;const e=t.querySelectorAll("canvas[data-spark-prof]");if(!e.length)return;const s={};for(const a of this._profiles||[])s[a.name]=a;e.forEach(a=>{const r=s[a.dataset.sparkProf];this._paintSparkline(a,r&&r.signature_curve||[])})}_htmlProfiles(){const t=this._busy.has("rebuild-envelopes"),e=this._canEdit(),s={};this._profiles.forEach(v=>{s[v.name]=v});const a=this._profileGroups||{groups:[]},r=new Set;a.groups.forEach(v=>(v.members||[]).forEach(T=>r.add(T)));const i=a.groups.map(v=>{const T=(v.members||[]).map(D=>s[D]?this._profileCardHtml(s[D]):"").join(""),C=Math.round((v.cohesion!=null?v.cohesion:1)*100),x=v.cohesive?`${this._t("lbl.cohesion_good",{pct:C},"cohesion "+C+"%")}`:`${this._t("lbl.cohesion_low",{pct:C},"\u26A0 low cohesion "+C+"%")}`,$=v.cohesive?"":`

${this._t("msg.group_not_cohesive",{},"These profiles aren't similar enough to group reliably, so matching treats them individually until you remove the outlier or split the group.")}

`;return`
${e?``:`\u{1F517} ${d(v.name)}`} ${x} ${e?``:""}
${$}
${T}
`}).join(""),o=this._profiles.filter(v=>!r.has(v.name)),n=o.map(v=>this._profileCardHtml(v)).join(""),l=this._profiles.length===0&&!(this._cycles||[]).length&&!(this._refCycles||[]).length,c=e&&this._onlineEnabled()&&l?`
\u{1F4E5} ${this._t("msg.onboard_download",{},"New device? Adopt a ready-made setup (programs, reference cycles and phases) from another WashData user with the same appliance.")}
`:"",_=(e?((this._coverageGaps||{}).profile_suggestions||[]).slice(0,2):[]).map(v=>{const T=Math.round((v.avg_duration_s||0)/60),C=(v.cycle_ids||[])[0]||"";return`
\u{1F9E9} ${this._t("msg.coverage_gap_cluster",{count:v.count,min:T},`${v.count} recent unlabelled cycles of about ${T} min look like one program you have not created yet.`)}
`}).join(""),f=c+_+`
${this._t("tab.profiles",{},"Profiles")} (${this._profiles.length})

${this._t("msg.profiles_intro",{},"Click a profile for stats, phases and cleanup. Group near-identical programs (same shape/duration, different temperature or spin) so matching reliably picks between them.")}

${e?`
`:""}
${i} ${this._profiles.length===0?`
\u{1F4CA}
${this._t("msg.no_profiles_yet",{},"No profiles yet. Create one from a labelled cycle.")}
`:o.length?`${i?`
${this._t("lbl.ungrouped",{},"Ungrouped")}
`:""}
${n}
`:""}`,u=[["profiles",this._t("tab.subtab_profiles",{},"Profiles")],["phase-catalog",this._t("tab.subtab_phase_catalog",{},"Phase Catalog")]].map(([v,T])=>``).join(""),b=this._profilesError||this._profileGroupsError?``:"";return`
${u}
${this._profSubtab==="phase-catalog"?this._htmlPhases():b+f} `}_htmlProfileGroupModal(t){const e=this._busy.has("pg-save"),s=this._profileEnvCache||{},a=_=>Q[Math.max(0,this._profiles.findIndex(f=>f.name===_))%Q.length],r=t.members||[],i=this._profiles.map(_=>{const f=r.includes(_.name),u=_.avg_duration?`~${Math.round(_.avg_duration/60)}m`:"",b=_.avg_energy!=null?` \xB7 ${gt(_.avg_energy)}`:"",v=f?``:"";return``}).join(""),o=r.filter(_=>s[_]&&(s[_].avg||[]).length),n=o.length?`
${o.map(_=>` ${d(_)}`).join("")}
`:"",l=o.length?`
${n}`:`

${this._t("msg.group_preview_hint",{},"Tick 2+ members to preview and compare their power curves.")}

`,c=((this._profileGroups||{}).groups||[]).find(_=>_.name===t.orig),p=c&&c.cohesion!=null?`${c.cohesive?this._t("lbl.cohesion_good",{pct:Math.round(c.cohesion*100)},"cohesion "+Math.round(c.cohesion*100)+"%"):this._t("lbl.cohesion_low",{pct:Math.round(c.cohesion*100)},"\u26A0 low cohesion "+Math.round(c.cohesion*100)+"%")}`:"";return`

${t.orig?this._t("modal.edit_group",{},"Edit profile group"):this._t("modal.new_group",{},"New profile group")}

${p}
${l}
${this._t("lbl.members",{},"Members")}${r.length?` (${r.length})`:""}
${i||`${this._t("msg.no_profiles_yet_short",{},"No profiles yet.")}`}

${this._t("msg.group_modal_help",{},"Group programs with the same shape that differ in temperature or spin. Matching scores the group as one, then picks the best-fitting member. Pick at least 2; the overlay shows how alike they are.")}

${t.orig?``:""}
`}_drawGroupCanvas(){const t=this._modal;if(!t||t.type!=="profile-group")return;const e=this._profileEnvCache||{},s=i=>Q[Math.max(0,this._profiles.findIndex(o=>o.name===i))%Q.length];let a=0;const r=(t.members||[]).filter(i=>e[i]&&(e[i].avg||[]).length).map(i=>{const o=e[i],n=o.avg[o.avg.length-1];return a=Math.max(a,o.target_duration||(n?n[0]:0)),{points:o.avg,stroke:s(i),width:2,alpha:.9,name:i}});r.length&&this._drawCurves("wd-pgroup-canvas",{series:r,xMax:a})}_settingsLevel(){return this._pref("settings_level","basic")==="advanced"?"advanced":"basic"}_settingFieldVisible(t){return t.internal&&!this._pref("show_internal",!1)&&!(this._internalReveal||new Set).has(t.key)?!1:this._settingsLevel()==="advanced"||!!t.basic}_computeInternalReveal(t){const e=new Set,s=this._optDefaults||{},a=this._conflictKeysFromOpts(),r=new Set((this._suggestions||[]).map(i=>i.key));for(const i of yt)for(const o of i.fields||(i.groups||[]).flatMap(n=>n.fields||[])){if(!o.internal)continue;const n=(this._opts||{})[o.key],l=s[o.key]!==void 0?s[o.key]:o.def;(n!=null&&n!==""&&String(n)!==String(l)||a.has(o.key)||r.has(o.key)||Object.prototype.hasOwnProperty.call(this._pendingSettings||{},o.key))&&e.add(o.key)}return e}_secHasBasicFields(t){return(t.fields||(t.groups||[]).flatMap(s=>s.fields||[])).some(s=>s.basic)}_htmlSettings(){const t=this._editedOpts();if(!Object.keys(t).length&&this._tabError==="settings")return``;if(!Object.keys(t).length)return`
\u2699\uFE0F
${this._t("msg.loading_settings",{},"Loading settings\u2026")}
`;const e=this._suggestionsError?``:"",a=this._settingsLevel()==="basic";this._internalReveal=this._computeInternalReveal(t);const r=new Set((this._suggestions||[]).map(W=>W.key)),i=W=>(W.fields||(W.groups||[]).flatMap(Y=>Y.fields||[])).some(Y=>r.has(Y.key)),o=this._conflictKeysFromOpts(),n=W=>(W.fields||(W.groups||[]).flatMap(Y=>Y.fields||[])).some(Y=>o.has(Y.key)),l=this._opts&&this._opts.device_type||"",c=yt.filter(W=>!(W.id==="ml_training"||l&&W.notDeviceTypes&&W.notDeviceTypes.includes(l)||l&&W.onlyDeviceTypes&&!W.onlyDeviceTypes.includes(l)||a&&!this._secHasBasicFields(W))),p=(c.find(W=>W.id===this._settingsSec)||c[0]||{}).id,_=c.map(W=>{const J=i(W),Y=n(W);return``}).join(""),f=``,u=a?`

${this._t("msg.settings_basic_note",{},"Showing essential settings. Switch to Advanced for the full list.")}

`:"",b=a?"":``,v=this._busy.has("save-settings"),T=o.size,C=T!==1?"s":"",x=T?`
\u26A0 ${this._t("conflict.settings_banner",{n:T},`Setting conflicts: ${T}. Check the highlighted sections and fix them before saving.`)}
`:"",$=this._suggestions.length,L=this._settingsSugOnly&&!this._settingsSearch,D=$?``:"",B=$?``:"",U=$?L?`
\u{1F4A1} ${this._t("msg.showing_suggestions",{count:$},`Showing ${$} setting${$>1?"s":""} with suggestions.`)} ${this._t("msg.show_all_settings",{},"Show all settings")}. ${D}
`:`
\u{1F4A1} ${this._t("msg.tuning_suggestions_available",{count:$},`${$} tuning suggestion${$>1?"s":""} available from observed cycles. They appear beside the relevant fields.`)} ${D} ${B}
`:"",z=this._busy.has("sug-analyze"),M=``,A=this._settingsSearch||"",O=A.trim().toLowerCase(),K=``,I=O?this._htmlSettingsSearch(t,O):L?this._htmlSettingsSugOnly(t):this._htmlSettingsSection(t),R=(this._lockedSuggestions||[]).length,G=R?`
\u{1F515} ${this._t("msg.n_suggestions_muted",{count:R},`${R} suggestion${R>1?"s":""} muted; the auto-tuner will not propose these.`)}
`:"";return` ${e}
${this._t("tab.settings",{},"Settings")}
${M}
${x}${U}${G}${u}
${K}
${_}
${f} ${b}
${I}

${this._t("msg.saving_triggers_reload",{},"Saving reloads the integration. Entities may briefly show as unavailable.")}

${this._htmlSettingsHistory()} `}_htmlSettingsHistory(){const t=this._settingsChangelog||[];if(!t.length)return"";const e=this._settingsHistoryOpen,s=this._canEdit(),a=t.slice(0,100).map(r=>{const i=s?``:"";return` ${d(this._t("setting."+r.key+".label",{},r.key))} ${d(xt(r.old))} \u2192 ${d(xt(r.new))} ${et(r.timestamp)} ${i} `}).join("");return`
${this._t("hdr.settings_history",{},"Settings history")} (${t.length})
${e?"\u25B2":"\u25BC"}
${e?`
${a}
${this._t("lbl.setting",{},"Setting")} ${this._t("lbl.change",{},"Change")} ${this._t("lbl.date",{},"Date")}
`:""}
`}_renderField(t,e){if(t.onlyDeviceType&&(e.device_type||"washing_machine")!==t.onlyDeviceType)return"";if(t.type==="storebrand"||t.type==="storemodel")return this._renderStorePicker(t,e);let s=e[t.key];if(s===void 0){const n=this._optDefaults;s=n&&n[t.key]!=null?n[t.key]:t.def}const a={};if(t.type==="devicetype")a.opts=this._deviceTypeOpts(s||e.device_type);else if(t.type==="device")a.opts=this._deviceOpts(s);else if(t.type==="select")a.opts=t.opts||[];else if(t.type==="entity"){const n=this._hass&&this._hass.states?this._hass.states:{},l=Array.isArray(t.domains)?t.domains:t.domain==="binary_sensor"?["binary_sensor","sensor"]:t.domain?[t.domain]:null,c=Object.keys(n).filter(p=>!l||l.some(_=>p.startsWith(_+"."))).filter(p=>!t.notPrice||Nt(p,e,this._conflictCtx())==null).sort().slice(0,500);this._entityListCache||(this._entityListCache={}),this._entityListCache[t.key]=c}else if(t.type==="entitylist"){const n=this._hass&&this._hass.states?this._hass.states:{},l=Object.keys(n).filter(c=>!t.domain||c.startsWith(t.domain+".")).sort();if(t.domain==="notify"&&this._hass&&this._hass.services&&this._hass.services.notify){const c=Object.keys(this._hass.services.notify).map(p=>`notify.${p}`);a.entities=[...new Set([...l,...c])].sort().slice(0,500)}else a.entities=l.slice(0,500);this._entityListCache||(this._entityListCache={}),this._entityListCache[t.key]=a.entities}else if(Array.isArray(t.suggestions)&&t.suggestions.length){const n=`wd-dl-${t.key}`;a.datalistId=n,a.datalist=`${t.suggestions.map(l=>``}const r=this._suggestions.find(n=>n.key===t.key);if(r){const n=Object.assign({},r.reason_params||{});r.exclusions&&r.exclusions.total&&(n.excl=this._exclNote(r.exclusions)),a.suggestion={suggested:r.suggested,current:r.current,reason:r.reason,reason_key:r.reason_key,reason_params:n}}a.useBtnLabel=this._t("btn.use",{},"Use"),a.t=this._t.bind(this),a.tText=this._tText.bind(this);const i=(this._settingsChangeByKey||{})[t.key];i&&(a.changed=this._tText("msg.setting_changed",{old:xt(i.old),new:xt(i.new),date:et(i.timestamp)},`Changed from ${xt(i.old)} to ${xt(i.new)} on ${et(i.timestamp)}`));const o=Object.assign({},t,{label:this._t("setting."+t.key+".label",{},t.label||""),doc:t.doc!=null?this._t("setting."+t.key+".doc",{},t.doc):t.doc});return Ge(o,s,a)}_renderStorePicker(t,e){const s=t.type==="storebrand",a=t.key,r=String(e[a]==null?"":e[a]),i=this._t("setting."+a+".label",{},t.label||""),o=this._t("setting."+a+".doc",{},t.doc||""),n=d(this._t("placeholder."+a,{},s?"e.g. Bosch":"e.g. WAT28660"));return this._onlineEnabled()?s?this._renderBrandPicker(a,r,i,o,n):this._renderModelPicker(a,e,r,i,o,n):`
${d(this._t("msg.store_picker_offline",{},"Enable online features in the settings gear to pick from the community catalog."))}
`}_statusTag(t){if(t&&t.status==="pending"){const e=d(this._t("badge.awaiting_tip",{},"Awaiting community approval")),s=typeof t.confirmCount=="number"?this._t("badge.awaiting_n",{n:t.confirmCount},`Awaiting approval \xB7 ${t.confirmCount} confirmed`):this._t("badge.awaiting",{},"Awaiting approval");return`${s}`}return t&&t.status==="approved"?`${this._t("badge.approved",{},"Approved")}`:""}_renderBrandPicker(t,e,s,a,r){this._ensureCatalogEntry();const i=Array.isArray(this._catalog.brands)?this._catalog.brands:[];this._entityListCache=this._entityListCache||{},this._entityListCache[t]=i.map(l=>l.brand).filter(Boolean);const o=this._statusTag(this._catalogEntryFor(e,"brand")),n=this._catalog.brands===null?` ${this._t("msg.loading",{},"Loading\u2026")}`:"";return`
`}_renderModelPicker(t,e,s,a,r,i){const o=String(e.store_brand||"");if(!o)return`
${d(this._t("msg.pick_brand_first",{},"Pick an appliance brand first."))}
`;this._ensureCatalogEntry(),this._catalog.forBrand!==o&&(this._catalog.forBrand=o,this._catalog.devices=void 0);const n=Array.isArray(this._catalog.devices)?this._catalog.devices:[];this._entityListCache=this._entityListCache||{},this._entityListCache[t]=n.map(u=>u.model).filter(Boolean);const l=this._catalogEntryFor(s,"device"),c=this._statusTag(l),p=this._catalog.devices===null?` ${this._t("msg.loading",{},"Loading\u2026")}`:"";let _="";if(l){const u=[],b=je(l.manualUrl);b&&u.push(`${this._t("link.manual",{},"Manual \u2197")}`),(this._constants&&this._constants.storePrefs||{}).show_contributor!==!1&&u.push(this._t("store.contributed_by",{name:l.createdByName||this._tText("lbl.anonymous",{},"Anonymous")},"by {name}"));const v=!!(this._storeStatus&&this._storeStatus.connected);let T="";if(v){const C=[1,2,3,4,5].map(x=>``).join("");T=` ${C}`}else T=`${this._t("msg.connect_to_confirm",{},"Connect in the settings gear to confirm or rate.")}`;_=`
${u.join(" \xB7 ")}
${T}
`}const f=this._onlineEnabled()&&this._storeDeviceDeclared()?`
`:"";return`
${_}${f}
`}_storeApplianceType(){return{washing_machine:"washer",washer:"washer",dryer:"dryer",dishwasher:"dishwasher",washer_dryer:"washer_dryer"}[this._opts.device_type]||""}_storeDeviceDeclared(){return!!((this._opts.store_brand||"").trim()&&(this._opts.store_model||"").trim())}_shareableByProgram(){const t=new Map;for(const s of this._shareableCycles||[]){const a=(s.profile_name||"").trim();a&&(t.has(a)||t.set(a,[]),t.get(a).push(s))}const e=Array.from(t.entries()).map(([s,a])=>({program:s,cycles:a})).sort((s,a)=>s.program.localeCompare(a.program));for(const s of this._shareAllPrograms||[])t.has(s)||e.push({program:s,cycles:[],noCycles:!0});return e}_editedOpts(){return Object.assign({},this._opts,this._pendingSettings)}_catalogEntryKey(){const t=this._editedOpts();return[(t.store_brand||"").trim().toLowerCase(),(t.store_model||"").trim().toLowerCase(),t.device_type||""].join("|")}_ensureCatalogEntry(){const t=this._catalogEntryKey(),e=this._catalogEntry;e&&e.key===t||(this._catalogEntry={key:t,brand:null,device:null,deviceId:null,loading:!0},this._loadCatalogEntry(t))}_catalogEntryFor(t,e){const s=this._catalogEntry;if(!s||s.key!==this._catalogEntryKey())return null;const a=e==="brand"?s.brand:s.device;if(!a)return null;const r=e==="brand"?a.brand:a.model;return String(r||"").toLowerCase()===String(t||"").trim().toLowerCase()?a:null}async _loadCatalogEntry(t){const e=this._devices[this._selIdx],s=this._editedOpts(),a=(s.store_brand||"").trim(),r=(s.store_model||"").trim();if(!e||!this._onlineEnabled()||!a||!r){this._catalogEntry&&this._catalogEntry.key===t&&(this._catalogEntry.loading=!1);return}let i=null;try{i=await this._ws({type:`${w}/store_get_catalog_entry`,entry_id:e.entry_id,brand:a,model:r,appliance_type:s.device_type||""})}catch{}!this._catalogEntry||this._catalogEntry.key!==t||(this._catalogEntry={key:t,loading:!1,brand:i&&i.brand||null,device:i&&i.device||null,deviceId:i&&i.device_id||null},this._isActiveEntry(e.entry_id)&&this._renderPreservingFormEdits())}_refreshComboAfterLoad(t,e,s=!0){const a=this.shadowRoot;for(const r of Array.isArray(t)?t:[t]){const i=a&&a.getElementById(r);if(i&&a.activeElement===i){if(s){i._wdSyntheticInput=!0;try{i.dispatchEvent(new Event("input",{bubbles:!0}))}finally{i._wdSyntheticInput=!1}}return}}this._isActiveEntry(e)&&this._render()}_ensureCatalogList(t,e,s=!1){if(this._onlineEnabled()){if(t==="store_brand"){this._ensureBrandCandidates(e);return}if(t==="store_search"){this._ensureBrandCandidates(e),s&&String(e||"").trim().length>=2&&this._ensureStoreTypeDevices();return}if(t==="store_model"){const a=String(this._editedOpts().store_brand||"").trim();if(!a)return;(this._catalog.forBrand!==a||this._catalog.devices===void 0)&&(this._catalog.forBrand=a,this._catalog.devices=null,this._loadCatalogDevices(a))}}}_ensureBrandCandidates(t){const e=String(t||"").trim().toLowerCase();if(this._catalog.brandsFull)return;if(!e){clearTimeout(this._brandSearchTimer),this._brandSearchTimer=null,this._catalog.brands===void 0&&(this._catalog.brands=null,this._loadCatalogBrands(""));return}(this._catalog.brandPrefixes||(this._catalog.brandPrefixes=[])).some(a=>e.startsWith(a))||(clearTimeout(this._brandSearchTimer),this._brandSearchTimer=setTimeout(()=>this._loadCatalogBrands(e),250))}_mergeBrandCandidates(t){const e=new Map((Array.isArray(this._catalog.brands)?this._catalog.brands:[]).map(s=>[String(s.id!=null?s.id:s.brand),s]));for(const s of t||[])e.set(String(s.id!=null?s.id:s.brand),s);this._catalog.brands=Array.from(e.values()).sort((s,a)=>String(s.brand||"").localeCompare(String(a.brand||"")))}async _loadCatalogBrands(t=""){this._entityListCache=this._entityListCache||{};let e=!1;const s=this._devices[this._selIdx];if(!s||!this._onlineEnabled()){this._catalog.brands=[],this._entityListCache.store_brand=[];return}if(!(t&&this._catalog.brandsFull)){this._catalog.brands===void 0&&(this._catalog.brands=null);try{const a=await this._ws({type:`${w}/store_list_brands`,entry_id:s.entry_id,query:t||null,include_pending:!this._catalog.approvedOnly}),r=a&&a.items||[];t?(this._mergeBrandCandidates(r),(this._catalog.brandPrefixes||(this._catalog.brandPrefixes=[])).push(t)):(this._catalog.brands=r,this._catalog.brandsFull=!0)}catch{Array.isArray(this._catalog.brands)||(this._catalog.brands=[]),e=!0}this._entityListCache.store_brand=(this._catalog.brands||[]).map(a=>a.brand).filter(Boolean),this._syncStoreSearchCandidates(),this._refreshComboAfterLoad(["wd-store-brand","wd-store-q"],s.entry_id,!e)}}_syncStoreSearchCandidates(){this._entityListCache=this._entityListCache||{};const t=new Map,e=[];for(const a of this._catalog.typeDevices||[]){const r=String(a&&a.model||"").trim();if(!r)continue;const i=a.brand?`${r} \xB7 ${a.brand}`:r;t.has(i)||(t.set(i,a),e.push(i))}this._catalog.modelIndex=t;const s=(this._catalog.brands||[]).map(a=>a.brand).filter(Boolean);this._entityListCache.store_search=s.concat(e)}_dropModelCandidates(){this._catalog.typeDevices=void 0,this._catalog.typeDevicesFor=null,this._catalog.modelIndex=new Map,this._entityListCache&&delete this._entityListCache.store_search}_ensureStoreTypeDevices(){const t=this._devices[this._selIdx];if(!t||!this._onlineEnabled())return;const e=this._storeApplianceType();this._catalog.typeDevicesFor===e&&this._catalog.typeDevices!==void 0||(this._catalog.typeDevicesFor=e,this._catalog.typeDevices=null,this._ws({type:`${w}/store_search_devices`,entry_id:t.entry_id,query:null,appliance_type:e,include_pending:!0}).then(s=>{this._catalog.typeDevicesFor===e&&(this._catalog.typeDevices=s&&s.items||[])}).catch(()=>{this._catalog.typeDevicesFor===e&&(this._catalog.typeDevices=[])}).finally(()=>{this._syncStoreSearchCandidates(),this._refreshComboAfterLoad(["wd-store-q"],t.entry_id,!!(this._catalog.typeDevices||[]).length)}))}async _loadCatalogDevices(t){this._entityListCache=this._entityListCache||{};const e=this._devices[this._selIdx];if(!e||!this._onlineEnabled()||!t){this._catalog.devices=[],this._entityListCache.store_model=[];return}try{const s=await this._ws({type:`${w}/store_search_devices`,entry_id:e.entry_id,query:t,appliance_type:this._storeApplianceType(),include_pending:!this._catalog.approvedOnly});this._catalog.forBrand===t&&(this._catalog.devices=s&&s.items||[])}catch{this._catalog.forBrand===t&&(this._catalog.devices=[])}this._entityListCache.store_model=(this._catalog.devices||[]).map(s=>s.model).filter(Boolean),this._refreshComboAfterLoad("wd-store-model",e.entry_id)}async _loadShareProfiles(){const t=this._devices[this._selIdx],e=this._modal;if(!t||!e||e.type!=="store-share")return;const s=(this._opts.store_brand||"").trim(),a=(this._opts.store_model||"").trim();if(!s||!a){e.profiles=[],this._modal===e&&this._render();return}try{const r=await this._ws({type:`${w}/store_get_device_profiles`,entry_id:t.entry_id,brand:s,model:a,appliance_type:this._opts.device_type||""});if(this._modal!==e)return;e.profiles=r&&r.items||[],e.deviceId=r&&r.device_id||null,e.storeUnreachable=!!(r&&r.error==="store_unreachable")}catch{this._modal===e&&(e.profiles=[])}this._modal===e&&this._render()}_htmlAutomations(){const t=this._deviceAutomations||[],e=t.length?t.map(r=>`\u{1F517} ${d(r.name)}${r.enabled?"":` ${this._t("lbl.automation_off",{},"(off)")}`}`).join(""):`${this._autoLoading?this._t("msg.loading",{},"Loading\u2026"):this._t("hdr.no_automations",{},"No automations reference this device yet.")}`,s=Array.isArray(this._opts.notify_actions)?this._opts.notify_actions:[],a=s.length?`
${this._t("msg.legacy_actions_title",{count:s.length},`${s.length} legacy custom action${s.length>1?"s":""} still running`)}

${this._t("msg.old_actions_warning",{},"Configured with the old actions editor (now removed). They still fire on cycle events but can no longer be edited here. Convert them into a normal automation, or remove them.")}

`:"";return`
${this._t("hdr.automations",{},"Automations")}

${this._t("msg.automations_intro",{start:vt("ha_washdata_cycle_started"),end:vt("ha_washdata_cycle_ended")},"WashData fires {start} / {end} events and exposes entities, so notifications and actions are best built as normal Home Assistant automations. Automations that use this device appear below.")}

${a}
${e}
${this._t("btn.from_template",{},"From template \u25BE")}
`}async _loadDeviceAutomations(t){this._deviceAutomations=[];const e=this._hass;if(!(!e||!e.callWS))try{let s=null;const a=e.devices||{};for(const n of Object.values(a))if((n.config_entries||[]).includes(t)){s=n.id;break}const r=s?await e.callWS({type:"search/related",item_type:"device",item_id:s}):await e.callWS({type:"search/related",item_type:"config_entry",item_id:t});if(!this._isActiveEntry(t))return;const i=r&&r.automation||[],o=e.states||{};this._deviceAutomations=i.map(n=>{const l=o[n]&&o[n].attributes||{};return{entity_id:n,id:l.id,name:l.friendly_name||n,enabled:o[n]?o[n].state==="on":!0}}).filter(n=>n.id)}catch{this._isActiveEntry(t)&&(this._deviceAutomations=[])}}_navigate(t){try{history.pushState(null,"",t),this.dispatchEvent(new CustomEvent("location-changed",{bubbles:!0,composed:!0,detail:{replace:!1}}))}catch{try{window.location.assign(t)}catch{}}}async _newAutomationFromEvent(t){const e=this._devices[this._selIdx];if(!e)return;const s=this._hass,a=t==="started"?"ha_washdata_cycle_started":"ha_washdata_cycle_ended",r=e.title||"WashData",i={alias:t==="started"?this._tText("msg.automation_alias_started",{device:r},"{device}: cycle started"):this._tText("msg.automation_alias_finished",{device:r},"{device}: cycle finished"),description:t==="started"?this._tText("msg.automation_desc_started",{device:r},"Runs when a {device} cycle starts. Add your actions (notify, lights, ...)."):this._tText("msg.automation_desc_finished",{device:r},"Runs when a {device} cycle finishes. Add your actions (notify, lights, ...)."),mode:"single",trigger:[{platform:"event",event_type:a,event_data:{entry_id:e.entry_id}}],condition:[],action:[]},o="washdata_"+Date.now().toString(36);try{s&&s.callApi?(await s.callApi("POST","config/automation/config/"+o,i),this._navigate("/config/automation/edit/"+o)):this._navigate("/config/automation/edit/new")}catch(n){this._showToast(this._tText("msg.toast_automation_failed",{error:n.message||n},"Could not create automation: "+(n.message||n)),"error")}}async _convertLegacyActions(){const t=this._devices[this._selIdx],e=this._hass,s=Array.isArray(this._opts.notify_actions)?this._opts.notify_actions:[];if(!t||!s.length)return;const a={alias:`${t.title||"WashData"}: migrated custom actions`,description:"Migrated from WashData legacy custom actions (which ran on cycle start, finish and live). Trim the triggers as needed. Note: old {device}/{duration}-style placeholders are NOT templated here - replace them with Jinja templates such as {{ trigger.event.data.device_name }}.",mode:"single",trigger:[{platform:"event",event_type:"ha_washdata_cycle_started",event_data:{entry_id:t.entry_id}},{platform:"event",event_type:"ha_washdata_cycle_ended",event_data:{entry_id:t.entry_id}}],condition:[],action:s},r="washdata_"+Date.now().toString(36);if(!e||!e.callApi){this._showToast(this._t("msg.toast_no_automation",{},"Cannot create automation here"),"error");return}let i=!1;try{await e.callApi("POST","config/automation/config/"+r,a),i=!0,await this._ws({type:`${w}/set_options`,entry_id:t.entry_id,options:{notify_actions:[]}}),this._isActiveEntry(t.entry_id)&&(this._opts={...this._opts,notify_actions:[]}),this._showToast(this._t("msg.toast_automation_migrated",{},"Actions migrated to an automation; opening editor")),this._navigate("/config/automation/edit/"+r)}catch(o){const n=o.message||o;if(!i){this._showToast(this._tText("msg.toast_convert_failed",{error:n},"Convert failed: "+n),"error");return}let l=null;try{const c=await this._ws({type:`${w}/get_options`,entry_id:t.entry_id}),p=c&&c.options||{},_=Array.isArray(p.notify_actions)?p.notify_actions:[];l=_.length>0,this._isActiveEntry(t.entry_id)&&(this._opts={...this._opts,notify_actions:_})}catch{l=null}if(l===!1){this._showToast(this._t("msg.toast_automation_migrated",{},"Actions migrated to an automation; opening editor")),this._navigate("/config/automation/edit/"+r);return}if(l===!0){try{await e.callApi("DELETE","config/automation/config/"+r),this._showToast(this._tText("msg.toast_convert_rolled_back",{error:n},"Migration failed and was rolled back (no automation left behind): "+n),"error")}catch{this._showToast(this._t("msg.toast_convert_orphan",{},"The automation was created, but clearing the old actions failed. Do not retry: remove the legacy actions manually to avoid a duplicate automation."),"error")}return}this._showToast(this._t("msg.toast_convert_orphan",{},"The automation was created, but clearing the old actions failed. Do not retry: remove the legacy actions manually to avoid a duplicate automation."),"error")}}_htmlSettingsSection(t){const e=this._opts&&this._opts.device_type||"",s=this._settingsLevel()==="basic",a=l=>!(l.id==="ml_training"||e&&l.notDeviceTypes&&l.notDeviceTypes.includes(e)||e&&l.onlyDeviceTypes&&!l.onlyDeviceTypes.includes(e)||s&&!this._secHasBasicFields(l)),r=yt.find(l=>l.id===this._settingsSec&&a(l))||yt.find(l=>a(l))||yt[0],i=r.intro||this._t("section."+r.id+".intro",{},"")?`

${d(this._t("section."+r.id+".intro",{},r.intro||""))}

`:"",o="";if(r.id==="notifications"){const l=`

${this._t("msg.notify_services_hint",{entity:vt("notify.<name>"),vars:vt(""+d(Pe)+"")},"Use {entity} service IDs (comma-separated for multiple). Template variables: {vars}.")}

`,c=r.groups.map(_=>{const f=(_.fields||[]).filter(u=>this._settingFieldVisible(u)).map(u=>this._renderField(u,t)).filter(Boolean).join("");return f?`
${d(this._t("setting_group."+ge(_.sub)+".label",{},_.sub))}
${f}
`:""}).join("");return`${s?"":this._htmlAutomations()}${l}${c}`}if(r.groups){const l=r.groups.map(c=>{const p=c.sub?`
${d(this._t("setting_group."+ge(c.sub)+".label",{},c.sub))}
`:"",_=(c.fields||[]).filter(f=>this._settingFieldVisible(f)).map(f=>this._renderField(f,t)).filter(Boolean).join("");return _?`${p}
${_}
`:""}).join("");return`${i}${o}${l}`}const n=(r.fields||[]).filter(l=>this._settingFieldVisible(l)).map(l=>this._renderField(l,t)).filter(Boolean).join("");return`${i}${o}
${n}
`}_htmlSettingsSearch(t,e){const s=this._opts&&this._opts.device_type||"",a=yt.filter(n=>!(n.id==="ml_training"||s&&n.notDeviceTypes&&n.notDeviceTypes.includes(s)||s&&n.onlyDeviceTypes&&!n.onlyDeviceTypes.includes(s))),r=n=>{const l=n.key?`${this._tText("setting."+n.key+".label",{},"")} ${n.doc!=null?this._tText("setting."+n.key+".doc",{},""):""}`:"";return`${n.label||""} ${n.key||""} ${n.doc||""} ${n.hint||""} ${l}`.toLowerCase().includes(e)};let i="",o=0;for(const n of a){const c=(n.fields||(n.groups||[]).flatMap(_=>_.fields||[])).filter(r);if(!c.length)continue;const p=c.map(_=>this._renderField(_,t)).filter(Boolean).join("");p&&(o+=c.length,i+=`
${d(this._t("section."+n.id+".label",{},n.label))}
${p}
`)}return o?i:`

${this._t("msg.no_settings_match",{q:e},'No settings match "{q}"')}

`}_sugCountForDevice(t){return t?Array.isArray(t.suggestion_keys)?t.suggestion_keys.length:t.suggestions_count||0:0}_htmlSettingsSugOnly(t){const e=new Set((this._suggestions||[]).map(i=>i.key));if(!e.size)return`

${this._t("msg.no_suggestions",{},"No active suggestions.")}

`;const s=this._opts&&this._opts.device_type||"",a=yt.filter(i=>!(i.id==="ml_training"||s&&i.notDeviceTypes&&i.notDeviceTypes.includes(s)||s&&i.onlyDeviceTypes&&!i.onlyDeviceTypes.includes(s)));let r="";for(const i of a){const n=(i.fields||(i.groups||[]).flatMap(c=>c.fields||[])).filter(c=>e.has(c.key));if(!n.length)continue;const l=n.map(c=>this._renderField(c,t)).filter(Boolean).join("");l&&(r+=`
${d(this._t("section."+i.id+".label",{},i.label))}
${l}
`)}return r||`

${this._t("msg.no_suggestions",{},"No active suggestions.")}

`}_htmlMlTab(){const t=this._opts;if(!Object.keys(t).length)return`
\u{1F916}
${this._t("msg.loading",{},"Loading\u2026")}
`;const e=this._mlTrainingStatus,s=this._devices[this._selIdx],a=s&&s.entry_id,r=yt.find(n=>n.id==="ml_training"),i=r?(r.fields||[]).map(n=>this._renderField(n,t)).filter(Boolean).join(""):"",o=this._busy.has("save-settings");return`
${this._t("hdr.ml_smart_learning",{},"Smart Learning")}

${this._t("msg.ml_intro",{},"WashData ships with smart models that work out of the box.")}

${this._htmlMlStatusSection(e,a)}
${this._t("hdr.ml_settings_card",{},"Settings")}

${this._t("msg.ml_settings_intro",{},"Two independent switches: one applies the models while a cycle runs, the other lets WashData fine-tune them to your machine over time.")}

${i}

${this._t("msg.saving_triggers_reload",{},"Saving reloads the integration. Entities may briefly show as unavailable.")}

${this._htmlMlLearnedSection(e)} `}_htmlMlStatusSection(t,e){const s=e&&this._busy.has("ml-train-now:"+e)||t&&t.running,a=this._canEdit()?``:"";if(!t)return`
${this._t("hdr.status",{},"Status")}

${this._t("msg.loading",{},"Loading\u2026")}

`;const r=Object.keys(t.on_device_models||{}).length,i=r?`${this._t("ml.personalized",{},"\u25CF Personalized to this machine")} ${this._t("lbl.models_fine_tuned",{count:r},"(fine-tuned models: "+r+")")}`:`${this._t("ml.builtin_models",{},"\u25CF Using built-in models")}`,o=t.cycle_count||0,n=t.min_cycles||0,l=o>=n,c=n>0?Math.min(100,Math.round(o/n*100)):100,p=l?"var(--success-color,#4caf50)":"var(--warning-color,#ff9800)",_=Math.max(0,n-o),f=l?this._t("msg.enough_data",{current:o,min:n},`Enough data to learn from (${o}/${n} cycles).`):this._t("msg.collecting_data",{need:_,current:o,min:n},`Collecting data. Cycles still needed before fine-tuning can start: ${_} (${o}/${n}).`),u=`
`,b=t.last_trained?et(t.last_trained):this._tText("ml.never",{},"never"),v=s?` ${this._t("status.fine_tuning",{},"fine-tuning now\u2026")}`:t.enabled?this._t("lbl.auto_fine_tune_on",{hour:String(t.hour).padStart(2,"0")},`auto fine-tune on (around ${String(t.hour).padStart(2,"0")}:00)`):this._t("lbl.auto_fine_tune_off",{},"auto fine-tune off");return`
${this._t("hdr.status",{},"Status")}
${a}
${i}

${f}

${u}

${this._t("lbl.last_checked",{},"Last checked:")} ${d(b)} \xB7 ${v}

`}_htmlMlLearnedSection(t){if(!t)return"";const e=t.on_device_models||{},s=Object.keys(e),a=this._busy.has("ml-revert-models"),r=t.last_run||{};let i;if(s.length){const o=s.map(l=>{const c=e[l]||{},p=c.trained_at?et(c.trained_at):this._tText("ml.trained_unknown",{},"at an unknown time");return`
${d(c.label_key?this._t(c.label_key,{},c.label||l):c.label||l)}${this._mlTrendBadge(c.trend)}
${d(c.blurb_key?this._t(c.blurb_key,{},c.blurb||""):c.blurb||"")} \xB7 ${this._t("ml.fine_tuned_at",{when:p},"fine-tuned {when}")}
${this._mlLastRunLine(r[l])}
${this._mlQualityChip(c)}
`}).join(""),n=this._canEdit()?``:"";i=`
${o}
${n}`}else{const o=Object.keys(r).map(n=>this._mlLastRunLine(r[n])).filter(Boolean).join("");i=`

${this._t("msg.no_fine_tuned",{},"Nothing fine-tuned yet - WashData is using its built-in models.")}

${o}`}return`
${this._t("hdr.ml_learned",{},"What WashData has learned")}

${this._t("msg.ml_learned_intro",{},"Models fine-tuned to this machine.")}

${i}
`}_mlQualityChip(t){let e=0,s="",a=t.metric_key?this._tText(t.metric_key,t.metric_params||{},t.metric||""):t.metric||"";if(t.model_mae!=null&&t.naive_mae!=null&&t.naive_mae>0){const i=Math.max(0,(t.naive_mae-t.model_mae)/t.naive_mae);e=Math.min(1,i)*100,s=i>=.5?this._t("ml.fit_strong",{},"Strong"):i>=.2?this._t("ml.fit_good",{},"Good"):this._t("ml.fit_slight",{},"Slight"),a=this._tText("ml.better_than_baseline",{pct:(i*100).toFixed(0),metric:a},`${(i*100).toFixed(0)}% better than the baseline estimate (${a})`)}else return"";Number.isInteger(t.held_out_cycles)&&(a+=" \xB7 "+this._tText("ml.held_out_count",{count:t.held_out_cycles},`measured on ${t.held_out_cycles} held-out cycles`));const r=e>=70?"var(--success-color,#4caf50)":e>=40?"var(--warning-color,#ff9800)":"var(--secondary-text-color)";return`
${s} ${this._t("ml.fit_word",{},"fit")}
`}_mlTrendBadge(t){if(!t)return"";const s={improving:[this._t("badge.improving",{},"\u2197 improving"),"var(--success-color,#4caf50)",this._t("ml.trend_improving_tip",{},"This model's fit has improved across recent re-checks.")],declining:[this._t("badge.declining",{},"\u2198 declining"),"var(--warning-color,#ff9800)",this._t("ml.trend_declining_tip",{},"This model's fit has slipped across recent re-checks \u2013 reviewing more cycles may help it re-learn.")],steady:[this._t("badge.steady",{},"\u2192 steady"),"var(--secondary-text-color)",this._t("ml.trend_steady_tip",{},"This model's fit has held roughly steady across recent re-checks.")]}[t];return s?` ${s[0]}`:""}_mlSkipReason(t){const e={insufficient_rows:"Not enough usable cycles yet: {count} labelled, clean cycles so far.",holdout_too_small:"Too few cycles to test a new model fairly: {held_out} could be set aside for testing, {min} are needed.",not_better_than_naive:"The new model was not clearly better than the simple estimate (error {model} vs {naive}).",not_better_than_incumbent:"The new model was not better than the one in use (error {model} vs {incumbent}), so the current one was kept.",fit_failed:"Training failed: {error}",unknown:"Nothing new was learnt."},s=Object.prototype.hasOwnProperty.call(e,t&&t.reason_code)?t.reason_code:"unknown",a=t&&t.reason_params||{},r=s==="insufficient_rows"?{count:Number(a.cycles)||0}:{...a};return[s,this._tText(`ml.skip_reason.${s}`,r,e[s])]}_mlLastRunLine(t){if(!t||t.promoted!==!1)return"";const[e,s]=this._mlSkipReason(t),a=t.ts?et(t.ts):this._tText("ml.trained_unknown",{},"at an unknown time");return`
${this._t("ml.last_run_not_promoted",{when:a,reason:s},"Last check ({when}): {reason}")}
`}_pgOverrideFields(){return[["start_threshold_w","Start Threshold","W","Minimum watts to count as started","detection"],["stop_threshold_w","Stop Threshold","W","Below this, machine counts as off","detection"],["off_delay","Off Delay","s","Seconds of low power before cycle ends","timing"],["min_off_gap","Min Off Gap","s","Gap required to separate two cycles","timing"],["completion_min_seconds","Min Cycle Duration","s","Shortest run that counts as a real cycle","timing"],["start_duration_threshold","Start Duration","s","Seconds above threshold to confirm start","timing"],["interrupted_min_seconds","Interrupted Min","s","Short cycles flagged as interrupted","advanced"],["anti_wrinkle_enabled","Enable Anti-Wrinkle Detection","","Absorb the tumble pulses after the main phase instead of reading them as new cycles","advanced","bool"],["anti_wrinkle_max_power","Max Anti-Wrinkle Power","W","A pulse above this ends anti-wrinkle and opens a new cycle","advanced"],["anti_wrinkle_max_duration","Max Anti-Wrinkle Duration","s","A pulse longer than this ends anti-wrinkle and opens a new cycle","advanced"],["anti_wrinkle_exit_power","Anti-Wrinkle Exit Power","W","Quiet level between tumble pulses; only takes effect above the Stop Threshold","advanced"],["anti_wrinkle_idle_timeout","Max Pulse Gap","s","Quiet time allowed between two tumble pulses before anti-wrinkle ends","advanced"],["dishwasher_end_spike_quiet_release","Passive-Dry Quiet Release","s","Dishwasher: quiet seconds after expected duration before the end-of-cycle drain wait is released","advanced"],["smart_termination_duration_ratio","Smart Termination Ratio","","Fraction of the program's average duration a cycle must reach before Smart Termination may end it early; lower it if run times vary","advanced"],["anti_crease_finalize_ratio","Anti-Crease Finalize Ratio","","Fraction of the program's average duration a cycle must reach before the anti-crease tail may finish it; lower it on dryers, keep washers at the default","advanced"],["curve_preroll_seconds","Curve Pre-roll","s","Seconds of readings from aborted start attempts carried into the front of a cycle's curve; 0 = off","timing"],["profile_match_min_duration_ratio","Min Duration Ratio","","Minimum cycle length as a fraction of the profile's duration; default 0.1 (10%)","matching"],["profile_match_max_duration_ratio","Max Duration Ratio","","Maximum cycle length as a fraction of the profile's duration; default 1.8 (180%)","matching"]]}_pgFieldVal(t,e){const s=e||{};if(s[t]!==void 0)return s[t];const a=this._pgEffective||{};if(a[t]!==void 0&&a[t]!==null)return a[t];const r=this._opts||{};if(r[t]!==void 0&&r[t]!==null)return r[t];const i=wt[t]||{};if(i.def!==void 0)return i.def;const o=this._constants&&this._constants.pgMatchDefaults||{};return o[t]!==void 0?o[t]:te[t]!==void 0?te[t]:""}async _pgFetchSettings(t,e=!0){try{const s=await this._ws({type:`${w}/get_playground_settings`,entry_id:t,include_suggestions:e});if(!this._isActiveEntry(t))return;this._pgEffective=s.effective||{},this._pgPublishable=Array.isArray(s.publishable)?s.publishable:null,this._pgPresets=Array.isArray(s.presets)?s.presets:[],this._pgPresetLimit=s.preset_limit||0,this._pgPresetSel&&!this._pgPresets.some(a=>a.name===this._pgPresetSel)&&(this._pgPresetSel=""),e&&this._pgApplySuggestions(s)}catch(s){this._pgIsUnknownCmd(s)&&(this._pgNeedsRestart=!0)}}_pgApplySuggestions(t){this._pgSuggClassic=t.classic_suggestions&&typeof t.classic_suggestions=="object"?t.classic_suggestions:{}}async _pgFetchSuggestions(t){try{const e=await this._ws({type:`${w}/get_playground_settings`,entry_id:t,include_suggestions:!0});if(!this._isActiveEntry(t))return;this._pgApplySuggestions(e),this._tab==="playground"&&this._render()}catch{}}_pgCurrentValues(){const t={};for(const[e]of this._pgOverrideFields()){const s=this._pgFieldVal(e,{});s!==""&&s!==null&&s!==void 0&&(t[e]=s)}this._pgThreshStart!=null&&(t.start_threshold_w=this._pgThreshStart),this._pgThreshStop!=null&&(t.stop_threshold_w=this._pgThreshStop);for(const[e,s]of Object.entries(this._pgParamOverrides||{}))t[e]=s;return t}_pgStagedVal(t){return t==="start_threshold_w"?this._pgThreshStart??void 0:t==="stop_threshold_w"?this._pgThreshStop??void 0:this._pgParamOverrides[t]}_pgSetStaged(t,e){t==="start_threshold_w"?this._pgThreshStart=e:t==="stop_threshold_w"?this._pgThreshStop=e:this._pgParamOverrides[t]=e}_pgClearStaged(t){t==="start_threshold_w"?this._pgThreshStart=null:t==="stop_threshold_w"?this._pgThreshStop=null:delete this._pgParamOverrides[t]}_pgChangedKeys(){const t=this._pgEffective||{},e=[];for(const[s]of this._pgOverrideFields()){const a=this._pgStagedVal(s);if(a==null)continue;const r=t[s];r!=null&&this._pgSameVal(a,r)||e.push(s)}return e}_pgSameVal(t,e){if(typeof t=="boolean"||typeof e=="boolean")return!!t==!!e;const s=parseFloat(t),a=parseFloat(e);return!isNaN(s)&&!isNaN(a)?Math.abs(s-a)<1e-9:String(t)===String(e)}_pgIsPublishable(t){return Array.isArray(this._pgPublishable)?this._pgPublishable.includes(t):!!wt[t]}_htmlPgControlPanel(){const t=this._pgChangedKeys(),e=this._canEdit(),s=!!this._pgEffective,a=t.filter(b=>this._pgIsPublishable(b)),r=s?t.length?`${this._t("msg.pg_n_changed",{n:t.length},t.length+" changed vs live settings")}`:`\u2713 ${this._t("msg.pg_matches_live",{},"Matches live settings")}`:`${this._t("msg.pg_live_unavailable",{},"Live settings unavailable, showing defaults.")}`,i=this._pgPresets||[],o=``+i.map(b=>``).join(""),n=!!this._pgPresetSel,l=this._pgPresetAtLimit((this._pgPresetName||"").trim()),c=`
${e?``:""}
`,p=e?`
${this._t("msg.pg_preset_limit",{n:this._pgPresetLimit},"Preset limit reached ({n})")}
`:"",_=e&&a.length?``:"",f=Object.keys(this._pgSuggClassic||{}).length,u=f>0?``:"";return`
${this._t("hdr.pg_settings_source",{},"Settings source")} ${r}

${this._t("msg.pg_ctrl_intro",{},"Values start from this device's live integration settings. Edits stay in the Playground until you publish them.")}

${u} ${_}
${c} ${p}
`}_pgPresetAtLimit(t){const e=this._pgPresets||[];return this._pgPresetLimit>0&&e.length>=this._pgPresetLimit&&!e.some(s=>s.name===t)}_pgApplyPresetValues(t){this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={};const e=this._pgEffective||{};for(const[s,a]of Object.entries(t||{})){if(a==null)continue;const r=e[s];r!=null&&this._pgSameVal(a,r)||this._pgSetStaged(s,a)}}_pgLoadPreset(){const t=(this._pgPresets||[]).find(e=>e.name===this._pgPresetSel);t&&(this._pgApplyPresetValues(t.values),this._showToast(this._tText("toast.pg_preset_loaded",{name:t.name},`Preset "${t.name}" loaded`)),this._render(),requestAnimationFrame(()=>this._pgDrawCanvas()))}async _pgSavePreset(){const t=this._devices[this._selIdx],e=(this._pgPresetName||"").trim();!t||!this._canEdit()||!e||(this._pgPresets||[]).some(s=>s.name===e)&&!confirm(this._tText("msg.pg_preset_overwrite",{name:e},`Overwrite the preset "${e}"?`))||(await this._busyRun("pg-preset-save",async()=>{try{const s=await this._ws({type:`${w}/save_playground_preset`,entry_id:t.entry_id,name:e,values:this._pgCurrentValues()});if(!this._isActiveEntry(t.entry_id))return;this._pgPresets=Array.isArray(s.presets)?s.presets:this._pgPresets,this._pgPresetSel=e,this._pgPresetName="",this._showToast(this._tText("toast.pg_preset_saved",{name:e},`Preset "${e}" saved`))}catch(s){this._showToast(this._tText("msg.toast_save_failed",{error:s.message||s},"Save failed: "+(s.message||s)),"error")}}),this._render())}async _pgDeletePreset(){const t=this._devices[this._selIdx],e=this._pgPresetSel;!t||!this._canEdit()||!e||confirm(this._tText("msg.pg_preset_delete_confirm",{name:e},`Delete the preset "${e}"?`))&&(await this._busyRun("pg-preset-delete",async()=>{try{const s=await this._ws({type:`${w}/delete_playground_preset`,entry_id:t.entry_id,name:e});if(!this._isActiveEntry(t.entry_id))return;this._pgPresets=Array.isArray(s.presets)?s.presets:(this._pgPresets||[]).filter(a=>a.name!==e),this._pgPresetSel=""}catch(s){this._showToast(this._tText("msg.toast_error",{error:s.message||s},"Error: "+(s.message||s)),"error")}}),this._render())}async _pgLoadLive(){const t=this._devices[this._selIdx];if(!t)return;const e=this._pgChangedKeys();e.length&&!confirm(this._tText("msg.pg_load_live_confirm",{n:e.length},`Discard ${e.length} Playground edit(s) and reload the integration's current settings?`))||(await this._busyRun("pg-load-live",async()=>{this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},await this._pgFetchSettings(t.entry_id)}),this._render(),requestAnimationFrame(()=>this._pgDrawCanvas()))}_pgLoadSuggested(){const t=this._pgSuggClassic||{},e=this._pgEffective||{};let s=0;for(const[a,r]of Object.entries(t)){if(r==null)continue;const i=e[a];i!=null&&this._pgSameVal(r,i)||(this._pgSetStaged(a,r),s++)}s===0?this._showToast(this._t("msg.pg_sugg_none",{},"All suggestions already match the current settings")):this._showToast(this._tText("toast.pg_sugg_loaded",{n:s},`Staged ${s} suggested value(s) - run the Playground to compare`)),this._render(),requestAnimationFrame(()=>this._pgDrawCanvas())}async _pgPublishOne(t){const e=this._devices[this._selIdx];if(!e||!this._canEdit()||!t||!this._pgIsPublishable(t))return;const s=this._pgStagedVal(t);if(s==null)return;const a=this._t("setting."+t+".label",{},t);confirm(this._tText("msg.pg_publish_one_confirm",{label:a,value:s},`Save ${a} = ${s} to this device's settings?`))&&(await this._busyRun("pg-publish-"+t,async()=>{try{if(await this._ws({type:`${w}/set_options`,entry_id:e.entry_id,options:{[t]:s}}),!this._isActiveEntry(e.entry_id))return;this._opts={...this._opts,[t]:s},this._pgEffective&&(this._pgEffective={...this._pgEffective,[t]:s}),this._pgClearStaged(t),this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading"))}catch(r){this._showToast(this._tText("msg.toast_save_failed",{error:r.message||r},"Save failed: "+(r.message||r)),"error")}}),this._render())}_htmlPlayground(){if(!this._devices[this._selIdx])return`
${this._t("msg.no_device_selected",{},"No device selected.")}
`;const e=this._cycles||[],s=this._profiles||[],a=e.map(u=>{const b=u.profile_name||u.matched_profile||this._t("lbl.unlabelled",{},"Unlabelled"),v=u.duration?` \xB7 ${Math.round(u.duration/60)} ${this._tText("lbl.timer_min",{},"min")}`:"",T=u.start_time?` \xB7 ${et(u.start_time)}`:"";return``}).join(""),r=``+s.map(u=>``).join(""),i=this._pgLoading,o=`
${i?``:""}
`,n=i?`
${this._t("msg.pg_simulating",{},"Simulating cycle\u2026")}
`:"",l=!this._pgPowerPts&&!i?`
〜
${this._t("msg.pg_canvas_empty3",{},"Pick a cycle above and press Run to simulate it. Then hover to read values, scroll or pinch to zoom, and drag to pan.")}
`:"",c=`${n}
${l}
`,p=this._htmlPgStrip(),_=this._pgNeedsRestart?`

\u26A0 ${this._t("msg.pg_restart_note",{},"Restart Home Assistant to enable simulation tools.")}

`:"",f=`${o}${c}${p}
${this._htmlPgParamRows()}
${this._htmlPgAlerts()}${this._htmlPgAnalysis()}
`;return`
${this._t("hdr.playground",{},"Playground")}

${this._t("msg.playground_intro",{},"Try settings on your real cycle data. Nothing here changes your live settings until you apply it.")}

${_} ${f} ${this._htmlPgDrawer()}
`}_htmlPgDrawer(){const t=this._pgAnalysisTab||"history",s=`
${[["history",this._t("lbl.pg_mode_history",{},"Test on history")],["sweep",this._t("lbl.pg_mode_optimize",{},"Optimize")]].map(([r,i])=>``).join("")}
`,a=t==="sweep"?this._htmlPgSweepMode():this._htmlPgHistoryMode();return`
${this._t("hdr.pg_across_cycles",{},"Across your cycles")} ${s}
${a}
`}_htmlPgParamRows(){const t=this._pgOverrideFields(),e=new Set(["start_threshold_w","stop_threshold_w"]),s={detection:"#2a78d6",timing:"#1baf7a",advanced:"#eda100",matching:"#a05cd6"},a={detection:this._t("lbl.pg_group_detection",{},"Detection triggers"),timing:this._t("lbl.pg_group_timing",{},"Timing rules"),advanced:this._t("lbl.pg_group_advanced",{},"Edge cases"),matching:this._t("lbl.pg_group_matching",{},"Program matching")};let r="";const i=t.map(([o,n,l,c,p,_])=>{const f=this._t("setting."+o+".label",{},n),u=this._pgFieldVal(o,{});let b;o==="start_threshold_w"?b=this._pgThreshStart??u:o==="stop_threshold_w"?b=this._pgThreshStop??u:b=this._pgParamOverrides[o]??u;const v=_==="bool",T=e.has(o),C=l||"",x=s[p]||"#2a78d6",$=a[p]||"";let L="";p&&p!==r&&(L=`
${d($)}
`,r=p);const D=this._pgStagedVal(o),B=D!=null&&!(u!==""&&u!==null&&u!==void 0&&this._pgSameVal(D,u)),U=this._canEdit()&&B&&this._pgIsPublishable(o)?``:'';return`${L}
${d(f)}${T?` \u2195`:""}
${c?`
${d(this._t("pg_desc."+o,{},c))}
`:""}
${v?``:``} ${C?`${d(C)}`:""} ${U}
`}).join("");return`
${this._t("hdr.pg_detection_params",{},"Detection settings")}
${this._htmlPgControlPanel()} ${i}
${this._pgDetailBusy?'':""}
`}_htmlPgAlerts(){const t=this._pgDetail;if(!t)return"";const e=t.outcome||{},s={error:"var(--error-color,#f44336)",warn:"var(--warning-color,#ff9800)",info:"var(--info-color,#2196f3)"},a=Array.isArray(t.alerts)?t.alerts:[],r=a.length?a.map(f=>`
${d(this._pgAlertLabel(f.code))}
${d(f.detail_key?this._tText(f.detail_key,f.detail_params||{},f.detail||""):f.detail||"")}
`).join(""):`
\u2713 ${this._t("msg.pg_no_alerts",{},"No issues detected in this run.")}
`,i=e.termination_reason?String(e.termination_reason):"\u2014",o=e.final_duration_s?Math.round(e.final_duration_s/60)+" min":"\u2014",n=e.projected_energy_wh!=null?e.projected_energy_wh>=1e3?it(e.projected_energy_wh/1e3,2)+" kWh":it(Math.round(e.projected_energy_wh),0)+" Wh":"\u2014",l=(t.events||[]).some(f=>String(f.type||"").startsWith("notify_")),c=(f,u)=>`
${d(u)}
${d(f)}
`,_=this._pgDetailSig!=null&&this._pgDetailSig!==this._pgOverrideSig(this._pgCurrentOverride())?`
\u26A0 ${this._t("msg.pg_outcome_stale",{},"Settings changed since this simulation ran. It still shows the old result.")}
`:"";return`
${this._t("hdr.pg_outcome",{},"Simulation outcome")}
${_}
${c(this._t("lbl.pg_ended",{},"Ended"),i)} ${c(this._t("lbl.duration",{},"Duration"),o)} ${c(this._t("lbl.pg_proj_energy",{},"Proj. energy"),n)} ${e.label_reason?c(this._t("lbl.pg_autolabel",{},"Auto-label"),this._pgLabelVerdict(e).short):""}
${r}
${l?`

\u{1F514} ${this._t("msg.pg_notify_partial",{},"Markers show start, almost-done, finish, milestone and quiet-hours notifications only. Live updates, unload reminders and overrun alerts are not simulated.")}

`:""}
`}_pgLabelVerdict(t){if(t.would_label&&t.label_profile)return{ok:!0,short:String(t.label_profile),tip:this._tText("msg.pg_label_yes",{name:t.label_profile},`Would be labelled "${t.label_profile}" at cycle end`)};const s={no_winner:this._tText("pg_label_reason.no_winner",{},"No program fit"),below_floor:this._tText("pg_label_reason.below_floor",{},"Below the learning confidence"),ambiguous:this._tText("pg_label_reason.ambiguous",{},"Flagged ambiguous"),margin:this._tText("pg_label_reason.margin",{},"Too close to the runner-up"),unknown_profile:this._tText("pg_label_reason.unknown_profile",{},"Program no longer exists")}[t.label_reason]||this._tText("pg_label_reason.not_evaluated",{},"Not evaluated");return{ok:!1,short:this._tText("lbl.pg_label_review",{},"Asks for review"),tip:this._tText("msg.pg_label_no",{reason:s},`Would ask for review: ${s}`)}}_pgAlertLabel(t){return{overrun:this._t("lbl.pg_alert_overrun",{},"Overrun"),underrun:this._t("lbl.pg_alert_underrun",{},"Underrun"),did_not_finish:this._t("lbl.pg_alert_did_not_finish",{},"Did not finish"),false_end:this._t("lbl.pg_alert_false_end",{},"Split into multiple cycles"),unmatched:this._t("lbl.pg_alert_unmatched",{},"Unmatched"),ambiguous:this._t("lbl.pg_alert_ambiguous",{},"Ambiguous match"),energy_anomaly:this._t("lbl.pg_alert_energy",{},"Energy anomaly"),timeout_end:this._t("lbl.pg_alert_timeout_end",{},"Ended by timeout, not prediction"),would_run_indefinitely:this._t("lbl.pg_alert_indefinite",{},"Would run indefinitely")}[t]||t}_htmlPgHistoryMode(){const t=this._busy.has("pg-history"),e=this._pgHistory,s=Object.keys(this._pgParamOverrides||{}).length>0||this._pgThreshStart!=null||this._pgThreshStop!=null,a=`
${this._t("lbl.last",{},"Last")} ${this._t("lbl.cycles_lc",{},"cycles")} ${s?`\u2699 ${this._t("msg.pg_override_active",{},"Using your edited settings vs. current")}`:""}
`,r=t?this._htmlPgBatchBar():"",i=`

${this._t("msg.pg_history_intro2",{},"Replay recent cycles through the real detector and matcher with the settings above. Click a row to load it in the graph; change a setting to compare before and after.")}

`;if(!e||!Array.isArray(e.rows))return`${i}${a}${this._htmlPgRecentRuns("pg_history")}${r}${t?"":`
${this._t("msg.pg_history_empty",{},"Press Run to replay your cycles.")}
`}`;let o="";if(e.diff){const u=(b,v,T)=>`${b} ${d(T)}`;o=`
${u((e.diff.newly_correct||[]).length,"var(--success-color,#4caf50)",this._t("lbl.pg_newly_correct",{},"newly correct"))} ${u((e.diff.regressed||[]).length,"var(--error-color,#f44336)",this._t("lbl.pg_regressed",{},"regressed"))} ${u((e.diff.end_timing_changed||[]).length,"var(--warning-color,#ff9800)",this._t("lbl.pg_end_timing_changed",{},"end-timing changed"))}
`}const n=e.summary||{},l=`
${this._t("msg.pg_history_summary",{detected:n.detected,correct:n.match_correct,total:n.cycles},`${n.detected}/${n.cycles} detected \xB7 ${n.match_correct} matched correctly`)}
`,c=u=>(this._cycles||[]).find(b=>b.id===u),p={};(e.baseline_rows||[]).forEach(u=>{p[u.cycle_id]=u});const _=e.rows.map(u=>{const b=c(u.cycle_id),v=b&&b.start_time?et(b.start_time):u.start_time?et(u.start_time):(u.cycle_id||"").slice(0,8),T=u.match_correct===!0?"\u2713":u.match_correct===!1?"\u2717":"\u2014",C=u.match_correct===!0?"var(--success-color,#4caf50)":u.match_correct===!1?"var(--error-color,#f44336)":"var(--secondary-text-color)",x=u.matched_profile||this._t("lbl.unlabelled",{},"Unlabelled"),$=u.duration_s?Math.round(u.duration_s/60)+"m":"\u2014",L=u.overrun_ratio!=null?` (${Math.round(u.overrun_ratio*100)}%)`:"",D=u.label_reason?this._pgLabelVerdict(u):null,B=D?` ${D.ok?"\u{1F3F7}":"?"}`:"",U=u.alerts&&u.alerts.length?` \u26A0`:"",z=p[u.cycle_id];let M="";return z&&(z.match_correct!==!0&&u.match_correct===!0?M=`\u25B2 ${this._t("lbl.pg_fixed",{},"fixed")}`:z.match_correct===!0&&u.match_correct!==!0?M=`\u25BC ${this._t("lbl.pg_broke",{},"broke")}`:String(z.termination_reason)!==String(u.termination_reason)&&(M=`${d(String(z.termination_reason||"\u2014"))}\u2192${d(String(u.termination_reason||"\u2014"))}`)),` ${d(v)} ${T} ${d(x)}${B} ${d(String(u.termination_reason||"\u2014"))} ${$}${L}${U} ${e.diff?`${M}`:""} `}).join(""),f=` ${e.diff?``:""} ${_}
${this._t("lbl.cycle",{},"Cycle")} ${this._t("lbl.match",{},"Match")} ${this._t("lbl.pg_ended",{},"Ended")} ${this._t("lbl.duration",{},"Duration")}${this._t("lbl.pg_vs_current",{},"vs current")}
`;return`${i}${a}${this._htmlPgRecentRuns("pg_history")}${o}${l}${this._htmlPgInSampleNote()}${f}`}_htmlPgInSampleNote(){return`

${this._t("msg.pg_in_sample_note",{},"These cycles helped build the profiles they are matched against, so results are optimistic: expect slightly fewer correct matches on new cycles.")}

`}_htmlPgBatchBar(){const t=this._pgBatchProgress;return t?`
${t.done}/${t.total}
`:""}_pgUpdateBatchBar(t,e){const s=this.shadowRoot;if(!s)return;const a=s.getElementById("wd-pg-batch-fill"),r=s.getElementById("wd-pg-batch-count"),i=e?Math.round(100*t/e):0;a&&(a.style.width=i+"%"),r&&(r.textContent=String(t))}async _pgRunHistory(){const t=this._devices[this._selIdx];if(!t)return;if(!(this._cycles||[]).length){this._showToast(this._t("msg.no_cycles_selected",{},"No cycles available."),"error");return}const e=Math.max(1,Math.min(St,this._pgSimCycles||20)),s=this._pgCurrentOverride();this._pgBatchProgress={done:0,total:e},this._busy.add("pg-history"),this._render();try{const a=await this._ws({type:`${w}/start_playground_history`,entry_id:t.entry_id,count:e,settings_override:s});if(this._pgHistoryTaskId=a&&a.task_id,this._pgNeedsRestart=!1,!this._pgHistoryTaskId)throw new Error("no task id");this._addProvisionalTask(this._pgHistoryTaskId,"pg_history",t.entry_id,e),this._tasksSubscribed||this._pgPollTask(this._pgHistoryTaskId)}catch(a){this._busy.delete("pg-history"),this._pgBatchProgress=null,this._pgRunFailed(a),this._render()}}_pgRunFailed(t){if(this._pgIsUnknownCmd(t)){this._pgNeedsRestart=!0;return}if((t&&(t.code||t.error&&t.error.code)||"")==="task_busy"){this._showToast(this._t("msg.pg_task_busy",{},"A Test-on-history or Optimize run is already in progress for this device. Wait for it or cancel it."),"error");return}this._showToast(this._tText("msg.toast_error",{error:t.message||t},"Error: "+(t.message||t)),"error")}_pgSweepObjectives(){return[["match_accuracy",this._t("lbl.pg_obj_match",{},"Match accuracy"),!1],["end_timing_accuracy",this._t("lbl.pg_obj_endtiming",{},"End-timing accuracy"),!1],["false_end_rate",this._t("lbl.pg_obj_falseend",{},"False-end rate"),!0],["median_overrun",this._t("lbl.pg_obj_overrun",{},"Duration off-target"),!0],["ambiguity_rate",this._t("lbl.pg_obj_ambiguity",{},"Ambiguity rate"),!0],["end_lag",this._t("lbl.pg_obj_endlag",{},"End delay (median)"),!0],["early_end_rate",this._t("lbl.pg_obj_early",{},"Early-end rate"),!0],["split_rate",this._t("lbl.pg_obj_split",{},"Split rate"),!0]]}_htmlPgSweepMode(){const t=this._busy.has("pg-sweep"),s=this._pgOverrideFields().filter(([,,,,,n])=>n!=="bool").map(([n,l])=>``).join(""),a=this._pgSweepObjectives().map(([n,l])=>``).join(""),r=`

${this._t("msg.pg_sweep_intro2",{},"Find the setting that best meets an objective across your recent cycles. Nothing changes until you apply it.")}

`,i=`
`,o=t?this._htmlPgBatchBar():"";return`${r}${i}${this._htmlPgRecentRuns("pg_sweep")}${o}${this._htmlPgSweepResult()}`}_htmlPgSweepResult(){const t=this._pgSweepNew;if(!t||!Array.isArray(t.points)||!t.points.length)return"";const e=this._pgSweepObjectives().find(x=>x[0]===t.objective),s=t.lower_is_better!=null?!!t.lower_is_better:e?e[2]:!1,a=t.points.filter(x=>x.metric!=null).map(x=>x.metric);if(!a.length)return`
${this._t("msg.pg_sweep_no_metric",{},"Not enough data to score this objective.")}
`;const r=Math.min(...a),i=Math.max(...a),o=t.best_value!==void 0,n=s?r:i,l=o?t.best_value:(t.points.find(x=>x.metric===n)||{}).value,c=o?t.best_metric:n,p=!!t.keep_current,_=(x,$)=>x!=null&&$!=null&&Math.abs(Number(x)-Number($))<1e-6,f=x=>x==null?"-":t.objective==="median_overrun"?Math.round(x*100)+"% off":t.objective==="end_lag"?`${it(x/60,1)} ${this._tText("lbl.timer_min",{},"min")}`:Math.round(x*100)+"%",u=t.points.map(x=>{const $=i>r&&x.metric!=null?(x.metric-r)/(i-r):x.metric!=null?1:0,L=!p&&_(x.value,l),D=_(x.value,t.current_value),B=L?"var(--success-color,#4caf50)":"var(--primary-color)",U=x.guarded?` \u26A0`:"";return`
${d(String(x.value))}${D?" \u25C0":""}${U}
${f(x.metric)}
`}).join(""),b=!p&&l!=null&&!_(l,t.current_value),v=this._canEdit()&&b?``:"",T=t.cycles?` \xB7 ${this._t("msg.pg_sweep_n_cycles",{count:t.cycles},`over ${t.cycles} cycles`)}`:"";return`
${p?`${this._t("msg.pg_sweep_keep_current",{value:String(t.current_value??"-"),metric:f(c)},`Keep the current value (${t.current_value??"-"}, ${f(c)}): no value did better by at least one cycle.`)}`:`${this._t("lbl.pg_best_value",{},"Best value found")}: ${d(String(l))} \xB7 ${d(f(c))}`}${T} \xB7 \u25C0 ${this._t("lbl.pg_current_value",{},"Current value:")}
${u}
${this._htmlPgInSampleNote()}${v}
`}async _pgRunSweep2(){const t=this._devices[this._selIdx];if(!t)return;const e=parseFloat(this._pgSweepFrom),s=parseFloat(this._pgSweepTo);if(isNaN(e)||isNaN(s)||e===s){this._showToast(this._t("msg.toast_range_required",{},"Set a valid From/To range."),"error");return}const a=Math.max(2,Math.min(12,this._pgSweepSteps||5)),r=Array.from({length:a},(c,p)=>+(e+(s-e)*p/(a-1)).toFixed(3)),i=this._pgSweepParam||"off_delay",o=this._pgSweepObjective||"match_accuracy",n=Math.max(1,Math.min(St,this._pgSimCycles||20)),l={type:`${w}/start_playground_sweep`,entry_id:t.entry_id,param:i,values:r,objective:o,count:n};this._pgBatchProgress={done:0,total:r.length+1},this._busy.add("pg-sweep"),this._render();try{const c=await this._ws(l);if(this._pgSweepTaskId=c&&c.task_id,this._pgNeedsRestart=!1,!this._pgSweepTaskId)throw new Error("no task id");this._addProvisionalTask(this._pgSweepTaskId,"pg_sweep",t.entry_id,r.length+1),this._tasksSubscribed||this._pgPollTask(this._pgSweepTaskId)}catch(c){this._busy.delete("pg-sweep"),this._pgBatchProgress=null,this._pgRunFailed(c),this._render()}}async _pgApplyToSettings(){const t=this._devices[this._selIdx];if(!t||!this._canEdit())return;const e={};for(const r of this._pgChangedKeys())this._pgIsPublishable(r)&&(e[r]=this._pgStagedVal(r));const s=Object.keys(e);if(!s.length)return;const a=s.map(r=>this._t("setting."+r+".label",{},r)).join(", ");confirm(this._tText("msg.pg_apply_settings_confirm",{n:s.length,list:a},`Save these ${s.length} setting(s) to this device? ${a}`))&&(await this._busyRun("pg-apply-settings",async()=>{try{if(await this._ws({type:`${w}/set_options`,entry_id:t.entry_id,options:e}),!this._isActiveEntry(t.entry_id))return;this._opts={...this._opts,...e},this._pgEffective&&(this._pgEffective={...this._pgEffective,...e});for(const r of s)this._pgClearStaged(r);this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading"))}catch(r){this._showToast(this._tText("msg.toast_save_failed",{error:r.message||r},"Save failed: "+(r.message||r)),"error")}}),this._render())}async _pgApplySweepValue(t){const e=this._devices[this._selIdx];if(!e||!this._canEdit()||t==null)return;const s=this._pgSweepParam,a=this._t("setting."+s+".label",{},s);confirm(this._tText("msg.pg_apply_confirm",{label:a,value:t},"Apply best value: "+a+" = "+t+"?"))&&await this._busyRun("pg-sweep-apply",async()=>{try{await this._ws({type:`${w}/set_options`,entry_id:e.entry_id,options:{[s]:+t}}),this._opts={...this._opts,[s]:+t},s==="start_threshold_w"?this._pgThreshStart=+t:s==="stop_threshold_w"?this._pgThreshStop=+t:this._pgParamOverrides[s]=+t,this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading")),this._render()}catch(r){this._showToast(this._tText("msg.toast_save_failed",{error:r.message||r},"Save failed: "+(r.message||r)),"error")}})}_htmlPgStrip(){return`
${this._t("lbl.pg_idle",{},"Idle")} ${this._t("lbl.power",{},"Power")} \u2014 \u2014% ${this._t("lbl.pg_time_left_model",{},"Time left (model)")} \u2014 ${this._t("lbl.energy",{},"Energy")} \u2014 ${this._t("lbl.match",{},"Match")} \u2014 ${this._t("lbl.phase",{},"Phase")} \u2014
`}_htmlPgAnalysis(){const t=this._pgDetail&&this._pgDetail.outcome,e=t&&t.matched_profile||this._pgProfileName||(this._cycles||[]).find(o=>o.id===this._pgCycleId)?.profile_name||"",s=t&&t.confidence;if(!t)return`

${this._t("msg.pg_analysis_hint2",{},"Pick a cycle and press Run to load match analysis.")}

`;let a="\u2014",r="var(--secondary-text-color)";s!=null&&(s>=.7?(a="\u2705 "+this._t("lbl.pg_strong_match",{},"Strong match"),r="var(--success-color, #4caf50)"):s>=.4?(a="\u26A0 "+this._t("lbl.pg_weak_match",{},"Weak match"),r="var(--warning-color, #ff9800)"):(a="\u274C "+this._t("lbl.pg_poor_match",{},"Poor match"),r="var(--error-color, #f44336)"));let i=`
${a}
`;if(e&&s!=null){const o=Math.round(Math.max(0,Math.min(1,s))*100);i+=`
${d(this._t("lbl.pg_match_confidence",{},"Match confidence"))}
`,i+=`
${d(e)}
${o}%
`}return`
${i}
`}async _pgFetchEnvelope(t,e){try{const s=await this._ws({type:`${w}/get_profile_envelope`,entry_id:t,profile_name:e});this._pgEnvData=s.envelope||null}catch{this._pgEnvData=null}}async _pgLoad(){const t=this._devices[this._selIdx];if(!t||this._pgLoading)return;const e=this._pgCycleId||this._cycles?.[0]?.id||"";if(!e)return;this._pgCycleId=e,this._pgLoading=!0,this._pgDetailBusy=!1,this._pgView=null,this._pgHoverT=null,this._pgPowerPts=null,this._pgEnvData=null,this._pgDetail=null;const s=++this._pgLoadSeq;this._render();try{const a=await this._ws({type:`${w}/get_cycle_power_data`,entry_id:t.entry_id,cycle_id:e}),r=a.samples||[],i=[];for(const l of r){if(!Array.isArray(l)||l.length<2)continue;const c=+l[0],p=+l[1];!isNaN(c)&&!isNaN(p)&&i.push({t:c,w:p})}if(this._pgPowerPts=i.length?i:null,typeof a.full_duration_s=="number"&&a.full_duration_s>0){const l=(this._cycles||[]).find(c=>c.id===e);l&&(l._pg_duration=a.full_duration_s)}const o=this._pgProfileName||(this._cycles||[]).find(l=>l.id===e)?.profile_name||"";if(o&&await this._pgFetchEnvelope(t.entry_id,o),s!==this._pgLoadSeq)return;await this._pgLoadDetail(t.entry_id,e,s);const n=this._pgDetail&&this._pgDetail.outcome&&this._pgDetail.outcome.matched_profile;!o&&n&&s===this._pgLoadSeq&&await this._pgFetchEnvelope(t.entry_id,n)}catch(a){this._showToast(this._tText("msg.toast_error",{error:a.message||a},"Error: "+(a.message||a)),"error")}s===this._pgLoadSeq&&(this._pgLoading=!1,this._render(),requestAnimationFrame(()=>{this._pgDrawCanvas(),this._pgUpdateStripAt(null)}),this._pgNeedsRestart&&this._tab==="playground"?(this._pgRestartRetries||0)<5&&!this._pgRestartRetryTimer&&(this._pgRestartRetries=(this._pgRestartRetries||0)+1,this._pgRestartRetryTimer=setTimeout(()=>{this._pgRestartRetryTimer=null,this._pgNeedsRestart&&this._tab==="playground"&&this._pgLoad()},3e3)):this._pgRestartRetries=0)}_pgCancelRun(){this._pgLoadSeq++,this._pgLoading=!1,this._pgDetailBusy=!1,this._pgCancelDetailTask(),this._render()}_pgCancelDetailTask(){const t=this._pgDetailTaskId;this._pgDetailTaskId=null,t&&this._ws({type:`${w}/cancel_task`,task_id:t}).catch(()=>{})}_pgCurrentOverride(){const t={...this._pgParamOverrides};return this._pgThreshStart!=null&&(t.start_threshold_w=this._pgThreshStart),this._pgThreshStop!=null&&(t.stop_threshold_w=this._pgThreshStop),t}_pgOverrideSig(t){return JSON.stringify(Object.keys(t).sort().map(e=>[e,t[e]]))}_pgSelectCycle(t){t&&(this._pgCycleId=t,this._pgProfileName="",this._pgView=null,this._pgHoverT=null,this._pgLoading=!1,this._pgLoad(),requestAnimationFrame(()=>{const e=this.shadowRoot&&this.shadowRoot.getElementById("wd-pg-canvas");e&&typeof e.scrollIntoView=="function"&&e.scrollIntoView({behavior:"smooth",block:"center"})}))}async _pgLoadDetail(t,e,s){const a=this._pgCurrentOverride(),r=this._pgOverrideSig(a);this._pgCancelDetailTask();try{const i=await this._ws({type:`${w}/start_playground_cycle_detail`,entry_id:t,cycle_id:e,settings_override:a}),o=i&&i.task_id;if(!o)throw new Error("no task id");this._pgDetailTaskId=o,this._addProvisionalTask(o,"pg_detail",t,0);const n=await new Promise(l=>{this._taskCallbacks[o]=async p=>{if(p.state==="done"||p.state==="cancelled")try{const _=await this._ws({type:`${w}/get_task_result`,task_id:p.id});l(_&&_.result)}catch{l(null)}else l(null)};const c=this._tasks[o];c&&c.state!=="running"?this._settleTaskCallback(c):this._tasksSubscribed||this._pollTaskGeneric(o)});if(this._pgDetailTaskId===o&&(this._pgDetailTaskId=null),!this._isActiveEntry(t)||s!=null&&s!==this._pgLoadSeq)return;this._pgDetail=n&&!n.error?n:null,this._pgDetailSig=this._pgDetail?r:null,this._pgNeedsRestart=!1}catch(i){if(s!=null&&s!==this._pgLoadSeq)return;this._pgIsUnknownCmd(i)&&(this._pgNeedsRestart=!0),this._pgDetail=null,this._pgDetailSig=null}}_pgRerunDetail(){const t=this._devices[this._selIdx];!t||!this._pgCycleId||this._pgLoading||(clearTimeout(this._pgDetailDebounceTimer),this._pgDetailDebounceTimer=setTimeout(async()=>{const e=++this._pgLoadSeq;this._pgDetailBusy=!0,this._render(),await this._pgLoadDetail(t.entry_id,this._pgCycleId,e),e===this._pgLoadSeq&&(this._pgDetailBusy=!1,this._render(),requestAnimationFrame(()=>this._pgDrawCanvas()))},220))}_pgMapState(t,e){return e?"stalled":t==="running"||t==="paused"?"running":t==="ending"?"ending":t==="starting"?"detecting":t==="anti_wrinkle"?"anti_wrinkle":"idle"}_pgSeriesAt(t){const e=this._pgDetail&&this._pgDetail.series;if(!e||!e.length)return null;let s=e[0];for(const a of e)if(a.t<=t)s=a;else break;return s}_pgStateSegsFromSeries(t){const e=this._pgDetail&&this._pgDetail.series;if(!e||!e.length)return[];const s=[];let a=null;for(const r of e){const i=this._pgMapState(r.state,r.stalled);!a||a.state!==i?(a={start:r.t,end:r.t,state:i},s.push(a)):a.end=r.t}return s.length&&(s[s.length-1].end=t),s}_pgDrawCanvas(){if(this._tab!=="playground")return;const t=this.shadowRoot,e=t&&t.getElementById("wd-pg-canvas");if(!e)return;const s=this._pgPowerPts,a=e.getBoundingClientRect(),r=window.devicePixelRatio||1,i=Math.max(1,Math.round(a.width*r)),o=Math.max(1,Math.round((a.height||280)*r));(e.width!==i||e.height!==o)&&(e.width=i,e.height=o);const n=e.getContext("2d"),l=getComputedStyle(this),c=(l.getPropertyValue("--primary-color")||"#03a9f4").trim(),p=(l.getPropertyValue("--divider-color")||"rgba(127,127,127,.2)").trim(),_=(l.getPropertyValue("--secondary-text-color")||"#888").trim(),f=(l.getPropertyValue("--secondary-background-color")||"#1a1a1a").trim();n.clearRect(0,0,i,o);const u=34*r,b=14*r,v=Bt*r,T=44*r,C=8*r,x=v+8*r,$=u+b+4*r,L=o-x-$;if(!s||!s.length){this._pgLoading&&(n.fillStyle=_,n.font=`${12*r}px sans-serif`,n.textAlign="center",n.textBaseline="middle",n.fillText(this._t("msg.loading",{},"Loading\u2026"),i/2,o/2));return}const D=(this._cycles||[]).find(E=>E.id===this._pgCycleId)?._pg_duration||s[s.length-1].t||1,B=Math.max(...s.map(E=>E.w),1),U=this._pgThreshStart??this._pgFieldVal("start_threshold_w",{})??50,z=this._pgThreshStop??this._pgFieldVal("stop_threshold_w",{})??5;let M=0,A=D;this._pgView&&this._pgView.max-this._pgView.min>1&&(M=Math.max(0,this._pgView.min),A=Math.min(D,this._pgView.max),A-M<=1&&(M=0,A=D));const O=Math.max(1e-6,A-M),K=i-T-C,I=E=>T+(E-M)/O*K,R=E=>x+(1-Math.max(0,E)/B)*L;this._pgMap={vMin:M,vMax:A,totalDur:D,padLpx:T/r,plotWpx:K/r},n.strokeStyle="rgba(127,127,127,0.12)",n.lineWidth=r,n.setLineDash([]);const G=Ie(B,5),W=[];for(let E=1;E*G<=B*1.0001;E++)W.push(E*G);W.forEach(E=>{const S=R(E);Sx+L||(n.beginPath(),n.moveTo(T,S),n.lineTo(i-C,S),n.stroke(),n.fillStyle=_,n.font=`${9*r}px sans-serif`,n.textAlign="right",n.textBaseline="middle",n.fillText(Ht(E,G),T-4*r,S))}),n.save(),n.beginPath(),n.rect(T,x,K,L),n.clip();const J=this._pgEnvData,Y=J&&Array.isArray(J.avg)?J.avg.filter(E=>Array.isArray(E)&&E.length>=2):[];if(Y.length){const E=Math.max(...Y.map(H=>H[0]))||1,S=H=>I(H/E*D),F=Array.isArray(J.min)?J.min.filter(H=>Array.isArray(H)&&H.length>=2):[],N=Array.isArray(J.max)?J.max.filter(H=>Array.isArray(H)&&H.length>=2):[];if(F.length&&N.length){n.beginPath(),N.forEach((H,Z)=>Z?n.lineTo(S(H[0]),R(H[1])):n.moveTo(S(H[0]),R(H[1])));for(let H=F.length-1;H>=0;H--)n.lineTo(S(F[H][0]),R(F[H][1]));n.closePath(),n.fillStyle="#eda10012",n.fill()}else n.beginPath(),n.strokeStyle="#eda100",n.lineWidth=2*r,n.setLineDash([5*r,4*r]),Y.forEach((H,Z)=>Z?n.lineTo(S(H[0]),R(H[1])):n.moveTo(S(H[0]),R(H[1]))),n.stroke(),n.setLineDash([])}n.beginPath(),n.moveTo(I(0),R(0)),s.forEach(E=>n.lineTo(I(E.t),R(E.w))),n.lineTo(I(s[s.length-1].t),R(0)),n.closePath(),n.fillStyle=Pt(c,.1),n.fill(),n.beginPath(),n.strokeStyle=c,n.lineWidth=2*r,s.forEach((E,S)=>S?n.lineTo(I(E.t),R(E.w)):n.moveTo(I(E.t),R(E.w))),n.stroke(),n.restore();const nt=(E,S,F)=>{const N=R(E);Nx+L+2||(n.save(),n.strokeStyle=S,n.lineWidth=2*r,n.setLineDash([8*r,4*r]),n.beginPath(),n.moveTo(T,N),n.lineTo(i-C,N),n.stroke(),n.setLineDash([]),n.fillStyle=S,n.beginPath(),n.arc(T+(i-T-C)*.06,N,5*r,0,Math.PI*2),n.fill(),n.fillStyle=S,n.font=`bold ${9*r}px sans-serif`,n.textAlign="left",n.textBaseline="bottom",n.fillText(F+" "+Ht(E),T+14*r,N-2*r),n.restore())};nt(+U,"#2a78d6",this._t("lbl.start",{},"Start")),nt(+z,"#e34948",this._t("btn.stop",{},"Stop"));const _t={idle:f,detecting:"#42a5f566",running:"#66bb6a66",ending:"#ef535066",anti_wrinkle:"#ab47bc66",stalled:"#eda10066"},ct={idle:this._t("lbl.pg_idle",{},"Idle"),detecting:this._t("lbl.pg_detecting",{},"Detecting"),running:this._t("lbl.pg_ev_running",{},"Running"),ending:this._t("lbl.pg_ev_ending",{},"Ending"),anti_wrinkle:this._t("lbl.pg_anti_wrinkle",{},"Anti-wrinkle"),stalled:this._t("status.stalled",{},"Stalled")},dt=o-u-b;n.fillStyle=f,n.fillRect(T,dt,i-T-C,u),n.save(),n.beginPath(),n.rect(T,dt,K,u),n.clip(),this._pgStateSegsFromSeries(D).forEach(E=>{const S=I(E.start),F=I(E.end);n.fillStyle=_t[E.state]||p,n.fillRect(S,dt,Math.max(1,F-S),u),F-S>50*r&&(n.fillStyle=_,n.font=`${8*r}px sans-serif`,n.textAlign="center",n.textBaseline="middle",n.fillText((ct[E.state]||E.state).toUpperCase(),(S+F)/2,dt+u/2))}),n.restore();const m=o-b,g=[];for(const E of this._pgDetail&&this._pgDetail.series||[]){const S=g[g.length-1];if(S&&S.name===(E.phase||null)){S.end=E.t;continue}S&&(S.end=E.t),g.push({name:E.phase||null,start:E.t,end:E.t})}g.some(E=>E.name)&&(n.save(),n.beginPath(),n.rect(T,m,K,b),n.clip(),g.filter(E=>E.name).forEach((E,S)=>{const F=I(E.start),N=I(E.end),H=S*47%360;n.fillStyle=`hsla(${H},60%,55%,0.55)`,n.fillRect(F,m,Math.max(1,N-F),b),N-F>40*r&&(n.fillStyle="#fff",n.font=`${7*r}px sans-serif`,n.textAlign="center",n.textBaseline="middle",n.fillText(E.name,(F+N)/2,m+b/2))}),n.restore()),this._pgEventHits=[];const k=this._pgDetail&&Array.isArray(this._pgDetail.events)?this._pgDetail.events.filter(E=>E.type!=="state"&&E.t>=M&&E.t<=A).slice().sort((E,S)=>E.t-S.t):[],y=11*r,P=x-v/2-2*r,X=y*2+3*r;let q=-1/0;const j=this._pgHoverEvent;if(k.forEach(E=>{const S=this._pgEventMeta(E.type),F=I(E.t);let N=Math.max(F,q+X);N=Math.max(T+y,Math.min(i-C-y,N)),q=N;const H=j&&j.t===E.t&&j.type===E.type;n.save(),n.strokeStyle=S.color,n.globalAlpha=H?.9:.5,n.lineWidth=(H?2:1)*r,n.beginPath(),n.moveTo(N,P+y),n.lineTo(F,x),n.stroke(),H&&(n.globalAlpha=.3,n.setLineDash([2*r,3*r]),n.beginPath(),n.moveTo(F,x),n.lineTo(F,o-b),n.stroke(),n.setLineDash([])),n.restore(),n.save(),n.beginPath(),n.arc(N,P,y,0,Math.PI*2),n.fillStyle=H?S.color:f||"#1a1a1a",n.fill(),n.lineWidth=(H?2:1.5)*r,n.strokeStyle=S.color,n.stroke(),n.fillStyle=H?"#fff":S.color,n.font=`${14*r}px sans-serif`,n.textAlign="center",n.textBaseline="middle",n.fillText(S.glyph,N,P+r),n.restore(),this._pgEventHits.push({cx:N/r,cy:P/r,r:y/r,type:E.type,label:S.label,detail:E.detail,t:E.t})}),j){const E=this._pgEventHits.find(S=>S.t===j.t&&S.type===j.type);if(E){const S=this._pgEventMeta(E.type),F=ft=>`${Math.floor(ft/60)}:${String(Math.round(ft%60)).padStart(2,"0")}`,N=this._pgEventDescription(E.type),H=[{t:E.label,bold:!0,col:S.color},...N?[{t:N}]:[],...E.detail?[{t:E.detail,dim:!0}]:[],{t:`${this._t("lbl.from_start",{},"From start")} ${F(E.t)}`,dim:!0}];n.save(),n.font=`${10*r}px sans-serif`;const Z=Math.min(220*r,i-2*T),tt=[];H.forEach(ft=>{const At=String(ft.t).split(" ");let bt="";At.forEach(Ot=>{const Jt=bt?bt+" "+Ot:Ot;n.measureText(Jt).width>Z&&bt?(tt.push({...ft,t:bt}),bt=Ot):bt=Jt}),tt.push({...ft,t:bt})});const st=14*r,rt=Math.min(Z+14*r,i-8*r),lt=tt.length*st+10*r;let pt=E.cx*r-rt/2;pt=Math.max(4*r,Math.min(i-rt-4*r,pt));const Rt=P+y+6*r;n.fillStyle=f,n.globalAlpha=.97,n.fillRect(pt,Rt,rt,lt),n.globalAlpha=1,n.strokeStyle=S.color,n.lineWidth=r,n.strokeRect(pt,Rt,rt,lt),n.textAlign="left",n.textBaseline="top",tt.forEach((ft,At)=>{n.font=`${ft.bold?"600 ":""}${10*r}px sans-serif`,n.fillStyle=ft.col||(ft.dim?_:(l.getPropertyValue("--primary-text-color")||"#ddd").trim()),n.fillText(ft.t,pt+7*r,Rt+5*r+At*st)}),n.restore()}}if(this._pgHoverT!=null&&this._pgHoverT>=M&&this._pgHoverT<=A){const E=I(this._pgHoverT);n.save(),n.strokeStyle="#e34948",n.lineWidth=1.5*r,n.setLineDash([4*r,3*r]),n.beginPath(),n.moveTo(E,x),n.lineTo(E,o-b),n.stroke(),n.setLineDash([]);const S=this._pgInterpPower(s,this._pgHoverT),F=R(S);n.fillStyle="#e34948",n.beginPath(),n.arc(E,F,3.5*r,0,Math.PI*2),n.fill();const N=at=>`${Math.floor(at/60)}:${String(Math.round(at%60)).padStart(2,"0")}`,H=at=>at>=1e3?it(at/1e3,2)+" kW":Lt(at),Z=[`${this._t("lbl.from_start",{},"From start")} ${N(this._pgHoverT)}`,`${this._t("lbl.to_end",{},"To end")} ${N(Math.max(0,D-this._pgHoverT))}`,`${this._t("lbl.power",{},"Power")} ${H(S)}`];n.font=`${9.5*r}px sans-serif`;const tt=Math.max(...Z.map(at=>n.measureText(at).width))+12*r,st=Z.length*13*r+8*r;let rt=E+8*r;rt+tt>i-C&&(rt=E-tt-8*r);const lt=x+4*r;n.fillStyle=f,n.globalAlpha=.95,n.fillRect(rt,lt,tt,st),n.globalAlpha=1,n.strokeStyle=p,n.lineWidth=r,n.strokeRect(rt,lt,tt,st),n.fillStyle=_,n.textAlign="left",n.textBaseline="top",Z.forEach((at,pt)=>n.fillText(at,rt+6*r,lt+5*r+pt*13*r)),n.restore()}n.fillStyle=_,n.font=`${9*r}px sans-serif`,n.textAlign="center",n.textBaseline="top";const V=E=>`${Math.floor(E/60)}:${String(Math.round(E%60)).padStart(2,"0")}`;for(let E=0;E<=4;E++){const S=M+O*E/4,F=I(S);n.textAlign=E===0?"left":E===4?"right":"center",n.fillText(V(S),Math.max(T,Math.min(i-C,F)),dt+u+1*r)}Oz.id===this._pgCycleId)?._pg_duration||e[e.length-1].t||1,r=t??a,i=this._pgInterpPower(e,r),o=this._pgSeriesAt(r),n=o?this._pgMapState(o.state,o.stalled):"idle",l={idle:[this._t("lbl.pg_idle",{},"Idle"),"var(--secondary-background-color)"],detecting:[this._t("lbl.pg_detecting",{},"Detecting"),"#42a5f5"],running:[this._t("lbl.pg_ev_running",{},"Running"),"#66bb6a"],ending:[this._t("lbl.pg_ev_ending",{},"Ending"),"#ef5350"],anti_wrinkle:[this._t("lbl.pg_anti_wrinkle",{},"Anti-wrinkle"),"#ab47bc"],stalled:[this._t("status.stalled",{},"Stalled"),"#eda100"]},[c,p]=l[n]||l.idle,_=o&&o.progress!=null?Math.round(o.progress):null,f=o&&o.remaining_s!=null?o.remaining_s:null,u=o&&o.energy_wh!=null?o.energy_wh:this._pgTrapEnergy(e,r),b=o&&o.confidence!=null?Math.round(o.confidence*100)+"%":"\u2014",v=o&&o.phase?o.phase:"\u2014",T=z=>Math.floor(z/60)+":"+String(Math.round(z%60)).padStart(2,"0"),C=z=>z>=1e3?it(z/1e3,2)+" kWh":it(z,0)+" Wh",x=z=>z>=1e3?it(z/1e3,1)+" kW":Lt(z),$=this.shadowRoot,L=z=>$&&$.getElementById(z),D=(z,M)=>{const A=L(z);A&&(A.textContent=M)},B=(z,M,A)=>{const O=L(z);O&&(O.style[M]=A)},U=L("wd-pg-state-badge");U&&(U.textContent=c,U.style.background=p,U.style.color=p.includes("var")?"":"#fff"),D("wd-pg-power",x(i)),D("wd-pg-pct",_!=null?_+"%":"\u2014%"),B("wd-pg-pbar","width",(_??0)+"%"),D("wd-pg-rem",f!=null?T(f):"\u2014"),D("wd-pg-energy",C(u)),D("wd-pg-conf",b),D("wd-pg-phase",v)}_pgIsUnknownCmd(t){const e=t&&(t.code||t.error&&t.error.code)||"",s=(t&&(t.message||t.error)||"").toString().toLowerCase();return e==="unknown_command"||s.includes("unknown command")||s.includes("unknown_command")}_pgInterpPower(t,e){if(!t.length)return 0;if(e<=t[0].t)return t[0].w;for(let s=1;s=e))break}return s}_drawPlaygroundCanvases(){this._tab==="playground"&&this._pgDrawCanvas()}_htmlPhases(){const t=this._devices[this._selIdx],e=t&&t.options.device_type||"washing_machine",s=this._canEdit(),a=this._phases.map(o=>{const n=o.is_default,l=o.translation_key?this._t(o.translation_key,{},o.description||""):o.description||"",c=s?` ${o.is_override?``:n?"":``} `:"";return` ${d(o.name)} ${n?`${this._t("badge.built_in_tag",{},"built-in")}`:""} ${d(l.length>60?l.slice(0,57)+"\u2026":l)} ${c} `}).join(""),r=s?`${this._t("lbl.actions",{},"Actions")}`:"",i=s?`
`:"";return`
${this._t("hdr.phase_catalog",{},"Phase Catalog")}

${this._t("msg.phase_catalog_intro",{},"Named segments of a cycle (Pre-wash, Heating, Spin\u2026). Assign them to a profile from its control panel.")}

${i} ${this._phases.length===0?`

${this._t("msg.no_phases",{},"No phases defined.")}

`:`${r}${a}
${this._t("lbl.name",{},"Name")}${this._t("lbl.description",{},"Description")}
`}
`}_htmlImportUndo(){const t=this._importUndo;if(!t||!t.created_at||!this._isAdmin())return"";const e=t.counts||{},s=(e.real_cycles||0)+(e.reference_cycles||0)+(e.backfill_cycles||0),a=this._busy.has("import-undo");return`

${this._t("msg.import_undo_hint",{when:et(t.created_at),profiles:e.profiles||0,cycles:s},"A restore point from before the last import ({when}) is kept: {profiles} profile(s) and {cycles} cycle(s).")}

`}_htmlDiagnostics(){const t=this._diag;let e;return t&&t._error?e=`

${this._t("msg.diagnostics_load_failed",{error:t._error},"Could not load diagnostics: {error}")}

`:t?e=`
${t.total_cycles??"-"}
${this._t("lbl.cycles_count",{},"Cycles")}
${t.total_profiles??"-"}
${this._t("tab.profiles",{},"Profiles")}
${t.debug_traces_count??"-"}
${this._t("lbl.debug_traces",{},"Debug Traces")}
${t.file_size_kb!=null?t.file_size_kb.toFixed(1):"-"}
${this._t("lbl.file_kb",{},"File (kB)")}
`:e=`

${this._t("msg.loading",{},"Loading\u2026")}

`,`
${this._t("hdr.storage_stats",{},"Storage Stats")}
${e}
${this._canFull()?`
${this._t("hdr.maintenance",{},"Maintenance Actions")}
${this._t("hdr.process_history",{},"Process History")}

${this._t("msg.process_history_hint",{},"Re-run matching on all stored cycles, refresh suggestions, retrain ML models (if enabled), recost cycles from recorded prices and recompute cycle health. Run it after a batch of reviews.")}

${this._t("hdr.clear_debug",{},"Clear Debug Traces")}

${this._t("msg.clear_debug_hint",{},"Remove stored debug data to free space.")}

${this._t("hdr.wipe_history",{},"Wipe History")}

${this._t("msg.wipe_history_warning",{},"Permanently delete all cycles and profiles. Cannot be undone.")}

${this._t("hdr.export_import",{},"Export / Import")}

${this._t("msg.export_description",{},"Choose exactly which profiles, cycles, settings and more to export to JSON, or analyze a file and import only the parts you want.")}

${this._htmlImportUndo()}
${this._t("hdr.import_power_history",{},"Import power history")}

${this._t("msg.import_history_description",{},"Had a smart plug before WashData? Upload its power history or read it from Home Assistant. Detection runs over it, and past cycles appear in your Cycles list ready to name.")}

`:`

${this._t("msg.maintenance_requires_access",{},"Maintenance and export/import require full access.")}

`}`}_maintLabel(t,e){const s={descale:this._t("maint.descale",{},"Descale"),filter_clean:this._t("maint.filter_clean",{},"Clean filter"),drum_clean:this._t("maint.drum_clean",{},"Clean drum"),bearing_service:this._t("maint.bearing_service",{},"Bearing service"),other:this._t("maint.other",{},"Other"),salt:this._t("maint.salt",{},"Refill salt"),rinse_aid:this._t("maint.rinse_aid",{},"Refill rinse aid"),lint_filter:this._t("maint.lint_filter",{},"Clean lint filter"),condenser_clean:this._t("maint.condenser_clean",{},"Clean condenser")};if(s[t])return s[t];const r=(this._maintenance&&this._maintenance.custom_tasks||[]).find(i=>i&&i.id===t);return r&&r.name?r.name:e||t}_maintStatusRows(t,e){if(Array.isArray(t.status))return t.status;const s=t.reminders||{},a=t.cycles_since||{};return e.map(r=>{const i=parseInt(s[r],10)||0,o=Math.max(0,parseInt(a[r],10)||0);return{id:r,custom:!1,cycles_interval:i,days_interval:0,cycles_since:o,days_since:null,due:i>0&&o>=i}}).filter(r=>r.cycles_interval>0)}_htmlMaintenance(){const t=this._canEdit(),e=this._maintenance;if(e&&e._error)return`

${this._t("msg.maintenance_load_error",{error:e._error},"Could not load maintenance data: {error}")}

`;if(!e)return`

${this._t("msg.loading",{},"Loading\u2026")}

`;const s=e.event_types&&e.event_types.length?e.event_types:["descale","filter_clean","drum_clean","bearing_service","other"],a=e.due||[],r=e.log||[],i=e.reminders||{},o=Array.isArray(e.custom_tasks)?e.custom_tasks:[],n=e.limits||{},l=parseInt(n.name_max,10)||60,c=parseInt(n.tasks_max,10)||20,p=Math.max(0,parseInt(e.lifetime_cycle_count,10)||0),_=this._canFull(),f=a.length?(()=>{const M=a.map(O=>this._maintLabel(O)).join(", "),A=t?`
${a.map(O=>``).join("")}
`:"";return`
${this._t("msg.maintenance_due",{items:M},"Maintenance due: {items}")} ${A}
`})():"",u=new Date().toISOString().slice(0,10),b=s.map(M=>``).join("")+o.map(M=>``).join(""),v=t?`
${this._t("hdr.add_maintenance",{},"Add Maintenance Event")}
`:"",T=r.length?r.map(M=>{const A=t?``:"",O=M.notes?`
${d(M.notes)}
`:"";return`
${d(this._maintLabel(M.event_type,M.task_name))}
${et(M.date)}
${O}
${A}
`}).join(""):`

${this._t("msg.no_maintenance",{},"No maintenance recorded yet.")}

`,C=(M,A,O,K)=>``,x=o.map(M=>`
${C("data-mtask-cycles",M.id,M.cycles,n.cycles_max||1e5)}
${C("data-mtask-days",M.id,M.days,n.days_max||3650)}
`).join(""),$=o.length
`:`

${this._t("msg.custom_task_limit",{max:c},"You have reached the limit of {max} tasks.")}

`,L=t?`
${this._t("hdr.maintenance_reminders",{},"Service Reminders")}

${this._t("msg.reminders_intro",{},"Show a reminder in the panel this many cycles after the last service. Leave blank or 0 to turn a reminder off.")}

${s.map(M=>`
`).join("")}
${this._t("hdr.custom_tasks",{},"Your own tasks")}

${this._t("msg.custom_tasks_intro",{},"Add anything else you want to be reminded of. A task is due when either interval is reached; leave one blank or 0 to use only the other.")}

${x} ${$}

${this._t("msg.maintenance_due_sensor_hint",{},"The Maintenance due binary sensor is on while any task is due, for your own notification automations.")}

`:"",D=this._maintStatusRows(e,s).map(M=>{const A=parseInt(M.cycles_interval,10)||0,O=parseInt(M.days_interval,10)||0,K=Math.max(0,parseInt(M.cycles_since,10)||0),I=M.days_since==null?null:Math.max(0,parseInt(M.days_since,10)||0),R=[];let G=0;if(A>0&&(R.push(this._t("lbl.cycles_since_service",{since:K,total:A},K+" / "+A+" cycles")),G=Math.max(G,K/A)),O>0){const Y=I??0;R.push(this._t("lbl.days_since_service",{since:Y,total:O},Y+" / "+O+" days")),G=Math.max(G,Y/O)}const W=Math.max(0,Math.min(100,Math.round(G*100))),J=M.due?"var(--warning-color,#ff9800)":"var(--primary-color)";return`
${d(M.custom?M.name||M.id:this._maintLabel(M.id))} ${R.join(" \xB7 ")}
`}).join(""),B=_?`

${this._t("msg.odometer_correct_hint",{},"Set this if WashData counted a run that never happened, or the appliance ran cycles before WashData was installed.")}

`:"",U=`
${this._t("hdr.service_status",{},"Service Status")}
${p} ${this._t("lbl.total_cycles_run",{},"cycles run in total")}

${this._t("msg.odometer_intro",{},"This total only goes up. Deleting a cycle does not lower it, so service reminders stay correct.")}

${D||`

${this._t("msg.no_reminders_set",{},"No service reminders are set yet.")}

`} ${B}
`,z=`
${this._t("hdr.maintenance_supporter",{},"Need more?")}

${this._t("msg.maintenance_supporter_intro",{},"Maintenance Supporter is a separate integration that does much more: any appliance or task, schedules by time, usage or a sensor, notifications and a history. It can use the WashData cycle counter.")}

${this._t("link.maintenance_supporter",{},"Maintenance Supporter on GitHub \u2197")}

`;return`${f} ${U} ${v}
${this._t("hdr.maintenance_log",{},"Maintenance Log")}

${this._t("msg.maintenance_intro",{},"Log servicing on this appliance and get reminded when each task is due again.")}

${T}
${L} ${z}`}_htmlPanel(){const t=this._canEdit(),e=t&&this._constants&&this._constants.mlTrainingAvailable,s=new Set(["diagnostics"]);e&&s.add("ml");let a=this._panelSubtab;s.has(a)||(a=this._panelSubtab="diagnostics");const r=[["diagnostics",this._t("tab.diagnostics",{},"Diagnostics")]];e&&r.push(["ml",this._t("tab.ml",{},"ML Training")]);const i=r.map(([n,l])=>``).join("");if(!t)return"";const o=a==="ml"&&e?this._htmlMlTab():this._htmlDiagnostics();return`
${i}
${o}`}_levelSelect(t,e,s){const a=(s?[["inherit",this._t("access.inherit",{},"Inherit")]]:[]).concat([["none",this._t("access.none",{},"None (hidden)")],["read",this._t("access.read",{},"Read")],["edit",this._t("access.edit",{},"Edit")],["full",this._t("access.full",{},"Full")]]);return``}_htmlPanelPrefs(){const t=this._panelCfg&&this._panelCfg.prefs||{},e=this._hass&&this._hass.locale&&this._hass.locale.language||"en",a=[["",this._t("pref.use_panel_default",{},"(panel default)")],["status",this._t("tab.status",{},"Overview")],["history",this._t("tab.history",{},"Cycles")],["profiles",this._t("tab.profiles",{},"Profiles")],["settings",this._t("tab.settings",{},"Settings")],["playground",this._t("tab.playground",{},"Playground")]].map(([l,c])=>``).join(""),i=[["relative",this._t("pref.date_relative",{},"Relative (e.g. 2 hours ago)")],["absolute",this._t("pref.date_absolute",{},"Absolute (e.g. 14:32 on 2 Jul)")]].map(([l,c])=>``).join(""),o=t.lang_override||"",n=[["",this._tText("pref.lang_auto",{lang:e.toUpperCase()},"System default ("+e.toUpperCase()+")")],["en",this._t("pref.lang_en",{},"English")]].map(([l,c])=>``).join("");return`
${this._t("hdr.my_preferences",{},"My Preferences")}

${this._t("msg.prefs_personal",{},"These apply to your Home Assistant account only.")}

${this._t("hdr.display",{},"Display")}
${Math.round((t.font_scale||1)*100)}%
${this._t("msg.font_size_hint",{},"Make everything in this panel larger or smaller. Applies to your account on this device.")}
${this._t("hdr.status_graph",{},"Status Graph")}
${kt(`id="wd-pref-expected" ${t.show_expected!==!1?"checked":""}`,this._t("lbl.show_expected",{},"Show expected curve overlay (matched profile, orange)"))} ${kt(`id="wd-pref-raw" ${t.show_raw?"checked":""}`,this._t("lbl.show_raw",{},"Show raw socket toggle in live power graph"))}
${this._t("hdr.diagnostics_pref",{},"Diagnostics")}
${kt(`id="wd-pref-debug" ${t.show_debug?"checked":""}`,this._t("lbl.show_debug",{},"Show live match debug card on the Status page (confidence, ambiguity, top candidates)"))}
`}_htmlPanelSettings(){const t=this._panelCfg&&this._panelCfg.panel||{},e=[["status",this._t("tab.status",{},"Overview")],["history",this._t("tab.history",{},"Cycles")],["profiles",this._t("tab.profiles",{},"Profiles")],["settings",this._t("tab.settings",{},"Settings")],["playground",this._t("tab.playground",{},"Playground")]],s=e.map(([i,o])=>``).join(""),a=t.hidden_tabs||[],r=e.filter(([i])=>i!=="status").map(([i,o])=>Xt(`data-hidetab="${i}" ${a.includes(i)?"checked":""}`,o)).join("");return`
${this._t("hdr.panel_settings",{},"Panel Settings (all users)")}
${r}
`}_htmlPanelAccess(){const t=this._panelCfg&&this._panelCfg.rbac||{enabled:!1,default_level:"none",users:{}},e=this._panelCfg&&this._panelCfg.users||[],s=this._devices||[],a=e.filter(i=>!i.is_admin).map(i=>{const o=(t.users||{})[i.id]||{default:"none",devices:{}},n=s.map(l=>`
${d(l.title)}${this._levelSelect(`data-rbacuser="${d(i.id)}" data-rbacdev="${d(l.entry_id)}"`,(o.devices||{})[l.entry_id]||"inherit",!0)}
`).join("");return`
${d(i.name)}
${this._t("lbl.default_other",{},"Default (other devices)")}${this._levelSelect(`data-rbacuser="${d(i.id)}" data-rbacdev="__default__"`,o.default||"none",!1)}
${n}
`}).join(""),r=e.filter(i=>i.is_admin).map(i=>`${d(i.name)} - full (admin)`).join(" ");return`
${this._t("hdr.access_control",{},"Access Control")}
${kt(`id="wd-rbac-enabled" ${t.enabled?"checked":""}`,this._t("lbl.enable_access_control",{},"Enable per-user access control"),"",this._t("msg.rbac_hint",{},"Off: every Home Assistant user has full access (default). Administrators always have full access and can manage everyone."))}
${this._levelSelect('id="wd-rbac-default"',t.default_level||"none",!1)}
${r?`
${r}
`:""}
${a||`

${this._t("msg.no_other_users",{},"No other Home Assistant users found.")}

`}`}_htmlStore(){if(!this._canEdit())return`
${this._t("msg.no_device_selected",{},"No device selected.")}
`;const t=this._storeStatus,e=`${this._t("store.website",{},"Store website \u2197")}`,s=`
${this._t("hdr.community_store",{},"Community Store")}
${e}
`;if(!this._onlineEnabled()||t&&t.enabled===!1)return`
${s}

${this._t("msg.store_enable_hint",{},"Turn on online features in Preferences > Online & Community to browse and import community reference cycles.")}

`;let a;return this._storeView==="device"?a=this._htmlStoreDevice():this._storeView==="profile"?a=this._htmlStoreProfile():a=this._htmlStoreBrands(),`
${s} ${this._htmlStoreCrumbs()} ${a}
`}_htmlStoreCrumbs(){const t=[``];if(this._storeDevice){const e=this._storeDevice,s=`${e.brand||""} ${e.model||""}`.trim()||this._t("store.device",{},"Device");t.push('\u203A'),t.push(this._storeView==="device"?`${d(s)}`:``)}return this._storeProfile&&this._storeView==="profile"&&(t.push('\u203A'),t.push(`${d(this._storeProfile.program||"")}`)),`
${t.join("")}
`}_htmlStoreLoading(){return`
\u23F3
${this._t("msg.loading",{},"Loading\u2026")}
`}_htmlStoreBrands(){const t=this._storeDevices||[],e=String((this._opts||{}).store_model||"").trim().toLowerCase(),s=t.map(o=>{const n=`${d(o.brand||"")} ${d(o.model||"")}`.trim()||this._t("store.device",{},"Device"),l=o.applianceType?`${d(this._deviceTypeLabel(o.applianceType))}`:"",p=e&&String(o.model||"").toLowerCase()===e?`${this._t("store.your_model",{},"Yours")}`:"",_=Dt(o.profileCount),f=_>0?`${this._t("store.programs_count",{n:_},`Programs: ${_}`)}`:"";return``}).join("");if(!this._storeBrandScope())return` ${this._storeSearchHtml()}

${this._t("msg.store_declare_appliance",{},"Tell WashData which appliance you own to see setups others shared for it. Or type a brand above to look around.")}

`;const a=`

${this._t("store.no_results",{},"No matching appliances found. Try a different search.")}

`,r=this._storeLoading?this._htmlStoreLoading():t.length?`
${s}
`:a,i=!this._storeLoading&&t.length>1?`

${this._t("msg.store_sibling_hint",{},"Nothing shared for your model? A related model from the same brand is usually a good start.")}

`:"";return` ${this._storeSearchHtml()} ${i} ${r}`}_storeSearchHtml(){return` `}_htmlStoreUnreachable(){return``}_htmlStoreDevice(){const t=this._storeProfiles||[],e=t.map(o=>``).join(""),s=this._storeLoading?this._htmlStoreLoading():t.length?`
${e}
`:this._storeBrowseErr?this._htmlStoreUnreachable():`

${this._t("store.no_programs",{},"No shared programs for this appliance yet.")}

`,a=this._storeDevice,r=this._busy.has("store-download-device");return(this._canEdit()&&a&&t.length?`
${this._t("msg.store_download_device_intro",{},"Add every shared program and its reference cycles to your device. Your own cycles and stats are not affected.")} ${Xt(`data-action="store-toggle-dl-settings" ${this._dlSettings?"checked":""} ${r?"disabled":""}`,this._t("lbl.adopt_settings",{},"Also adopt settings"),ot(this._t("msg.adopt_settings_hint",{},"Overwrite this device's detection & matching thresholds with the shared ones. Your notifications, entities and energy price are never changed.")))}
`:"")+s}_htmlStoreProfile(){const t=this._storeCycles||[],e=t.map(a=>{const r=a.stats&&typeof a.stats=="object"?a.stats:{},i=this._storeSparkline(a.trace&&a.trace.points||[]),o=ht(r.duration),n=gt(r.energy_wh!=null?Number(r.energy_wh)/1e3:null),l=Lt(r.peak_w),c=typeof a.uploaderName=="string"&&a.uploaderName||this._tText("store.anon",{},"anonymous"),p=this._statusTag(a),_=a.rating&&typeof a.rating=="object"?a.rating:{},f=Number(_.avg),u=Dt(_.count),b=_.avg!=null&&isFinite(f)&&u>0?this._t("store.rating_summary",{avg:it(f,1),n:u},"\u2605 {avg} ({n})"):this._t("store.no_ratings",{},"No ratings yet");return`
${i}
${o} \xB7 ${n} \xB7 ${l} ${p}
${this._t("store.uploaded_by",{name:c},"Shared by {name}")} \xB7 \u2B07 ${it(Dt(a.downloads),0)} \xB7 ${b}
`}).join("");return`
${this._storeLoading?this._htmlStoreLoading():t.length?e:this._storeBrowseErr?this._htmlStoreUnreachable():`

${this._t("store.no_cycles",{},"No reference cycles shared for this program yet.")}

`}
`}_storeSparkline(t){const e=Array.isArray(t)?t.filter(u=>Array.isArray(u)&&u.length>=2).map(u=>[Number(u[0]),Number(u[1])]).filter(u=>isFinite(u[0])&&isFinite(u[1])):[];if(e.length<2)return'';const s=e.map(u=>u[0]),a=e.map(u=>u[1]),r=Math.min(...s),i=Math.max(...s),o=Math.max(1,...a),n=120,l=36,c=2,p=u=>c+(i>r?(u-r)/(i-r):0)*(n-2*c),_=u=>l-c-Math.max(0,u)/o*(l-2*c),f=e.map(u=>`${p(u[0]).toFixed(1)},${_(u[1]).toFixed(1)}`).join(" ");return``}_htmlGearModal(t){const e=this._isAdmin(),s=[["prefs",this._t("hdr.my_preferences",{},"My Preferences")]];e&&s.push(["panel",this._t("hdr.panel_settings",{},"Panel Settings")],["access",this._t("hdr.access_control",{},"Access Control")]),e&&this._constants&&this._constants.storeOnlineAvailable&&s.push(["online",this._t("hdr.online_account_tab",{},"Online & Community")]);let a=t.tab;s.some(([o])=>o===a)||(a=t.tab="prefs");const r=s.map(([o,n])=>``).join(""),i=a==="panel"&&e?this._htmlPanelSettings():a==="access"&&e?this._htmlPanelAccess():a==="online"&&e?this._htmlOnlineSettings():this._htmlPanelPrefs();return`

${this._t("settings.gear.title",{},"Preferences")}

${r}
${i}
`}_htmlOnlineSettings(){if(!(this._constants&&this._constants.storeOnlineAvailable))return`

${this._t("msg.online_unavailable",{},"Online features are not available on this server.")}

`;const t=this._onlineEnabled(),e=this._storeStatus||{},s=!!(t&&e.connected),a=this._busy.has("store-account"),r=t?s?`
${this._t("store.connected_as",{name:e.name||e.uid||""},"Connected as {name}")}
`:`
${this._t("store.not_connected",{},"Not connected. Connect a GitHub account to confirm appliances and share your own cycles.")}
`:"";return`
${this._t("hdr.online_account",{},"Community Store & online features")}

${this._t("msg.online_intro_global",{},"Browse and share reference recordings with other WashData users. One connection covers all your WashData devices; brand and model are set per device under Basic. Online features are off until you turn them on.")}

${kt(`data-action="store-toggle-online" ${t?"checked":""} ${a?"disabled":""}`,this._t("lbl.enable_online",{},"Enable online features"))} ${t?this._htmlStorePrefs(a):""} ${t?r:""}
`}_htmlStorePrefs(t){const e=this._constants&&this._constants.storePrefs||{},s=ke.map(r=>{const i=e[r.key],o=i===void 0?r.def!==!1:i!==!1;return kt(`data-action="store-toggle-pref" data-pref="${d(r.key)}" ${o?"checked":""} ${t?"disabled":""}`,this._t(r.labelKey,{},r.labelFb),ot(this._t(r.docKey,{},r.docFb)))}).join(""),a=this._busy.has("store-refresh-catalog");return s+`
${this._t("msg.refresh_catalog_hint",{},"Community brand and appliance lists are cached to save the store's daily budget. Refresh to see entries others added or approved.")}
`}async _loadStoreStatus(t){if(!this._onlineEnabled()){this._storeStatus={enabled:!1},this._storeConnected=!1;return}try{const e=await this._ws({type:`${w}/store_status`,entry_id:t});if(!this._isActiveEntry(t))return;this._storeStatus=e||null,this._storeConnected=!!(e&&e.connected)}catch{}}_storeBrandScope(){return String(this._storeQuery||(this._opts||{}).store_brand||"").trim()}_renderKeepingStoreQFocus(){const t=this.shadowRoot,e=t&&t.getElementById("wd-store-q"),s=!!e&&t.activeElement===e,a=s?e.selectionStart:0;if(this._render(),!s)return;const r=t.getElementById("wd-store-q");if(!r)return;r.focus();try{r.setSelectionRange(a,a)}catch{}const i=r.parentElement&&r.parentElement.querySelector(".wd-combo-drop");i&&(i.hidden=!0)}_storeOpenDevice(t){const e=this._devices[this._selIdx];if(!e||!t)return;const s=e.entry_id;this._storeDevice=t,this._storeProfile=null,this._storeView="device",this._storeProfiles=[],this._storeCycles=[],this._storeLoading=!0,this._storeBrowseErr=!1,this._render(),this._ws({type:`${w}/store_get_profiles`,entry_id:s,device_id:t.id}).then(a=>{!this._isActiveEntry(s)||this._storeView!=="device"||(this._storeProfiles=a&&a.items||[],this._storeBrowseErr=!!(a&&a.error))}).catch(()=>{this._isActiveEntry(s)&&(this._storeProfiles=[],this._storeBrowseErr=!0)}).finally(()=>{this._isActiveEntry(s)&&(this._storeLoading=!1,this._render())})}async _storeOpenModel(t){if(!t)return;await this._storeSearch(String(t.brand||""));const e=(this._storeDevices||[]).find(s=>String(s.id)===String(t.id))||t;this._storeOpenDevice(e)}async _storeSearch(t){const e=this._devices[this._selIdx];if(!e)return;const s=e.entry_id;this._storeQuery=t||"",this._storeView="brands",this._storeDevice=null,this._storeProfile=null,this._storeProfiles=[],this._storeCycles=[];const a=this._storeBrandScope();if(!a){this._storeDevices=[],this._storeLoading=!1,this._renderKeepingStoreQFocus();return}this._storeLoading=!0,this._renderKeepingStoreQFocus();try{const r=await this._ws({type:`${w}/store_search_devices`,entry_id:s,query:a,appliance_type:this._storeApplianceType(),include_pending:!0});if(!this._isActiveEntry(s))return;r&&r.disabled?(this._storeStatus={enabled:!1},this._storeDevices=[]):this._storeDevices=this._sortStoreDevices(r&&r.items||[])}catch(r){this._isActiveEntry(s)&&(this._storeDevices=[],this._showToast(this._tText("toast.store_search_failed",{error:r.message||r},"Search failed: "+(r.message||r)),"error"))}finally{this._isActiveEntry(s)&&(this._storeLoading=!1,this._renderKeepingStoreQFocus())}}_sortStoreDevices(t){const e=String((this._opts||{}).store_model||"").trim().toLowerCase(),s=a=>e&&String(a.model||"").toLowerCase()===e?0:this._storeItemHasContent(a)?1:2;return t.map((a,r)=>({d:a,i:r})).sort((a,r)=>s(a.d)-s(r.d)||a.i-r.i).map(a=>a.d)}_storeItemHasContent(t){return(Number(t&&t.profileCount)||0)>0||(Number(t&&t.cycleCount)||0)>0}_ensureStoreConnectListener(){if(this._storeConnectListener)return;const t=this._constants.storeWebOrigin;if(!t)return;let e;try{e=new URL(t).origin}catch{return}this._storeConnectListener=async s=>{if(s.origin!==e)return;const a=s.data;if(!a)return;const r=this._devices[this._selIdx];if(!r)return;const i=r.entry_id;if(a.type==="washdata-device-created"){const o={};a.brand&&(o.store_brand=a.brand),a.model&&(o.store_model=a.model),this._catalog.brands=void 0,this._catalog.devices=void 0,this._catalog.forBrand=null,this._catalog.brandsFull=!1,this._catalog.brandPrefixes=[],this._dropModelCandidates(),this._catalogEntry=null;const n=this._storeContribEid||i,l=Object.keys(o).length>0,c=this._isActiveEntry(n);this._showToast(l&&!c?this._t("toast.contrib_device_changed",{},"Added to the community store, but the device changed while the form was open. Select it in Settings."):this._t("toast.appliance_added",{},"Appliance added - awaiting approval")),l&&c&&await this._saveStoreOptions(o,{silent:!0}),this._render();return}if(a.type==="washdata-brand-created"){this._catalog.brands=void 0,this._catalog.brandsFull=!1,this._catalog.brandPrefixes=[],this._dropModelCandidates(),this._catalogEntry=null;const o=this._isActiveEntry(this._storeContribEid||i);this._showToast(a.brand&&!o?this._t("toast.contrib_device_changed",{},"Added to the community store, but the device changed while the form was open. Select it in Settings."):this._t("toast.brand_added",{},"Brand added - awaiting approval")),a.brand&&o&&await this._saveStoreOptions({store_brand:a.brand,store_model:""},{silent:!0}),this._render();return}if(a.type==="washdata-profile-created"){const o=this._modal;o&&o.type==="store-share"&&(o.program=a.program||o.program,this._loadShareProfiles()),this._showToast(this._t("toast.profile_added",{},"Profile added - awaiting approval"));return}if(a.type==="washdata-connect")try{const o=await this._ws({type:`${w}/store_connect`,entry_id:i,refresh_token:a.refreshToken,uid:a.uid,name:a.displayName});if(o&&o.error){this._showToast(this._tText("toast.store_connect_failed",{error:o.error},"Connect failed: "+o.error),"error");return}if(await this._loadStoreStatus(i),!this._isActiveEntry(i))return;this._showToast(this._t("toast.store_connected",{},"Connected to the community store")),this._render()}catch(o){this._showToast(this._tText("toast.store_connect_failed",{error:o.message||o},"Connect failed: "+(o.message||o)),"error")}},window.addEventListener("message",this._storeConnectListener)}async _saveStoreOptions(t,{silent:e=!1}={}){const s=this._devices[this._selIdx];if(!s)return!1;const a=s.entry_id;try{if(await this._ws({type:`${w}/set_options`,entry_id:a,options:t}),!this._isActiveEntry(a))return!0;this._opts={...this._opts,...t};for(const r of Object.keys(t))delete this._pendingSettings[r],this._dirtyOptKeys.delete(r);return e||this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading")),!0}catch(r){return this._showToast(this._tText("msg.toast_save_failed",{error:r.message||r},"Save failed: "+(r.message||r)),"error"),!1}}_htmlLogDrawer(){return`
${this._t("hdr.logs",{},"Logs")}
${this._htmlLogFilters("drawer")}

${this._t("msg.log_buffer_hint",{},"Newest first \xB7 buffers the last 500 ha_washdata records since restart \xB7 drag the left edge to resize.")}

${this._logLinesHtml()}
`}_drawCurves(t,e){const s=this.shadowRoot&&this.shadowRoot.getElementById(t);if(!s)return null;const a=s.getBoundingClientRect(),r=window.devicePixelRatio||1,i=Math.max(1,Math.round(a.width*r)),o=Math.max(1,Math.round((a.height||240)*r));(s.width!==i||s.height!==o)&&(s.width=i,s.height=o);const n=s.getContext("2d"),l=getComputedStyle(this),c=(l.getPropertyValue("--primary-color")||"#03a9f4").trim()||"#03a9f4",p=(l.getPropertyValue("--divider-color")||"rgba(127,127,127,.3)").trim()||"rgba(127,127,127,.3)",_=(l.getPropertyValue("--secondary-text-color")||"#888").trim()||"#888",f=44*r,u=12*r,b=12*r,v=22*r,T=e.series||[];let C=e.xMax||0;C||(T.forEach(A=>(A.points||[]).forEach(O=>{O[0]>C&&(C=O[0])})),e.band&&(e.band.max||[]).forEach(A=>{A[0]>C&&(C=A[0])})),C=C||1;const x=Math.min(0,Number(e.xStart)||0),$=this._canvasZoom&&this._canvasZoom[t],L=$?$.xMin:x,D=$?$.xMax:C;let B=e.yMax||0;if(!B){const A=O=>{const K=O||[];for(let I=0;I=L)||R>D&&!(I>0&&K[I-1][0]<=D)||K[I][1]>B&&(B=K[I][1])}};T.forEach(O=>{O.noScale||A(O.points)}),e.band&&A(e.band.max),B=(B||10)*1.08}const U=i-f-u,z=A=>f+(A-L)/(D-L)*U,M=A=>o-v-A/B*(o-b-v);n.clearRect(0,0,i,o),n.strokeStyle=p,n.lineWidth=r,n.fillStyle=_,n.font=`${11*r}px sans-serif`,n.textAlign="right",n.textBaseline="middle";for(let A=0;A<=2;A++){const O=b+A/2*(o-b-v);n.beginPath(),n.moveTo(f,O),n.lineTo(i-u,O),n.stroke(),n.fillText(Ht(B*(1-A/2),B/2),f-4*r,O)}if(n.save(),n.beginPath(),n.rect(f,b,U,o-b),n.clip(),(e.bands||[]).forEach(A=>{n.fillStyle=A.fill;const O=z(A.x0),K=z(A.x1);n.fillRect(Math.min(O,K),b,Math.abs(K-O),o-b-v)}),e.band&&(e.band.min||[]).length&&(e.band.max||[]).length){n.beginPath(),e.band.max.forEach((A,O)=>O?n.lineTo(z(A[0]),M(A[1])):n.moveTo(z(A[0]),M(A[1])));for(let A=e.band.min.length-1;A>=0;A--)n.lineTo(z(e.band.min[A][0]),M(e.band.min[A][1]));n.closePath(),n.fillStyle=e.band.fill||Pt(c,.13),n.fill()}return T.forEach(A=>{const O=A.points||[];if(!O.length)return;const K=A.stroke==="primary"?c:A.stroke;if(A.fill){n.beginPath(),O.forEach((G,W)=>W?n.lineTo(z(G[0]),M(G[1])):n.moveTo(z(G[0]),M(G[1]))),n.lineTo(z(O[O.length-1][0]),M(0)),n.lineTo(z(O[0][0]),M(0)),n.closePath();const R=n.createLinearGradient(0,b,0,o-v);R.addColorStop(0,Pt(K,.33)),R.addColorStop(1,Pt(K,.03)),n.fillStyle=R,n.fill()}n.beginPath();let I=!1;O.forEach(R=>{if(R[1]==null){I=!1;return}I?n.lineTo(z(R[0]),M(R[1])):(n.moveTo(z(R[0]),M(R[1])),I=!0)}),n.strokeStyle=K,n.lineWidth=(A.width||1.5)*r,n.lineJoin="round",A.dash&&n.setLineDash([6*r,4*r]),n.globalAlpha=A.alpha!=null?A.alpha:1,n.stroke(),n.globalAlpha=1,A.dash&&n.setLineDash([])}),n.textAlign="center",n.textBaseline="top",(e.vlines||[]).forEach(A=>{const O=z(A.x);n.beginPath(),n.moveTo(O,b),n.lineTo(O,o-v),n.strokeStyle=A.color,n.lineWidth=2*r,n.setLineDash([4*r,3*r]),n.stroke(),n.setLineDash([]),A.handle&&(n.fillStyle=A.color,n.beginPath(),n.arc(O,b+4*r,4.5*r,0,Math.PI*2),n.fill()),A.label&&(n.fillStyle=A.color,n.fillText(A.label,O,b+(A.handle?12*r:2*r)))}),n.restore(),n.fillStyle=_,n.font=`${11*r}px sans-serif`,n.textBaseline="bottom",($||x<0)&&(n.textAlign="left",n.fillText((L/60).toFixed($?1:0)+" min",f,o-2*r)),n.textAlign="right",n.fillText((D/60).toFixed(0)+" min",i-u,o-2*r),s._wd={xMax:C,xMin:L,xViewMax:D,yMax:B,dpr:r,padT:b,padB:v,padL:f,ch:o,primary:c,xStart:x,cycleSpan:e.cycleSpan||null,plotWcss:U/r,Xpx:z,Ypx:M,xToCss:A=>z(A)/r,cssToX:A=>Math.max(L,Math.min(D,L+(A*r-f)/U*(D-L))),series:(e.series||[]).map(A=>({points:A.points,stroke:A.stroke,name:A.name,cid:A.cid,bounded:A.bounded,aside:A.aside,kind:A.kind})),band:e.band||null,artifacts:e.artifacts||null,_opts:e},s._wd}_drawModalCanvas(){const t=this._modal;t&&(t.type==="cycle-detail"?this._drawCycleEditor():t.type==="compare-cycles"?this._drawCompareCanvas():t.type==="profile-group"?this._drawGroupCanvas():t.type==="profile-panel"&&(t.tab==="stats"?this._drawProfileEnvelope():t.tab==="phases"?this._drawPhaseEditor():t.tab==="cleanup"&&this._drawSpaghetti()))}_redrawCanvas(t){if(t==="wd-status-canvas")this._drawStatusCurve();else if(t==="wd-cyc-canvas")this._drawCycleEditor();else if(t==="wd-compare-canvas")this._drawCompareCanvas();else if(t==="wd-env-canvas")this._drawProfileEnvelope();else if(t==="wd-phase-canvas")this._drawPhaseEditor();else if(t==="wd-spag-canvas")this._drawSpaghetti();else if(t==="wd-pgroup-canvas")this._drawGroupCanvas();else{const e=this.shadowRoot&&this.shadowRoot.getElementById(t);e&&e._wd&&e._wd._opts&&this._drawCurves(t,e._wd._opts)}}_pref(t,e){const s=this._panelCfg&&this._panelCfg.prefs||{};return s[t]===void 0?e:s[t]}_setPref(t,e){this._panelCfg||(this._panelCfg={}),this._panelCfg.prefs={...this._panelCfg.prefs||{},[t]:e},this._ws({type:`${w}/set_user_prefs`,prefs:{[t]:e}}).catch(()=>{})}_drawStatusCurve(){const t=this._powerData||{},e=t.live||[];if(e.length<2)return;const s=this._statusEnv,a=this._pref("show_expected",!0),r=this._pref("show_raw_active",!1),i=[];let o=e[e.length-1][0];if(t.cycle_active&&s&&(s.avg||[]).length&&a){const l=s.target_duration||s.avg[s.avg.length-1][0];i.push({points:s.avg,stroke:"#ff9800",width:2,alpha:.4,name:this._t("lbl.expected",{},"Expected")}),o=Math.max(o,l)}i.push({points:e,stroke:"primary",fill:!0,width:2,name:this._t("lbl.power",{},"Power")}),r&&(t.raw||[]).length>1&&i.push({points:t.raw,stroke:"#9e9e9e",width:1,alpha:.65,name:this._t("lbl.raw_socket",{},"Raw socket"),noScale:!0});const n=[];(t.restart_gaps||[]).forEach(l=>{if(!e.length)return;const c=t.cycle_start_iso;if(!c)return;const p=new Date(c).getTime(),_=Math.max(0,(new Date(l.start_ts).getTime()-p)/1e3),f=Math.max(_+1,(new Date(l.end_ts).getTime()-p)/1e3);n.push({x0:_,x1:f,fill:"rgba(96,125,139,.20)"})}),this._drawCurves("wd-status-canvas",{series:i,xMax:o,bands:n})}_attachGraphGestures(t){const e=this.shadowRoot,s=e&&e.getElementById(t);if(!s)return;this._syncZoomReset(t),this._attachTouchOwnershipGuard(s);const a=new Map;let r=null;const i=n=>n-s.getBoundingClientRect().left;s.addEventListener("pointerdown",n=>{if(a.set(n.pointerId,{x:n.clientX,y:n.clientY}),a.size===1){if(n.pointerType==="touch"&&this._unpinGraphTip(),n.pointerType==="touch"&&this._onAxisHandle(s,t,n)){s._wdOwnGesture=!0;try{s.setPointerCapture(n.pointerId)}catch{}}return}if(a.size!==2)return;s._wdPinching=!0,s._wdOwnGesture=!1,s._wdAbortEdit&&s._wdAbortEdit(),this._hideGraphTip();const[l,c]=[...a.values()];r={dist:Math.max(1,Math.abs(l.x-c.x)),centerCss:i((l.x+c.x)/2)}}),s.addEventListener("pointermove",n=>{if(a.has(n.pointerId)&&a.set(n.pointerId,{x:n.clientX,y:n.clientY}),s._wdPinching){r&&a.size>=2&&this._onGraphPinch(s,t,a,r,i);return}this._onGraphHover(n,t)});const o=n=>{a.delete(n.pointerId),a.size<2&&(r=null,s._wdPinching=!1),a.size===0&&(s._wdOwnGesture=!1)};s.addEventListener("pointerup",n=>{const l=s._wdPinching;o(n),n.pointerType==="touch"&&!l&&this._pinGraphTip()}),s.addEventListener("pointercancel",n=>{o(n),this._hideGraphTip()}),s.addEventListener("pointerleave",n=>{n.pointerType!=="touch"&&this._hideGraphTip()}),s.addEventListener("wheel",n=>{const l=s._wd;if(!l)return;n.preventDefault();const c=n.ctrlKey?Math.min(4,Math.max(.25,Math.exp(n.deltaY*.01))):n.deltaY>0?1.3:.75;this._zoomCanvasAbout(t,l.cssToX(i(n.clientX)),c)},{passive:!1}),s.addEventListener("dblclick",n=>{const l=s._wd;if(l){if(this._canvasZoom[t]){this._resetCanvasZoom(t),this._hideGraphTip();return}this._zoomCanvasAbout(t,l.cssToX(i(n.clientX)),Qt)}})}_attachTouchOwnershipGuard(t){t._wdTouchGuard||(t._wdTouchGuard=!0,t.addEventListener("touchmove",e=>{(e.touches.length>=2||t._wdOwnGesture)&&e.preventDefault()},{passive:!1}))}_onAxisHandle(t,e,s){const a=this._gAxisHandle?this._gAxisHandle[e]:null,r=t._wd;if(a==null||!r)return!1;const i=t.getBoundingClientRect(),o=(r.ch-r.padB)/r.dpr;return Math.abs(s.clientX-i.left-a)<=Tt&&Math.abs(s.clientY-i.top-o)<=Tt}_onGraphPinch(t,e,s,a,r){const i=t._wd;if(!i)return;const[o,n]=[...s.values()],l=Math.max(1,Math.abs(o.x-n.x)),c=r((o.x+n.x)/2),p=this._gPinchPending&&this._gPinchPending.id===e?this._gPinchPending:null,_=p?{xMin:p.lo,xMax:p.hi}:this._canvasZoom[e],f=_?_.xMin:i.xStart||0,u=(_?_.xMax:i.xMax||1)-f,b=Math.max(1,i.plotWcss),v=Math.max(0,Math.min(1,c/b)),T=f+v*u,C=u*(a.dist/l),x=T-v*C-(c-a.centerCss)/b*C;a.dist=l,a.centerCss=c,this._gPinchPending={id:e,lo:x,hi:x+C},!this._gPinchRaf&&(this._gPinchRaf=requestAnimationFrame(()=>{this._gPinchRaf=null;const $=this._gPinchPending;this._gPinchPending=null,$&&this._setCanvasViewport($.id,$.lo,$.hi)}))}_setCanvasViewport(t,e,s){const a=this.shadowRoot&&this.shadowRoot.getElementById(t),r=a&&a._wd;if(!r)return;const i=r.xStart||0,o=(r.xMax||1)-i,n=Math.min(o,Math.max(o/Se,Te)),l=Math.min(o,Math.max(n,s-e));if(l>=o*.99){this._resetCanvasZoom(t);return}const c=Math.max(i,Math.min(i+o-l,e)),p=this._canvasZoom[t];p&&Math.abs(p.xMin-c)<1e-6&&Math.abs(p.xMax-c-l)<1e-6||(this._canvasZoom[t]={xMin:c,xMax:c+l},this._redrawCanvas(t),this._syncZoomReset(t))}_zoomCanvasAbout(t,e,s){const a=this.shadowRoot&&this.shadowRoot.getElementById(t),r=a&&a._wd;if(!r)return;const i=r.xMax||1,o=this._canvasZoom[t],n=o?o.xMin:r.xStart||0,l=Math.max(1e-6,(o?o.xMax:i)-n),c=Math.max(0,Math.min(1,(e-n)/l)),p=l*s;this._setCanvasViewport(t,e-c*p,e+(1-c)*p)}_resetCanvasZoom(t){if(!this._canvasZoom[t]){this._syncZoomReset(t);return}delete this._canvasZoom[t],this._redrawCanvas(t),this._syncZoomReset(t)}_ensureZoomReset(t,e){const s=e.parentElement;if(!s)return null;let a=s.querySelector(".wd-zoom-reset");a||(a=document.createElement("button"),a.type="button",a.className="wd-zoom-reset",a.textContent="\u21BB",a.addEventListener("click",i=>{i.stopPropagation(),this._resetCanvasZoom(t)}),s.appendChild(a));const r=this._t("btn.reset_zoom",{},"Reset zoom");return a.title!==r&&(a.title=r,a.setAttribute("aria-label",r)),a}_syncZoomReset(t){const e=this.shadowRoot&&this.shadowRoot.getElementById(t);if(!e)return;const s=this._ensureZoomReset(t,e);s&&s.classList.toggle("wd-zoom-reset--on",!!this._canvasZoom[t])}_onGraphHover(t,e){const s=t.clientX,a=t.clientY,r=t.pointerType==="touch";if(this._hoverRafId){this._hoverPending={px:s,py:a,id:e,touch:r};return}this._hoverPending={px:s,py:a,id:e,touch:r},this._hoverRafId=requestAnimationFrame(()=>{this._hoverRafId=null;const{px:i,py:o,id:n,touch:l}=this._hoverPending||{};n&&this._onGraphHoverInner(i,o,n,{touch:l})})}_onGraphHoverInner(t,e,s,a={}){const r=this.shadowRoot&&this.shadowRoot.getElementById(s),i=r&&r._wd;if(!i)return;const o=r.getBoundingClientRect(),n=i.cssToX(t-o.left),l=(e-o.top)*i.dpr;this._redrawCanvas(s);const c=r.getContext("2d"),p=i.Xpx(n);c.save(),c.strokeStyle="rgba(140,140,140,.75)",c.lineWidth=i.dpr,c.setLineDash([3*i.dpr,3*i.dpr]),c.beginPath(),c.moveTo(p,i.padT),c.lineTo(p,i.ch-i.padB),c.stroke(),c.setLineDash([]);const _=$=>$.stroke==="primary"?i.primary:$.stroke,f=($,L)=>{c.fillStyle=L,c.beginPath(),c.arc(p,i.Ypx($),3.4*i.dpr,0,6.2832),c.fill()},u=i.cycleSpan,b=u&&n${$t(u[0]-n)}`]:u&&n>u[1]?[`${this._t("lbl.after_end",{},"After end")}: ${$t(n-u[1])}`]:[`${this._t("lbl.from_start",{},"From start")}: ${$t(n)}`,`${this._t("lbl.to_end",{},"To end")}: ${$t(Math.max(0,(u?u[1]:i.xMax)-n))}`],v=i.series||[],T=$=>$.bounded?Ue($.points,n):Gt($.points,n);if(this._hoverNearest=null,v.filter($=>!$.aside).length>4){let $=null,L=1/0;if(v.forEach(D=>{const B=T(D);if(B==null)return;const U=Math.abs(i.Ypx(B)-l);UU?c.lineTo(i.Xpx(B[0]),i.Ypx(B[1])):c.moveTo(i.Xpx(B[0]),i.Ypx(B[1]))),c.stroke(),f($.v,D),b.push(`${d($.s.name||"")}: ${it($.v,$.v<100?1:0)} W`),$.s.cid&&(b.push(`${this._t("lbl.click_to_select",{},"click to select")}`),this._hoverNearest={id:s,cid:$.s.cid})}}else v.forEach($=>{const L=T($);L!=null&&(f(L,_($)),b.push(`${d($.name||this._t("lbl.power",{},"Power"))}: ${it(L,L<100?1:0)} W`))});if(i.band){const $=Gt(i.band.min,n),L=Gt(i.band.max,n);$!=null&&L!=null&&b.push(`${this._t("lbl.envelope",{},"Envelope")}: ${it($,$<100?1:0)}\u2013${it(L,L<100?1:0)} W`)}(i.artifacts||[]).forEach($=>{if(n>=$.start_s&&n<=$.end_s){const L=$.detail_key?this._tText($.detail_key,$.detail_params||{},$.detail||""):$.detail||"";b.push(`\u26A0 ${d(ue($.type,(D,B,U)=>this._t(D,B,U)))}: ${d(L)}`)}});const C=!!this._canvasZoom[s];let x=null;if(a.touch?x=C?this._t("lbl.zoom_hint_touch_zoomed",{},"pinch to zoom \xB7 double-tap to reset"):this._t("lbl.zoom_hint_touch",{},"pinch to zoom"):C&&(x=this._t("lbl.zoom_hint",{},"scroll to zoom \xB7 dblclick to reset")),x&&b.push(`${d(x)}`),a.touch){const $=i.ch-i.padB;c.beginPath(),c.arc(p,$,Tt*.41*i.dpr,0,6.2832),c.fillStyle=i.primary,c.fill(),c.strokeStyle="rgba(255,255,255,.92)",c.lineWidth=1.6*i.dpr,c.stroke(),this._gAxisHandle||(this._gAxisHandle={}),this._gAxisHandle[s]=i.xToCss(n)}else this._gAxisHandle&&delete this._gAxisHandle[s];c.restore(),this._showGraphTip(t,e,b,{touch:a.touch,rect:o}),this._syncSpagRowHighlight(this._hoverNearest?this._hoverNearest.cid:null)}_showGraphTip(t,e,s,a={}){const r=this._gtip;if(!r)return;this._gtipLines=s,r.innerHTML=s.join("
"),r.classList.remove("wd-gtip--pinned"),this._gtipPinned=!1,r.style.display="block";const i=r.offsetWidth,o=r.offsetHeight,n=16;let l,c;if(a.touch&&a.rect){const p=a.rect,_=8;p.top-o-_>=6?c=p.top-o-_:p.bottom+_+o<=window.innerHeight-6?c=p.bottom+_:c=e>p.top+p.height*.5?p.top+6:p.bottom-o-6,l=t-i/2}else l=t+n,c=e+n,c+o>window.innerHeight-6&&(c=e-o-n),l+i>window.innerWidth-6&&(l=t-i-n);r.style.left=Math.max(6,Math.min(l,window.innerWidth-i-6))+"px",r.style.top=Math.max(6,Math.min(c,window.innerHeight-o-6))+"px"}_pinGraphTip(){const t=this._gtip;if(!t||t.style.display==="none"||this._gtipPinned)return;this._gtipPinned=!0,t.classList.add("wd-gtip--pinned");const e=this._t("lbl.tap_to_dismiss",{},"tap the chart to dismiss");t.innerHTML=(this._gtipLines||[]).join("
")+`${d(e)}`}_maybeDismissGraphTip(t){if(!this._gtipPinned)return;const e=t.target;e&&e.closest&&e.closest(".wd-canvas-wrap")||this._unpinGraphTip()}_unpinGraphTip(){this._gtipPinned&&(this._gtipPinned=!1,this._gtip&&(this._gtip.classList.remove("wd-gtip--pinned"),this._gtip.style.display="none"))}_hideGraphTip(){this._hoverRafId&&(cancelAnimationFrame(this._hoverRafId),this._hoverRafId=null,this._hoverPending=null),this._gtipPinned=!1,this._gtip&&(this._gtip.classList.remove("wd-gtip--pinned"),this._gtip.style.display="none"),this._syncSpagRowHighlight(null)}_positionTip(t){const e=t.querySelector(".wd-tip-pop");if(!e)return;e.style.transform="";const s=!e.offsetWidth,a=s?e.style.cssText:"";s&&(e.style.visibility="hidden",e.style.display="block");const r=e.getBoundingClientRect();if(s&&(e.style.cssText=a),!r.width)return;const i=be(t),o=6,n=i.left+o,l=i.right-o-r.width,c=li,l=Math.max(64,Math.floor(n?o:i));e.classList.toggle("wd-drop-up",n),e.style.maxHeight=`${Math.min(220,l)}px`}_syncSpagRowHighlight(t){if(t===this._spagHoverCid)return;this._spagHoverCid=t;const e=this.shadowRoot;e&&e.querySelectorAll("tr[data-cid]").forEach(s=>{s.style.backgroundColor=t&&s.dataset.cid===t?"var(--secondary-background-color,rgba(0,0,0,.06))":""})}_showToast(t,e="success",s={}){this._toastTimer&&clearTimeout(this._toastTimer);const a=s.duration||3500;this._toast={msg:t,cls:`wd-toast-${e}`,actionLabel:s.actionLabel||null,actionToken:s.actionToken||null},this._render(),this._toastTimer=setTimeout(()=>{this._toast=null,this._render()},a)}async _openAutoLabelModal(t){let e=this._opts?this._opts.auto_label_confidence:void 0;if(e==null&&t)try{const r=await this._ws({type:`${w}/get_options`,entry_id:t});if(!this._isActiveEntry(t))return;e=(r&&r.options||{}).auto_label_confidence}catch{}e==null&&(e=(wt.auto_label_confidence||{}).def);const s=Number(e),a=Number.isFinite(s)?+Math.min(.95,Math.max(.5,s)).toFixed(2):.9;this._modal={type:"auto-label",threshold:a},this._render()}_autoLabelToast(t){const e=t&&t.result;return!e||typeof e.labeled!="number"?this._tText("msg.toast_auto_label_complete",{},"Auto-label complete"):t.state==="cancelled"?this._tText("toast.auto_label_cancelled",{count:e.labeled},"Auto-label cancelled after labelling {count} cycles."):this._tText("toast.auto_label_done",{count:e.labeled,skipped:e.skipped||0},"Auto-label complete: {count} cycles labelled, {skipped} left unlabelled.")}_profileOptions(t){return(this._profiles||[]).map(e=>``).join("")}_htmlModal(){const t=this._modal;if(t.type==="cycle-detail")return`
`;if(t.type==="profile-panel")return`
`;if(t.type==="profile-group")return`
`;if(t.type==="compare-cycles")return`
`;if(t.type==="gear-settings")return`
`;if(t.type==="export-select")return`
`;if(t.type==="import-wizard")return`
`;if(t.type==="history-import")return`
`;let e="";if(t.type==="confirm"){const s=(t.items||[]).map(a=>`
  • ${d(a.text)}${a.sub?`
    ${d(a.sub)}
    `:""}
  • `).join("");e=`

    ${d(t.title)}

    ${d(t.message)}

    ${s?`
      ${s}
    `:""}
    `}else if(t.type==="label-cycle")e=`

    ${this._t("modal.label_cycle",{},"Label Cycle")}

    `;else if(t.type==="create-profile"){const s=(this._cycles||[]).slice(0,40);let a=s.map(r=>``).join("");t.prefillCycle&&!s.some(r=>r.id===t.prefillCycle)&&(a=``+a),e=`

    ${this._t("modal.create_profile",{},"Create Profile")}

    ${this._t("msg.manual_duration_ref_hint",{},"Only used without a reference cycle; a reference cycle sets the duration from its own length.")}
    `}else if(t.type==="create-phase")e=`

    ${this._t("modal.new_phase",{},"New Phase")}

    `;else if(t.type==="edit-phase"){const s=t.isDefault?`

    ${this._t("msg.edit_builtin_phase",{},"This is a built-in phase. Saving creates a custom copy; delete the copy to restore the original.")}

    `:"";e=`

    ${this._t("modal.edit_phase",{},"Edit Phase")} ${t.isDefault?`${this._t("badge.built_in_tag",{},"built-in")}`:""}

    ${s}
    `}else if(t.type==="process-recording")e=`

    ${this._t("modal.process_recording",{},"Process Recording")}

    ${d(this._t("msg.head_trim_hint",{},"Remove this many seconds from the start"))}
    ${d(this._t("msg.tail_trim_hint",{},"Remove this many seconds from the end"))}
    `;else if(t.type==="correct-feedback")e=`

    ${this._t("modal.correct_feedback",{},"Correct Feedback")}

    ${this._t("msg.feedback_detected",{name:vt(`${d(t.detectedProfile)}`)},"WashData detected: {name}")}

    `;else if(t.type==="auto-label")e=`

    ${this._t("modal.auto_label",{},"Auto-Label Cycles")}

    ${this._t("msg.auto_label_intro",{},"Assign profiles to unlabelled cycles whose match confidence clears the threshold.")}

    `;else if(t.type==="merge-cycles")e=`

    ${this._t("modal.merge_cycles",{n:t.ids.length},`Merge ${t.ids.length} Cycles`)}

    ${this._t("msg.merge_intro",{},"The selected cycles are combined into one (chronological order; gaps filled with 0 W). Pick the resulting profile.")}

    `;else if(t.type==="bulk-relabel")e=`

    ${this._t("modal.relabel_cycles",{count:t.ids.length},`Relabel ${t.ids.length} cycles`)}

    `;else if(t.type==="store-import"){const s=t.mode!=="merge",a=``;e=`

    ${this._t("modal.store_import",{},"Import reference cycle")}

    ${s?`
    `:`
    ${a}
    `}
    `}else if(t.type==="store-share"){const s=Array.isArray(t.profiles)?t.profiles:[],a=[],r=new Set,i=l=>{const c=(l||"").toLowerCase();l&&!r.has(c)&&(r.add(c),a.push(l))};i(t.program),s.forEach(l=>i(l.program));const o=t.profiles==null?` ${this._t("msg.loading",{},"Loading\u2026")}`:"",n=a.length?a.map(l=>``).join(""):t.storeUnreachable?'':``;e=`

    ${this._t("modal.store_share",{},"Share to community store")}

    ${this._t("msg.store_share_intro",{},"Upload this reference cycle so others with the same appliance can use it. It is reviewed before appearing publicly.")}

    ${t.storeUnreachable?this._htmlStoreUnreachable():""}
    `}else t.type==="store-share-device"&&(e=this._htmlShareDeviceModal(t));return`
    `}_htmlShareDeviceModal(t){const e=this._shareableByProgram(),s=this._busy.has("store-share-device"),a=t.selected||new Set,r=!!t.consented,i=e.reduce((u,b)=>u+b.cycles.filter(v=>a.has(v.id)).length,0),o=e.some(u=>!u.noCycles);let n;t.loading?n=`
    \u23F3
    ${this._t("msg.loading",{},"Loading\u2026")}
    `:e.length?n=e.map(u=>{if(u.noCycles)return`
    ${d(u.program)} ${this._t("msg.share_profile_no_cycles",{},"No reference cycles \u2013 mark a cycle as \u2B50 in the Cycles tab to include this profile")}
    `;const b=u.cycles.every($=>a.has($.id)),v=!b&&u.cycles.some($=>a.has($.id)),T=u.cycles.map($=>{const L=$.start_time?et($.start_time):"",D=$.duration!=null?ht($.duration):"";return``}).join(""),x=(this._sharePhasePrograms||[]).includes(u.program)?``:"";return`
    ${T}
    ${x}
    `}).join(""):n=`
    \u{1F4E4}
    ${this._t("msg.share_device_none",{},"No shareable cycles yet. Mark a recorded or hand-picked cycle as a reference cycle (\u2B50) in the Cycles tab first.")}
    `;const l=(this._opts.store_brand||"").trim(),c=(this._opts.store_model||"").trim(),p=``,_=`
    ${this._t("msg.share_guidelines_title",{},"Before you share")}
    • ${this._t("msg.share_guideline_naming",{},"Name each profile exactly as shown on the appliance dial or display (e.g. 'Cotton 40', 'Eco 60').")}
    • ${this._t("msg.share_guideline_quality",{},"Only share cycles that finished normally: no interruptions, door openings or power blips.")}
    • ${this._t("msg.share_guideline_review",{},"Your upload starts as pending and appears publicly once enough community members confirm it.")}
    `,f=o?``:"";return`

    ${this._t("modal.store_share_device",{},"Share this device")}

    ${this._t("msg.store_share_device_intro",{brand:l,model:c},"Upload {brand} {model} with the reference cycles you select. Others with the same appliance can adopt your programs. Entries are reviewed before appearing publicly.")}

    ${_}
    ${n}
    ${o?p:""} ${f}
    `}_wizCatOrder(){return["profiles","real_cycles","reference_cycles","backfill_cycles","custom_phases","profile_groups","settings","ml_models","feedback","suggestions","maintenance_log","history_logs","lifetime_stats"]}_wizCycleCats(){return["real_cycles","reference_cycles","backfill_cycles"]}_wizIdSet(t,e){return e==="real_cycles"?t.realIds:e==="backfill_cycles"?t.bfIds:t.refIds}_wizEmptySel(){return{cats:new Set,profiles:new Set,realIds:new Set,refIds:new Set,bfIds:new Set}}_wizAnySel(t){return!!(t&&(t.profiles.size||t.realIds.size||t.refIds.size||t.bfIds.size||t.cats.size))}_wizCatLabel(t){return{profiles:this._t("lbl.cat_profiles",{},"Profiles (programs)"),real_cycles:this._t("lbl.cat_real_cycles",{},"Cycles (run history)"),reference_cycles:this._t("lbl.cat_reference_cycles",{},"Reference cycles (imported)"),backfill_cycles:this._t("lbl.cat_backfill_cycles",{},"Cycles found in imported power history"),custom_phases:this._t("lbl.cat_custom_phases",{},"Custom phases"),profile_groups:this._t("lbl.cat_profile_groups",{},"Profile groups"),settings:this._t("lbl.cat_settings",{},"Detection & matching settings"),ml_models:this._t("lbl.cat_ml_models",{},"ML models"),feedback:this._t("lbl.cat_feedback",{},"Feedback & review labels"),suggestions:this._t("lbl.cat_suggestions",{},"Suggestions"),maintenance_log:this._t("lbl.cat_maintenance_log",{},"Maintenance log"),history_logs:this._t("lbl.cat_history_logs",{},"History & change logs"),lifetime_stats:this._t("lbl.cat_lifetime_stats",{},"Lifetime totals")}[t]||t}_wizInitSel(t,e){const s=this._wizEmptySel(),a=t&&t.categories||{},r=i=>!!(i&&i.present&&(!e||i.importable!==!1));return r(a.profiles)&&(a.profiles.items||[]).forEach(i=>s.profiles.add(i.name)),this._wizCycleCats().forEach(i=>{const o=this._wizIdSet(s,i);r(a[i])&&(a[i].groups||[]).forEach(n=>n.cycles.forEach(l=>{l.id!=null&&o.add(String(l.id))}))}),Object.keys(a).forEach(i=>{i==="profiles"||this._wizCycleCats().includes(i)||r(a[i])&&s.cats.add(i)}),s}_wizSelectionPayload(t){const e=t.sel,s=[];e.profiles.size&&s.push("profiles"),e.realIds.size&&s.push("real_cycles"),e.refIds.size&&s.push("reference_cycles"),e.bfIds.size&&s.push("backfill_cycles"),e.cats.forEach(r=>s.push(r));const a={categories:s};return e.profiles.size&&(a.profiles=Array.from(e.profiles)),e.realIds.size&&(a.real_cycle_ids=Array.from(e.realIds)),e.refIds.size&&(a.reference_cycle_ids=Array.from(e.refIds)),e.bfIds.size&&(a.backfill_cycle_ids=Array.from(e.bfIds)),a}_wizGroupIds(t,e,s){const r=(((t.categories||{})[e]||{}).groups||[]).find(i=>i.profile===s);return r?r.cycles.filter(i=>i.id!=null).map(i=>String(i.id)):[]}_wizCatState(t,e,s){const a=t.sel,r=(s.categories||{})[e]||{};if(e==="profiles"){const o=r.items||[],n=o.length,l=o.filter(c=>a.profiles.has(c.name)).length;return{sel:l,total:n,state:l===0?"none":l===n?"all":"some"}}if(this._wizCycleCats().includes(e)){const o=this._wizIdSet(a,e),n=r.count||0;let l=0;return(r.groups||[]).forEach(c=>c.cycles.forEach(p=>{o.has(String(p.id))&&l++})),{sel:l,total:n,state:l===0?"none":l===n?"all":"some"}}const i=a.cats.has(e);return{sel:i?1:0,total:1,state:i?"all":"none"}}_htmlSelectionTree(t,e,s={}){const a=e&&e.categories||{},r=new Set;s.conflicts&&(a.profiles&&a.profiles.items||[]).forEach(p=>{p.conflict&&r.add(p.name)});const i=this._wizCatOrder().filter(p=>a[p]&&a[p].present);let o=!1,n=!0;i.forEach(p=>{if(s.importableOnly&&a[p].importable===!1)return;const _=this._wizCatState(t,p,e);_.state!=="none"&&(o=!0),_.state!=="all"&&(n=!1)});const l=``,c=i.map(p=>{const _=a[p],f=s.importableOnly&&_.importable===!1,u=this._wizCatState(t,p,e),v=p==="profiles"||this._wizCycleCats().includes(p)?`${u.sel}/${_.count}`:"",T=``;let C="";if(!f&&p==="profiles")C=`
    ${(_.items||[]).map(x=>``).join("")}
    `;else if(!f&&this._wizCycleCats().includes(p)){const x=this._wizIdSet(t.sel,p);C=`
    ${(_.groups||[]).map($=>{const L=$.cycles.map(O=>String(O.id)),D=L.filter(O=>x.has(O)).length,B=D===L.length&&L.length>0,U=D>0&&!B,z=`${p}:${$.profile}`,M=t.expanded&&t.expanded.has(z),A=M?$.cycles.map(O=>``).join(""):"";return`
    ${A}
    `}).join("")}
    `}return`
    ${T}${C}
    `}).join("");return`
    ${l}${c}
    `}_htmlExportSelectModal(t){const e=this._busy.has("export-select");let s;t.loading||!t.inventory?s=`
    \u23F3
    ${this._t("msg.loading",{},"Loading\u2026")}
    `:s=this._htmlSelectionTree(t,{categories:t.inventory},{importableOnly:!1});const a=this._wizAnySel(t.sel);return`

    ${this._t("modal.export_select",{},"Export - choose data")}

    ${this._t("msg.export_select_intro",{},"Tick what to include. Profiles without their cycles still export a matchable program.")}

    ${s}
    `}_htmlImportWizardModal(t){const e=this._busy.has("import-wizard"),s=`

    ${this._t("modal.import_wizard",{},"Import - choose data")}

    `;if(t.step==="input"||t.step==="analyze"){const T=t.step==="analyze";return`${s}

    ${this._t("msg.import_analyze_hint",{},"Load an exported file (or paste its JSON). WashData shows what can be imported before anything changes.")}

    ${t.error?`

    ${d(t.error)}

    `:""}
    `}const a=t.manifest||{},r=a.device_type_match===!1,i=a.source_device_type||"?",o=a.local_device_type||"?",n=r?`
    ${this._t("msg.device_type_mismatch_warn",{src:i,local:o},"This export is from a different appliance type ({src} vs {local}). Programs and cycles can still be imported as reference data, but not device settings or real history.")}
    `:"",l=this._htmlSelectionTree(t,a,{importableOnly:!0,conflicts:!0}),c=`
    ${t.mode==="replace"?this._t("msg.replace_warn",{},"Each ticked category is wiped and replaced from the file. Unticked categories are left untouched.")+" "+this._t("msg.replace_undo_hint",{},"A restore point is saved first, so you can undo this import from Export / Import."):this._t("msg.merge_hint",{},"Imported items are added; nothing local is lost. Name clashes are resolved below.")}
    `,p=a.real_history_allowed!==!1,_=`
    ${t.cycleDest==="real_history"?this._t("msg.dest_real_history_hint",{},"Imported cycles count as this device's own history and statistics. Use this to move an appliance to a new install."):this._t("msg.dest_reference_hint",{},"Imported cycles only improve program matching and never affect usage/energy statistics.")}
    `;let f="";const b=(a.categories&&a.categories.profiles&&a.categories.profiles.items||[]).filter(T=>T.conflict&&t.sel.profiles.has(T.name));t.mode==="merge"&&b.length&&(f=`
    ${b.map(T=>`
    ${d(T.name)}
    `).join("")}
    `);const v=this._wizAnySel(t.sel);return`${s} ${n} ${l} ${c} ${_} ${f}
    `}_histSkipReason(t){return{idle:this._t("lbl.hist_skip_idle",{},"nothing running"),sparse:this._t("lbl.hist_skip_sparse",{},"readings too far apart"),too_few_samples:this._t("lbl.hist_skip_short",{},"too few readings"),too_long:this._t("lbl.hist_skip_long",{},"no break long enough to split on")}[t]||t||""}_histSegReason(t){return{shorter_than_minimum:this._t("lbl.hist_reason_short",{},"shorter than this appliance's shortest real cycle"),no_clean_end:this._t("lbl.hist_reason_no_end",{},"never ended cleanly"),already_recorded:this._t("lbl.hist_reason_already_recorded",{},"overlaps a cycle WashData already recorded")}[t]||""}_htmlHistoryImportModal(t){const e=`

    ${this._t("modal.history_import",{},"Import power history")}

    `,s=t.error?`

    ${d(t.error)}

    `:"";if(t.step==="input"){const x=this._busy.has("hist-import"),$=x?"disabled":"";return`${e}

    ${this._t("msg.hist_input_hint",{},"Upload a CSV from the History panel (entity, state, last changed), or let WashData read the sensor's history directly. Live detection runs over it and you choose which cycles to keep.")}

    ${this._t("lbl.hist_since",{},"Since")}
    ${this._t("msg.hist_recorder_hint",{},"Reads from the date you pick until now. Home Assistant keeps detailed history for 10 days by default; older hourly averages are too coarse for cycle detection.")}
    ${s}
    `}if(t.step==="scan"){const x=t.scanTaskId?(this._tasks||{})[t.scanTaskId]:null,$=x&&x.total>0?Math.round(x.done/x.total*100):null;return`${e}

    ${this._t("msg.hist_scanning",{},"Running your history through the detector in the background. You can close this dialog and come back.")}

    ${$==null?this._t("status.preparing",{},"Preparing\u2026"):`${$}%`}

    ${s}
    `}if(t.step==="done"){const x=t.done||{},$=[this._tText("msg.hist_imported_count",{n:x.imported||0},`${x.imported||0} cycles imported.`),x.duplicates?this._tText("msg.hist_duplicates",{n:x.duplicates},`${x.duplicates} were already imported and were skipped.`):"",x.capped?this._t("msg.hist_capped",{},"The per-device limit for imported cycles was reached; the rest were not stored."):""].filter(Boolean);return`${e} ${$.map(L=>`

    ${d(L)}

    `).join("")}

    ${this._t("msg.hist_next_step",{},"They are in your Cycles list, tagged as imported history. Open one and use Label to name its program.")}

    `}const a=t.result||{},r=a.segments||[],i=a.parse||{},o=this._busy.has("hist-apply"),n=t.accept||new Set,l=[];i.rows_total&&l.push(this._tText("msg.hist_rows_read",{n:i.rows_total},`${i.rows_total} readings read`)),i.first&&i.last&&l.push(`${et(i.first)} \u2013 ${et(i.last)}`),i.breaks&&l.push(this._tText("msg.hist_breaks",{n:i.breaks},`${i.breaks} gaps where the sensor was unavailable`)),i.rows_other_entity&&l.push(this._tText("msg.hist_other_entity",{n:i.rows_other_entity},`${i.rows_other_entity} readings for other entities ignored`)),i.truncated&&l.push(this._tText("msg.hist_truncated",{n:i.rows_total||0},`stopped at the ${i.rows_total||0}-reading limit, so the rest of the history was not scanned`)),i.entity_substituted_from&&l.push(this._tText("msg.hist_entity_substituted",{used:i.entity_id||"?",wanted:i.entity_substituted_from},`read ${i.entity_id||"?"} (this device is configured for ${i.entity_substituted_from})`));const c={looks_like_kw:this._tText("msg.hist_warn_kw",{w:i.peak_w},`The highest reading is only ${i.peak_w}. This sensor seems to report kilowatts; WashData needs watts, so no cycles can be detected.`),naive_timestamps:this._tText("msg.hist_warn_naive_time",{n:i.rows_naive_time||0},`${i.rows_naive_time||0} timestamps have no time zone and were read as UTC. If the file uses local time, cycles are shifted by your UTC offset and readings near a DST change may be dropped.`)},p=(i.warnings||[]).map(x=>c[x]).filter(Boolean).map(x=>`

    \u26A0 ${d(x)}

    `).join(""),_=a.skipped||[],f={};_.forEach(x=>{f[x.reason]=(f[x.reason]||0)+1});const u=Object.entries(f).map(([x,$])=>`${$} \xD7 ${this._histSkipReason(x)}`).join(", "),b=a.settings||{},v=b.min_power!=null?this._tText("msg.hist_settings_used",{w:b.min_power,s:b.off_delay},`Detected using this device's current settings (minimum power ${b.min_power} W, off delay ${b.off_delay} s).`):"";if(!r.length)return`${e}

    ${this._t("msg.hist_none_found",{},"No cycles could be detected in that history.")}

    ${p} ${l.length?`

    ${d(l.join(" \xB7 "))}

    `:""} ${u?`

    ${this._t("msg.hist_skipped_spans",{},"Skipped stretches")}: ${d(u)}

    `:""} ${v?`

    ${d(v)}

    `:""}
    `;const T=r.map(x=>{const $=n.has(x.index),L=this._histSegReason(x.reason);return` ${d(et(x.start_time))} ${d(ht(x.duration_s))}${x.banked_tail_s?" \u2702":""} ${x.energy_wh!=null?d(it(x.energy_wh/1e3,2))+" kWh":"\u2013"} ${d(String(Math.round(x.peak_w)))} W ${L?`\u26A0 ${d(L)}`:`${d(this._t("lbl.hist_looks_complete",{},"complete"))}`} `}).join(""),C=r.every(x=>n.has(x.index));return`${e}

    ${this._t("msg.hist_found",{n:r.length},`Found ${r.length} cycles. Untick anything that does not look like a real run - nothing is stored until you import.`)}

    ${p} ${l.length?`

    ${d(l.join(" \xB7 "))}

    `:""} ${u?`

    ${this._t("msg.hist_skipped_spans",{},"Skipped stretches")}: ${d(u)}

    `:""} ${v?`

    ${d(v)}

    `:""}
    ${T}
    ${this._t("lbl.date",{},"Date")} ${this._t("lbl.duration",{},"Duration")} ${this._t("lbl.energy",{},"Energy")} ${this._t("lbl.peak_power_short",{},"Peak")} ${this._t("lbl.shape",{},"Shape")} ${this._t("lbl.notes",{},"Notes")}
    ${a.capped?`

    ${this._t("msg.hist_scan_capped",{n:a.found},`Only the first candidates are shown (${a.found} were found).`)}

    `:""} ${s}
    `}_drawHistorySparklines(){const t=this.shadowRoot;if(!t)return;const e=this._modal,s=e&&e.result&&e.result.segments||[],a={};s.forEach(r=>{a[String(r.index)]=r.curve||[]}),t.querySelectorAll("canvas[data-hist-spark]").forEach(r=>{this._paintSparkline(r,a[r.dataset.histSpark]||[])})}_htmlCycleModal(t){if(!t.loaded)return`

    ${this._t("modal.cycle",{},"Cycle")}

    \u23F3
    ${this._t("msg.loading_curve",{},"Loading curve\u2026")}
    `;const e=t.curve||{},s=!!e.is_reference,a=e.labelable!==!1,r=e.editable!==!1,i=e.cycle_origin==="backfill",o=e.full_duration_s||e.duration||0,n=e.energy_kwh!=null?e.energy_kwh:null,l=t.ml||null,c=`
    ${ht(e.duration||o)}
    ${this._t("lbl.duration",{},"Duration")}
    ${gt(n)}
    ${this._t("lbl.energy",{},"Energy")}
    ${d(e.profile_name||this._t("lbl.unlabelled",{},"Unlabelled"))}
    ${this._t("lbl.profile",{},"Profile")}
    ${d(e.status?this._cycleStatusLabel(e.status):"-")}
    ${this._t("lbl.status",{},"Status")}
    `,p=l&&l.ml_review||{},f=(this._feedbacks||[]).some(I=>I.cycle_id===t.cycleId),u=f&&t.mode!=="review"?` \u25CF`:"",b=this._canEdit()&&r?`
    `:s?`
    \u{1F4E5} ${i?this._t("msg.imported_history_readonly",{},"Found in imported power history. It helps matching but is not counted in your statistics and cannot be trimmed or split. Label it to name the program."):this._t("msg.imported_readonly",{},"Imported from the community store. Used for reference and matching, not counted in your stats, and cannot be edited.")}
    `:"",v=this._canEdit()?(this._feedbacks||[]).find(I=>I.cycle_id===t.cycleId):null,T=v?v.detected_profile||v.profile_name||this._t("lbl.unknown",{},"Unknown"):"",C=v?`
    \u26A0 ${this._t("msg.pending_feedback",{},"Pending detection feedback")}

    ${this._t("msg.unsure_detected_prefix",{},"WashData is unsure it detected")} ${d(T)}${v.confidence!=null?` (${this._t("lbl.confidence",{},"confidence").toLowerCase()} ${(v.confidence*100).toFixed(0)}%)`:""}. ${this._t("msg.feedback_prompt",{},"Confirm it was right, correct the program, or ignore.")} ${this._t("msg.feedback_relabel_hint",{},"Re-labelling this cycle resolves it too.")}

    `:"";let x="";if(t.mode==="view"){const I=!!(l&&l.ml_review&&l.ml_review.golden),G=this._canEdit()&&this._onlineEnabled()&&this._storeConnected&&I?``:"",W=this._canEdit()?`${a?` `:""}`:"";x=`${C}
    ${G} ${W}
    `}else if(t.mode==="trim"){const I=this._busy.has("cyc-trim-apply"),R=t.timeMode||"s",G=R==="clock"?this._offsetToClock(t.trim.start):Math.round(t.trim.start),W=R==="clock"?this._offsetToClock(t.trim.end):Math.round(t.trim.end),J=R==="clock"?"time":"number",Y=R==="clock"?'step="1"':`min="0" max="${Math.ceil(o)}" step="1"`,nt=R==="clock"?"":" "+this._t("lbl.unit_s",{},"(s)");x=`

    ${this._t("msg.trim_intro",{},"Drag the red handles, or enter values. Everything outside the window is removed.")}

    ${this._t("lbl.input",{},"Input:")}
    `}else if(t.mode==="split"){const I=this._busy.has("cyc-split-apply"),R=(t.split.offsets||[]).slice().sort((J,Y)=>J-Y),G=[0,...R,o],W=G.slice(0,-1).map((J,Y)=>{const nt=G[Y+1];return`
    ${ht(J)} \u2013 ${ht(nt)}
    `}).join("");x=`

    ${this._t("msg.split_intro",{},"Click the graph to add or remove a split point, or auto-detect by idle gaps. Each part can get its own profile.")}

    ${R.length?W:`

    ${this._t("msg.no_split_points",{},"No split points yet.")}

    `}
    `}else if(t.mode==="review"){const I=l&&l.ml_review||{},R=this._busy.has("cyc-review-save"),G=(m,g)=>``,J=[["late_start",this._t("tag.late_start",{},"Late start")],["early_end",this._t("tag.early_end",{},"Early end")],["merged",this._t("tag.merged",{},"Merged cycles")],["split",this._t("tag.split",{},"Split cycle")],["noise",this._t("tag.noise",{},"Noise")],["wrong_profile",this._t("tag.wrong_profile",{},"Wrong profile")],["sensor_gap",this._t("tag.sensor_gap",{},"Sensor gap")]].map(([m,g])=>``).join(""),Y=I.reviewed_at?`${this._t("lbl.reviewed_on",{date:Oe(I.reviewed_at)},"reviewed {date}")}`:"",nt=ot(this._t("msg.review_profile_tip",{},"The program this cycle is labelled as. If it is wrong, correct it here; labels teach matching.")),_t=ot(this._t("msg.review_quality_tip",{},"How clean this cycle is. Good: a textbook run of this program. Bad: detected, but noisy or unusual. Unusable: mis-detected (merged, cut short or spurious). Affects the health score and which cycles train the model.")),ct=ot(this._t("msg.review_recorded_tip",{},"Mark this as a hand-picked reference cycle for its program, like a manually recorded cycle. Reference cycles are always kept, seed the matching template, and are never removed by cleanup.")),dt=ot(this._t("msg.review_tags_tip",{},"Optional flags for what went wrong, so training and cleanup can allow for it.")),ut=ot(this._t("msg.review_notes_tip",{},"Free-text notes for your own reference. Not used by matching or training."));x=` ${C}

    ${this._t("msg.review_confirm_help",{},"Was this cycle detected correctly? Confirming cycles improves matching for your machine. A quick Good/Bad is enough.")}

    ${Y}
    ${this._t("lbl.compare_profiles",{},"Compare with profiles")}${ot(this._t("msg.compare_profiles_tip",{},"Overlay other profiles on the chart to see which fits this cycle best."))}
    ${(this._profiles||[]).map(m=>{const g=(t.overlays||[]).includes(m.name),k=g?``:"";return``}).join("")||`${this._t("msg.no_profiles_compare",{},"No profiles to compare.")}`}
    ${this._t("lbl.tags",{},"Tags")}${dt}
    ${J}
    ${this._t("lbl.notes",{},"Notes")}${ut}
    `}let $="";const L=t.mode==="view"||t.mode==="review"?e.artifacts||[]:[];if(L.length){const I=L.map(R=>{const G=R.detail_key?this._tText(R.detail_key,R.detail_params||{},R.detail||""):R.detail||"";return`
  • ${d(ue(R.type,(W,J,Y)=>this._t(W,J,Y)))} ${this._t("lbl.at",{},"at")} ${$t(R.start_s)}\u2013${$t(R.end_s)} \u2014 ${d(G)}
  • `}).join("");$=`
    \u26A0 ${this._t("msg.artifact_header",{n:L.length},`${L.length} anomal${L.length>1?"ies":"y"} detected during this cycle`)}
      ${I}
    ${this._t("msg.artifact_footer",{},"Highlighted on the graph above. These are transient artifacts (e.g. the door opened mid-cycle), not necessarily problems.")}
    `}let D="";const B=t.mode==="view"||t.mode==="review"?e.restart_gaps||[]:[];if(B.length){const I=B.map(R=>{const G=Math.round((R.gap_seconds||0)/60),W=G>=1?`${G}m`:`${Math.round(R.gap_seconds||0)}s`,J=R.match_confidence!=null?` \xB7 ${this._t("lbl.pct_match_confidence",{pct:Math.round(R.match_confidence*100)},`${Math.round(R.match_confidence*100)}% match confidence`)}`:"",Y=R.profile?` (${d(R.profile)})`:"";return`
  • ${this._t("msg.restart_gap_item",{dur:W},`${W} gap`)}: ${this._t("lbl.ha_restarted",{},"HA restarted")}${Y}${J}
  • `}).join("");D=`
    \u21BB ${this._t("msg.restart_gap_header",{n:B.length},`${B.length} HA restart gap${B.length>1?"s":""} during this cycle`)}
      ${I}
    ${this._t("msg.restart_gap_footer",{},"Highlighted on the graph. Power data is missing for these intervals; matching used only real readings.")}
    `}let U="";if(e.decimated){const I=(e.samples||[]).length,R=e.sample_count||I;U=`
    ${this._t("msg.samples_decimated",{shown:I,total:R},`Showing ${I} of ${R} samples (thinned for display; peaks kept). A wide gap here is thinning, not missing data.`)}
    `}const z=this._cycleProfileOverlays(t),M=I=>I.dash?`background:repeating-linear-gradient(90deg,${I.stroke} 0 5px,transparent 5px 8px)`:`background:${I.stroke}`,A=this._cycleContextSeries(t),O=A?[...z,A.series]:z,K=O.length?`
    ${this._t("lbl.power",{},"Power")} ${O.map(I=>` ${d(I.name)}`).join("")}
    `:"";return`

    ${this._t("lbl.cycle",{},"Cycle")} \xB7 ${d(et(e.start_time))}

    ${c}${b}
    ${this._htmlCycleContextBar(t,e)} ${K} ${U} ${$} ${D} ${x}`}_htmlCycleContextBar(t,e){if(t.mode!=="view"&&t.mode!=="review"||e.is_reference||!(e.samples||[]).length)return"";const s=t.ctx||{minutes:this._cycleContextMinutes(t.entryId),loading:!1,data:null},a=Ft.map(n=>``).join("");let r="",i="";s.minutes&&s.loading?(r="loading",i=this._t("msg.cycle_context_loading",{},"Loading recorder history\u2026")):s.minutes&&s.data&&this._cycleContextSeries(t)?(r="shown",i=this._t("msg.cycle_context_note",{},"Grey: the power sensor's recorder history around this cycle. It is not part of the cycle and does not affect duration, energy or matching.")):s.minutes&&s.data&&(r="none",i=this._t("msg.cycle_context_none",{},"The recorder holds no readings around this cycle (Home Assistant keeps 10 days of history by default)."));const o=ot(this._tText("msg.cycle_context_tip",{},"Display only: shows the power sensor's recorder history around this cycle, if still kept. To add failed start attempts to future cycles, use Curve Pre-roll (Settings > Detection > Cycle Start), which changes stored durations."));return`
    ${o} ${i?`${i}`:""}
    `}_htmlProfilePanel(t){const e=this._canEdit();t.tab==="danger"&&!e&&(t.tab="stats");const s=[["stats",this._t("tab.pp_overview",{},"Overview")],["phases",this._t("tab.pp_phases",{},"Phases")],["cleanup",this._t("tab.pp_cleanup",{},"Cleanup")]];e&&s.push(["danger",this._t("tab.pp_manage",{},"Manage")]);const a=s.map(([o,n])=>``).join("");let r="";if(!t.loaded)r=`
    \u23F3
    ${this._t("msg.loading",{},"Loading\u2026")}
    `;else if(t.tab==="stats"){const o=t.stats||{},n=t.env||{},l=this._hass&&this._hass.config&&this._hass.config.currency||"",c=o.avg_energy!=null&&o.cycle_count?o.avg_energy*o.cycle_count:null,p=v=>v?Math.round(v/60)+"m":"-",_=(this._profileHealth||{})[t.name],f=(this._profileTrends||{})[t.name],u=_&&_.health_status!=="unknown"?(()=>{const v={healthy:["var(--success-color,#4caf50)","rgba(76,175,80,.12)"],fair:["var(--warning-color,#ff9800)","rgba(255,152,0,.12)"],poor:["var(--error-color,#f44336)","rgba(244,67,54,.12)"]},[T,C]=v[_.health_status]||v.fair,x=Math.round((_.health_score||0)*100),$=_.duration_cv!=null?` \xB7 ${this._t("stat.duration_cv",{pct:Math.round(_.duration_cv*100)},`duration CV ${Math.round(_.duration_cv*100)}%`)}`:"",L=_.confidence_mean!=null?` \xB7 ${this._t("stat.avg_confidence",{pct:Math.round(_.confidence_mean*100)},`avg confidence ${Math.round(_.confidence_mean*100)}%`)}`:"";return`
    ${_.health_status==="poor"?this._t("health.poor",{},"\u26A0 Poor match fit"):_.health_status==="fair"?this._t("health.fair",{},"Fair match fit"):this._t("health.good",{},"\u2713 Good match fit")} ${this._t("stat.score",{pct:x},`score ${x}%`)}${$}${L} ${this._profileAdvisory(t.name,"poor_health")?`${d(this._advisoryText(this._profileAdvisory(t.name,"poor_health")))}`:""}
    `})():"",b=f&&(f.duration_trend!=="stable"||f.energy_trend&&f.energy_trend!=="stable")?(()=>{const v=[];f.duration_trend==="up"?v.push(this._t("msg.trend_duration_longer",{pct:`${f.duration_slope_pct>0?"+":""}${f.duration_slope_pct}`,avg:`${Math.round(f.duration_recent_mean_s/60)}m`},`Duration trending longer (${f.duration_slope_pct>0?"+":""}${f.duration_slope_pct}%/cycle) \u2014 recent avg ${Math.round(f.duration_recent_mean_s/60)}m`)):f.duration_trend==="down"&&v.push(this._t("msg.trend_duration_shorter",{pct:`${f.duration_slope_pct}`,avg:`${Math.round(f.duration_recent_mean_s/60)}m`},`Duration trending shorter (${f.duration_slope_pct}%/cycle) \u2014 recent avg ${Math.round(f.duration_recent_mean_s/60)}m`)),f.energy_trend==="up"?v.push(this._t("msg.trend_energy_up",{pct:`${f.energy_slope_pct>0?"+":""}${f.energy_slope_pct}`,avg:gt(f.energy_recent_mean_wh)},`Energy trending up (${f.energy_slope_pct>0?"+":""}${f.energy_slope_pct}%/cycle) \u2014 recent avg ${gt(f.energy_recent_mean_wh)}`)):f.energy_trend==="down"&&v.push(this._t("msg.trend_energy_down",{pct:`${f.energy_slope_pct}`},`Energy trending down (${f.energy_slope_pct}%/cycle)`));const T=f.duration_trend==="up"||f.energy_trend==="up",C=this._profileAdvisory(t.name,"duration_trend_up")||this._profileAdvisory(t.name,"energy_trend_up");return`
    ${this._t("msg.performance_trend",{n:f.cycle_count},`Performance trend (${f.cycle_count} cycles)`)}
    ${v.map(L=>`${L}`).join("
    ")} ${C?`
    ${d(this._advisoryText(C))}`:""}
    `})():"";r=`
    ${this._t("lbl.duration",{},"Duration")}
    ${p(o.avg_duration)}${this._t("stat.avg",{},"avg")}
    ${this._t("stat.min",{v:p(o.min_duration)},`min ${p(o.min_duration)}`)} \xB7 ${this._t("stat.max",{v:p(o.max_duration)},`max ${p(o.max_duration)}`)}${n.duration_std_dev!=null?` \xB7 ${this._t("stat.consistency",{v:`${Math.round(n.duration_std_dev/60)}m`},`consistency \xB1${Math.round(n.duration_std_dev/60)}m`)}`:""}
    ${this._t("lbl.energy",{},"Energy")}
    ${gt(o.avg_energy)}${this._t("stat.avg",{},"avg")}
    ${this._t("stat.total",{v:gt(c)},`total ${gt(c)}`)}
    ${o.avg_cost!=null?`
    ${this._t("lbl.avg_cost",{},"Avg cost")}
    ${It(o.avg_cost,l)}${this._t("stat.avg",{},"avg")}
    ${this._t("stat.total",{v:It(o.total_cost,l)},"total {v}")}
    `:""}
    ${this._t("lbl.activity",{},"Activity")}
    ${o.cycle_count||0}${this._t("lbl.cycles_lc",{},"cycles")}
    ${this._t("stat.last_run",{v:o.last_run?et(o.last_run):"-"},`last run ${o.last_run?et(o.last_run):"-"}`)}
    ${u} ${b} ${this._profileAdvisory(t.name,"shape_drift")?`
    \u26A0 ${d(this._advisoryText(this._profileAdvisory(t.name,"shape_drift")))}
    `:""} ${n.avg&&n.avg.length?`
    `:`

    ${this._t("msg.no_envelope",{},"No envelope yet - rebuild after labelling cycles.")}

    `}`}else if(t.tab==="phases"){const o=t.catalog||[],n=(t.phases||[]).map((c,p)=>{const _=o.map(f=>``).join("");return`
    \u2013 ${this._t("lbl.timer_min",{},"min")}
    `}).join(""),l=this._busy.has("pp-phase-save");r=`

    ${this._t("msg.phase_ranges_intro",{},"Phase ranges (minutes from cycle start) overlaid on the average curve. Edit values to preview live.")}

    ${t.env&&t.env.avg&&t.env.avg.length?`
    `:`

    ${this._t("msg.no_envelope_overlay",{},"No envelope available to overlay.")}

    `}
    ${n||`

    ${this._t("msg.no_phases_assigned",{},"No phases assigned.")}

    `}
    ${e?`
    `:""}`}else if(t.tab==="cleanup"){const o=t.cleanup&&t.cleanup.cycles||[],n=t.cleanup&&t.cleanup.selected||new Set,{col:l,dir:c}=this._cleanupSort,p={date:C=>C.start_time?new Date(C.start_time).getTime():0,duration:C=>C.duration,energy:C=>C.energy_kwh,status:C=>C.status||""},f=_e(o,p[l]||p.date,c).map((C,x)=>{const $=o.indexOf(C),L=e?` `:"";return` ${et(C.start_time)} ${ht(C.duration)} ${C.energy_kwh!=null?gt(C.energy_kwh):"-"} ${d(C.status||"completed")} ${L} `}).join(""),u=` ${mt(this._t("lbl.date",{},"Date"),"date",l==="date",c,"cleanupsort")} ${mt(this._t("lbl.duration",{},"Duration"),"duration",l==="duration",c,"cleanupsort","right")} ${mt(this._t("lbl.energy",{},"Energy"),"energy",l==="energy",c,"cleanupsort","right")} ${mt(this._t("lbl.status",{},"Status"),"status",l==="status",c,"cleanupsort")} ${e?"":""} `,b=this._busy.has("pp-cleanup-del"),v=this._busy.has("pp-cleanup-unlabel"),T=b||v;r=`

    ${this._t("msg.cleanup_intro",{},"All labelled cycles overlaid. Tick outliers, then unlabel them (kept in history, no longer shaping this profile) or delete them.")}

    ${o.length?`
    `:`

    ${this._t("msg.no_cycles_profile",{},"No cycles for this profile.")}

    `} ${o.length?`
    ${u}${f}
    `:""} ${e?`
    `:""}`}else if(t.tab==="danger"){const o=this._busy.has("pp-rebuild"),n=t.stats&&t.stats.avg_duration?Math.round(t.stats.avg_duration/60):0,l=!!(t.stats&&t.stats.duration_learned),c=l?this._t("msg.duration_learned_hint",{},"Worked out from this program's cycles (outliers left out) and updated after each new one. To change it, trim or remove the cycles that are off."):this._t("msg.manual_duration_hint",{},"The profile's expected cycle length, used for time-remaining estimates. Leave it unchanged to keep the current value.");r=`
    ${c}
    `}const i=this._onlineEnabled()&&this._storeDeviceDeclared()?``:"";return`

    ${this._t("modal.profile_title",{name:t.name},"Profile \xB7 {name}")}

    ${a}
    ${r}
    ${i}
    `}_cycleProfileOverlays(t){const e=[];if(!t||t.mode!=="view"&&t.mode!=="review")return e;const s=t.curve||{},a=t.overlayPlan||null,r=!!(a&&a.suspected),i=a&&a.name||s.profile_name||"profile",o=r?this._tText("lbl.suspected_profile",{name:i},"{name} (suspected)"):`${this._tText("lbl.expected",{},"Expected")} (${i})`,n=t.profileEnv,l=p=>p.target_duration||p.avg[p.avg.length-1][0]||0;!r&&(s.expected||[]).length>1?e.push({kind:"expected",points:s.expected,stroke:"#ff9800",width:2,alpha:.45,name:o}):n&&(n.avg||[]).length&&e.push({kind:r?"suspected":"expected",points:n.avg,stroke:"#ff9800",width:2,alpha:.45,dash:r,name:o,xEnd:l(n)});const c=r&&a.runnerUp?t.runnerUpEnv:null;return c&&(c.avg||[]).length&&e.push({kind:"runner_up",points:c.avg,stroke:"#9c27b0",width:1.6,alpha:.55,dash:!0,xEnd:l(c),name:this._tText("lbl.runner_up_profile",{name:a.runnerUp},"{name} (runner-up)")}),e}_drawCycleEditor(){const t=this._modal;if(!t||t.type!=="cycle-detail"||!t.loaded)return;const e=t.curve||{},s=e.samples||[];if(!s.length)return;let a=e.full_duration_s||s[s.length-1][0]||1;const r=[];if(this._cycleProfileOverlays(t).forEach(c=>{r.push(c),a=Math.max(a,c.xEnd||0)}),t.mode==="review"&&(t.overlays||[]).length){const c=this._profileEnvCache||{};(t.overlays||[]).forEach(p=>{const _=c[p];if(!_||!(_.avg||[]).length)return;const f=Q[Math.max(0,(this._profiles||[]).findIndex(b=>b.name===p))%Q.length];r.push({points:_.avg,stroke:f,width:1.6,alpha:.7,name:p});const u=_.avg[_.avg.length-1];a=Math.max(a,_.target_duration||(u?u[0]:0))})}const i=this._cycleContextSeries(t);i&&(r.forEach(c=>{c.bounded=!0}),r.push(i.series),a=Math.max(a,i.xEnd)),r.push({points:s,stroke:"primary",fill:!0,width:2,name:this._t("lbl.power",{},"Power"),bounded:!!i});const o=[],n=[];if(i){const c="rgba(127,127,127,.10)";i.xStarti.traceEnd&&o.push({x0:i.traceEnd,x1:i.xEnd,fill:c})}let l=[];if(t.mode==="trim"){const c=t.trim.start,p=t.trim.end;o.push({x0:0,x1:c,fill:"rgba(244,67,54,.18)"}),o.push({x0:p,x1:a,fill:"rgba(244,67,54,.18)"}),n.push({x:c,color:"#f44336",label:"S"},{x:p,color:"#f44336",label:"E"})}else if(t.mode==="split")(t.split.offsets||[]).slice().sort((c,p)=>c-p).forEach((c,p)=>n.push({x:c,color:"#ff9800",label:"#"+(p+1)}));else{l=e.artifacts||[];const c={pause:"rgba(255,152,0,.22)",dip:"rgba(33,150,243,.18)",spike:"rgba(244,67,54,.18)"};l.forEach(_=>o.push({x0:_.start_s,x1:Math.max(_.end_s,_.start_s+1),fill:c[_.type]||"rgba(158,158,158,.18)"}));const p=e.start_time;(e.restart_gaps||[]).forEach(_=>{if(!p)return;const f=new Date(p).getTime(),u=Math.max(0,(new Date(_.start_ts).getTime()-f)/1e3),b=Math.max(u+1,(new Date(_.end_ts).getTime()-f)/1e3);o.push({x0:u,x1:b,fill:"rgba(96,125,139,.20)",label:"\u21BB"})})}this._drawCurves("wd-cyc-canvas",{series:r,xMax:a,bands:o,vlines:n,artifacts:l,...i?{xStart:i.xStart,cycleSpan:[i.traceStart,i.traceEnd]}:{}})}_cycleContextMinutes(t){const e=this._pref("cycle_context_min",null),s=e&&typeof e=="object"?e[t]:void 0;return Ft.includes(s)?s:Ee}async _loadCycleContext(t,e,s){const a=this._modal;if(!a||a.type!=="cycle-detail"||a.cycleId!==e)return;const r=this._cycleContextMinutes(t);if(!r||!s||s.is_reference||!(s.samples||[]).length){a.ctx={minutes:r,loading:!1,data:null};return}a.ctx={minutes:r,loading:!0,data:null};let i=null;try{i=await this._ws({type:`${w}/get_cycle_context`,entry_id:t,cycle_id:e,before_s:r*60,after_s:r*60})}catch{i={available:!1,reason:"no_history"}}const o=this._modal;!o||o.type!=="cycle-detail"||o.cycleId!==e||o.entryId!==t||!o.ctx||o.ctx.minutes!==r||(o.ctx={minutes:r,loading:!1,data:i},this._render())}_cycleContextSeries(t){if(!t||t.mode!=="view"&&t.mode!=="review")return null;const e=t.ctx&&t.ctx.data,s=t.curve&&t.curve.samples||[];if(!e||!e.available||!s.length)return null;const a=s[0],r=s[s.length-1],i=[];let o=null;const n=(u,b)=>{if(o!=null&&i.push([u,o]),b==null){i.push([u,null]),o=null;return}i.push([u,b]),o=b},l=(u,b)=>(Array.isArray(u)?u:[]).filter(v=>Array.isArray(v)&&Number.isFinite(v[0])&&b(v[0])),c=l(e.before,u=>uu>=r[0]);if(c.forEach(([u,b])=>n(u,b)),o!=null&&(i.push([a[0],o]),i.push([a[0],a[1]])),p.length){i.length&&i.push([r[0],null]),o=null,n(r[0],r[1]),p.forEach(([b,v])=>n(b,v));const u=Number(e.after_end_s)||0;o!=null&&u>i[i.length-1][0]&&i.push([u,o])}if(!i.some(u=>u[1]!=null))return null;const _=c.length?Math.min(c[0][0],-(Number(e.before_s)||0)):a[0],f=p.length?Math.max(i[i.length-1][0],Number(e.after_end_s)||0):r[0];return{series:{kind:"context",points:i,stroke:"#9e9e9e",width:2,alpha:1,bounded:!0,aside:!0,name:this._tText("lbl.cycle_context_series",{},"Recorder history")},xStart:Math.min(0,_),xEnd:f,traceStart:a[0],traceEnd:r[0]}}_htmlCompareModal(t){const e=t.ids||[],s={};(this._cycles||[]).forEach(o=>{s[o.id]=o});const a=t.hidden||new Set,r=e.map((o,n)=>{const l=s[o]||{},c=Q[n%Q.length],p=!a.has(o),_=!!(t.cycles&&t.cycles[o]),f=`${et(l.start_time)||String(o).slice(0,8)} \xB7 ${Math.round((l.duration||0)/60)}m \xB7 ${d(l.profile_name||this._t("lbl.unlabelled",{},"Unlabelled"))}`;return``}).join(""),i=(this._profiles||[]).map(o=>{const n=(t.overlays||[]).includes(o.name),l=Q[Math.max(0,(this._profiles||[]).findIndex(p=>p.name===o.name))%Q.length],c=n?``:"";return``}).join("")||`${this._t("msg.no_profiles_overlay",{},"No profiles to overlay.")}`;return`

    ${this._t("msg.compare_cycles_title",{count:e.length},`Compare ${e.length} cycles`)}

    ${t.loaded?"":`
    ${this._t("msg.loading",{},"Loading\u2026")}
    `}
    ${this._t("msg.compare_selected_cycles",{},"Selected cycles (solid) \u2013 show / hide")}
    ${r}
    ${this._t("msg.compare_overlay_profiles",{},"Overlay profiles (faint)")}${ot(this._t("msg.compare_overlay_tip",{},"Overlay learned profile envelopes to see which program each cycle resembles."))}
    ${i}
    `}_drawCompareCanvas(){const t=this._modal;if(!t||t.type!=="compare-cycles")return;const e=t.ids||[],s={};(this._cycles||[]).forEach(n=>{s[n.id]=n});const a=t.hidden||new Set,r=[];let i=0;const o=this._profileEnvCache||{};(t.overlays||[]).forEach(n=>{const l=o[n];if(!l||!(l.avg||[]).length)return;const c=Q[Math.max(0,(this._profiles||[]).findIndex(_=>_.name===n))%Q.length];r.push({points:l.avg,stroke:c,width:2,alpha:.4,name:n});const p=l.avg[l.avg.length-1];i=Math.max(i,l.target_duration||(p?p[0]:0))}),e.forEach((n,l)=>{if(a.has(n))return;const c=t.cycles&&t.cycles[n],p=c&&c.samples;if(!p||!p.length)return;const _=Q[l%Q.length],f=s[n]||{};r.push({points:p,stroke:_,width:1.8,alpha:.9,name:et(f.start_time)||String(n).slice(0,8)}),i=Math.max(i,c.full_duration_s||p[p.length-1][0]||0)}),this._drawCurves("wd-compare-canvas",{series:r,xMax:i||1})}_drawProfileEnvelope(){const t=this._modal;if(!t||!t.env||!(t.env.avg||[]).length)return;const e=t.env;this._drawCurves("wd-env-canvas",{series:[{points:e.avg,stroke:"primary",width:2,name:"Average"}],band:{min:e.min,max:e.max},xMax:e.target_duration||e.avg[e.avg.length-1][0]})}_drawPhaseEditor(){const t=this._modal;if(!t||!t.env||!(t.env.avg||[]).length)return;const e=t.env,s=e.target_duration||e.avg[e.avg.length-1][0],a=(t.phases||[]).map((i,o)=>({x0:i.start,x1:i.end,fill:Pt(Q[o%Q.length],.2)})),r=[];(t.phases||[]).forEach((i,o)=>{const n=Q[o%Q.length];r.push({x:i.start,color:n,label:i.name?i.name.slice(0,7):"",handle:!0}),r.push({x:i.end,color:n,handle:!0})}),this._drawCurves("wd-phase-canvas",{series:[{points:e.avg,stroke:"primary",width:2,name:"Average"}],band:{min:e.min,max:e.max},bands:a,vlines:r,xMax:s})}_drawSpaghetti(){const t=this._modal;if(!t||!t.cleanup||!(t.cleanup.cycles||[]).length)return;const e=t.cleanup.cycles,s=t.cleanup.selected||new Set,a=this._spagTableHoverCid||null;let r=1;e.forEach(o=>{const n=o.samples||[];n.length&&n[n.length-1][0]>r&&(r=n[n.length-1][0])});const i=e.map((o,n)=>{const l=s.has(o.cycle_id),c=a===o.cycle_id;return{points:o.samples||[],stroke:Q[n%Q.length],width:l||c?2.6:1,alpha:s.size?l?1:.22:a?c?1:.22:.7,name:et(o.start_time),cid:o.cycle_id}});this._drawCurves("wd-spag-canvas",{series:i,xMax:r})}_wire(){const t=this.shadowRoot;if(!t)return;const e=t.getElementById("wd-burger");e&&e.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("hass-toggle-menu",{bubbles:!0,composed:!0}))}),t.querySelectorAll(".wd-devcard[data-idx]").forEach(m=>m.addEventListener("click",()=>this._guardUnsaved(()=>this._selectDevice(parseInt(m.dataset.idx,10))))),t.querySelectorAll("[data-tab]").forEach(m=>m.addEventListener("click",()=>{const g=m.dataset.tab,k=()=>{g!=="settings"&&(this._pendingSettings={},this._dirtyOptKeys=new Set),this._tab=g,this._fetchTabData()};if(this._tab==="settings"&&g!=="settings"){this._guardUnsaved(k);return}k()})),t.querySelectorAll("[data-sec]").forEach(m=>m.addEventListener("click",()=>{this._snapshotFormToPending(t),this._settingsSec=m.dataset.sec,this._settingsSearch="",this._settingsSugOnly=!1,this._render()})),t.querySelectorAll("[data-ptab]").forEach(m=>m.addEventListener("click",()=>{const g=this._panelSubtab=m.dataset.ptab;this._render();const k=this._devices[this._selIdx];k&&(g==="diagnostics"&&!this._diag?this._fetchToolsData(k.entry_id).then(()=>{this._panelSubtab==="diagnostics"&&this._render()}):g==="logs"?this._fetchLogs().then(()=>{this._panelSubtab==="logs"&&this._render()}):g==="ml"&&this._fetchTabData())})),t.querySelectorAll("[data-hsub]").forEach(m=>m.addEventListener("click",()=>{this._historySub=m.dataset.hsub,this._render();const g=this._devices[this._selIdx];this._historySub==="maintenance"&&g?this._fetchMaintenance(g.entry_id).then(()=>{this._tab==="history"&&this._render()}):this._fetchTabData()})),t.querySelectorAll("[data-gtab]").forEach(m=>m.addEventListener("click",()=>{this._gearTab=m.dataset.gtab,this._modal&&this._modal.type==="gear-settings"&&(this._modal.tab=this._gearTab);const g=this._devices[this._selIdx];this._gearTab==="online"&&g&&this._onlineEnabled()&&(this._ensureStoreConnectListener(),this._loadStoreStatus(g.entry_id).then(()=>{this._modal&&this._modal.type==="gear-settings"&&this._render()})),this._render()}));const s=(m,g)=>{this._pendingSettings[m]=g,this._dirtyOptKeys.add(m)},a=t.getElementById("wd-store-brand");a&&a.addEventListener("change",()=>{const m=a.value.trim();if(m!==String(this._editedOpts().store_brand||"").trim()){s("store_model","");const g=t.getElementById("wd-store-model");g&&(g.value="")}s("store_brand",m),this._catalog.forBrand=m,this._catalog.devices=void 0,this._renderPreservingFormEdits()});const r=t.getElementById("wd-store-model");r&&r.addEventListener("change",()=>{s("store_model",r.value.trim()),this._renderPreservingFormEdits()});const i=t.getElementById("wd-pg-canvas");if(i){const m=()=>{const S=i.getBoundingClientRect(),F=S.height||330,N=Bt+8,H=F-N-52;return{rect:S,padT:N,powerH:H}},g=(S,F)=>{const N=i.getBoundingClientRect(),H=S-N.left,Z=F-N.top;if(Z>Bt+4)return null;const tt=this._pgEventHits||[];let st=null,rt=1/0;for(const lt of tt){const at=(H-lt.cx)**2+(Z-lt.cy)**2;at<=(lt.r+4)**2&&at{const{rect:F,padT:N,powerH:H}=m(),Z=this._pgPowerPts;if(!Z?.length)return 0;const tt=Math.max(...Z.map(st=>st.w),1);return Math.max(0,(1-Math.max(0,S-F.top-N)/H)*tt)},y=S=>{const{rect:F,padT:N,powerH:H}=m(),Z=this._pgPowerPts;if(!Z?.length)return 0;const tt=Math.max(...Z.map(st=>st.w),1);return F.top+N+(1-Math.max(0,+S)/tt)*H},P=S=>{const F=this._pgMap;if(!F)return null;const{rect:N}=m(),H=(S-N.left-F.padLpx)/Math.max(1,F.plotWpx);return F.vMin+Math.max(0,Math.min(1,H))*(F.vMax-F.vMin)};i.addEventListener("pointermove",S=>{if(q.has(S.pointerId)&&q.set(S.pointerId,{x:S.clientX,y:S.clientY}),i._wdPinching){E();return}if(this._pgDragging==="start_thr"){this._pgThreshStart=Math.max(0,k(S.clientY)),this._pgUpdateParamInput("start_threshold_w",this._pgThreshStart),this._pgDrawCanvas();return}if(this._pgDragging==="stop_thr"){this._pgThreshStop=Math.max(0,k(S.clientY)),this._pgUpdateParamInput("stop_threshold_w",this._pgThreshStop),this._pgDrawCanvas();return}if(this._pgDragging==="pan"&&this._pgPanStart){const st=this._pgMap,rt=(S.clientX-this._pgPanStart.clientX)/Math.max(1,st.plotWpx),lt=this._pgPanStart.vMax-this._pgPanStart.vMin;let at=this._pgPanStart.vMin-rt*lt;at=Math.max(0,Math.min(this._pgPanStart.totalDur-lt,at)),this._pgView={min:at,max:at+lt},this._pgDrawCanvas();return}if(!this._pgPowerPts?.length)return;const F=g(S.clientX,S.clientY);if(F){i.style.cursor="pointer",this._pgHoverEvent={t:F.t,type:F.type},this._pgHoverT=null,this._pgDrawCanvas();return}this._pgHoverEvent&&(this._pgHoverEvent=null,this._pgDrawCanvas());const N=this._pgThreshStart??this._pgFieldVal("start_threshold_w",{})??50,H=this._pgThreshStop??this._pgFieldVal("stop_threshold_w",{})??5,Z=Math.abs(S.clientY-y(N))<8||Math.abs(S.clientY-y(H))<8;i.style.cursor=Z?"ns-resize":"crosshair";const tt=P(S.clientX);tt!=null&&(this._pgHoverT=tt,this._pgUpdateStripAt(tt),this._pgDrawCanvas())});const X=(S,F)=>{const N=this._pgMap;if(!N||S==null)return;const H=N.vMax-N.vMin,Z=Math.max(30,Math.min(N.totalDur,H*F)),tt=H>0?Math.max(0,Math.min(1,(S-N.vMin)/H)):.5;let st=S-tt*Z;st=Math.max(0,Math.min(N.totalDur-Z,st)),this._pgView=Z>=N.totalDur-1?null:{min:st,max:st+Z},this._pgDrawCanvas()},q=new Map;let j=null;i.addEventListener("pointerdown",S=>{if(!this._pgPowerPts?.length)return;if(q.set(S.pointerId,{x:S.clientX,y:S.clientY}),q.size===2&&this._pgMap){i._wdPinching=!0,i._wdOwnGesture=!1,this._pgDragging=null,this._pgPanStart=null,i.classList.remove("wd-pg-panning");const[Z,tt]=[...q.values()];j={dist:Math.max(1,Math.abs(Z.x-tt.x)),centerX:(Z.x+tt.x)/2};return}if(q.size!==1||g(S.clientX,S.clientY))return;i.setPointerCapture(S.pointerId);const F=this._pgThreshStart??this._pgFieldVal("start_threshold_w",{})??50,N=this._pgThreshStop??this._pgFieldVal("stop_threshold_w",{})??5,H=S.pointerType==="touch"?18:10;Math.abs(S.clientY-y(F)){try{i.releasePointerCapture(S.pointerId)}catch{}q.delete(S.pointerId),q.size<2&&(j=null,i._wdPinching=!1),q.size===0&&(i._wdOwnGesture=!1);const F=this._pgDragging==="start_thr"||this._pgDragging==="stop_thr";return this._pgDragging=null,this._pgPanStart=null,i.classList.remove("wd-pg-panning"),F};i.addEventListener("pointerup",S=>{V(S)&&this._pgDrawCanvas()}),i.addEventListener("pointercancel",S=>{V(S)&&this._pgDrawCanvas(),this._pgHoverT=null,this._pgHoverEvent=null,this._pgUpdateStripAt(null),this._pgDrawCanvas()}),i.addEventListener("pointerleave",S=>{this._pgDragging||S.pointerType==="touch"||(this._pgHoverT=null,this._pgHoverEvent=null,this._pgUpdateStripAt(null),this._pgDrawCanvas())}),i.addEventListener("wheel",S=>{if(!this._pgPowerPts?.length||!this._pgMap)return;S.preventDefault();const F=S.ctrlKey?Math.min(4,Math.max(.25,Math.exp(S.deltaY*.01))):S.deltaY>0?1.25:.8;X(P(S.clientX),F)},{passive:!1}),i.addEventListener("dblclick",S=>{if(this._pgView){this._pgView=null,this._pgDrawCanvas();return}X(P(S.clientX),Qt)});const E=()=>{const S=this._pgMap;if(!S||!j||q.size<2)return;const[F,N]=[...q.values()],H=Math.max(1,Math.abs(F.x-N.x)),Z=(F.x+N.x)/2,tt=S.vMax-S.vMin,st=Math.max(30,Math.min(S.totalDur,tt*(j.dist/H))),rt=P(Z),lt=(Z-j.centerX)/Math.max(1,S.plotWpx)*st;if(j.dist=H,j.centerX=Z,rt==null)return;const at=tt>0?Math.max(0,Math.min(1,(rt-S.vMin)/tt)):.5;let pt=rt-at*st-lt;pt=Math.max(0,Math.min(S.totalDur-st,pt)),this._pgView=st>=S.totalDur-1?null:{min:pt,max:pt+st},this._pgDrawCanvas()};this._attachTouchOwnershipGuard(i)}t.querySelectorAll("input[data-pgkey]").forEach(m=>{m.addEventListener("input",()=>{const g=m.dataset.pgkey;if(m.dataset.pgtype==="bool"){const q=m.checked;g==="start_threshold_w"?this._pgThreshStart=q:g==="stop_threshold_w"?this._pgThreshStop=q:this._pgParamOverrides[g]=q,this._render();const j=t.querySelector(`input[data-pgkey="${g}"]`);j&&j.focus({preventScroll:!0}),requestAnimationFrame(()=>this._pgDrawCanvas());return}const k=m.value.trim();if(!k)g==="start_threshold_w"?this._pgThreshStart=null:g==="stop_threshold_w"?this._pgThreshStop=null:delete this._pgParamOverrides[g];else{const q=parseFloat(k);isNaN(q)||(g==="start_threshold_w"?this._pgThreshStart=q:g==="stop_threshold_w"?this._pgThreshStop=q:this._pgParamOverrides[g]=q)}const y=m.selectionStart,P=m.value;this._render();const X=t.querySelector(`input[data-pgkey="${g}"]`);if(X){X.value=P,X.focus({preventScroll:!0});try{X.setSelectionRange(y,y)}catch{}}requestAnimationFrame(()=>this._pgDrawCanvas())})});const o=t.getElementById("wd-pg-cyc-sel");o&&o.addEventListener("change",()=>this._pgSelectCycle(o.value));const n=t.getElementById("wd-pg-prof-sel");n&&n.addEventListener("change",()=>{this._pgProfileName=n.value,this._pgLoad()});const l=t.getElementById("wd-pg-preset-sel");l&&l.addEventListener("change",()=>{this._pgPresetSel=l.value,this._render()});const c=t.getElementById("wd-pg-preset-name");c&&c.addEventListener("input",()=>{this._pgPresetName=c.value;const m=(this._pgPresetName||"").trim(),g=this._pgPresetAtLimit(m),k=t.querySelector('[data-action="pg-preset-save"]');k&&(k.disabled=!(m&&!g));const y=t.getElementById("wd-pg-preset-limit-note");y&&(y.style.display=g?"":"none")}),t.querySelectorAll("#wd-pg-simn, #wd-pg-sw-n").forEach(m=>m.addEventListener("input",()=>{this._pgSimCycles=Math.max(1,Math.min(St,parseInt(m.value,10)||20))}));const p=t.getElementById("wd-pg-sw-param");p&&p.addEventListener("change",()=>{this._pgSweepParam=p.value,this._pgSweepNew=null,this._render()});const _=t.getElementById("wd-pg-sw-obj");_&&_.addEventListener("change",()=>{this._pgSweepObjective=_.value,this._pgSweepNew=null,this._render()});const f=t.getElementById("wd-pg-sw-paramy"),u=t.getElementById("wd-pg-sw-from");u&&u.addEventListener("input",()=>{this._pgSweepFrom=u.value});const b=t.getElementById("wd-pg-sw-to");b&&b.addEventListener("input",()=>{this._pgSweepTo=b.value});const v=t.getElementById("wd-pg-sw-steps");v&&v.addEventListener("input",()=>{this._pgSweepSteps=parseInt(v.value,10)||5}),t.querySelectorAll("[data-statustoggle]").forEach(m=>m.addEventListener("change",async()=>{const g=m.dataset.statustoggle,k=m.checked;this._panelCfg||(this._panelCfg={}),this._panelCfg.prefs={...this._panelCfg.prefs||{},[g]:k},this._ws({type:`${w}/set_user_prefs`,prefs:{[g]:k}}).catch(()=>{});const y=this._devices[this._selIdx];if(y&&this._tab==="status")try{this._powerData=await this._ws({type:`${w}/get_power_history`,entry_id:y.entry_id,with_raw:this._pref("show_raw_active",!1)})}catch{}this._render()})),t.querySelectorAll("[data-sortact]").forEach(m=>m.addEventListener("click",()=>{const g=m.dataset.sortact,k=m.dataset.sortcol,y=P=>{P.col===k?P.dir*=-1:(P.col=k,P.dir=k==="date"?-1:1)};g==="cycsort"?y(this._cycleSort):g==="cleanupsort"&&y(this._cleanupSort),this._render()}));const T=t.getElementById("wd-cyc-filter-text");T&&T.addEventListener("input",m=>{const g=m.target.selectionStart;this._cycleFilter.text=T.value,this._render();const k=this.shadowRoot.getElementById("wd-cyc-filter-text");k&&(k.focus({preventScroll:!0}),k.setSelectionRange(g,g))});const C=t.getElementById("wd-settings-search");C&&C.addEventListener("input",m=>{const g=m.target.selectionStart;this._settingsSearch=C.value,C.value.trim()&&(this._settingsSugOnly=!1),this._render();const k=this.shadowRoot.getElementById("wd-settings-search");k&&(k.focus({preventScroll:!0}),k.setSelectionRange(g,g))});const x=t.getElementById("wd-cyc-filter-status");x&&x.addEventListener("change",()=>{this._cycleFilter.status=x.value,this._render()});const $=m=>{const g=document.createElement("span");g.className="wd-pill",g.dataset.val=m,g.title=m;const k=document.createElement("span");k.className="wd-pill-txt",k.textContent=m,g.appendChild(k);const y=document.createElement("button");return y.type="button",y.className="wd-pill-x",y.setAttribute("aria-label",this._tText("btn.remove",{},"Remove")),y.textContent="\xD7",y.addEventListener("click",()=>g.remove()),g.appendChild(y),g};t.querySelectorAll(".wd-pillbox").forEach(m=>{const g=m.querySelector(".wd-pill-add"),k=y=>{const P=String(y||"").trim();if(!P)return;Array.from(m.querySelectorAll(".wd-pill")).some(q=>q.dataset.val===P)||m.insertBefore($(P),g.closest(".wd-combo")||g),g&&(g.value="")};m.querySelectorAll(".wd-pill-x").forEach(y=>y.addEventListener("click",()=>y.closest(".wd-pill")?.remove())),g&&(g.addEventListener("change",()=>k(g.value)),g.addEventListener("keydown",y=>{y.key==="Enter"&&(y.preventDefault(),k(g.value))}),g.addEventListener("blur",()=>k(g.value)))}),t.querySelectorAll(".wd-combo").forEach(m=>{const g=m.querySelector(".wd-combo-inp, .wd-pill-add"),k=m.querySelector(".wd-combo-drop");if(!g||!k)return;const y=m.classList.contains("wd-combo-pill"),P=g.dataset.catalog||g.dataset.opt||m.closest("[data-opt]")?.dataset.opt,X=(j,V=!1)=>{this._ensureCatalogList(P,j,V);const E=(this._entityListCache||{})[P]||[],S=(j||"").toLowerCase(),F=S?E.filter(N=>String(N).toLowerCase().includes(S)).slice(0,40):E.slice(0,20);if(!F.length){k.hidden=!0;return}k.innerHTML=F.map(N=>`
    ${d(N)}
    `).join(""),k._kbd=-1,k.hidden=!1,this._positionComboDrop(g,k)},q=j=>{if(j){if(y){const V=m.closest(".wd-pillbox");V&&!Array.from(V.querySelectorAll(".wd-pill")).some(E=>E.dataset.val===j)&&V.insertBefore($(j),m),g.value=""}else g.value=j,g.dispatchEvent(new Event("change",{bubbles:!0}));k.hidden=!0}};g.addEventListener("focus",()=>X(g.value)),g.addEventListener("input",()=>X(g.value,!g._wdSyntheticInput)),g.addEventListener("blur",()=>setTimeout(()=>{k.hidden=!0},150)),g.addEventListener("keydown",j=>{if(k.hidden&&j.key!=="ArrowDown")return;const V=k.querySelectorAll(".wd-combo-item");let E=k._kbd||-1;if(j.key==="ArrowDown"){if(j.preventDefault(),k.hidden){X(g.value);return}E=Math.min(E+1,V.length-1)}else if(j.key==="ArrowUp")j.preventDefault(),E=Math.max(E-1,0);else if(j.key==="Enter"&&!k.hidden){j.preventDefault(),E>=0?q(V[E].dataset.val):y&&g.value.trim()&&q(g.value.trim());return}else if(j.key==="Escape"){k.hidden=!0;return}else return;k._kbd=E,V.forEach((S,F)=>S.classList.toggle("kbd",F===E)),V[E]?.scrollIntoView({block:"nearest"})}),k.addEventListener("mousedown",j=>{const V=j.target.closest(".wd-combo-item");V&&(j.preventDefault(),q(V.dataset.val))})});const L=t.getElementById("wd-store-q");if(L){const m=()=>{const g=String(L.value||"").trim(),k=(this._catalog.modelIndex||new Map).get(g);if(k){this._storeOpenModel(k);return}g.toLowerCase()!==String(this._storeQuery||"").trim().toLowerCase()&&this._storeSearch(g)};L.addEventListener("change",m),L.addEventListener("keydown",g=>{if(g.key!=="Enter")return;const k=L.parentElement&&L.parentElement.querySelector(".wd-combo-drop");k&&!k.hidden&&k._kbd>=0||(g.preventDefault(),m())}),L.addEventListener("input",()=>{L._wdSyntheticInput||(clearTimeout(this._storeQTimer),this._storeQTimer=setTimeout(()=>{const g=String(L.value||"").trim();if(!g)return;((this._entityListCache||{}).store_brand||[]).some(y=>String(y).toLowerCase()===g.toLowerCase())&&m()},450))})}t.querySelectorAll(".wd-timerlist").forEach(m=>{const g=m.dataset.opt,k=()=>{if(!Array.isArray(this._pendingSettings[g])){const V=Array.isArray(this._opts&&this._opts[g])?this._opts[g]:[];this._pendingSettings[g]=V.map(E=>({...E}))}return this._pendingSettings[g]},y=V=>({offset_minutes:parseFloat(V.querySelector('[data-field="offset_minutes"]').value)||0,message:(V.querySelector('[data-field="message"]').value||"").trim(),auto_pause:V.querySelector('[data-field="auto_pause"]').checked}),P=V=>{const E=parseInt(V.dataset.tidx,10);if(isNaN(E))return;const S=k();S[E]=y(V)},X=V=>{const E=parseInt(V.dataset.tidx,10);isNaN(E)||(this._pendingSettings[g]=k().filter((S,F)=>F!==E)),V.remove(),m.querySelectorAll(".wd-timer-row").forEach((S,F)=>{S.dataset.tidx=F})},q=V=>{const E=V.querySelector(".wd-timer-remove");E&&E.addEventListener("click",()=>X(V)),V.querySelector('[data-field="auto_pause"]')?.addEventListener("change",()=>P(V)),V.querySelectorAll('[data-field="offset_minutes"],[data-field="message"]').forEach(S=>S.addEventListener("input",()=>P(V)))};m.querySelectorAll(".wd-timer-row").forEach(V=>q(V));const j=m.querySelector(".wd-timer-add");j&&j.addEventListener("click",()=>{const V=m.querySelectorAll(".wd-timer-row").length;k().push({offset_minutes:0,message:"",auto_pause:!1});const E=document.createElement("div");E.className="wd-timer-row",E.dataset.tidx=V,E.innerHTML=`
    `,q(E),m.insertBefore(E,j)})});const D=t.getElementById("wd-status-prog");D&&D.addEventListener("change",()=>{const m=this._devices[this._selIdx];if(!m)return;const g=D.value,k=!Mt.includes(m.detector_state||"");this._ws({type:`${w}/set_program`,entry_id:m.entry_id,program:g}).then(()=>(this._showToast(g==="auto_detect"?this._t("msg.toast_auto_detect_enabled",{},"Auto-detect enabled"):k?this._tText("msg.toast_program_armed",{program:g},`Program armed for the next cycle: ${g}`):this._tText("msg.toast_program_set",{program:g},`Program set: ${g}`)),this._fetchAll())).catch(y=>this._showToast(this._tText("msg.toast_failed",{error:y.message||y},"Failed: "+(y.message||y)),"error"))}),t.querySelectorAll("[data-cid]").forEach(m=>m.addEventListener("click",g=>{if(g.target.tagName==="INPUT"||m.dataset.action)return;const k=m.dataset.cid;m.dataset.selmode==="1"?(this._cycleSel.has(k)?this._cycleSel.delete(k):this._cycleSel.add(k),this._render()):this._onAction({dataset:{action:"open-cycle",cid:k}})})),t.querySelectorAll('tr[data-cid][role="button"]').forEach(m=>m.addEventListener("keydown",g=>{g.target!==m||g.key!=="Enter"&&g.key!==" "||(g.preventDefault(),m.click())})),t.querySelectorAll(".wd-cyc-overlay").forEach(m=>m.addEventListener("change",()=>{const g=this._modal;if(!g||g.type!=="cycle-detail")return;this._snapshotCycleReviewForm(t);const k=new Set(g.overlays||[]);m.checked?k.add(m.value):k.delete(m.value),g.overlays=[...k];const y=this._devices[this._selIdx];m.checked&&y?this._ensureProfileEnvs(y.entry_id,[m.value]).then(()=>this._render()):this._render()})),t.querySelectorAll(".wd-compare-cyc").forEach(m=>m.addEventListener("change",()=>{const g=this._modal;if(!g||g.type!=="compare-cycles")return;const k=g.hidden instanceof Set?g.hidden:new Set(g.hidden||[]);m.checked?k.delete(m.value):k.add(m.value),g.hidden=k,this._render()})),t.querySelectorAll(".wd-compare-overlay").forEach(m=>m.addEventListener("change",()=>{const g=this._modal;if(!g||g.type!=="compare-cycles")return;const k=new Set(g.overlays||[]);m.checked?k.add(m.value):k.delete(m.value),g.overlays=[...k];const y=this._devices[this._selIdx];m.checked&&y?this._ensureProfileEnvs(y.entry_id,[m.value]).then(()=>this._render()):this._render()})),t.querySelectorAll(".wd-pg-mem").forEach(m=>m.addEventListener("change",()=>{const g=this._modal;if(!g||g.type!=="profile-group")return;const k=t.getElementById("wd-pg-name");k&&(g.name=k.value);const y=new Set(g.members||[]);m.checked?y.add(m.value):y.delete(m.value),g.members=[...y],this._render()}));const B=t.getElementById("wd-pg-name");B&&B.addEventListener("input",()=>{const m=this._modal;m&&m.type==="profile-group"&&(m.name=B.value)}),t.querySelectorAll(".wd-csel").forEach(m=>m.addEventListener("change",()=>{const g=m.closest("[data-cid]"),k=g&&g.dataset.cid;k&&(m.checked?this._cycleSel.add(k):this._cycleSel.delete(k),this._render())}));const U=t.getElementById("wd-merge-prof");U&&U.addEventListener("change",()=>{const m=t.getElementById("wd-merge-new");m&&(m.style.display=U.value==="__create_new__"?"":"none")}),t.querySelectorAll(".wd-log-filter").forEach(m=>{const g=m.dataset.logfilter,k={level:"_logLevel",device:"_logDevice",component:"_logComponent",search:"_logSearch"}[g];if(!k)return;const y=g==="search"?"input":"change";m.addEventListener(y,()=>{this[k]=m.value,this._refreshLogViews(),this._syncLogFilters(m)})});const z=t.querySelector(".wd-log-resize");z&&z.addEventListener("pointerdown",m=>{m.preventDefault(),z.setPointerCapture(m.pointerId),z.classList.add("dragging");const g=t.querySelector(".wd-log-drawer"),k=m.clientX,y=g.offsetWidth,P=X=>{const q=Math.max(280,Math.min(900,y+(k-X.clientX)));g.style.width=q+"px",this._logDrawerWidth=q};z.addEventListener("pointermove",P),z.addEventListener("pointerup",()=>{z.removeEventListener("pointermove",P),z.classList.remove("dragging");try{localStorage.setItem("wd-log-width",String(this._logDrawerWidth))}catch{}},{once:!0})});const M=t.getElementById("wd-hist-file");M&&M.addEventListener("change",()=>{const m=M.files&&M.files[0];if(!m)return;const g=this._modal,k=new FileReader;k.onload=()=>{const y=t.getElementById("wd-hist-csv"),P=String(k.result||"");y&&(y.value=P),g&&g.type==="history-import"&&(g.csvText=P)},k.onerror=()=>this._showToast(this._t("toast.file_read_failed",{},"Could not read that file"),"error"),k.readAsText(m)}),t.querySelectorAll("[data-hist-pick]").forEach(m=>m.addEventListener("change",()=>{const g=this._modal;if(!g||g.type!=="history-import")return;const k=parseInt(m.dataset.histPick,10);m.checked?g.accept.add(k):g.accept.delete(k),this._render(),requestAnimationFrame(()=>this._drawHistorySparklines())}));const A=t.getElementById("wd-import-file");A&&A.addEventListener("change",()=>{const m=A.files&&A.files[0];if(!m)return;const g=new FileReader;g.onload=()=>{const k=t.getElementById("wd-import-json");k&&(k.value=String(g.result||""))},g.readAsText(m)}),t.querySelectorAll("[data-stab]").forEach(m=>m.addEventListener("click",()=>{this._toolsSubtab=m.dataset.stab;const g=this._devices[this._selIdx];g&&(this._tabLoading=!0,this._render(),this._fetchToolsData(g.entry_id).then(()=>{this._tabLoading=!1,this._render()}))})),t.querySelectorAll("[data-proftab]").forEach(m=>m.addEventListener("click",async()=>{this._profSubtab=m.dataset.proftab;const g=this._devices[this._selIdx];g&&this._profSubtab==="phase-catalog"&&!this._phases.length&&(this._tabLoading=!0,this._render(),await this._fetchPhases(g.entry_id),this._tabLoading=!1),this._render()}));const O=t.getElementById("wd-settings-save");O&&O.addEventListener("click",()=>this._saveSettings());const K=t.getElementById("wd-ml-save");K&&K.addEventListener("click",()=>this._saveSettings());const I=t.getElementById("wd-settings-form"),R=m=>{const g=m.target&&m.target.closest?m.target.closest("[data-opt]"):null;g&&g.dataset.opt&&this._dirtyOptKeys.add(g.dataset.opt)},G=m=>{const g=m.target;if(!g||!g.closest)return;const k=g.closest(".wd-colorfield");if(!k)return;const y=k.querySelector("input[data-opt]"),P=k.querySelector(".wd-color-sw");!y||!P||(g===P?(y.value=String(P.value).toUpperCase(),this._dirtyOptKeys.add(y.dataset.opt)):g===y&&Kt.test(y.value.trim())&&(P.value=y.value.trim().toLowerCase()))};I&&(I.addEventListener("submit",m=>m.preventDefault()),I.addEventListener("input",m=>{G(m),R(m),this._liveValidateSettings(t)}),I.addEventListener("change",m=>{G(m),R(m),this._liveValidateSettings(t)}),I.addEventListener("click",m=>{const g=m.target.closest&&m.target.closest(".wd-color-clear");if(!g)return;const k=g.parentElement&&g.parentElement.querySelector("input[data-opt]");if(!k)return;k.value="";const y=g.parentElement.querySelector(".wd-color-sw");y&&(y.value=ye(k.placeholder)),this._dirtyOptKeys.add(k.dataset.opt),this._liveValidateSettings(t)}),I.addEventListener("click",m=>{const g=m.target.closest(".wd-conflict-fix");if(!g)return;const k=g.dataset.ckey,y=parseFloat(g.dataset.cval),P=I.querySelector(`[data-opt="${k}"]`);P&&!isNaN(y)&&(P.value=y,this._dirtyOptKeys.add(k),this._cascadeConflictFix(t,I,k))}),this._liveValidateSettings(t));const W=t.getElementById("wd-ml-form");W&&(W.addEventListener("input",R),W.addEventListener("change",R));const J=t.getElementById("wd-settings-revert");J&&J.addEventListener("click",async()=>{if(!this._prevOpts)return;const m=this._devices[this._selIdx];m&&await this._busyRun("save-settings",async()=>{try{const g=this._prevOpts;await this._ws({type:`${w}/set_options`,entry_id:m.entry_id,options:g}),this._opts={...g},this._prevOpts=null,this._cascadePending={},this._preCascadeOpts=null,this._pendingSettings={},this._dirtyOptKeys=new Set,this._showToast(this._t("toast.settings_reverted",{},"Settings reverted; integration reloading")),this._render()}catch(g){this._showToast(this._tText("msg.toast_revert_failed",{error:g.message||g},"Revert failed: "+(g.message||g)),"error")}})});const Y=t.getElementById("wd-settings-reload");Y&&Y.addEventListener("click",async()=>{const m=this._devices[this._selIdx];if(m){this._prevOpts=null,this._cascadePending={},this._preCascadeOpts=null,this._pendingSettings={},this._dirtyOptKeys=new Set;const g=await this._ws({type:`${w}/get_options`,entry_id:m.entry_id});this._opts=g.options||{},this._optDefaults=g.defaults||{},await this._fetchSuggestions(m.entry_id),this._render()}});const nt=t.getElementById("wd-pref-fontscale");if(nt){const m=t.getElementById("wd-pref-fontscale-val");nt.addEventListener("input",()=>{const g=parseFloat(nt.value)||1;this._applyFontScale(g),m&&(m.textContent=Math.round(g*100)+"%")}),nt.addEventListener("change",()=>{this._setPref("font_scale",parseFloat(nt.value)||1)})}t.querySelectorAll("[data-action]").forEach(m=>m.addEventListener("click",g=>this._onAction(g.currentTarget))),t.querySelectorAll("[data-maction]").forEach(m=>m.addEventListener("click",g=>this._onModalAction(g.currentTarget.dataset.maction,g.currentTarget))),t.querySelectorAll("select[data-maction]").forEach(m=>m.addEventListener("change",g=>this._onModalAction(g.currentTarget.dataset.maction,g.currentTarget))),t.querySelectorAll("input[data-indeterminate]").forEach(m=>{m.indeterminate=!0});const _t=t.querySelector("[data-toast-undo]");_t&&_t.addEventListener("click",()=>this._undoDelete(_t.dataset.toastUndo)),t.querySelectorAll("[data-suglock]").forEach(m=>m.addEventListener("click",async()=>{const g=m.dataset.suglock,k=this._devices[this._selIdx],y=k&&k.entry_id;if(!(!y||!g))try{if(await this._ws({type:`${w}/set_suggestion_lock`,entry_id:y,key:g,locked:!0}),!this._isActiveEntry(y))return;this._suggestions=(this._suggestions||[]).filter(P=>P.key!==g),(this._lockedSuggestions||[]).includes(g)||(this._lockedSuggestions=this._lockedSuggestions||[]).push(g),this._showToast(this._t("msg.sug_muted",{},"Won't suggest this setting again"),"info"),this._render()}catch{this._showToast(this._t("msg.sug_mute_failed",{},"Could not mute suggestion"),"error")}})),t.querySelectorAll("[data-sugkey]").forEach(m=>m.addEventListener("click",()=>{const g=m.dataset.sugkey,k=m.dataset.sugval,y=parseFloat(k);this._pendingSettings[g]=isNaN(y)?k:y,this._dirtyOptKeys.add(g),this._stagedSuggestions=!0,this._suggestions=this._suggestions.filter(q=>q.key!==g),this._showToast(this._tText("msg.sug_staged",{key:g,val:k},`Set ${g} = ${k}. Save to apply.`),"info"),this._render();const P=this.shadowRoot,X=P?.getElementById("wd-settings-form");X&&this._cascadeConflictFix(P,X,g)}));const ct=t.getElementById("wd-label-profile");ct&&ct.addEventListener("change",()=>{const m=t.getElementById("wd-new-profile-row"),g=ct.value==="__create_new__";if(m&&(m.style.display=g?"":"none"),!g){const k=t.getElementById("wd-new-profile-name");k&&(k.value="")}});const dt=t.getElementById("wd-cp-cycle");if(dt){const m=()=>{const g=t.getElementById("wd-cp-dur");if(!g)return;const k=!!dt.value;g.disabled=k,g.style.opacity=k?"0.5":""};dt.addEventListener("change",m),m()}const ut=t.getElementById("wd-pr-mode");ut&&ut.addEventListener("change",()=>{const m=t.getElementById("wd-pr-profile"),g=t.getElementById("wd-pr-existing");if(!m||!g)return;const k=ut.value==="existing_profile";m.style.display=k?"none":"",g.style.display=k?"":"none"}),this._wireCycleCanvas(t),this._wirePhaseInputs(t),this._wirePhaseCanvas(t),this._wireCleanup(t),this._wireSplitSegments(t)}_syncTrimInputs(){const t=this.shadowRoot,e=this._modal,s=(e.timeMode||"s")==="clock",a=t.getElementById("wd-trim-start"),r=t.getElementById("wd-trim-end");a&&(a.value=s?this._offsetToClock(e.trim.start):Math.round(e.trim.start)),r&&(r.value=s?this._offsetToClock(e.trim.end):Math.round(e.trim.end))}_snapTrimBounds(){const t=this._modal,e=t.curve&&t.curve.samples||[];if(e.length<2)return;const s=a=>e.reduce((r,i)=>Math.abs(i[0]-a)r[0]===t.trim.start);a>=0&&a+10&&(t.trim.start=e[a-1][0])}}_offsetToClock(t){const e=this._modal,s=e&&e.curve&&e.curve.start_time;if(!s)return"";const a=new Date(new Date(s).getTime()+(t||0)*1e3);return`${String(a.getHours()).padStart(2,"0")}:${String(a.getMinutes()).padStart(2,"0")}:${String(a.getSeconds()).padStart(2,"0")}`}_clockToOffset(t){const e=this._modal,s=e&&e.curve&&e.curve.start_time;if(!s||!t)return null;const a=String(t).split(":").map(Number);if(!a.length||a.some(l=>!Number.isFinite(l)))return null;const r=new Date(s),i=new Date(r);i.setHours(a[0]||0,a[1]||0,a[2]||0,0);let o=(i-r)/1e3;const n=e.curve&&e.curve.full_duration_s||0;return o<-1&&o+86400<=n&&(o+=86400),Math.max(0,Math.min(n,o))}_trimInputToOffset(t){if(t===""||t==null)return null;if(this._modal.timeMode==="clock")return this._clockToOffset(t);const e=pe(t,NaN);return Number.isFinite(e)?e:null}_toggleSplit(t){const e=this._modal,s=e.curve&&e.curve.full_duration_s||0,a=Math.max(20,s*.025),r=e.split.offsets,i=r.findIndex(o=>Math.abs(o-t)=0?r.splice(i,1):r.push(Math.round(t)),r.sort((o,n)=>o-n),e.split.profiles=[],this._render()}_wireCycleCanvas(t){const e=this._modal;if(!e||e.type!=="cycle-detail"||!e.loaded)return;const s=t.getElementById("wd-cyc-canvas");if(s)if(e.mode==="trim"){const a=t.getElementById("wd-trim-start"),r=t.getElementById("wd-trim-end");a&&a.addEventListener("input",()=>{const n=this._trimInputToOffset(a.value);n!==null&&(e.trim.start=Math.max(0,Math.min(n,e.trim.end-1)),this._drawCycleEditor())}),r&&r.addEventListener("input",()=>{const n=this._trimInputToOffset(r.value);n!==null&&(e.trim.end=Math.min(e.curve.full_duration_s,Math.max(n,e.trim.start+1)),this._drawCycleEditor())});const i=()=>{this._snapTrimBounds(),this._syncTrimInputs(),this._drawCycleEditor()};a&&a.addEventListener("change",i),r&&r.addEventListener("change",i);const o=()=>{e.drag&&(this._snapTrimBounds(),this._syncTrimInputs(),this._drawCycleEditor()),e.drag=null,s._wdOwnGesture=!1};s._wdAbortEdit=()=>{e.drag=null,s._wdOwnGesture=!1},s.addEventListener("pointerdown",n=>{const l=s._wd;if(!l)return;const c=s.getBoundingClientRect(),p=n.clientX-c.left,_=Math.abs(p-l.xToCss(e.trim.start)),f=Math.abs(p-l.xToCss(e.trim.end));if(n.pointerType==="touch"&&Math.min(_,f)>Tt){e.drag=null;return}e.drag=_<=f?"start":"end",s._wdOwnGesture=!0,s.setPointerCapture(n.pointerId)}),s.addEventListener("pointermove",n=>{if(!e.drag||s._wdPinching)return;const l=s._wd;if(!l)return;const c=s.getBoundingClientRect(),p=l.cssToX(n.clientX-c.left);e.drag==="start"?e.trim.start=Math.min(p,e.trim.end-1):e.trim.end=Math.max(p,e.trim.start+1),this._syncTrimInputs(),this._drawCycleEditor()}),s.addEventListener("pointerup",o),s.addEventListener("pointercancel",o)}else e.mode==="split"&&s.addEventListener("pointerdown",a=>{const r=s._wd;if(!r||s._wdPinching)return;const i=s.getBoundingClientRect();this._toggleSplit(r.cssToX(a.clientX-i.left))})}_wireSplitSegments(t){const e=this._modal;!e||e.type!=="cycle-detail"||e.mode!=="split"||t.querySelectorAll("[data-segidx]").forEach(s=>s.addEventListener("change",()=>{e.split.profiles[+s.dataset.segidx]=s.value||null}))}_wirePhaseInputs(t){const e=this._modal;!e||e.type!=="profile-panel"||e.tab!=="phases"||t.querySelectorAll("[data-phidx]").forEach(s=>{const a=()=>{const r=+s.dataset.phidx,i=s.dataset.phfield,o=e.phases[r];o&&(i==="name"?o.name=s.value:o[i]=Math.max(0,pe(s.value,0)*60),this._drawPhaseEditor())};s.addEventListener("input",a),s.addEventListener("change",a)})}_wirePhaseCanvas(t){const e=this._modal;if(!e||e.type!=="profile-panel"||e.tab!=="phases"||!e.env||!(e.env.avg||[]).length)return;const s=t.getElementById("wd-phase-canvas");if(!s)return;const a=e.env.target_duration||e.env.avg[e.env.avg.length-1][0],r=Math.max(5,a*.01),i=(n,l)=>{const c=s._wd;if(!c)return null;let p=null,_=l==="touch"?Tt:12;return(e.phases||[]).forEach((f,u)=>{[["start",f.start],["end",f.end]].forEach(([b,v])=>{const T=Math.abs(n-c.xToCss(v));T<_&&(_=T,p={idx:u,edge:b})})}),p},o=()=>{e.phaseDrag=null,s._wdOwnGesture=!1};s._wdAbortEdit=o,s.addEventListener("pointerdown",n=>{const l=s.getBoundingClientRect();e.phaseDrag=i(n.clientX-l.left,n.pointerType),e.phaseDrag&&(s._wdOwnGesture=!0,s.setPointerCapture(n.pointerId))}),s.addEventListener("pointermove",n=>{if(!e.phaseDrag||s._wdPinching)return;const l=s._wd;if(!l)return;const c=s.getBoundingClientRect(),p=l.cssToX(n.clientX-c.left),_=e.phases[e.phaseDrag.idx];_&&(e.phaseDrag.edge==="start"?_.start=Math.max(0,Math.min(p,_.end-r)):_.end=Math.min(a,Math.max(p,_.start+r)),this._syncPhaseInputs(e.phaseDrag.idx),this._drawPhaseEditor())}),s.addEventListener("pointerup",o),s.addEventListener("pointercancel",o)}_syncPhaseInputs(t){const e=this.shadowRoot,s=this._modal.phases[t];if(!s)return;const a=e.querySelector(`[data-phidx="${t}"][data-phfield="start"]`),r=e.querySelector(`[data-phidx="${t}"][data-phfield="end"]`);a&&(a.value=(s.start/60).toFixed(1)),r&&(r.value=(s.end/60).toFixed(1))}_wireCleanup(t){const e=this._modal;if(!e||e.type!=="profile-panel"||e.tab!=="cleanup"||!e.cleanup)return;t.querySelectorAll("[data-cleanidx]").forEach(a=>a.addEventListener("change",()=>{const r=e.cleanup.cycles[+a.dataset.cleanidx];if(!r)return;a.checked?e.cleanup.selected.add(r.cycle_id):e.cleanup.selected.delete(r.cycle_id);const i=e.cleanup.selected;if(!this._busy.has("pp-cleanup-del")&&!this._busy.has("pp-cleanup-unlabel")){const o=[["pp-cleanup-unlabel","btn.unlabel_selected",`Unlabel selected (${i.size})`],["pp-cleanup-del","btn.delete_selected",`Delete selected (${i.size})`]];for(const[n,l,c]of o){const p=t.querySelector(`[data-maction="${n}"]`);p&&(p.disabled=i.size===0,p.textContent=this._tText(l,{n:i.size},c))}}this._drawSpaghetti()})),t.querySelectorAll("tr[data-cid]").forEach(a=>{a.addEventListener("mouseenter",()=>{this._spagTableHoverCid=a.dataset.cid,this._drawSpaghetti()}),a.addEventListener("mouseleave",()=>{this._spagTableHoverCid=null,this._drawSpaghetti()})});const s=t.getElementById("wd-spag-canvas");s&&s.addEventListener("pointerdown",a=>{if(s._wdPinching)return;this._onGraphHoverInner(a.clientX,a.clientY,"wd-spag-canvas",{touch:a.pointerType==="touch"});const r=this._hoverNearest;if(r&&r.cid){const i=e.cleanup.selected;i.has(r.cid)?i.delete(r.cid):i.add(r.cid),this._render()}})}_onAction(t){const e=t.dataset.action,s=this.shadowRoot;if(e==="open-settings"){this._modal={type:"gear-settings",tab:this._gearTab||"prefs"},this._render();return}if(e==="retry-load"){this._loading=!0,this._fetchAll();return}const a=this._devices[this._selIdx];if(!a)return;const r=a.entry_id;if(e.startsWith("sug-"))return this._onActSuggestions(e,t,a,r,s);if(e.startsWith("ml-"))return this._onActMl(e,t,a,r);if(e.startsWith("store-"))return this._onActStore(e,t,a,r,s);if(e.startsWith("auto-"))return this._onActAuto(e,t,a,r,s);if(e.startsWith("maint-"))return this._onActMaintenance(e,t,a,r,s);if(e.startsWith("pg-")&&e!=="pg-new"&&e!=="pg-edit")return this._onActPlayground(e,t,a,r);if(e==="open-cycle"){const i=t.dataset.cid,o=t.dataset.mode==="review"?"review":"view";this._modal={type:"cycle-detail",entryId:r,cycleId:i,loaded:!1,mode:o,curve:null,ml:(this._mlById||{})[i]||null,trim:{start:0,end:0},split:{offsets:[],profiles:[]},drag:null},this._profiles.length||this._fetchProfiles(r),this._render(),this._ws({type:`${w}/get_cycle_power_data`,entry_id:r,cycle_id:i}).then(n=>{this._modal&&this._modal.cycleId===i&&(this._modal.curve=n,this._modal.loaded=!0,this._modal.trim={start:0,end:n.full_duration_s||0},this._loadCycleOverlays(r,i,n),this._loadCycleContext(r,i,n),this._render())}).catch(n=>this._showToast(this._tText("toast.could_not_load_cycle",{error:n.message||n},"Could not load cycle: "+(n.message||n)),"error"))}else if(e==="cleanup-edit-cycle"){const i=t.dataset.cid;this._prevModal=this._modal,this._modal={type:"cycle-detail",entryId:r,cycleId:i,loaded:!1,mode:"view",curve:null,ml:(this._mlById||{})[i]||null,trim:{start:0,end:0},split:{offsets:[],profiles:[]},drag:null},this._profiles.length||this._fetchProfiles(r),this._render(),this._ws({type:`${w}/get_cycle_power_data`,entry_id:r,cycle_id:i}).then(o=>{this._modal&&this._modal.cycleId===i&&(this._modal.curve=o,this._modal.loaded=!0,this._modal.trim={start:0,end:o.full_duration_s||0},this._loadCycleOverlays(r,i,o),this._loadCycleContext(r,i,o),this._render())}).catch(o=>this._showToast(this._tText("toast.could_not_load_cycle",{error:o.message||o},"Could not load cycle: "+(o.message||o)),"error"))}else if(e==="open-profile"){const i=t.dataset.pname;this._prevModal=null;const o=(this._profiles||[]).find(n=>n.name===i)||{name:i};this._modal={type:"profile-panel",name:i,tab:"stats",loaded:!1,stats:o,env:null,phases:[],catalog:[],cleanup:null},this._render(),Promise.all([this._ws({type:`${w}/get_profile_envelope`,entry_id:r,profile_name:i}).catch(()=>({envelope:null})),this._ws({type:`${w}/get_profile_phases`,entry_id:r,profile_name:i}).catch(()=>({phases:[]})),this._ws({type:`${w}/get_phase_catalog`,entry_id:r}).catch(()=>({phases:[]}))]).then(([n,l,c])=>{!this._modal||this._modal.name!==i||(this._modal.env=n.envelope,this._modal.phases=(l.phases||[]).map(p=>({name:p.name,start:p.start,end:p.end})),this._modal.catalog=(c.phases||[]).map(p=>p.name),this._modal.loaded=!0,this._render())})}else if(e==="create-profile")this._modal={type:"create-profile"},this._render();else if(e==="coverage-create")this._modal={type:"create-profile",prefillCycle:t.dataset.cid||"",prefillCycleMin:t.dataset.min||""},this._render();else if(e==="setup-cta"){const i=t.dataset.ctaAction||"";let o={};try{o=JSON.parse(t.dataset.ctaParams||"{}")}catch{}this._dispatchSetupCta(i,o)}else if(e==="setup-skip"){const i=t.dataset.step,o=t.dataset.snooze;if(i){let n;if(o==="never")n="never";else{const l=new Date;l.setDate(l.getDate()+14),n=l.toISOString()}this._setPref(i,n),this._reloadSetupStatus()}}else if(e==="hide-setup-card")this._setPref("setup_card_dismissed",!0),this._render();else if(e==="expand-setup")this._setPref("setup_card_dismissed",!1),this._render();else if(e==="set-settings-internal")this._snapshotFormToPending(s),this._setPref("show_internal",!!t.checked),this._render();else if(e==="set-settings-level"){const i=(t.type==="checkbox"?t.checked:t.dataset.slevel==="advanced")?"advanced":"basic";i!==this._pref("settings_level","basic")&&(this._snapshotFormToPending(s),this._setPref("settings_level",i),this._render())}else if(e==="pg-new"||e==="pg-edit"){if(e==="pg-new")this._modal={type:"profile-group",orig:null,name:"",members:[]};else{const i=t.dataset.gname,o=((this._profileGroups||{}).groups||[]).find(n=>n.name===i);this._modal={type:"profile-group",orig:i,name:i,members:o?[...o.members||[]]:[]}}this._render(),this._ensureProfileEnvs(r,(this._profiles||[]).map(i=>i.name)).then(()=>{this._modal&&this._modal.type==="profile-group"&&this._render()})}else if(e==="rebuild-envelopes")this._kickAndTrack({type:`${w}/rebuild_envelopes`,entry_id:r},"rebuild-envelopes",async()=>{this._showToast(this._t("toast.envelopes_rebuilt",{},"Envelopes rebuilt")),await this._fetchProfiles(r)});else if(e==="rec-start")this._ws({type:`${w}/start_recording`,entry_id:r}).then(()=>(this._showToast(this._t("toast.recording_started",{},"Recording started")),this._fetchRecState(r))).then(()=>this._render()).catch(i=>this._showToast(this._tText("toast.start_failed",{error:i.message||i},"Start failed: "+(i.message||i)),"error"));else if(e==="rec-stop")this._ws({type:`${w}/stop_recording`,entry_id:r}).then(()=>(this._showToast(this._t("toast.recording_stopped",{},"Recording stopped")),this._fetchRecState(r))).then(()=>this._render()).catch(i=>this._showToast(this._tText("toast.stop_failed",{error:i.message||i},"Stop failed: "+(i.message||i)),"error"));else if(e==="rec-process-open")this._fetchProfiles(r).then(()=>{this._modal={type:"process-recording"},this._render()});else if(e==="rec-discard")this._modal={type:"confirm",title:this._t("modal.discard_recording_title",{},"Discard Recording"),message:this._t("modal.discard_recording_msg",{},"Discard the saved recording? This cannot be undone."),okLabel:this._t("btn.discard",{},"Discard"),onOk:async()=>{try{await this._ws({type:`${w}/discard_recording`,entry_id:r}),this._showToast(this._t("toast.recording_discarded",{},"Recording discarded")),await this._fetchRecState(r)}catch(i){this._showToast(this._tText("toast.discard_failed",{error:i.message||i},"Discard failed: "+(i.message||i)),"error")}}},this._render();else if(e==="fb-confirm"){const i=t.dataset.cid;this._ws({type:`${w}/resolve_feedback`,entry_id:r,cycle_id:i,action:"confirm"}).then(()=>(this._showToast(this._t("toast.feedback_confirmed",{},"Feedback confirmed")),this._fetchFeedbacks(r))).then(()=>{const o=this._modal;o&&o.type==="cycle-detail"&&o.cycleId===i&&o.overlayPlan&&o.overlayPlan.suspected&&(o.curve&&(o.curve.profile_name=o.overlayPlan.name),o.overlayPlan={name:o.overlayPlan.name,suspected:!1,runnerUp:null}),this._render()}).catch(o=>this._showToast(this._tText("msg.toast_error",{error:o.message||o},"Error: "+(o.message||o)),"error"))}else if(e==="fb-ignore")this._ws({type:`${w}/resolve_feedback`,entry_id:r,cycle_id:t.dataset.cid,action:"ignore"}).then(()=>(this._showToast(this._t("toast.feedback_dismissed",{},"Feedback dismissed")),this._fetchFeedbacks(r))).then(()=>this._render()).catch(i=>this._showToast(this._tText("msg.toast_error",{error:i.message||i},"Error: "+(i.message||i)),"error"));else if(e==="fb-correct")this._fetchProfiles(r).then(()=>{this._modal={type:"correct-feedback",cycleId:t.dataset.cid,detectedProfile:t.dataset.prof},this._render()});else if(e==="fb-dismiss-all")this._modal={type:"confirm",title:this._t("modal.dismiss_all_title",{},"Dismiss All Feedbacks"),message:this._tText("modal.dismiss_all_msg",{count:this._feedbacks.length},`Dismiss all ${this._feedbacks.length} pending feedback requests?`),okLabel:this._t("modal.dismiss_all_ok",{},"Dismiss All"),onOk:async()=>{try{await this._ws({type:`${w}/dismiss_all_feedbacks`,entry_id:r}),this._showToast(this._t("toast.feedback_all_dismissed",{},"All feedbacks dismissed")),await this._fetchFeedbacks(r)}catch(i){this._showToast(this._tText("msg.toast_error",{error:i.message||i},"Error: "+(i.message||i)),"error")}}},this._render();else if(e==="create-phase")this._modal={type:"create-phase",deviceType:t.dataset.dtype},this._render();else if(e==="edit-phase")this._modal={type:"edit-phase",phaseId:t.dataset.pid,phaseName:t.dataset.pname,phaseDesc:t.dataset.pdesc,isDefault:t.dataset.pisdefault==="true"},this._render();else if(e==="del-phase"){const i=t.dataset.pname,o=t.dataset.pid,n=t.dataset.reset==="1",l=n?this._tText("modal.reset_phase_msg",{name:i},`Undo your edit of "${i}" and restore the built-in phase? Programs keep their ranges.`):this._tText("modal.delete_phase_msg",{name:i},`Delete phase "${i}"?`),c=n?this._tText("toast.phase_reset",{name:i},`"${i}" restored to the built-in phase`):this._tText("toast.phase_deleted",{name:i},`Phase "${i}" deleted`);this._modal={type:"confirm",title:n?this._t("modal.reset_phase_title",{},"Reset Phase"):this._t("modal.delete_phase_title",{},"Delete Phase"),message:l,okLabel:n?this._t("btn.reset",{},"Reset"):this._t("btn.delete",{},"Delete"),onOk:async()=>{try{await this._ws({type:`${w}/delete_phase`,entry_id:r,phase_id:o}),this._showToast(c),await this._fetchPhases(r)}catch(p){this._showToast(this._tText("msg.toast_delete_failed",{error:p.message||p},"Delete failed: "+(p.message||p)),"error")}}},this._render()}else if(e==="diag-refresh")this._fetchToolsData(r).then(()=>this._render());else if(e==="reprocess-history")this._modal={type:"confirm",title:this._t("modal.process_history_title",{},"Process History"),message:this._t("modal.process_history_msg",{},"Re-run matching, refresh suggestions, retrain ML (if enabled), recost cycles against recorded energy prices and recompute cycle health across all stored cycles. This may take a while."),okLabel:this._t("modal.process_history_ok",{},"Process"),onOk:()=>this._kickAndTrack({type:`${w}/reprocess_history`,entry_id:r},"reprocess",async i=>{const o=i.count||0,n=[this._tText("toast.processed_cycles",{n:o},o+" cycles")];i.suggestions!=null&&n.push(this._tText("toast.processed_suggestions",{n:i.suggestions},i.suggestions+" suggestion(s)"));const l=i.ml_training&&i.ml_training.ok&&(i.ml_training.promoted||[]).length||0;l&&n.push(this._tText("toast.processed_models",{n:l},l+" model(s) promoted")),this._showToast(this._tText("toast.processed",{bits:n.join(", ")},"Processed "+n.join(", "))),await this._fetchToolsData(r)})},this._render();else if(e==="clear-debug")this._modal={type:"confirm",title:this._t("modal.clear_debug_title",{},"Clear Debug Data"),message:this._t("modal.clear_debug_msg",{},"Delete all stored debug traces?"),okLabel:this._t("btn.clear",{},"Clear"),onOk:()=>this._busyRun("clear-debug",async()=>{try{const i=await this._ws({type:`${w}/clear_debug_data`,entry_id:r});this._showToast(this._tText("toast.debug_cleared",{count:i.count||0},`Cleared ${i.count||0} debug traces`)),await this._fetchToolsData(r)}catch(i){this._showToast(this._tText("msg.toast_error",{error:i.message||i},"Error: "+(i.message||i)),"error")}})},this._render();else if(e==="wipe-history")this._modal={type:"confirm",title:this._t("modal.wipe_all_title",{},"Wipe All Data"),message:this._t("modal.wipe_all_msg",{},"\u26A0\uFE0F This permanently deletes ALL cycles and profiles. This cannot be undone."),okLabel:this._t("modal.wipe_all_ok",{},"Wipe Everything"),onOk:()=>this._busyRun("wipe",async()=>{try{await this._ws({type:`${w}/wipe_history`,entry_id:r}),this._showToast(this._t("toast.all_wiped",{},"All data wiped")),this._cycles=[],this._profiles=[],await this._fetchToolsData(r)}catch(i){this._showToast(this._tText("msg.toast_error",{error:i.message||i},"Error: "+(i.message||i)),"error")}})},this._render();else if(e==="import-undo"){const i=this._importUndo||{};this._modal={type:"confirm",title:this._tText("modal.undo_import_title",{},"Undo last import"),message:this._tText("modal.undo_import_msg",{when:et(i.created_at)},`This device's data and settings go back to how they were before the last import (${et(i.created_at)}). Cycles recorded and changes made since then are lost.`),okLabel:this._tText("modal.undo_import_ok",{},"Undo import"),onOk:()=>this._busyRun("import-undo",async()=>{try{const o=await this._ws({type:`${w}/undo_import`,entry_id:r}),n=(o&&o.summary||{}).counts||{},l=(n.real_cycles||0)+(n.reference_cycles||0)+(n.backfill_cycles||0);this._showToast(this._tText("toast.import_undone",{profiles:n.profiles||0,cycles:l},`Import undone: ${n.profiles||0} profile(s) and ${l} cycle(s) restored`))}catch(o){this._showToast(this._tText("toast.import_undo_failed",{error:o.message||o},"Could not undo the import: "+(o.message||o)),"error")}this._isActiveEntry(r)&&(await this._fetchToolsData(r),await this._fetchCycles(r),await this._fetchProfiles(r))})},this._render()}else if(e==="export-config")this._ws({type:`${w}/export_config`,entry_id:r}).then(i=>{const o=new Blob([i.json_data],{type:"application/json"}),n=URL.createObjectURL(o),l=document.createElement("a");l.href=n,l.download=`washdata_export_${r.slice(0,8)}.json`,document.body.appendChild(l),l.click(),document.body.removeChild(l),URL.revokeObjectURL(n),this._showToast(this._t("toast.export_downloaded",{},"Export downloaded"))}).catch(i=>this._showToast(this._tText("toast.export_failed",{error:i.message||i},"Export failed: "+(i.message||i)),"error"));else if(e==="export-select-open")this._modal={type:"export-select",inventory:null,loading:!0,sel:this._wizEmptySel(),expanded:new Set},this._render(),(async()=>{let i=null;try{const o=await this._ws({type:`${w}/get_export_inventory`,entry_id:r});i=o&&o.manifest||null}catch(o){this._showToast(this._tText("toast.export_failed",{error:o.message||o},"Export failed: "+(o.message||o)),"error")}if(!(!this._isActiveEntry(r)||!this._modal||this._modal.type!=="export-select")){if(!i){this._modal=null,this._render();return}this._modal.inventory=i,this._modal.sel=this._wizInitSel({categories:i},!1),this._modal.loading=!1,this._render()}})();else if(e==="cyc-select-toggle")this._selectMode=!this._selectMode,this._selectMode||this._cycleSel.clear(),this._render();else if(e==="cyc-auto-open")this._openAutoLabelModal(r);else if(e==="cyc-merge"){const i=Array.from(this._cycleSel);if(i.length<2)return;this._fetchProfiles(r).then(()=>{this._modal={type:"merge-cycles",ids:i},this._render()})}else if(e==="cyc-relabel"){const i=Array.from(this._cycleSel);if(!i.length)return;this._fetchProfiles(r).then(()=>{this._modal={type:"bulk-relabel",ids:i},this._render()})}else if(e==="cyc-load-more")this._busyRun("cyc-load-more",async()=>{try{await this._loadMoreCycles(r)}catch(i){this._showToast(this._tText("toast.load_more_failed",{error:i.message||i},"Could not load more: "+(i.message||i)),"error")}});else if(e==="task-cancel"){const i=t.dataset.taskId;i&&(this._cancellingTasks.add(i),this._updateTaskPills(),this._ws({type:`${w}/cancel_task`,task_id:i}).catch(()=>{this._cancellingTasks.delete(i),this._updateTaskPills()}))}else if(e==="cyc-compare"){const i=Array.from(this._cycleSel);if(i.length<2)return;this._modal={type:"compare-cycles",ids:i,cycles:{},hidden:new Set,overlays:[],loaded:!1},this._profiles.length||this._fetchProfiles(r),this._render(),Promise.all(i.map(o=>this._ws({type:`${w}/get_cycle_power_data`,entry_id:r,cycle_id:o}).then(n=>({cid:o,r:n})).catch(()=>({cid:o,r:null})))).then(o=>{!this._modal||this._modal.type!=="compare-cycles"||(o.forEach(({cid:n,r:l})=>{l&&(this._modal.cycles[n]=l)}),this._modal.loaded=!0,this._render())})}else if(e==="cyc-bulk-del"){const i=Array.from(this._cycleSel);if(!i.length)return;this._deleteCyclesWithUndo(r,i)}else if(e==="retry-tab")this._fetchTabData();else if(e==="retry-cycles")this._fetchCycles(r).then(()=>this._render());else if(e==="retry-profiles")Promise.all([this._fetchProfiles(r),this._fetchProfileGroups(r)]).then(()=>this._render());else if(e==="retry-suggestions")this._fetchSuggestions(r).then(()=>this._render());else if(e==="goto-suggestions")this._settingsSugOnly=!0,this._tab="settings",this._fetchTabData();else if(e==="goto-conflicts")this._tab="settings",this._fetchTabData();else if(e==="goto-standby")this._settingsSec="detection",this._settingsSearch="",this._settingsSugOnly=!1,this._tab="settings",this._fetchTabData();else if(e==="conf-goto-section"){const i=this._conflictKeysFromOpts();for(const o of yt)if((o.fields||(o.groups||[]).flatMap(l=>l.fields||[])).some(l=>i.has(l.key))){this._settingsSec=o.id,this._render();break}}else if(e==="toggle-settings-history")this._settingsHistoryOpen=!this._settingsHistoryOpen,this._render();else if(e==="settings-revert-key"){const i=t.dataset.key,o=JSON.parse(t.dataset.val);if(!i)return;const n=a.entry_id;this._ws({type:`${w}/set_options`,entry_id:n,options:{[i]:o}}).then(()=>this._ws({type:`${w}/get_options`,entry_id:n})).then(l=>(this._opts=l.options||{},this._optDefaults=l.defaults||{},this._fetchSettingsChangelog(n))).then(()=>{this._showToast(this._tText("msg.toast_reverted",{key:this._t("setting."+i+".label",{},i)},"{key} reverted"),"success"),this._render()}).catch(l=>this._showToast(this._tText("msg.toast_error",{error:l.message||l},"Error: "+(l.message||l)),"error"))}else if(e==="toggle-log-drawer"){this._logOpen=!this._logOpen;try{localStorage.setItem("wd-log-open",this._logOpen?"1":"0")}catch{}this._render(),this._logOpen&&this._fetchLogs().then(()=>{this._logOpen&&this._render()})}else if(e==="add-device")this._navigate(`/config/integrations/integration/${w}`);else if(e==="goto-feedbacks")this._tab="history",this._historySub="cycles",this._cycleFilter={...this._cycleFilter,status:"needs_review"},this._fetchTabData();else if(e==="goto-recording")this._tab="status",this._fetchTabData();else if(e==="logs-refresh")this._fetchLogs().then(()=>this._render());else if(e==="logs-export")this._ws({type:`${w}/get_logs`,limit:500}).then(i=>{const o=(i.logs||[]).map(p=>`${new Date(p.ts*1e3).toISOString()} ${p.level} ${p.msg}`).join(` `),n=new Blob([o],{type:"text/plain"}),l=URL.createObjectURL(n),c=document.createElement("a");c.href=l,c.download=`washdata_logs_${Date.now()}.txt`,document.body.appendChild(c),c.click(),document.body.removeChild(c),URL.revokeObjectURL(l),this._showToast(this._t("toast.logs_exported",{},"Logs exported"))}).catch(i=>this._showToast(this._tText("toast.export_failed",{error:i.message||i},"Export failed: "+(i.message||i)),"error"));else if(e==="import-config-open")this._modal={type:"import-wizard",step:"input",jsonText:"",manifest:null,error:null,sel:this._wizEmptySel(),expanded:new Set,mode:"merge",cycleDest:"reference",conflicts:{}},this._render();else if(e==="hist-import-open")this._modal={type:"history-import",step:"input",csvText:"",since:Ut(),token:null,scanTaskId:null,applyTaskId:null,result:null,accept:new Set,done:null,error:null},this._render();else if(e==="save-prefs"){const i=s.getElementById("wd-pref-tab")?.value||"",o=!!s.getElementById("wd-pref-debug")?.checked,n=s.getElementById("wd-pref-expected")?!!s.getElementById("wd-pref-expected").checked:!0,l=!!s.getElementById("wd-pref-raw")?.checked,c=s.getElementById("wd-pref-datefmt")?.value||"relative",p=s.getElementById("wd-pref-lang")?.value||"",_=parseFloat(s.getElementById("wd-pref-fontscale")?.value)||1,f={default_tab:i,show_debug:o,show_expected:n,show_raw:l,date_format:c,lang_override:p,font_scale:_};this._busyRun("save-prefs",async()=>{try{await this._ws({type:`${w}/set_user_prefs`,prefs:f}),this._panelCfg&&(this._panelCfg.prefs={...this._panelCfg.prefs||{},...f});const u=p||this._hass&&this._hass.locale&&this._hass.locale.language;await this._loadPanelLang(u),this._render(),this._showToast(this._t("toast.preferences_saved",{},"Preferences saved"))}catch(u){this._showToast(this._tText("toast.save_failed",{error:u.message||u},"Save failed: "+(u.message||u)),"error")}})}else if(e==="save-panel"){const i={default_tab:s.getElementById("wd-ps-deftab")?.value||"status",hidden_tabs:Array.from(s.querySelectorAll("[data-hidetab]")).filter(o=>o.checked).map(o=>o.dataset.hidetab)};this._busyRun("save-panel",async()=>{try{await this._ws({type:`${w}/set_panel_config`,panel:i}),this._panelCfg=await this._ws({type:`${w}/get_panel_config`}),this._tabInitialized=!0,this._applyPanelConfig(),this._showToast(this._t("toast.panel_settings_saved",{},"Panel settings saved"))}catch(o){this._showToast(this._tText("msg.toast_save_failed",{error:o.message||o},"Save failed: "+(o.message||o)),"error")}})}else if(e==="pause-cycle")this._ws({type:`${w}/pause_cycle`,entry_id:r}).then(i=>{if(i&&i.ok===!1){this._showToast(this._t("toast.pause_no_cycle",{},"No active cycle to pause"),"error");return}return this._showToast(this._t("toast.cycle_paused",{},"Cycle paused")),this._fetchAll()}).catch(i=>this._showToast(this._tText("toast.pause_failed",{error:i.message||i},"Pause failed: "+(i.message||i)),"error"));else if(e==="resume-cycle")this._ws({type:`${w}/resume_cycle`,entry_id:r}).then(i=>{if(i&&i.ok===!1){this._showToast(this._t("toast.resume_no_cycle",{},"No paused cycle to resume"),"error");return}return this._showToast(this._t("toast.cycle_resumed",{},"Cycle resumed")),this._fetchAll()}).catch(i=>this._showToast(this._tText("msg.toast_resume_failed",{error:i.message||i},"Resume failed: "+(i.message||i)),"error"));else if(e==="terminate-cycle")this._modal={type:"confirm",title:this._t("modal.force_stop_title",{},"Force Stop Cycle"),message:this._t("modal.force_stop_msg",{},"Force-stop the active cycle now? The cycle will be saved as interrupted."),okLabel:this._t("btn.force_stop",{},"Force Stop"),onOk:async()=>{try{await this._ws({type:`${w}/terminate_cycle`,entry_id:r}),this._showToast(this._t("toast.cycle_force_stopped",{},"Cycle force-stopped")),await this._fetchAll()}catch(i){this._showToast(this._tText("msg.toast_force_stop_failed",{error:i.message||i},"Force stop failed: "+(i.message||i)),"error")}}},this._render();else if(e==="save-rbac"){const i=!!s.getElementById("wd-rbac-enabled")?.checked,o=s.getElementById("wd-rbac-default")?.value||"none",n={};s.querySelectorAll("[data-rbacuser]").forEach(l=>{const c=l.dataset.rbacuser,p=l.dataset.rbacdev,_=l.value;n[c]||(n[c]={default:"none",devices:{}}),p==="__default__"?n[c].default=_:_&&_!=="inherit"&&(n[c].devices[p]=_)}),this._busyRun("save-rbac",async()=>{try{await this._ws({type:`${w}/set_panel_config`,rbac:{enabled:i,default_level:o,users:n}}),this._panelCfg=await this._ws({type:`${w}/get_panel_config`}),this._showToast(this._t("toast.access_saved",{},"Access control saved"))}catch(l){this._showToast(this._tText("msg.toast_save_failed",{error:l.message||l},"Save failed: "+(l.message||l)),"error")}})}}_applyAllSuggestions(t,e){this._busyRun("save-settings",async()=>{try{await this._ws({type:`${w}/apply_suggestions`,entry_id:t,keys:e}),this._showToast(this._t("toast.suggestions_applied",{},"Suggestions applied; integration reloading")),await this._fetchSuggestions(t);const s=await this._ws({type:`${w}/get_options`,entry_id:t});this._opts=s.options||{},this._optDefaults=s.defaults||{},this._prevOpts=null,this._cascadePending={},this._preCascadeOpts=null}catch(s){this._showToast(this._tText("toast.apply_failed",{error:s.message||s},"Apply failed: "+(s.message||s)),"error")}})}_onActSuggestions(t,e,s,a,r){if(t==="sug-apply-all"){const i=this._suggestions.map(n=>n.key),o=this._suggestions.map(n=>{const l=wt[n.key]||{},c=l.unit?` ${l.unit}`:"",p=n.current==null||n.current===""?"-":`${n.current}${c}`;return{text:`${this._t("setting."+n.key+".label",{},l.label||n.key)}: ${p} \u2192 ${n.suggested}${c}`,sub:we(this._t.bind(this),n.key,n.suggested,n.current)}});this._modal={type:"confirm",okPrimary:!0,items:o,title:this._t("modal.apply_suggestions_title",{},"Apply suggestions"),message:this._tText("modal.apply_suggestions_msg",{n:o.length},`Change these ${o.length} settings? The integration reloads after saving.`),okLabel:this._t("btn.apply_all",{},"Apply all"),onOk:()=>this._applyAllSuggestions(a,i)},this._render()}else t==="sug-show-all"?(this._settingsSugOnly=!1,this._render()):t==="sug-dismiss"?(this._settingsSugOnly=!1,this._busyRun("save-settings",async()=>{try{await this._ws({type:`${w}/clear_suggestions`,entry_id:a}),this._suggestions=[],this._showToast(this._t("toast.suggestions_dismissed",{},"Suggestions dismissed"))}catch(i){this._showToast(this._tText("toast.error",{error:i.message||i},"Error: "+(i.message||i)),"error")}})):t==="sug-unmute-all"?this._busyRun("save-settings",async()=>{try{const i=[...this._lockedSuggestions||[]],o=await Promise.allSettled(i.map(c=>this._ws({type:`${w}/set_suggestion_lock`,entry_id:a,key:c,locked:!1})));if(!this._isActiveEntry(a))return;const n=o.filter(c=>c.status==="rejected").length,l=o.slice().reverse().find(c=>c.status==="fulfilled");this._lockedSuggestions=l&&l.value&&l.value.locked_suggestions||[],await this._fetchSuggestions(a),n?this._showToast(this._tText("toast.error",{error:`${n} suggestion(s) failed to unlock`},`${n} suggestion(s) failed to unlock`),"error"):this._showToast(this._t("msg.sug_unmuted_all",{},"Muted suggestions reset"),"success")}catch(i){this._showToast(this._tText("toast.error",{error:i.message||i},"Error: "+(i.message||i)),"error")}}):t==="sug-analyze"&&this._busyRun("sug-analyze",async()=>{try{const i=await this._ws({type:`${w}/run_suggestion_analysis`,entry_id:a}),o=i&&i.count||0;this._showToast(o?this._tText("toast.analysis_complete",{count:o},`Analysis complete: ${o} suggestion(s)`):this._t("toast.analysis_complete_none",{},"Analysis complete: no new suggestions")),await this._fetchSuggestions(a)}catch(i){this._showToast(this._tText("toast.analysis_failed",{error:i.message||i},"Analysis failed: "+(i.message||i)),"error")}})}_onActMl(t,e,s,a){t==="ml-train-now"?this._kickAndTrack({type:`${w}/trigger_ml_training`,entry_id:a},"ml-train-now:"+a,async r=>{if(r&&r.ok){const i=(r.promoted||[]).length,o=!i&&(r.results||[]).find(n=>n&&n.promoted===!1&&n.reason_code);this._showToast(i?this._tText("toast.ml_training_promoted",{count:i},`Training complete: promoted ${i} model(s)`):o?this._mlSkipReason(o)[1]:this._t("toast.ml_training_no_improvement",{},"Training complete: baseline kept (no improvement)"))}else this._showToast(this._t("toast.ml_training_no_improvement",{},"Training complete: baseline kept (no improvement)"),"info");await this._loadMlTrainingStatus(a)}):t==="ml-revert-models"&&this._busyRun("ml-revert-models",async()=>{try{await this._ws({type:`${w}/revert_ml_models`,entry_id:a}),this._showToast(this._t("toast.models_reverted",{},"On-device models reverted to baseline")),await this._loadMlTrainingStatus(a)}catch(r){this._showToast(this._tText("msg.toast_revert_failed",{error:r.message||r},"Revert failed: "+(r.message||r)),"error")}})}_onActStore(t,e,s,a,r){if(t==="store-toggle-online"){const i=!!e.checked;this._busyRun("store-account",async()=>{try{const o=await this._ws({type:`${w}/store_set_online`,entry_id:a,enabled:i});this._constants.storeOnlineEnabled=!!(o&&o.enabled),this._constants.storeOnlineEnabled?(await this._loadStoreStatus(a),this._ensureStoreConnectListener()):(this._storeStatus={enabled:!1},this._storeConnected=!1)}catch(o){this._showToast(this._tText("toast.store_error",{error:o.message||o},"Error: "+(o.message||o)),"error")}})}else if(t==="store-toggle-pref"){const i=e.dataset.pref,o=!!e.checked;if(!i)return;this._busyRun("store-account",async()=>{try{const n=await this._ws({type:`${w}/store_set_prefs`,entry_id:a,prefs:{[i]:o}});n&&n.prefs&&(this._constants={...this._constants,storePrefs:n.prefs})}catch(n){this._showToast(this._tText("toast.store_error",{error:n.message||n},"Error: "+(n.message||n)),"error")}})}else if(t==="store-goto-identity")this._tab="settings",this._settingsSec="basic",this._fetchTabData(),requestAnimationFrame(()=>requestAnimationFrame(()=>{const i=this.shadowRoot&&this.shadowRoot.getElementById("wd-store-brand");i&&(i.focus(),i.scrollIntoView({block:"center"}))}));else if(t==="store-refresh-catalog")this._busyRun("store-refresh-catalog",async()=>{try{await this._ws({type:`${w}/store_refresh_catalog`,entry_id:a}),clearTimeout(this._brandSearchTimer),this._brandSearchTimer=null,this._catalog.brands=void 0,this._catalog.devices=void 0,this._catalog.forBrand=null,this._catalog.brandsFull=!1,this._catalog.brandPrefixes=[],this._dropModelCandidates(),this._catalogEntry=null,this._entityListCache&&(delete this._entityListCache.store_brand,delete this._entityListCache.store_model),this._showToast(this._t("toast.catalog_refreshed",{},"Community catalog refreshed"))}catch(i){this._showToast(this._tText("toast.store_error",{error:i.message||i},"Error: "+(i.message||i)),"error")}});else if(t==="store-connect"){const i=this._constants.storeWebOrigin;if(!i){this._showToast(this._t("toast.store_unavailable",{},"The community store is not available."),"error");return}this._ensureStoreConnectListener(),window.open(i+"/connect.html?origin="+encodeURIComponent(location.origin),"washdata_connect","width=480,height=640")}else if(t==="store-disconnect")this._busyRun("store-account",async()=>{try{await this._ws({type:`${w}/store_disconnect`,entry_id:a}),await this._loadStoreStatus(a),this._showToast(this._t("toast.store_disconnected",{},"Disconnected from the community store"))}catch(i){this._showToast(this._tText("toast.store_error",{error:i.message||i},"Error: "+(i.message||i)),"error")}});else if(t==="store-add-appliance"){const i=this._constants.storeWebOrigin;if(!i){this._showToast(this._t("toast.store_unavailable",{},"The community store is not available."),"error");return}this._ensureStoreConnectListener();const o=r.getElementById("wd-store-model"),n=r.getElementById("wd-store-brand"),l=new URLSearchParams({mode:"device",type:this._storeApplianceType(),brand:n&&n.value||this._opts.store_brand||"",model:o&&o.value||this._opts.store_model||"",origin:location.origin}).toString();this._storeContribEid=a,window.open(i+"/create.html?"+l,"washdata_create","width=560,height=760")}else if(t==="store-add-brand"){const i=this._constants.storeWebOrigin;if(!i){this._showToast(this._t("toast.store_unavailable",{},"The community store is not available."),"error");return}this._ensureStoreConnectListener();const o=r.getElementById("wd-store-brand"),n=new URLSearchParams({mode:"brand",brand:o&&o.value||this._opts.store_brand||"",origin:location.origin}).toString();this._storeContribEid=a,window.open(i+"/create.html?"+n,"washdata_create","width=560,height=760")}else if(t==="store-confirm-device"){const i=e.dataset.deviceId;this._busyRun("store-account",async()=>{try{const o=await this._ws({type:`${w}/store_confirm_device`,entry_id:a,device_id:i});if(o&&o.error){this._showToast(this._tText("toast.store_error",{error:o.error},"Error: "+o.error),"error");return}const n=[...this._catalog.devices||[],...this._storeDevices||[],this._catalogEntry&&this._catalogEntry.device||null];for(const l of n)o&&l&&String(l.id)===String(i)&&(l.confirmCount=o.confirmCount,l.status=o.status);this._showToast(o&&o.status==="approved"?this._t("toast.device_approved",{},"Approved by the community"):this._t("toast.thanks_confirming",{},"Thanks for confirming")),this._render()}catch(o){this._showToast(this._tText("toast.store_error",{error:o.message||o},"Error: "+(o.message||o)),"error")}})}else if(t==="store-rate-device"){const i=e.dataset.deviceId,o=parseInt(e.dataset.rating,10);if(!(o>=1&&o<=5))return;this._busyRun("store-account",async()=>{try{const n=await this._ws({type:`${w}/store_rate_device`,entry_id:a,device_id:i,rating:o});if(n&&n.error){this._showToast(this._tText("toast.store_error",{error:n.error},"Error: "+n.error),"error");return}this._showToast(this._t("toast.rating_saved",{},"Quality rating saved"))}catch(n){this._showToast(this._tText("toast.store_error",{error:n.message||n},"Error: "+(n.message||n)),"error")}})}else if(t==="store-nav"){const i=e.dataset.view;i==="brands"?(this._storeView="brands",this._storeDevice=null,this._storeProfile=null,this._render()):i==="device"&&(this._storeView="device",this._storeProfile=null,this._render())}else if(t==="store-open-device"){const i=e.dataset.deviceId,o=(this._storeDevices||[]).find(n=>String(n.id)===String(i));o&&this._storeOpenDevice(o)}else if(t==="store-open-profile"){const i=e.dataset.profileId,o=(this._storeProfiles||[]).find(n=>String(n.id)===String(i));if(!o)return;this._storeProfile=o,this._storeView="profile",this._storeCycles=[],this._storeLoading=!0,this._storeBrowseErr=!1,this._render(),this._ws({type:`${w}/store_get_cycles`,entry_id:a,profile_id:o.id}).then(n=>{!this._isActiveEntry(a)||this._storeView!=="profile"||(this._storeCycles=n&&n.items||[],this._storeBrowseErr=!!(n&&n.error))}).catch(()=>{this._isActiveEntry(a)&&(this._storeCycles=[],this._storeBrowseErr=!0)}).finally(()=>{this._isActiveEntry(a)&&(this._storeLoading=!1,this._render())})}else if(t==="store-onboard")this._storeQuery=(this._opts.store_brand||"").trim(),this._storeView="brands",this._storeDevice=null,this._storeProfile=null,this._tab="store",this._fetchTabData();else if(t==="store-toggle-dl-settings")this._dlSettings=!!e.checked;else if(t==="store-download-device"){const i=e.dataset.deviceId;if(!i)return;const o=!!this._dlSettings;this._busyRun("store-download-device",async()=>{try{const n=await this._ws({type:`${w}/store_download_device`,entry_id:a,device_id:i,include_settings:o}),l=n&&n.task_id?(await this._awaitTask(n.task_id)||{}).result:n;if(l&&(l.error||l.disabled)){const x=l.error||"unavailable";this._showToast(this._tText("toast.store_download_failed",{error:x},"Download failed: "+x),"error");return}const c=l&&l.profiles_adopted||0,p=l&&l.cycles_imported||0,_=l&&l.settings_applied||0,f=l&&l.cycles_skipped||0,u=f?this._tText("toast.store_download_skipped",{n:f},`${f} recording(s) skipped: too short, too gappy or already on your device.`):"";if(!c&&!p&&!_){this._showToast(u||this._t("toast.store_download_nothing",{},"Nothing new to download - this setup is already on your device."),"info");return}await this._fetchProfiles(a),await this._fetchCycles(a);const b=l&&l.phases_applied||0;let v;_?v=this._tText("toast.store_device_downloaded_settings",{p:c,c:p,ph:b,s:_},`${c} program(s), ${p} recording(s), ${b} phase map(s), ${_} setting(s) added`):b?v=this._tText("toast.store_device_downloaded_phases",{p:c,c:p,ph:b},`${c} program(s), ${p} recording(s), ${b} phase map(s) added`):v=this._tText("toast.store_device_downloaded",{p:c,c:p},`${c} program(s), ${p} recording(s) added`);const T=l&&l.partial&&l.failed_profiles||0,C=T?this._tText("toast.store_download_partial",{n:T},`${T} program(s) could not be downloaded; try again later.`):"";this._showToast([v,u,C].filter(Boolean).join(". "),T?"info":void 0)}catch(n){this._showToast(this._tText("toast.store_download_failed",{error:n.message||n},"Download failed: "+(n.message||n)),"error")}})}else if(t==="store-import"){const i=e.dataset.cycleId,o=this._storeProfile&&this._storeProfile.program||"";this._modal={type:"store-import",cycleId:i,program:o,mode:"new"},this._render()}else if(t==="store-share-cycle"){const i=e.dataset.cid,o=e.dataset.prof||"";this._modal={type:"store-share",cycleId:i,program:o,profiles:null,deviceId:null},this._render(),this._loadShareProfiles()}else if(t==="store-share-device")this._modal={type:"store-share-device",selected:new Set,includePhases:new Set,includeSettings:!1,loading:!0},this._render(),(async()=>{try{const o=await this._ws({type:`${w}/get_shareable_cycles`,entry_id:a});this._shareableCycles=o&&o.items||[],this._sharePhasePrograms=o&&o.phase_programs||[],this._shareAllPrograms=o&&o.all_programs||[]}catch{this._shareableCycles=[],this._sharePhasePrograms=[],this._shareAllPrograms=[]}if(!this._isActiveEntry(a)||!this._modal||this._modal.type!=="store-share-device")return;const i=new Set;this._shareableByProgram().forEach(o=>o.cycles.forEach(n=>i.add(n.id))),this._modal.selected=i,this._modal.includePhases=new Set(this._sharePhasePrograms),this._modal.loading=!1,this._render()})();else if(t==="store-share-profile"){const i=e.dataset.prog||"";this._modal={type:"store-share-device",selected:new Set,includePhases:new Set,includeSettings:!1,loading:!0,focusProfile:i},this._render(),(async()=>{try{const n=await this._ws({type:`${w}/get_shareable_cycles`,entry_id:a});this._shareableCycles=n&&n.items||[],this._sharePhasePrograms=n&&n.phase_programs||[],this._shareAllPrograms=n&&n.all_programs||[]}catch{this._shareableCycles=[],this._sharePhasePrograms=[],this._shareAllPrograms=[]}if(!this._isActiveEntry(a)||!this._modal||this._modal.type!=="store-share-device")return;const o=new Set;this._shareableByProgram().filter(n=>n.program===i).forEach(n=>n.cycles.forEach(l=>o.add(l.id))),this._modal.selected=o,this._modal.includePhases=new Set((this._sharePhasePrograms||[]).filter(n=>n===i)),this._modal.loading=!1,this._render()})()}else if(t==="store-share-add-profile"){const i=this._modal,o=this._constants.storeWebOrigin;if(!o||!i){o||this._showToast(this._t("toast.store_unavailable",{},"The community store is not available."),"error");return}this._ensureStoreConnectListener();const n=new URLSearchParams({mode:"profile",device:i.deviceId||"",type:this._storeApplianceType(),brand:this._opts.store_brand||"",model:this._opts.store_model||"",origin:location.origin}).toString();window.open(o+"/create.html?"+n,"washdata_create","width=560,height=760")}}_onActAuto(t,e,s,a,r){if(t==="auto-new")this._navigate("/config/automation/edit/new");else if(t==="auto-new-started")this._newAutomationFromEvent("started");else if(t==="auto-new-finished")this._newAutomationFromEvent("finished");else if(t==="auto-delete"){const i=e.dataset.autoid,o=e.dataset.autoname||"this automation";this._modal={type:"confirm",title:this._t("modal.delete_automation_title",{},"Delete Automation"),message:this._tText("modal.delete_automation_msg",{name:o},`Delete the automation "${o}" from Home Assistant? This cannot be undone.`),okLabel:this._t("btn.delete",{},"Delete"),onOk:async()=>{try{await this._hass.callApi("DELETE","config/automation/config/"+i),this._showToast(this._t("toast.automation_deleted",{},"Automation deleted")),await this._loadDeviceAutomations(a)}catch(n){this._showToast(this._tText("toast.delete_failed",{error:n.message||n},"Delete failed: "+(n.message||n)),"error")}}},this._render()}else t==="auto-convert-legacy"?this._convertLegacyActions():t==="auto-remove-legacy"&&(this._modal={type:"confirm",title:this._t("modal.remove_legacy_title",{},"Remove Legacy Actions"),message:this._t("modal.remove_legacy_msg",{},"Remove the legacy custom actions? They will stop firing on cycle events. This cannot be undone from the panel."),okLabel:this._t("btn.remove",{},"Remove"),onOk:async()=>{try{await this._ws({type:`${w}/set_options`,entry_id:a,options:{notify_actions:[]}}),this._opts={...this._opts,notify_actions:[]},this._showToast(this._t("toast.legacy_removed",{},"Legacy actions removed"))}catch(i){this._showToast(this._tText("toast.remove_failed",{error:i.message||i},"Remove failed: "+(i.message||i)),"error")}}},this._render())}_onActMaintenance(t,e,s,a,r){if(t==="maint-add"){const i=r.getElementById("wd-maint-type")?.value||"",o=r.getElementById("wd-maint-date")?.value||"",n=(r.getElementById("wd-maint-notes")?.value||"").trim();if(!i){this._showToast(this._tText("toast.maint_add_failed",{error:this._t("lbl.event_type",{},"Event type")},"Could not add event: Event type"),"error");return}this._busyRun("maint-add",async()=>{try{const l={type:`${w}/add_maintenance_event`,entry_id:a,event_type:i};o&&(l.date=o),n&&(l.notes=n),await this._ws(l),await this._fetchMaintenance(a),this._showToast(this._t("toast.maint_added",{},"Maintenance event added")),this._render()}catch(l){this._showToast(this._tText("toast.maint_add_failed",{error:l.message||l},"Could not add event: "+(l.message||l)),"error")}})}else if(t==="maint-delete"){const i=e.dataset.mid;this._modal={type:"confirm",title:this._t("modal.delete_maintenance_title",{},"Delete Maintenance Event"),message:this._t("modal.delete_maintenance_msg",{},"Delete this maintenance record? This cannot be undone."),okLabel:this._t("btn.delete",{},"Delete"),onOk:()=>this._busyRun("maint-delete",async()=>{try{await this._ws({type:`${w}/delete_maintenance_event`,entry_id:a,event_id:i}),await this._fetchMaintenance(a),this._showToast(this._t("toast.maint_deleted",{},"Maintenance event deleted"))}catch(o){this._showToast(this._tText("toast.maint_delete_failed",{error:o.message||o},"Could not delete event: "+(o.message||o)),"error")}})},this._render()}else if(t==="maint-save-odometer"){const i=r.getElementById("wd-maint-odometer")?.value,o=parseInt(i,10);if(isNaN(o)||o<0){this._showToast(this._tText("toast.odometer_save_failed",{error:this._t("lbl.total_cycles_run",{},"cycles run in total")},"Could not save total: cycles run in total"),"error");return}this._busyRun("maint-save-odometer",async()=>{try{await this._ws({type:`${w}/set_lifetime_cycle_count`,entry_id:a,count:o}),await this._fetchMaintenance(a),this._showToast(this._t("toast.odometer_saved",{},"Total cycles saved")),this._render()}catch(n){this._showToast(this._tText("toast.odometer_save_failed",{error:n.message||n},"Could not save total: "+(n.message||n)),"error")}})}else if(t==="maint-save-reminders"){const i=this._maintenance||{},o=i.reminders||{},n={};r.querySelectorAll("[data-maint-rem]").forEach(_=>{const f=_.dataset.maintRem,u=parseInt(_.value,10);n[f]=!isNaN(u)&&u>0?u:0});const l=Object.keys(n).some(_=>n[_]!==(parseInt(o[_],10)||0)),c=[];for(const _ of Array.isArray(i.custom_tasks)?i.custom_tasks:[]){const f=r.querySelector(`[data-mtask-name="${CSS.escape(_.id)}"]`);if(!f)continue;const u=f.value.trim();if(!u){this._showToast(this._tText("toast.task_name_required",{},"Give the task a name"),"error");return}const b=this._maintInterval(r.querySelector(`[data-mtask-cycles="${CSS.escape(_.id)}"]`)),v=this._maintInterval(r.querySelector(`[data-mtask-days="${CSS.escape(_.id)}"]`)),T={};u!==_.name&&(T.name=u),b!==(_.cycles||0)&&(T.cycles=b),v!==(_.days||0)&&(T.days=v),Object.keys(T).length&&c.push({task_id:_.id,...T})}const p=this._maintNewTaskInput(r);this._busyRun("maint-save-reminders",async()=>{try{l&&await this._ws({type:`${w}/set_options`,entry_id:a,options:{maintenance_reminder_cycles:n}});for(const _ of c)await this._ws({type:`${w}/update_maintenance_task`,entry_id:a,..._});p.name&&await this._maintAddTask(a,p),await this._fetchMaintenance(a),this._showToast(this._t("toast.reminders_saved",{},"Service reminders saved")),this._render()}catch(_){await this._fetchMaintenance(a).catch(()=>{}),this._render(),this._showToast(this._tText("toast.reminders_save_failed",{error:_.message||_},"Could not save reminders: "+(_.message||_)),"error")}})}else if(t==="maint-task-add"){const i=this._maintNewTaskInput(r);if(!i.name){this._showToast(this._tText("toast.task_name_required",{},"Give the task a name"),"error");return}this._busyRun("maint-task-add",async()=>{try{await this._maintAddTask(a,i),await this._fetchMaintenance(a),this._showToast(this._t("toast.task_added",{},"Task added")),this._render()}catch(o){this._showToast(this._tText("toast.task_save_failed",{error:o.message||o},"Could not save task: "+(o.message||o)),"error")}})}else if(t==="maint-task-remove"){const i=e.dataset.mtask,o=(this._maintenance&&this._maintenance.custom_tasks||[]).find(l=>l.id===i),n=o?o.name:i;this._modal={type:"confirm",title:this._t("modal.remove_task_title",{},"Remove Task"),message:this._tText("modal.remove_task_msg",{task:n},'Remove the reminder "{task}"? Its entries in the maintenance log are kept.'),okLabel:this._t("btn.remove",{},"Remove"),onOk:()=>this._busyRun("maint-task-remove",async()=>{try{await this._ws({type:`${w}/delete_maintenance_task`,entry_id:a,task_id:i}),await this._fetchMaintenance(a),this._showToast(this._t("toast.task_removed",{},"Task removed"))}catch(l){this._showToast(this._tText("toast.task_save_failed",{error:l.message||l},"Could not save task: "+(l.message||l)),"error")}})},this._render()}else if(t==="maint-log-done"){const i=e.dataset.mtype;if(!i)return;const o=this._maintLabel(i);this._busyRun("maint-log-done",async()=>{try{await this._ws({type:`${w}/add_maintenance_event`,entry_id:a,event_type:i}),await this._fetchMaintenance(a),this._showToast(this._tText("toast.maint_logged",{task:o},"Logged as done: {task}")),this._render()}catch(n){this._showToast(this._tText("toast.maint_add_failed",{error:n.message||n},"Could not add event: "+(n.message||n)),"error")}})}}_maintInterval(t){const e=String(t&&t.value||"").trim();if(!e)return 0;const s=Number(e);return isNaN(s)?e:s}_maintNewTaskInput(t){return{name:(t.getElementById("wd-mtask-new-name")?.value||"").trim(),cycles:this._maintInterval(t.getElementById("wd-mtask-new-cycles")),days:this._maintInterval(t.getElementById("wd-mtask-new-days"))}}async _maintAddTask(t,e){const s={type:`${w}/add_maintenance_task`,entry_id:t,name:e.name};return e.cycles&&(s.cycles=e.cycles),e.days&&(s.days=e.days),this._ws(s)}_onActPlayground(t,e,s,a){if(t==="pg-analysis-tab"){const r=e.dataset.subtab||"history";r!==this._pgAnalysisTab&&(this._pgAnalysisTab=r,this._render(),requestAnimationFrame(()=>this._drawPlaygroundCanvases()))}else if(t==="pg-run-history")this._pgRunHistory();else if(t==="pg-batch-cancel"){this._pgBatchCancel=!0;const r=this._pgHistoryTaskId||this._pgSweepTaskId;r&&this._ws({type:`${w}/cancel_task`,task_id:r}).catch(()=>{})}else if(t==="pg-load-run"){const r=e.dataset.taskId;if(!r)return;const i=this._tasks[r];if(!i||i.kind!=="pg_history"&&i.kind!=="pg_sweep"){this._showToast(this._t("toast.pg_run_gone",{},"That run is no longer available."),"info");return}const o=i.entry_id;this._ws({type:`${w}/get_task_result`,task_id:r}).then(n=>{if(!this._isActiveEntry(o))return;const l=n&&n.result;if(!l){this._showToast(this._t("toast.pg_run_gone",{},"That run is no longer available."),"info");return}i.kind==="pg_history"?(this._pgHistory=l,this._pgAnalysisTab="history"):(this._pgSweepNew=l.error?null:l,this._pgAnalysisTab="sweep"),this._render()}).catch(()=>this._showToast(this._t("toast.pg_run_gone",{},"That run is no longer available."),"info"))}else t==="pg-open-cycle"?this._pgSelectCycle(e.dataset.cid):t==="pg-sweep-run2"?this._pgRunSweep2():t==="pg-sweep-apply2"?this._pgApplySweepValue(e.dataset.val):t==="pg-run"||t==="pg-load"?this._pgLoad():t==="pg-rerun"?this._pgRerunDetail():t==="pg-cancel-run"?this._pgCancelRun():t==="pg-reset-params"?(this._pgThreshStart=null,this._pgThreshStop=null,this._pgParamOverrides={},this._render(),requestAnimationFrame(()=>this._pgDrawCanvas())):t==="pg-apply-settings"?this._pgApplyToSettings():t==="pg-load-live"?this._pgLoadLive():t==="pg-load-suggested"?this._pgLoadSuggested():t==="pg-preset-save"?this._pgSavePreset():t==="pg-preset-load"?this._pgLoadPreset():t==="pg-preset-delete"?this._pgDeletePreset():t==="pg-publish-one"&&this._pgPublishOne(e.dataset.pgkey)}async _onModalAction(t,e){const s=this.shadowRoot,a=this._devices[this._selIdx],r=a?a.entry_id:null,i=this._modal;if(t==="cancel"){if(i&&i.type==="cycle-detail"&&this._prevModal){const o=this._devices[this._selIdx];o?await this._closeCycleDetail(o.entry_id):(this._modal=null,this._render())}else this._modal=null,this._render();return}if(t==="ok"&&i&&i.onOk){const o=i.onOk;this._modal=null,this._render(),await o(),this._render();return}if(i&&i.type==="profile-group"){if(t==="pg-save"){const o=s.getElementById("wd-pg-name")?.value?.trim(),n=Array.from(s.querySelectorAll(".wd-pg-mem")).filter(l=>l.checked).map(l=>l.value);if(!o){this._showToast(this._t("toast.group_name_required",{},"Group name is required"),"error");return}if(n.length<2){this._showToast(this._t("toast.min_2_profiles",{},"Select at least 2 profiles for a group"),"error");return}await this._busyRun("pg-save",async()=>{try{i.orig&&i.orig!==o&&await this._ws({type:`${w}/rename_profile_group`,entry_id:r,name:i.orig,new_name:o}),await this._ws({type:`${w}/save_profile_group`,entry_id:r,name:o,members:n}),this._showToast(this._t("toast.group_saved",{},"Group saved")),this._modal=null,await this._fetchProfileGroups(r)}catch(l){this._showToast(this._tText("msg.toast_save_failed",{error:l.message||l},"Save failed: "+(l.message||l)),"error")}});return}if(t==="pg-delete"&&i.orig){await this._busyRun("pg-save",async()=>{try{await this._ws({type:`${w}/delete_profile_group`,entry_id:r,name:i.orig}),this._showToast(this._t("toast.group_deleted",{},"Group deleted")),this._modal=null,await this._fetchProfileGroups(r)}catch(o){this._showToast(this._tText("msg.toast_delete_failed",{error:o.message||o},"Delete failed: "+(o.message||o)),"error")}});return}}if(t.startsWith("store-import-")||t==="store-share-ok"||t.startsWith("wiz-")||t.startsWith("imp-")||t==="import-back"||t==="import-analyze"||t==="import-apply-ok")return this._onMActImport(t,e,i,r,s);if(t.startsWith("hist-"))return this._onMActHistoryImport(t,e,i,r,s);if(t.startsWith("sd-")||t==="store-share-device-ok")return this._onMActStoreShare(t,e,i,r);if(i&&i.type==="export-select"&&t==="export-generate"&&r){const o=this._wizSelectionPayload(i);await this._busyRun("export-select",async()=>{try{const n=await this._ws({type:`${w}/export_config_selective`,entry_id:r,selection:o}),l=new Blob([n.json_data],{type:"application/json"}),c=URL.createObjectURL(l),p=document.createElement("a");p.href=c,p.download=`washdata_export_${r.slice(0,8)}.json`,document.body.appendChild(p),p.click(),document.body.removeChild(p),URL.revokeObjectURL(c),this._modal=null,this._showToast(this._t("toast.export_selective_done",{},"Export downloaded"))}catch(n){this._showToast(this._tText("toast.export_failed",{error:n.message||n},"Export failed: "+(n.message||n)),"error")}});return}if(t.startsWith("cyc-")||t.startsWith("trim-mode-"))return this._onMActCycleDetail(t,i,r,s);if(i&&i.type==="profile-panel"){if(t.indexOf("pp-tab-")===0){const o=t.slice(7);i.tab=o,this._render(),o==="cleanup"&&!i.cleanup&&this._ws({type:`${w}/get_profile_cycles`,entry_id:r,profile_name:i.name}).then(n=>{this._modal&&this._modal.name===i.name&&(this._modal.cleanup={cycles:n.cycles||[],selected:new Set},this._render())}).catch(()=>{this._modal&&(this._modal.cleanup={cycles:[],selected:new Set},this._render())});return}return this._onMActProfilePanel(t,e,i,r,s)}if(t==="label-ok"&&r){const o=s.getElementById("wd-label-profile"),n=o?o.value:"",l=n||null,c=n==="__create_new__"&&s.getElementById("wd-new-profile-name")?.value?.trim()||null;this._modal=null;try{await this._ws({type:`${w}/label_cycle`,entry_id:r,cycle_id:i.cycleId,profile_name:l||null,new_profile_name:c}),this._showToast(this._t("toast.cycle_labelled",{},"Cycle labelled")),await this._fetchCycles(r),await this._fetchProfiles(r),await this._fetchFeedbacks(r)}catch(p){this._showToast(this._tText("toast.label_failed",{error:p.message||p},"Label failed: "+(p.message||p)),"error")}this._render()}else if(t==="create-profile-ok"&&r){const o=s.getElementById("wd-cp-name")?.value?.trim(),n=s.getElementById("wd-cp-cycle")?.value||null,l=parseFloat(s.getElementById("wd-cp-dur")?.value||0);if(this._modal=null,!o){this._showToast(this._t("toast.profile_name_required",{},"Profile name is required"),"error"),this._render();return}const c=!n&&l>0?l:null;try{await this._ws({type:`${w}/create_profile`,entry_id:r,name:o,reference_cycle:n||null,manual_duration_min:c}),this._showToast(this._tText("toast.profile_created",{name:o},`Profile "${o}" created`)),await this._fetchProfiles(r)}catch(p){this._showToast(this._tText("toast.create_failed",{error:p.message||p},"Create failed: "+(p.message||p)),"error")}this._render()}else if(t==="create-phase-ok"&&r){const o=s.getElementById("wd-ph-name")?.value?.trim(),n=s.getElementById("wd-ph-desc")?.value?.trim()||"";if(this._modal=null,!o){this._showToast(this._t("toast.phase_name_required",{},"Phase name is required"),"error"),this._render();return}try{await this._ws({type:`${w}/create_phase`,entry_id:r,device_type:i.deviceType||"",name:o,description:n}),this._showToast(this._tText("toast.phase_created",{name:o},`Phase "${o}" created`)),await this._fetchPhases(r)}catch(l){this._showToast(this._tText("msg.toast_create_failed",{error:l.message||l},"Create failed: "+(l.message||l)),"error")}this._render()}else if(t==="edit-phase-ok"&&r){const o=s.getElementById("wd-eph-name")?.value?.trim(),n=s.getElementById("wd-eph-desc")?.value?.trim()||"";if(this._modal=null,!o){this._showToast(this._t("toast.name_required",{},"Name required"),"error"),this._render();return}try{await this._ws({type:`${w}/update_phase`,entry_id:r,phase_id:i.phaseId,new_name:o,description:n}),this._showToast(this._t("toast.phase_updated",{},"Phase updated")),await this._fetchPhases(r)}catch(l){this._showToast(this._tText("toast.update_failed",{error:l.message||l},"Update failed: "+(l.message||l)),"error")}this._render()}else if(t==="process-rec-ok"&&r){const o=s.getElementById("wd-pr-mode")?.value;let n=s.getElementById("wd-pr-profile")?.value?.trim();o==="existing_profile"&&(n=s.getElementById("wd-pr-profile-sel")?.value||n);const l=parseFloat(s.getElementById("wd-pr-head")?.value||0),c=parseFloat(s.getElementById("wd-pr-tail")?.value||0);if(this._modal=null,!n){this._showToast(this._t("msg.toast_profile_name_required",{},"Profile name is required"),"error"),this._render();return}try{await this._ws({type:`${w}/process_recording`,entry_id:r,profile_name:n,save_mode:o,head_trim:l,tail_trim:c}),this._showToast(this._t("toast.recording_saved",{},"Recording saved to profile")),await this._fetchRecState(r),await this._fetchProfiles(r)}catch(p){this._showToast(this._tText("msg.toast_save_failed",{error:p.message||p},"Save failed: "+(p.message||p)),"error")}this._render()}else if(t==="correct-fb-ok"&&r){const o=s.getElementById("wd-fb-profile")?.value,n=parseFloat(s.getElementById("wd-fb-dur")?.value||0)||null;this._modal=null;try{await this._ws({type:`${w}/resolve_feedback`,entry_id:r,cycle_id:i.cycleId,action:"correct",corrected_profile:o,corrected_duration_min:n}),this._showToast(this._t("toast.correction_submitted",{},"Correction submitted")),await this._fetchFeedbacks(r)}catch(l){this._showToast(this._tText("msg.toast_error",{error:l.message||l},"Error: "+(l.message||l)),"error")}this._render()}else if(t==="auto-run"&&r){const o=parseFloat(s.getElementById("wd-al-thr")?.value||""),n={type:`${w}/auto_label_cycles`,entry_id:r};Number.isFinite(o)&&(n.confidence_threshold=Math.min(.95,Math.max(.5,o))),this._modal=null,this._render(),await this._busyRun("auto-label",async()=>{try{const l=await this._ws(n),c=l&&l.task_id?await this._awaitTask(l.task_id):null;this._showToast(this._autoLabelToast(c)),await this._fetchCycles(r)}catch(l){this._showToast(this._tText("msg.toast_auto_label_failed",{error:l.message||l},"Auto-label failed: "+(l.message||l)),"error")}})}else if(t==="merge-ok"&&r){const o=s.getElementById("wd-merge-prof")?.value||"",n=s.getElementById("wd-merge-newname")?.value?.trim()||null,l=i.ids||[];this._modal=null,this._render(),this._kickAndTrack({type:`${w}/apply_merge`,entry_id:r,cycle_ids:l,target_profile:o||null,new_profile_name:n},"cyc-merge",async()=>{this._showToast(this._t("toast.cycles_merged",{},"Cycles merged")),this._cycleSel.clear(),this._selectMode=!1,await this._fetchCycles(r),await this._fetchProfiles(r)})}else if(t==="bulk-relabel-ok"&&r){const o=s.getElementById("wd-relabel-profile")?.value||"",n=(i.ids||[]).slice();if(this._modal=null,this._render(),!n.length)return;await this._busyRun("cyc-relabel",async()=>{try{for(const l of n)await this._ws({type:`${w}/label_cycle`,entry_id:r,cycle_id:l,profile_name:o||null});this._showToast(this._tText("toast.relabel_done",{count:n.length},`Relabelled ${n.length} cycle(s)`)),this._cycleSel.clear(),this._selectMode=!1,await this._fetchCycles(r),await this._fetchProfiles(r),await this._fetchFeedbacks(r)}catch(l){this._showToast(this._tText("toast.relabel_failed",{error:l.message||l},"Relabel failed: "+(l.message||l)),"error")}})}}async _histUpload(t,e){const s=await this._ws({type:`${w}/history_import_begin`,entry_id:t}),a=Math.max(4096,s.chunk_bytes||512*1024);let r=0,i=0;for(;ii&&(o=n+1)}await this._ws({type:`${w}/history_import_chunk`,entry_id:t,token:s.token,seq:r,text:e.slice(i,o)}),r+=1,i=o}return s.token}async _histStartScan(t,e,s){e.token=s,e.step="scan",e.error=null,this._render();const a=await this._ws({type:`${w}/start_history_import_scan`,entry_id:t,token:s});e.scanTaskId=a.task_id,this._addProvisionalTask(a.task_id,"history_import",t,0),this._tasksSubscribed||this._pollTaskGeneric(a.task_id),this._histAdopt(a.task_id)}async _histAdopt(t){if(!t)return;let e=(this._tasks||{})[t];if(!e||e.state==="running")try{e=await this._ws({type:`${w}/get_task_result`,task_id:t})}catch{return}e&&e.state&&e.state!=="running"&&this._histTaskFinished(e)}async _histTaskFinished(t){const e=this._modal;if(!(!e||e.type!=="history-import")&&!(t.id!==e.scanTaskId&&t.id!==e.applyTaskId)&&(this._histSettled=this._histSettled||new Set,!this._histSettled.has(t.id))){if(this._histSettled.add(t.id),t.id===e.scanTaskId){if(t.state==="error"){e.step="input",e.error=t.error||this._t("msg.hist_scan_failed",{},"Scanning failed."),this._render();return}try{const s=t.result?t:await this._ws({type:`${w}/get_task_result`,task_id:t.id});e.result=s.result||{},e.accept=new Set((e.result.segments||[]).filter(a=>a.accept).map(a=>a.index)),e.step="review",this._render(),requestAnimationFrame(()=>this._drawHistorySparklines())}catch(s){e.step="input",e.error=String(s&&s.message||s),this._render()}return}if(t.id===e.applyTaskId){if(t.state==="error"){e.error=t.error==="scan_expired"?this._t("msg.hist_scan_expired",{},"That scan is no longer available. Scan again."):t.error||this._t("msg.hist_import_failed",{},"Import failed."),e.step="review",this._render();return}try{const a=t.result?t:await this._ws({type:`${w}/get_task_result`,task_id:t.id});e.done=a.result||{}}catch{e.done={}}e.step="done",this._render();const s=this._devices[this._selIdx];s&&(this._fetchCycles(s.entry_id),this._fetchProfiles(s.entry_id))}}}async _onMActHistoryImport(t,e,s,a,r){if(!(!s||s.type!=="history-import"||!a)){if(t==="hist-scan"){const i=r.getElementById("wd-hist-csv"),o=i?i.value:s.csvText||"";if(!o.trim()){this._showToast(this._t("toast.hist_csv_required",{},"Load a CSV file or paste its contents first"),"error");return}s.csvText=o,await this._busyRun("hist-import",async()=>{try{const n=await this._histUpload(a,o);await this._histStartScan(a,s,n)}catch(n){s.error=String(n&&n.message||n),this._render()}});return}if(t==="hist-recorder"){const i=r.getElementById("wd-hist-since"),o=i&&i.value||Ut();s.since=o,await this._busyRun("hist-import",async()=>{try{const n=await this._ws({type:`${w}/history_import_recorder`,entry_id:a,start_date:o});if(!n.rows){s.error=this._t("msg.hist_recorder_empty",{},"Home Assistant has no detailed history for this sensor in that window."),this._render();return}await this._histStartScan(a,s,n.token)}catch(n){s.error=String(n&&n.message||n),this._render()}});return}if(t==="hist-cancel-scan"){if(s.scanTaskId)try{await this._ws({type:`${w}/cancel_task`,task_id:s.scanTaskId})}catch{}s.step="input",s.scanTaskId=null,this._render();return}if(t==="hist-back"){s.step="input",s.result=null,s.error=null,this._render();return}if(t==="hist-toggle-all"){const i=s.result&&s.result.segments||[],o=i.every(n=>s.accept.has(n.index));s.accept=o?new Set:new Set(i.map(n=>n.index)),this._render(),requestAnimationFrame(()=>this._drawHistorySparklines());return}if(t==="hist-apply"){if(!s.accept||!s.accept.size)return;await this._busyRun("hist-apply",async()=>{try{const i=await this._ws({type:`${w}/apply_history_import`,entry_id:a,scan_task_id:s.scanTaskId,accept:[...s.accept].sort((o,n)=>o-n)});s.applyTaskId=i.task_id,this._addProvisionalTask(i.task_id,"history_import_apply",a,s.accept.size),this._tasksSubscribed||this._pollTaskGeneric(i.task_id),this._histAdopt(i.task_id)}catch(i){s.error=String(i&&i.message||i),this._render()}});return}if(t==="hist-goto-cycles"){this._modal=null,this._tab="history",this._historySub="cycles",this._cycleFilter={...this._cycleFilter||{},status:"imported"},this._fetchTabData();return}}}async _onMActImport(t,e,s,a,r){if(s&&s.type==="store-import"){if(t==="store-import-mode-new"){s.mode="new",this._render();return}if(t==="store-import-mode-merge"){s.mode="merge",this._render();return}if(t==="store-import-ok"){const i={type:`${w}/store_import_cycle`,entry_id:a,cycle_id:s.cycleId};if(s.mode==="merge"){const o=r.getElementById("wd-store-import-target")?.value||"";if(!o){this._showToast(this._t("toast.store_pick_profile",{},"Pick a profile to merge into"),"error");return}i.target_profile=o}else{const o=(r.getElementById("wd-store-import-name")?.value||"").trim()||s.program;if(!o){this._showToast(this._t("toast.store_name_required",{},"Enter a profile name"),"error");return}i.new_profile_name=o}await this._busyRun("store-import",async()=>{try{const o=await this._ws(i);if(o&&o.error==="low_quality"){this._showToast(this._t("toast.store_import_low_quality",{},"This recording is too short, too gappy or implausible to import."),"error");return}if(o&&o.error==="duplicate"){this._showToast(this._t("toast.store_import_duplicate",{},"This recording is already on your device."),"info");return}if(o&&o.error){this._showToast(this._tText("toast.store_import_failed",{error:o.error},"Import failed: "+o.error),"error");return}this._modal=null,this._showToast(this._tText("toast.store_imported",{profile:o&&o.profile||""},`Imported into ${o&&o.profile||"profile"}`)),await this._fetchProfiles(a)}catch(o){this._showToast(this._tText("toast.store_import_failed",{error:o.message||o},"Import failed: "+(o.message||o)),"error")}});return}}if(s&&s.type==="store-share"&&t==="store-share-ok"){const i=(r.getElementById("wd-store-share-prog")?.value||"").trim(),o=(r.getElementById("wd-store-share-desc")?.value||"").trim();if(!i){this._showToast(this._t("toast.store_pick_profile_share",{},"Pick a profile to share into"),"error");return}await this._busyRun("store-share",async()=>{try{const n=await this._ws({type:`${w}/store_upload_cycle`,entry_id:a,local_cycle_id:s.cycleId,program:i,description:o});if(n&&n.error){if(n.error==="no_appliance_declared")this._showToast(this._t("toast.store_no_appliance",{},"Set your appliance brand and model in Settings first."),"error");else{const l=n.detail?`${n.error} - ${n.detail}`:n.error;this._showToast(this._tText("toast.store_share_failed",{error:l},"Share failed: "+l),"error")}return}this._modal=null,this._showToast(this._t("toast.store_shared",{},"Shared to the community store - pending review."))}catch(n){this._showToast(this._tText("toast.store_share_failed",{error:n.message||n},"Share failed: "+(n.message||n)),"error")}});return}if(s&&(s.type==="export-select"||s.type==="import-wizard")){const i=s.type==="export-select"?{categories:s.inventory||{}}:s.manifest||{categories:{}};if(t==="wiz-toggle-all"){const o=i.categories||{},n=s.type==="import-wizard";let l=!0;this._wizCatOrder().filter(c=>o[c]&&o[c].present).forEach(c=>{n&&o[c].importable===!1||this._wizCatState(s,c,i).state!=="all"&&(l=!1)}),s.sel=l?this._wizEmptySel():this._wizInitSel(i,n),this._render();return}if(t==="wiz-toggle-cat"){const o=e.dataset.cat,l=this._wizCatState(s,o,i).state!=="all";if(o==="profiles"){const c=i.categories.profiles&&i.categories.profiles.items||[];s.sel.profiles=new Set(l?c.map(p=>p.name):[])}else if(this._wizCycleCats().includes(o)){const c=this._wizIdSet(s.sel,o);c.clear(),l&&(i.categories[o]&&i.categories[o].groups||[]).forEach(p=>p.cycles.forEach(_=>{_.id!=null&&c.add(String(_.id))}))}else l?s.sel.cats.add(o):s.sel.cats.delete(o);this._render();return}if(t==="wiz-toggle-profile"){const o=e.dataset.name;s.sel.profiles.has(o)?s.sel.profiles.delete(o):s.sel.profiles.add(o),this._render();return}if(t==="wiz-toggle-cycgroup"){const o=e.dataset.cat,n=e.dataset.prof,l=this._wizIdSet(s.sel,o),c=this._wizGroupIds(i,o,n),p=c.length>0&&c.every(_=>l.has(_));c.forEach(_=>{p?l.delete(_):l.add(_)}),this._render();return}if(t==="wiz-toggle-cyc"){const o=e.dataset.cat,n=e.dataset.cid,l=this._wizIdSet(s.sel,o);l.has(n)?l.delete(n):l.add(n),this._render();return}if(t==="wiz-expand"){const o=e.dataset.key;s.expanded||(s.expanded=new Set),s.expanded.has(o)?s.expanded.delete(o):s.expanded.add(o),this._render();return}}if(s&&s.type==="import-wizard"){if(t==="import-back"){s.step="input",s.error=null,this._render();return}if(t==="imp-mode-merge"){s.mode="merge",this._render();return}if(t==="imp-mode-replace"){s.mode="replace",this._render();return}if(t==="imp-dest-reference"){s.cycleDest="reference",this._render();return}if(t==="imp-dest-real"){(s.manifest||{}).real_history_allowed!==!1&&(s.cycleDest="real_history",this._render());return}if(t==="imp-conflict"){s.conflicts[e.dataset.prof]=e.value;return}if(t==="import-analyze"&&a){const i=r.getElementById("wd-import-json"),o=i?i.value:s.jsonText||"";if(s.jsonText=o,!o.trim()){this._showToast(this._t("toast.json_required",{},"JSON data is required"),"error");return}s.step="analyze",s.error=null,this._render();try{const n=await this._ws({type:`${w}/analyze_import`,entry_id:a,json_data:o});if(!this._isActiveEntry(a)||!this._modal||this._modal.type!=="import-wizard")return;const l=n&&n.manifest||{};if(l.error){s.step="input",s.error=l.error,this._render();return}s.manifest=l,s.sel=this._wizInitSel(l,!0),s.conflicts={},(l.categories&&l.categories.profiles&&l.categories.profiles.items||[]).forEach(c=>{c.conflict&&(s.conflicts[c.name]="import_as_copy")}),s.step="select",this._render()}catch(n){if(!this._isActiveEntry(a)||!this._modal||this._modal.type!=="import-wizard")return;s.step="input",s.error=n&&n.message||String(n),this._render()}return}if(t==="import-apply-ok"&&a){const i=this._wizSelectionPayload(s);await this._busyRun("import-wizard",async()=>{try{const o=await this._ws({type:`${w}/import_config_selective`,entry_id:a,json_data:s.jsonText,selection:i,mode:s.mode,conflict_resolutions:s.conflicts,cycle_destination:s.cycleDest,apply_settings:!0}),n=o&&o.summary||{};this._modal=null;const l=(n.real_cycles_imported||0)+(n.reference_cycles_imported||0)+(n.backfill_cycles_imported||0);(s.mode==="replace"||Object.values(s.conflicts||{}).includes("overwrite"))&&n.restore_point_saved===!1?this._showToast(this._tText("toast.import_no_restore_point",{},"Imported, but no restore point could be saved, so this import cannot be undone"),"error"):this._showToast(this._tText("toast.import_selective_done",{profiles:n.profiles_imported||0,cycles:l},`Imported ${n.profiles_imported||0} profile(s) and ${l} cycle(s)`)),await this._fetchCycles(a),await this._fetchProfiles(a),n.restore_point_saved&&await this._fetchToolsData(a)}catch(o){this._showToast(this._tText("toast.import_failed",{error:o.message||o},"Import failed: "+(o.message||o)),"error")}});return}}}async _onMActStoreShare(t,e,s,a){if(s&&s.type==="store-share-device"){if(t==="sd-toggle-cyc"){const r=e.dataset.cid;s.selected.has(r)?s.selected.delete(r):s.selected.add(r),this._render();return}if(t==="sd-toggle-prof"){const r=e.dataset.prog,i=this._shareableByProgram().find(o=>o.program===r);if(i){const o=i.cycles.every(n=>s.selected.has(n.id));i.cycles.forEach(n=>{o?s.selected.delete(n.id):s.selected.add(n.id)})}this._render();return}if(t==="sd-toggle-phases"){const r=e.dataset.prog;s.includePhases||(s.includePhases=new Set),s.includePhases.has(r)?s.includePhases.delete(r):s.includePhases.add(r),this._render();return}if(t==="sd-toggle-settings"){s.includeSettings=!s.includeSettings,this._render();return}if(t==="sd-toggle-consent"){s.consented=!s.consented,this._render();return}if(t==="sd-toggle-guide"){s.guideOpen=!s.guideOpen,this._render();return}if(t==="store-share-device-ok"){const r=new Map;(this._shareableCycles||[]).forEach(l=>r.set(l.id,(l.profile_name||"").trim()));const i=Array.from(s.selected).map(l=>({local_cycle_id:l,program:r.get(l)||""})).filter(l=>l.program);if(!i.length){this._showToast(this._t("toast.share_device_none_sel",{},"Select at least one cycle to share"),"error");return}const o=new Set(i.map(l=>l.program)),n=Array.from(s.includePhases||[]).filter(l=>o.has(l));await this._busyRun("store-share-device",async()=>{try{const l=await this._ws({type:`${w}/store_upload_device`,entry_id:a,items:i,include_phases:n,include_settings:!!s.includeSettings});if(l&&l.error){if(l.error==="no_appliance_declared")this._showToast(this._t("toast.store_no_appliance",{},"Set your appliance brand and model in Settings first."),"error");else{const u=l.detail?`${l.error} - ${l.detail}`:l.error;this._showToast(this._tText("toast.store_share_failed",{error:u},"Share failed: "+u),"error")}return}const c=l&&l.cycle_ids&&l.cycle_ids.length||0,p=l&&l.errors&&l.errors.length||0,_=l&&l.duplicates||0,f=l&&l.created!=null?l.created:c;if(!c){const u=l&&l.errors&&l.errors[0]||l&&l.detail||"upload_failed";this._showToast(this._tText("toast.store_share_failed",{error:u},"Share failed: "+u),"error");return}this._modal=null,p?this._showToast(this._tText("toast.store_device_shared_partial",{n:c,failed:p},`Shared ${c} cycle(s); ${p} could not be uploaded.`),"info"):_&&!f?this._showToast(this._tText("toast.store_device_shared_all_dup",{n:_},`All ${_} cycle(s) were already in the community store.`),"info"):_?this._showToast(this._tText("toast.store_device_shared_some_dup",{created:f,dup:_},`Shared ${f} cycle(s); ${_} were already in the store.`)):this._showToast(this._tText("toast.store_device_shared",{n:f},`Shared ${f} cycle(s) to the community store - pending review.`))}catch(l){this._showToast(this._tText("toast.store_share_failed",{error:l.message||l},"Share failed: "+(l.message||l)),"error")}});return}}}async _onMActCycleDetail(t,e,s,a){if(e&&e.type==="cycle-detail"){if(t==="cyc-view"){e.mode="view",this._render();return}if(t==="cyc-trim"){e.mode="trim",(!e.trim||e.trim.end<=0)&&(e.trim={start:0,end:e.curve&&e.curve.full_duration_s||0}),this._render();return}if(t==="cyc-split"){e.mode="split",this._render();return}if(t==="cyc-review"){e.mode="review",this._render();return}if(t==="cyc-ctx"){const r=Number(a.getElementById("wd-cyc-ctx")?.value),i=e.entryId||s;if(!Ft.includes(r)||e.ctx&&e.ctx.minutes===r)return;const o=this._pref("cycle_context_min",null);this._setPref("cycle_context_min",{...o&&typeof o=="object"?o:{},[i]:r}),delete this._canvasZoom["wd-cyc-canvas"],this._loadCycleContext(i,e.cycleId,e.curve),this._render();return}if(t==="cyc-review-save"){const r=e.cycleId,i=a.getElementById("wd-cyc-rev-quality")?.value||"",o=!!a.getElementById("wd-cyc-rev-golden")?.checked,n=a.getElementById("wd-cyc-rev-notes")?.value||"",l=Array.from(a.querySelectorAll(".wd-cyc-rev-tag")).filter(_=>_.checked).map(_=>_.value),c=a.getElementById("wd-cyc-rev-label")?.value??"",p=e.curve&&e.curve.profile_name||"";await this._busyRun("cyc-review-save",async()=>{try{await this._ws({type:`${w}/set_ml_review`,entry_id:s,cycle_id:r,quality:i,golden:o,tags:l,notes:n}),c!==p&&await this._ws({type:`${w}/label_cycle`,entry_id:s,cycle_id:r,profile_name:c||null}),this._showToast(this._t("toast.review_saved",{},"Review saved")),await this._fetchCycles(s),c!==p&&await this._fetchFeedbacks(s),await this._loadMlIndex(s);const _=this._modal;_&&_.type==="cycle-detail"&&_.cycleId===r&&(_.ml=(this._mlById||{})[r]||_.ml,delete _.reviewLabel,c!==p&&_.curve&&(_.curve.profile_name=c||null,_.profileEnv=null,_.runnerUpEnv=null,this._loadCycleOverlays(s,r,_.curve)))}catch(_){this._showToast(this._tText("msg.toast_save_failed",{error:_.message||_},"Save failed: "+(_.message||_)),"error")}});return}if(t==="trim-mode-s"){e.timeMode="s",this._render();return}if(t==="trim-mode-clock"){e.timeMode="clock",this._render();return}if(t==="cyc-reset-trim"){e.trim={start:0,end:e.curve&&e.curve.full_duration_s||0},this._render();return}if(t==="cyc-clear-split"){e.split={offsets:[],profiles:[]},this._render();return}if(t==="cyc-label"){this._profiles.length||await this._fetchProfiles(s),this._modal={type:"label-cycle",cycleId:e.cycleId},this._render();return}if(t==="cyc-delete"){const r=e.cycleId;this._modal=null,this._render(),this._deleteCyclesWithUndo(s,[r]);return}if(t==="cyc-auto-split"){const r=parseInt(a.getElementById("wd-split-gap")?.value||"900",10);await this._busyRun("cyc-auto",async()=>{try{const i=await this._ws({type:`${w}/analyze_split`,entry_id:s,cycle_id:e.cycleId,gap_seconds:r});e.split.offsets=(i.split_offsets||[]).slice(),e.split.profiles=[],e.split.offsets.length||this._showToast(this._t("toast.no_split_found",{},"No idle gaps found to split on"),"info")}catch(i){this._showToast(this._tText("toast.auto_detect_failed",{error:i.message||i},"Auto-detect failed: "+(i.message||i)),"error")}});return}if(t==="cyc-apply-trim"){const r=e.cycleId,i=e.trim.start,o=e.trim.end,n=e.curve&&e.curve.full_duration_s||0,l=n>0?Math.max(0,Math.round((o-i)/n*100)):100;if(l<50&&!confirm(this._tText("msg.trim_destructive_confirm",{pct:l},`This keeps only ${l}% of the cycle and cannot be undone. Continue?`)))return;this._kickAndTrack({type:`${w}/trim_cycle`,entry_id:s,cycle_id:r,start_s:i,end_s:o},"cyc-trim-apply",async()=>{this._showToast(this._t("toast.cycle_trimmed",{},"Cycle trimmed")),await this._closeCycleDetail(s),await this._fetchCycles(s)});return}if(t==="cyc-apply-split"){const r=e.cycleId,i=e.split.offsets.slice(),o=e.split.profiles.slice();this._kickAndTrack({type:`${w}/apply_split`,entry_id:s,cycle_id:r,split_offsets:i,segment_profiles:o},"cyc-split-apply",async n=>{this._showToast(this._tText("toast.split_complete",{count:(n.new_ids||[]).length},`Split into ${(n.new_ids||[]).length} cycles`)),await this._closeCycleDetail(s),await this._fetchCycles(s),await this._fetchProfiles(s)});return}}}async _onMActProfilePanel(t,e,s,a,r){if(s&&s.type==="profile-panel"){if(t==="pp-phase-add"){const i=s.env&&s.env.target_duration||(s.env&&s.env.avg&&s.env.avg.length?s.env.avg[s.env.avg.length-1][0]:600),o=s.phases.length?s.phases[s.phases.length-1].end:0,n=Math.min(o,i);s.phases.push({name:s.catalog[0]||"",start:n,end:Math.min(n+Math.max(60,i*.1),i)}),this._render();return}if(t==="pp-phase-rm"){const i=+(e&&e.dataset.idx||-1);i>=0&&(s.phases.splice(i,1),this._render());return}if(t==="pp-phase-save"){const i=s.phases.filter(o=>o.name).map(o=>({name:o.name,start:o.start,end:o.end}));await this._busyRun("pp-phase-save",async()=>{try{await this._ws({type:`${w}/set_profile_phases`,entry_id:a,profile_name:s.name,phases:i}),this._showToast(this._t("toast.phases_saved",{},"Phases saved"))}catch(o){this._showToast(this._tText("msg.toast_save_failed",{error:o.message||o},"Save failed: "+(o.message||o)),"error")}});return}if(t==="pp-cleanup-del"){const i=s.cleanup?Array.from(s.cleanup.selected):[];if(!i.length)return;await this._busyRun("pp-cleanup-del",async()=>{try{for(const n of i)await this._ws({type:`${w}/delete_cycle`,entry_id:a,cycle_id:n});this._showToast(this._tText("toast.cycles_deleted",{count:i.length},`Deleted ${i.length} cycle(s)`));const o=await this._ws({type:`${w}/get_profile_cycles`,entry_id:a,profile_name:s.name});this._modal&&(this._modal.cleanup={cycles:o.cycles||[],selected:new Set}),await this._fetchProfiles(a)}catch(o){this._showToast(this._tText("msg.toast_delete_failed",{error:o.message||o},"Delete failed: "+(o.message||o)),"error")}});return}if(t==="pp-cleanup-unlabel"){const i=s.cleanup?Array.from(s.cleanup.selected):[];if(!i.length)return;await this._busyRun("pp-cleanup-unlabel",async()=>{try{for(const n of i)await this._ws({type:`${w}/label_cycle`,entry_id:a,cycle_id:n,profile_name:null});this._showToast(this._tText("toast.unlabel_done",{count:i.length},`Unlabelled ${i.length} cycle(s)`));const o=await this._ws({type:`${w}/get_profile_cycles`,entry_id:a,profile_name:s.name});this._modal===s&&(s.cleanup={cycles:o.cycles||[],selected:new Set}),await this._fetchProfiles(a),await this._fetchCycles(a),await this._fetchFeedbacks(a)}catch(o){this._showToast(this._tText("toast.unlabel_failed",{error:o.message||o},"Unlabel failed: "+(o.message||o)),"error")}});return}if(t==="pp-rename"){const i=r.getElementById("wd-pp-rename")?.value?.trim(),o=r.getElementById("wd-pp-dur"),n=parseFloat(o?.value||"0"),l=!!o&&!o.disabled&&n>0&&n!==parseFloat(o.dataset.orig||"0");if(!i){this._showToast(this._t("msg.toast_name_required",{},"Name required"),"error");return}try{await this._ws({type:`${w}/rename_profile`,entry_id:a,profile_name:s.name,new_name:i,manual_duration_min:l?n:null}),this._showToast(this._t("toast.profile_renamed",{},"Profile renamed")),s.name=i,await Promise.all([this._fetchProfiles(a),this._fetchProfileGroups(a)]),s.stats=(this._profiles||[]).find(c=>c.name===i)||s.stats,this._render()}catch(c){this._showToast(this._tText("toast.rename_failed",{error:c.message||c},"Rename failed: "+(c.message||c)),"error")}return}if(t==="pp-rebuild"){this._kickAndTrack({type:`${w}/rebuild_envelopes`,entry_id:a},"pp-rebuild",async()=>{try{const i=await this._ws({type:`${w}/get_profile_envelope`,entry_id:a,profile_name:s.name});this._modal&&(this._modal.env=i.envelope)}catch{}this._showToast(this._t("toast.envelope_rebuilt",{},"Envelope rebuilt"))});return}if(t==="pp-delete"){this._deleteProfileWithUndo(a,s.name);return}}}_snapshotFormToPending(t){t&&t.querySelectorAll("#wd-settings-form [data-opt], #wd-ml-form [data-opt]").forEach(e=>{const s=e.dataset.opt;if(!this._dirtyOptKeys.has(s))return;const a=wt[s],r=a&&a.type||e.dataset.ftype||"text";if(e.type==="checkbox"){this._pendingSettings[s]=e.checked;return}if(r==="checkboxlist"){this._pendingSettings[s]=this._collectCheckboxlist(e,s);return}if(r==="entitylist"){this._pendingSettings[s]=Array.from(e.querySelectorAll(".wd-pill")).map(i=>i.dataset.val).filter(Boolean);return}if(r==="timerlist"){this._pendingSettings[s]=Array.from(e.querySelectorAll(".wd-timer-row")).map(i=>({offset_minutes:parseFloat(i.querySelector('[data-field="offset_minutes"]').value)||0,message:(i.querySelector('[data-field="message"]').value||"").trim(),auto_pause:i.querySelector('[data-field="auto_pause"]').checked})).filter(i=>i.offset_minutes>0);return}if(r==="number"){const i=String(e.value).trim();if(i===""){a&&a.clearable?this._pendingSettings[s]=null:delete this._pendingSettings[s];return}const o=parseFloat(i);isNaN(o)||(this._pendingSettings[s]=o);return}if(r==="list"){this._pendingSettings[s]=String(e.value).split(",").map(i=>i.trim()).filter(Boolean);return}if(r==="intlist"){this._pendingSettings[s]=me(e.value);return}if(r==="json"){const i=String(e.value).trim();if(!i){this._pendingSettings[s]=[];return}try{this._pendingSettings[s]=JSON.parse(i)}catch{}return}if(r==="entity"||r==="device"){const i=String(e.value).trim();this._pendingSettings[s]=i||null;return}this._pendingSettings[s]=e.value})}_collectCheckboxlist(t,e){const s=Array.from(t.querySelectorAll("[data-choice]")).filter(r=>r.checked).map(r=>r.dataset.choice);if(s.length)return s;const a=wt[e];return a&&Array.isArray(a.def)&&a.def.length?a.def.slice():s}_conflictKeysForOpts(t,e){const s=Object.assign({},e||{},t),a=this._conflictCtx(),r=new Set;for(const i of ee)if(i.check(s,a))for(const o of Object.keys(i.fieldErrors(s,a)))r.add(o);return r}_conflictCtx(){const t=this._hass&&this._hass.states?this._hass.states:{};return{stateOf:e=>e&&t[e]||null}}_conflictCountForOpts(t,e){return this._conflictKeysForOpts(t,e).size}_conflictKeysFromOpts(){return this._conflictKeysForOpts(this._editedOpts(),this._optDefaults)}_readSettingsFormValues(t){const e=Object.assign({},this._optDefaults,this._opts,this._pendingSettings);return t&&t.querySelectorAll("#wd-settings-form [data-opt]").forEach(s=>{const a=s.dataset.opt;if(s.type==="checkbox"){e[a]=s.checked;return}if(s.dataset.ftype==="checkboxlist"){e[a]=this._collectCheckboxlist(s,a);return}if(s.dataset.ftype==="entity"||s.dataset.ftype==="device"){e[a]=String(s.value).trim()||null;return}const r=parseFloat(s.value);isNaN(r)?s.value!==""&&(e[a]=s.value):e[a]=r}),e}_liveValidateSettings(t){if(!t)return{};const e=this._readSettingsFormValues(t),s=t.getElementById("wd-settings-form");if(!s)return{};const a={};for(const n of this._suggestions||[])n.key!=null&&n.suggested!=null&&(a[n.key]=+n.suggested);const r={},i=this._conflictCtx();for(const n of ee){if(!n.check(e,i))continue;const l=n.fieldErrors(e,i);for(const[c,p]of Object.entries(l)){const _=a[c],f=_!=null&&!n.check({...e,[c]:_},i)?{...p,suggFix:_}:p;(r[c]=r[c]||[]).push(f)}}const o={};for(const n of Ae)if(n.check(e,i))for(const[l,c]of Object.entries(n.fieldNotes(e,i)))(o[l]=o[l]||[]).push(c);return s.querySelectorAll("[data-cnote]").forEach(n=>{const l=o[n.dataset.cnote];if(!l||!l.length){n.hidden=!0,n.innerHTML="";return}n.hidden=!1,n.innerHTML=l.map(c=>d(this._tText(c.msgKey,c.msgVars,c.msgFb))).join("
    ")}),s.querySelectorAll("[data-cerr]").forEach(n=>{const l=n.dataset.cerr,c=r[l],p=s.querySelector(`.wd-field[data-field="${l}"]`);!c||!c.length?(n.hidden=!0,n.innerHTML="",p&&p.classList.remove("wd-has-conflict")):(n.hidden=!1,p&&p.classList.add("wd-has-conflict"),n.innerHTML=c.map(_=>{const f=this._tText(_.msgKey,_.msgVars,_.msgFb);let u="";if(_.suggFix!=null){const b=+_.suggFix.toFixed(2);u=`${this._t("conflict.suggestion_resolves",{val:b},`Stage the pending suggestion (${b}) below to fix this`)}`}else if(_.fixVal!=null&&!isNaN(+_.fixVal)){const b=Number.isInteger(_.fixVal)?_.fixVal:+_.fixVal.toFixed(2);u=``}return`
    \u26A0 ${d(f)}${u}
    `}).join(""))}),r}_cascadeConflictFix(t,e,s){const a=new Set;for(let r=0;r<10;r++){const i=this._liveValidateSettings(t);let o=!1;for(const[n,l]of Object.entries(i)){if(n===s)continue;const c=l.find(_=>_.fixVal!=null&&!isNaN(+_.fixVal)&&+_.fixVal>0);if(!c)continue;const p=e.querySelector(`[data-opt="${n}"]`);p?(p.value=c.fixVal,this._dirtyOptKeys.add(n)):(this._preCascadeOpts==null&&(this._preCascadeOpts=JSON.parse(JSON.stringify(this._opts||{}))),this._opts={...this._opts,[n]:c.fixVal},(this._cascadePending??={})[n]=c.fixVal),a.add(n),o=!0;break}if(!o)break}if(this._liveValidateSettings(t),this._snapshotFormToPending(t),a.size>0){const r=a.size,i=r>1?"s":"";this._showToast(this._tText("conflict.cascade_toast",{n:r},`Other settings adjusted for consistency: ${r}`),"success")}}_changedOptions(t){const e=this._preCascadeOpts||this._opts||{},s={};for(const[a,r]of Object.entries(t))if(JSON.stringify(r)!==JSON.stringify(e[a])&&!(!(a in e)&&(r===""||r===null))){if(!(a in e)&&typeof r=="boolean"){const i=this._optDefaults&&a in this._optDefaults?this._optDefaults[a]:(wt[a]||{}).def;if(i!==void 0&&JSON.stringify(r)===JSON.stringify(i))continue}s[a]=r}return s}async _saveSettings(){const t=this.shadowRoot,e=this._devices[this._selIdx];if(!e)return;const s=Object.assign({},this._pendingSettings,this._cascadePending);this._invalidJson=null,t.querySelectorAll("[data-opt]").forEach(n=>{const l=n.dataset.opt,c=wt[l],p=c&&c.type||n.dataset.ftype||"text";if(n.type==="checkbox"){s[l]=n.checked;return}if(p==="checkboxlist"){s[l]=this._collectCheckboxlist(n,l);return}if(p==="entitylist"){s[l]=Array.from(n.querySelectorAll(".wd-pill")).map(f=>f.dataset.val).filter(Boolean);return}if(p==="timerlist"){s[l]=Array.from(n.querySelectorAll(".wd-timer-row")).map(f=>({offset_minutes:parseFloat(f.querySelector('[data-field="offset_minutes"]').value)||0,message:(f.querySelector('[data-field="message"]').value||"").trim(),auto_pause:f.querySelector('[data-field="auto_pause"]').checked})).filter(f=>f.offset_minutes>0);return}const _=n.value;if(p==="number"){const f=String(_).trim();if(f===""){c&&c.clearable?s[l]=null:delete s[l];return}const u=parseFloat(f);isNaN(u)||(s[l]=u);return}if(p==="list"){s[l]=String(_).split(",").map(f=>f.trim()).filter(Boolean);return}if(p==="intlist"){s[l]=me(_);return}if(p==="json"){const f=String(_).trim();if(!f){s[l]=[];return}try{s[l]=JSON.parse(f)}catch{this._invalidJson=l}return}if(p==="entity"||p==="device"){const f=String(_).trim();s[l]=f||null;return}s[l]=_});const a=this._optDefaults||{};for(const n of Object.keys(a))!(n in this._opts)&&n in s&&JSON.stringify(s[n])===JSON.stringify(a[n])&&delete s[n];if(this._invalidJson){this._showToast(this._tText("toast.invalid_json",{key:this._invalidJson},`"${this._invalidJson}" is not valid JSON - fix it or clear the field before saving.`),"error");return}const r=this._liveValidateSettings(t),i=new Set(Object.keys(r));if(i.size>0){const n={},l={};for(const[p,_]of Object.entries(s))i.has(p)?l[p]=_:n[p]=_;const c=this._changedOptions(n);this._pendingSettings={...this._pendingSettings,...l},Object.keys(c).length?await this._busyRun("save-settings",async()=>{try{await this._ws({type:`${w}/set_options`,entry_id:e.entry_id,options:c}),this._opts={...this._opts,...c},this._showToast(this._t("toast.saved_except_conflicts",{},"Saved. Fix the highlighted conflicts to save the rest."),"info")}catch(p){this._showToast(this._tText("msg.toast_save_failed",{error:p.message||p},"Save failed: "+(p.message||p)),"error")}}):this._showToast(this._t("toast.settings_conflicts",{},"Fix the highlighted setting conflicts before saving."),"error");return}const o=this._changedOptions(s);if(!Object.keys(o).length){this._pendingSettings={},this._dirtyOptKeys=new Set,this._showToast(this._t("toast.settings_no_changes",{},"No changes to save"),"info");return}await this._busyRun("save-settings",async()=>{try{const n=JSON.parse(JSON.stringify(this._preCascadeOpts||this._opts||{}));if(await this._ws({type:`${w}/set_options`,entry_id:e.entry_id,options:o}),this._opts={...this._opts,...o},this._prevOpts=n,this._cascadePending={},this._preCascadeOpts=null,this._pendingSettings={},this._dirtyOptKeys=new Set,this._stagedSuggestions){try{await this._ws({type:`${w}/clear_suggestions`,entry_id:e.entry_id})}catch{}this._stagedSuggestions=!1,this._suggestions=[]}this._showToast(this._t("toast.settings_saved",{},"Settings saved; integration reloading"))}catch(n){this._showToast(this._tText("msg.toast_save_failed",{error:n.message||n},"Save failed: "+(n.message||n)),"error")}})}}customElements.get("ha-washdata-panel")||customElements.define("ha-washdata-panel",Ze);