@@ -1,199 +1,199 @@
/ * *
* Profiles — profile cards , editor , condition builder , process picker , scene selector .
* Automations — automation cards , editor , condition builder , process picker , scene selector .
* /
import { apiKey , _profile sCache , set _profile sCache , _profile sLoading , set _profile sLoading } from '../core/state.js' ;
import { apiKey , _automation sCache , set _automation sCache , _automation sLoading , set _automation sLoading } from '../core/state.js' ;
import { fetchWithAuth , escapeHtml } from '../core/api.js' ;
import { t } from '../core/i18n.js' ;
import { showToast , showConfirm , setTabRefreshing } from '../core/ui.js' ;
import { Modal } from '../core/modal.js' ;
import { CardSection } from '../core/card-sections.js' ;
import { updateTabBadge } from './tabs.js' ;
import { ICON _SETTINGS , ICON _START , ICON _PAUSE , ICON _CLOCK , ICON _PROFILE , ICON _HELP , ICON _OK , ICON _TIMER , ICON _MONITOR , ICON _RADIO , ICON _SCENE } from '../core/icons.js' ;
import { ICON _SETTINGS , ICON _START , ICON _PAUSE , ICON _CLOCK , ICON _AUTOMATION , ICON _HELP , ICON _OK , ICON _TIMER , ICON _MONITOR , ICON _RADIO , ICON _SCENE } from '../core/icons.js' ;
// ===== Scene presets cache (shared by both selectors) =====
let _scenesCache = [ ] ;
class Profile EditorModal extends Modal {
constructor ( ) { super ( 'profile -editor-modal' ) ; }
class Automation EditorModal extends Modal {
constructor ( ) { super ( 'automation -editor-modal' ) ; }
snapshotValues ( ) {
return {
name : document . getElementById ( 'profile -editor-name' ) . value ,
enabled : document . getElementById ( 'profile -editor-enabled' ) . checked . toString ( ) ,
logic : document . getElementById ( 'profile -editor-logic' ) . value ,
conditions : JSON . stringify ( getProfile EditorConditions ( ) ) ,
scenePresetId : document . getElementById ( 'profile -scene-id' ) . value ,
deactivationMode : document . getElementById ( 'profile -deactivation-mode' ) . value ,
deactivationScenePresetId : document . getElementById ( 'profile -fallback-scene-id' ) . value ,
name : document . getElementById ( 'automation -editor-name' ) . value ,
enabled : document . getElementById ( 'automation -editor-enabled' ) . checked . toString ( ) ,
logic : document . getElementById ( 'automation -editor-logic' ) . value ,
conditions : JSON . stringify ( getAutomation EditorConditions ( ) ) ,
scenePresetId : document . getElementById ( 'automation -scene-id' ) . value ,
deactivationMode : document . getElementById ( 'automation -deactivation-mode' ) . value ,
deactivationScenePresetId : document . getElementById ( 'automation -fallback-scene-id' ) . value ,
} ;
}
}
const profile Modal = new Profile EditorModal( ) ;
const csProfile s = new CardSection ( 'profile s' , { titleKey : 'profile s.title' , gridClass : 'devices-grid' , addCardOnclick : "openProfile Editor()" , keyAttr : 'data-profile -id' } ) ;
const automation Modal = new Automation EditorModal( ) ;
const csAutomation s = new CardSection ( 'automation s' , { titleKey : 'automation s.title' , gridClass : 'devices-grid' , addCardOnclick : "openAutomation Editor()" , keyAttr : 'data-automation -id' } ) ;
// Re-render profile s when language changes (only if tab is active)
// Re-render automation s when language changes (only if tab is active)
document . addEventListener ( 'languageChanged' , ( ) => {
if ( apiKey && ( localStorage . getItem ( 'activeTab' ) || 'dashboard' ) === 'profile s' ) loadProfile s ( ) ;
if ( apiKey && ( localStorage . getItem ( 'activeTab' ) || 'dashboard' ) === 'automation s' ) loadAutomation s ( ) ;
} ) ;
// React to real-time profile state changes from global events WS
document . addEventListener ( 'server:profile _state_changed' , ( ) => {
if ( apiKey && ( localStorage . getItem ( 'activeTab' ) || 'dashboard' ) === 'profile s' ) {
loadProfile s ( ) ;
// React to real-time automation state changes from global events WS
document . addEventListener ( 'server:automation _state_changed' , ( ) => {
if ( apiKey && ( localStorage . getItem ( 'activeTab' ) || 'dashboard' ) === 'automation s' ) {
loadAutomation s ( ) ;
}
} ) ;
export async function loadProfile s ( ) {
if ( _profile sLoading ) return ;
set _profile sLoading ( true ) ;
const container = document . getElementById ( 'profile s-content' ) ;
if ( ! container ) { set _profile sLoading ( false ) ; return ; }
setTabRefreshing ( 'profile s-content' , true ) ;
export async function loadAutomation s ( ) {
if ( _automation sLoading ) return ;
set _automation sLoading ( true ) ;
const container = document . getElementById ( 'automation s-content' ) ;
if ( ! container ) { set _automation sLoading ( false ) ; return ; }
setTabRefreshing ( 'automation s-content' , true ) ;
try {
const [ profile sResp, scenesResp ] = await Promise . all ( [
fetchWithAuth ( '/profile s' ) ,
const [ automation sResp, scenesResp ] = await Promise . all ( [
fetchWithAuth ( '/automation s' ) ,
fetchWithAuth ( '/scene-presets' ) ,
] ) ;
if ( ! profile sResp. ok ) throw new Error ( 'Failed to load profile s' ) ;
const data = await profile sResp. json ( ) ;
if ( ! automation sResp. ok ) throw new Error ( 'Failed to load automation s' ) ;
const data = await automation sResp. json ( ) ;
const scenesData = scenesResp . ok ? await scenesResp . json ( ) : { presets : [ ] } ;
_scenesCache = scenesData . presets || [ ] ;
// Build scene name map for card rendering
const sceneMap = new Map ( _scenesCache . map ( s => [ s . id , s ] ) ) ;
set _profile sCache ( data . profile s) ;
const activeCount = data . profile s. filter ( p => p . is _active ) . length ;
updateTabBadge ( 'profile s' , activeCount ) ;
renderProfile s ( data . profile s, sceneMap ) ;
set _automation sCache ( data . automation s) ;
const activeCount = data . automation s. filter ( a => a . is _active ) . length ;
updateTabBadge ( 'automation s' , activeCount ) ;
renderAutomation s ( data . automation s, sceneMap ) ;
} catch ( error ) {
if ( error . isAuth ) return ;
console . error ( 'Failed to load profile s:' , error ) ;
console . error ( 'Failed to load automation s:' , error ) ;
container . innerHTML = ` <p class="error-message"> ${ error . message } </p> ` ;
} finally {
set _profile sLoading ( false ) ;
setTabRefreshing ( 'profile s-content' , false ) ;
set _automation sLoading ( false ) ;
setTabRefreshing ( 'automation s-content' , false ) ;
}
}
export function expandAllProfile Sections ( ) {
CardSection . expandAll ( [ csProfile s ] ) ;
export function expandAllAutomation Sections ( ) {
CardSection . expandAll ( [ csAutomation s ] ) ;
}
export function collapseAllProfile Sections ( ) {
CardSection . collapseAll ( [ csProfile s ] ) ;
export function collapseAllAutomation Sections ( ) {
CardSection . collapseAll ( [ csAutomation s ] ) ;
}
function renderProfiles ( profile s , sceneMap ) {
const container = document . getElementById ( 'profile s-content' ) ;
function renderAutomations ( automation s , sceneMap ) {
const container = document . getElementById ( 'automation s-content' ) ;
const items = csProfile s . applySortOrder ( profile s. map ( p => ( { key : p . id , html : createProfile Card ( p , sceneMap ) } ) ) ) ;
const toolbar = ` <div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllProfile Sections()" title=" ${ t ( 'section.expand_all' ) } ">⊞</button><button class="btn-expand-collapse" onclick="collapseAllProfile Sections()" title=" ${ t ( 'section.collapse_all' ) } ">⊟</button><button class="tutorial-trigger-btn" onclick="startProfile sTutorial()" title=" ${ t ( 'tour.restart' ) } "> ${ ICON _HELP } </button></span></div> ` ;
container . innerHTML = toolbar + csProfile s . render ( items ) ;
csProfile s . bind ( ) ;
const items = csAutomation s . applySortOrder ( automation s. map ( a => ( { key : a . id , html : createAutomation Card ( a , sceneMap ) } ) ) ) ;
const toolbar = ` <div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllAutomation Sections()" title=" ${ t ( 'section.expand_all' ) } ">⊞</button><button class="btn-expand-collapse" onclick="collapseAllAutomation Sections()" title=" ${ t ( 'section.collapse_all' ) } ">⊟</button><button class="tutorial-trigger-btn" onclick="startAutomation sTutorial()" title=" ${ t ( 'tour.restart' ) } "> ${ ICON _HELP } </button></span></div> ` ;
container . innerHTML = toolbar + csAutomation s . render ( items ) ;
csAutomation s . bind ( ) ;
// Localize data-i18n elements within the profile s container only
// Localize data-i18n elements within the automation s container only
container . querySelectorAll ( '[data-i18n]' ) . forEach ( el => {
el . textContent = t ( el . getAttribute ( 'data-i18n' ) ) ;
} ) ;
}
function createProfileCard ( profile , sceneMap = new Map ( ) ) {
const statusClass = ! profile . enabled ? 'disabled' : profile . is _active ? 'active' : 'inactive' ;
const statusText = ! profile . enabled ? t ( 'profile s.status.disabled' ) : profile . is _active ? t ( 'profile s.status.active' ) : t ( 'profile s.status.inactive' ) ;
function createAutomationCard ( automation , sceneMap = new Map ( ) ) {
const statusClass = ! automation . enabled ? 'disabled' : automation . is _active ? 'active' : 'inactive' ;
const statusText = ! automation . enabled ? t ( 'automation s.status.disabled' ) : automation . is _active ? t ( 'automation s.status.active' ) : t ( 'automation s.status.inactive' ) ;
let condPills = '' ;
if ( profile . conditions . length === 0 ) {
condPills = ` <span class="stream-card-prop"> ${ t ( 'profile s.conditions.empty' ) } </span> ` ;
if ( automation . conditions . length === 0 ) {
condPills = ` <span class="stream-card-prop"> ${ t ( 'automation s.conditions.empty' ) } </span> ` ;
} else {
const parts = profile . conditions . map ( c => {
const parts = automation . conditions . map ( c => {
if ( c . condition _type === 'always' ) {
return ` <span class="stream-card-prop"> ${ ICON _OK } ${ t ( 'profile s.condition.always' ) } </span> ` ;
return ` <span class="stream-card-prop"> ${ ICON _OK } ${ t ( 'automation s.condition.always' ) } </span> ` ;
}
if ( c . condition _type === 'application' ) {
const apps = ( c . apps || [ ] ) . join ( ', ' ) ;
const matchLabel = t ( 'profile s.condition.application.match_type.' + ( c . match _type || 'running' ) ) ;
return ` <span class="stream-card-prop stream-card-prop-full"> ${ t ( 'profile s.condition.application' ) } : ${ apps } ( ${ matchLabel } )</span> ` ;
const matchLabel = t ( 'automation s.condition.application.match_type.' + ( c . match _type || 'running' ) ) ;
return ` <span class="stream-card-prop stream-card-prop-full"> ${ t ( 'automation s.condition.application' ) } : ${ apps } ( ${ matchLabel } )</span> ` ;
}
if ( c . condition _type === 'time_of_day' ) {
return ` <span class="stream-card-prop"> ${ ICON _CLOCK } ${ c . start _time || '00:00' } – ${ c . end _time || '23:59' } </span> ` ;
}
if ( c . condition _type === 'system_idle' ) {
const mode = c . when _idle !== false ? t ( 'profile s.condition.system_idle.when_idle' ) : t ( 'profile s.condition.system_idle.when_active' ) ;
const mode = c . when _idle !== false ? t ( 'automation s.condition.system_idle.when_idle' ) : t ( 'automation s.condition.system_idle.when_active' ) ;
return ` <span class="stream-card-prop"> ${ ICON _TIMER } ${ c . idle _minutes || 5 } m ( ${ mode } )</span> ` ;
}
if ( c . condition _type === 'display_state' ) {
const stateLabel = t ( 'profile s.condition.display_state.' + ( c . state || 'on' ) ) ;
return ` <span class="stream-card-prop"> ${ ICON _MONITOR } ${ t ( 'profile s.condition.display_state' ) } : ${ stateLabel } </span> ` ;
const stateLabel = t ( 'automation s.condition.display_state.' + ( c . state || 'on' ) ) ;
return ` <span class="stream-card-prop"> ${ ICON _MONITOR } ${ t ( 'automation s.condition.display_state' ) } : ${ stateLabel } </span> ` ;
}
if ( c . condition _type === 'mqtt' ) {
return ` <span class="stream-card-prop stream-card-prop-full"> ${ ICON _RADIO } ${ t ( 'profile s.condition.mqtt' ) } : ${ escapeHtml ( c . topic || '' ) } = ${ escapeHtml ( c . payload || '*' ) } </span> ` ;
return ` <span class="stream-card-prop stream-card-prop-full"> ${ ICON _RADIO } ${ t ( 'automation s.condition.mqtt' ) } : ${ escapeHtml ( c . topic || '' ) } = ${ escapeHtml ( c . payload || '*' ) } </span> ` ;
}
return ` <span class="stream-card-prop"> ${ c . condition _type } </span> ` ;
} ) ;
const logicLabel = profile . condition _logic === 'and' ? t ( 'profile s.logic.and' ) : t ( 'profile s.logic.or' ) ;
condPills = parts . join ( ` <span class="profile -logic-label"> ${ logicLabel } </span> ` ) ;
const logicLabel = automation . condition _logic === 'and' ? t ( 'automation s.logic.and' ) : t ( 'automation s.logic.or' ) ;
condPills = parts . join ( ` <span class="automation -logic-label"> ${ logicLabel } </span> ` ) ;
}
// Scene info
const scene = profile . scene _preset _id ? sceneMap . get ( profile . scene _preset _id ) : null ;
const sceneName = scene ? escapeHtml ( scene . name ) : t ( 'profile s.scene.none_selected' ) ;
const scene = automation . scene _preset _id ? sceneMap . get ( automation . scene _preset _id ) : null ;
const sceneName = scene ? escapeHtml ( scene . name ) : t ( 'automation s.scene.none_selected' ) ;
const sceneColor = scene ? scene . color || '#4fc3f7' : '#888' ;
// Deactivation mode label
let deactivationLabel = '' ;
if ( profile . deactivation _mode === 'revert' ) {
deactivationLabel = t ( 'profile s.deactivation_mode.revert' ) ;
} else if ( profile . deactivation _mode === 'fallback_scene' ) {
const fallback = profile . deactivation _scene _preset _id ? sceneMap . get ( profile . deactivation _scene _preset _id ) : null ;
deactivationLabel = fallback ? ` ${ t ( 'profile s.deactivation_mode.fallback_scene' ) } : ${ escapeHtml ( fallback . name ) } ` : t ( 'profile s.deactivation_mode.fallback_scene' ) ;
if ( automation . deactivation _mode === 'revert' ) {
deactivationLabel = t ( 'automation s.deactivation_mode.revert' ) ;
} else if ( automation . deactivation _mode === 'fallback_scene' ) {
const fallback = automation . deactivation _scene _preset _id ? sceneMap . get ( automation . deactivation _scene _preset _id ) : null ;
deactivationLabel = fallback ? ` ${ t ( 'automation s.deactivation_mode.fallback_scene' ) } : ${ escapeHtml ( fallback . name ) } ` : t ( 'automation s.deactivation_mode.fallback_scene' ) ;
}
let lastActivityMeta = '' ;
if ( profile . last _activated _at ) {
const ts = new Date ( profile . last _activated _at ) ;
lastActivityMeta = ` <span class="card-meta" title=" ${ t ( 'profile s.last_activated' ) } "> ${ ICON _CLOCK } ${ ts . toLocaleString ( ) } </span> ` ;
if ( automation . last _activated _at ) {
const ts = new Date ( automation . last _activated _at ) ;
lastActivityMeta = ` <span class="card-meta" title=" ${ t ( 'automation s.last_activated' ) } "> ${ ICON _CLOCK } ${ ts . toLocaleString ( ) } </span> ` ;
}
return `
< div class = "card${!profile .enabled ? ' profile -status-disabled' : ''}" data - profile - id = "${profile .id}" >
< div class = "card${!automation .enabled ? ' automation -status-disabled' : ''}" data - automation - id = "${automation .id}" >
< div class = "card-top-actions" >
< button class = "card-remove-btn" onclick = "deleteProfile('${profile .id}', '${escapeHtml(profile .name)}')" title = "${t('common.delete')}" > & # x2715 ; < / b u t t o n >
< button class = "card-remove-btn" onclick = "deleteAutomation('${automation .id}', '${escapeHtml(automation .name)}')" title = "${t('common.delete')}" > & # x2715 ; < / b u t t o n >
< / d i v >
< div class = "card-header" >
< div class = "card-title" >
$ { escapeHtml ( profile . name ) }
< span class = "badge badge-profile -${statusClass}" > $ { statusText } < / s p a n >
$ { escapeHtml ( automation . name ) }
< span class = "badge badge-automation -${statusClass}" > $ { statusText } < / s p a n >
< / d i v >
< / d i v >
< div class = "card-subtitle" >
< span class = "card-meta" > $ { profile . condition _logic === 'and' ? t ( 'profile s.logic.all' ) : t ( 'profile s.logic.any' ) } < / s p a n >
< span class = "card-meta" > $ { automation . condition _logic === 'and' ? t ( 'automation s.logic.all' ) : t ( 'automation s.logic.any' ) } < / s p a n >
< span class = "card-meta" > $ { ICON _SCENE } < span style = "color:${sceneColor}" > & # x25CF ; < / s p a n > $ { s c e n e N a m e } < / s p a n >
$ { deactivationLabel ? ` <span class="card-meta"> ${ deactivationLabel } </span> ` : '' }
$ { lastActivityMeta }
< / d i v >
< div class = "stream-card-props" > $ { condPills } < / d i v >
< div class = "card-actions" >
< button class = "btn btn-icon btn-secondary" onclick = "openProfileEditor('${profile .id}')" title = "${t('profile s.edit')}" > $ { ICON _SETTINGS } < / b u t t o n >
< button class = "btn btn-icon ${profile .enabled ? 'btn-warning' : 'btn-success'}" onclick = "toggleProfile Enabled('${profile.id}', ${!profile .enabled})" title = "${profile .enabled ? t('profile s.action.disable') : t('profile s.status.active')}" >
$ { profile . enabled ? ICON _PAUSE : ICON _START }
< button class = "btn btn-icon btn-secondary" onclick = "openAutomationEditor('${automation .id}')" title = "${t('automation s.edit')}" > $ { ICON _SETTINGS } < / b u t t o n >
< button class = "btn btn-icon ${automation .enabled ? 'btn-warning' : 'btn-success'}" onclick = "toggleAutomation Enabled('${automation.id}', ${!automation .enabled})" title = "${automation .enabled ? t('automation s.action.disable') : t('automation s.status.active')}" >
$ { automation . enabled ? ICON _PAUSE : ICON _START }
< / b u t t o n >
< / d i v >
< / d i v > ` ;
}
export async function openProfile Editor ( profile Id) {
const modal = document . getElementById ( 'profile -editor-modal' ) ;
const titleEl = document . getElementById ( 'profile -editor-title' ) ;
const idInput = document . getElementById ( 'profile -editor-id' ) ;
const nameInput = document . getElementById ( 'profile -editor-name' ) ;
const enabledInput = document . getElementById ( 'profile -editor-enabled' ) ;
const logicSelect = document . getElementById ( 'profile -editor-logic' ) ;
const condList = document . getElementById ( 'profile -conditions-list' ) ;
const errorEl = document . getElementById ( 'profile -editor-error' ) ;
export async function openAutomation Editor ( automation Id) {
const modal = document . getElementById ( 'automation -editor-modal' ) ;
const titleEl = document . getElementById ( 'automation -editor-title' ) ;
const idInput = document . getElementById ( 'automation -editor-id' ) ;
const nameInput = document . getElementById ( 'automation -editor-name' ) ;
const enabledInput = document . getElementById ( 'automation -editor-enabled' ) ;
const logicSelect = document . getElementById ( 'automation -editor-logic' ) ;
const condList = document . getElementById ( 'automation -conditions-list' ) ;
const errorEl = document . getElementById ( 'automation -editor-error' ) ;
errorEl . style . display = 'none' ;
condList . innerHTML = '' ;
@@ -208,66 +208,66 @@ export async function openProfileEditor(profileId) {
} catch { /* use cached */ }
// Reset deactivation mode
document . getElementById ( 'profile -deactivation-mode' ) . value = 'none' ;
document . getElementById ( 'profile -fallback-scene-group' ) . style . display = 'none' ;
document . getElementById ( 'automation -deactivation-mode' ) . value = 'none' ;
document . getElementById ( 'automation -fallback-scene-group' ) . style . display = 'none' ;
if ( profile Id) {
titleEl . innerHTML = ` ${ ICON _PROFILE } ${ t ( 'profile s.edit' ) } ` ;
if ( automation Id) {
titleEl . innerHTML = ` ${ ICON _AUTOMATION } ${ t ( 'automation s.edit' ) } ` ;
try {
const resp = await fetchWithAuth ( ` /profiles/ ${ profile Id } ` ) ;
if ( ! resp . ok ) throw new Error ( 'Failed to load profile ' ) ;
const profile = await resp . json ( ) ;
const resp = await fetchWithAuth ( ` /automations/ ${ automation Id } ` ) ;
if ( ! resp . ok ) throw new Error ( 'Failed to load automation ' ) ;
const automation = await resp . json ( ) ;
idInput . value = profile . id ;
nameInput . value = profile . name ;
enabledInput . checked = profile . enabled ;
logicSelect . value = profile . condition _logic ;
idInput . value = automation . id ;
nameInput . value = automation . name ;
enabledInput . checked = automation . enabled ;
logicSelect . value = automation . condition _logic ;
for ( const c of profile . conditions ) {
addProfile ConditionRow ( c ) ;
for ( const c of automation . conditions ) {
addAutomation ConditionRow ( c ) ;
}
// Scene selector
_initSceneSelector ( 'profile -scene' , profile . scene _preset _id ) ;
_initSceneSelector ( 'automation -scene' , automation . scene _preset _id ) ;
// Deactivation mode
document . getElementById ( 'profile -deactivation-mode' ) . value = profile . deactivation _mode || 'none' ;
document . getElementById ( 'automation -deactivation-mode' ) . value = automation . deactivation _mode || 'none' ;
_onDeactivationModeChange ( ) ;
_initSceneSelector ( 'profile -fallback-scene' , profile . deactivation _scene _preset _id ) ;
_initSceneSelector ( 'automation -fallback-scene' , automation . deactivation _scene _preset _id ) ;
} catch ( e ) {
showToast ( e . message , 'error' ) ;
return ;
}
} else {
titleEl . innerHTML = ` ${ ICON _PROFILE } ${ t ( 'profile s.add' ) } ` ;
titleEl . innerHTML = ` ${ ICON _AUTOMATION } ${ t ( 'automation s.add' ) } ` ;
idInput . value = '' ;
nameInput . value = '' ;
enabledInput . checked = true ;
logicSelect . value = 'or' ;
_initSceneSelector ( 'profile -scene' , null ) ;
_initSceneSelector ( 'profile -fallback-scene' , null ) ;
_initSceneSelector ( 'automation -scene' , null ) ;
_initSceneSelector ( 'automation -fallback-scene' , null ) ;
}
// Wire up deactivation mode change
document . getElementById ( 'profile -deactivation-mode' ) . onchange = _onDeactivationModeChange ;
document . getElementById ( 'automation -deactivation-mode' ) . onchange = _onDeactivationModeChange ;
profile Modal. open ( ) ;
automation Modal. open ( ) ;
modal . querySelectorAll ( '[data-i18n]' ) . forEach ( el => {
el . textContent = t ( el . getAttribute ( 'data-i18n' ) ) ;
} ) ;
modal . querySelectorAll ( '[data-i18n-placeholder]' ) . forEach ( el => {
el . placeholder = t ( el . getAttribute ( 'data-i18n-placeholder' ) ) ;
} ) ;
profile Modal. snapshot ( ) ;
automation Modal. snapshot ( ) ;
}
function _onDeactivationModeChange ( ) {
const mode = document . getElementById ( 'profile -deactivation-mode' ) . value ;
document . getElementById ( 'profile -fallback-scene-group' ) . style . display = mode === 'fallback_scene' ? '' : 'none' ;
const mode = document . getElementById ( 'automation -deactivation-mode' ) . value ;
document . getElementById ( 'automation -fallback-scene-group' ) . style . display = mode === 'fallback_scene' ? '' : 'none' ;
}
export async function closeProfile EditorModal ( ) {
await profile Modal. close ( ) ;
export async function closeAutomation EditorModal ( ) {
await automation Modal. close ( ) ;
}
// ===== Scene selector logic =====
@@ -296,7 +296,7 @@ function _initSceneSelector(prefix, selectedId) {
const filtered = query ? _scenesCache . filter ( s => s . name . toLowerCase ( ) . includes ( query ) ) : _scenesCache ;
if ( filtered . length === 0 ) {
dropdown . innerHTML = ` <div class="scene-selector-empty"> ${ t ( 'profile s.scene.none_available' ) } </div> ` ;
dropdown . innerHTML = ` <div class="scene-selector-empty"> ${ t ( 'automation s.scene.none_available' ) } </div> ` ;
} else {
dropdown . innerHTML = filtered . map ( s => {
const selected = s . id === hiddenInput . value ? ' selected' : '' ;
@@ -370,27 +370,27 @@ function _initSceneSelector(prefix, selectedId) {
// ===== Condition editor =====
export function addProfile Condition ( ) {
addProfile ConditionRow ( { condition _type : 'application' , apps : [ ] , match _type : 'running' } ) ;
export function addAutomation Condition ( ) {
addAutomation ConditionRow ( { condition _type : 'application' , apps : [ ] , match _type : 'running' } ) ;
}
function addProfile ConditionRow ( condition ) {
const list = document . getElementById ( 'profile -conditions-list' ) ;
function addAutomation ConditionRow ( condition ) {
const list = document . getElementById ( 'automation -conditions-list' ) ;
const row = document . createElement ( 'div' ) ;
row . className = 'profile -condition-row' ;
row . className = 'automation -condition-row' ;
const condType = condition . condition _type || 'application' ;
row . innerHTML = `
< div class = "condition-header" >
< select class = "condition-type-select" >
< option value = "always" $ { condType === 'always' ? 'selected' : '' } > $ { t ( 'profile s.condition.always' ) } < / o p t i o n >
< option value = "application" $ { condType === 'application' ? 'selected' : '' } > $ { t ( 'profile s.condition.application' ) } < / o p t i o n >
< option value = "time_of_day" $ { condType === 'time_of_day' ? 'selected' : '' } > $ { t ( 'profile s.condition.time_of_day' ) } < / o p t i o n >
< option value = "system_idle" $ { condType === 'system_idle' ? 'selected' : '' } > $ { t ( 'profile s.condition.system_idle' ) } < / o p t i o n >
< option value = "display_state" $ { condType === 'display_state' ? 'selected' : '' } > $ { t ( 'profile s.condition.display_state' ) } < / o p t i o n >
< option value = "mqtt" $ { condType === 'mqtt' ? 'selected' : '' } > $ { t ( 'profile s.condition.mqtt' ) } < / o p t i o n >
< option value = "always" $ { condType === 'always' ? 'selected' : '' } > $ { t ( 'automation s.condition.always' ) } < / o p t i o n >
< option value = "application" $ { condType === 'application' ? 'selected' : '' } > $ { t ( 'automation s.condition.application' ) } < / o p t i o n >
< option value = "time_of_day" $ { condType === 'time_of_day' ? 'selected' : '' } > $ { t ( 'automation s.condition.time_of_day' ) } < / o p t i o n >
< option value = "system_idle" $ { condType === 'system_idle' ? 'selected' : '' } > $ { t ( 'automation s.condition.system_idle' ) } < / o p t i o n >
< option value = "display_state" $ { condType === 'display_state' ? 'selected' : '' } > $ { t ( 'automation s.condition.display_state' ) } < / o p t i o n >
< option value = "mqtt" $ { condType === 'mqtt' ? 'selected' : '' } > $ { t ( 'automation s.condition.mqtt' ) } < / o p t i o n >
< / s e l e c t >
< button type = "button" class = "btn-remove-condition" onclick = "this.closest('.profile -condition-row').remove()" title = "Remove" > & # x2715 ; < / b u t t o n >
< button type = "button" class = "btn-remove-condition" onclick = "this.closest('.automation -condition-row').remove()" title = "Remove" > & # x2715 ; < / b u t t o n >
< / d i v >
< div class = "condition-fields-container" > < / d i v >
` ;
@@ -400,7 +400,7 @@ function addProfileConditionRow(condition) {
function renderFields ( type , data ) {
if ( type === 'always' ) {
container . innerHTML = ` <small class="condition-always-desc"> ${ t ( 'profile s.condition.always.hint' ) } </small> ` ;
container . innerHTML = ` <small class="condition-always-desc"> ${ t ( 'automation s.condition.always.hint' ) } </small> ` ;
return ;
}
if ( type === 'time_of_day' ) {
@@ -409,14 +409,14 @@ function addProfileConditionRow(condition) {
container . innerHTML = `
< div class = "condition-fields" >
< div class = "condition-field" >
< label > $ { t ( 'profile s.condition.time_of_day.start_time' ) } < / l a b e l >
< label > $ { t ( 'automation s.condition.time_of_day.start_time' ) } < / l a b e l >
< input type = "time" class = "condition-start-time" value = "${startTime}" >
< / d i v >
< div class = "condition-field" >
< label > $ { t ( 'profile s.condition.time_of_day.end_time' ) } < / l a b e l >
< label > $ { t ( 'automation s.condition.time_of_day.end_time' ) } < / l a b e l >
< input type = "time" class = "condition-end-time" value = "${endTime}" >
< / d i v >
< small class = "condition-always-desc" > $ { t ( 'profile s.condition.time_of_day.overnight_hint' ) } < / s m a l l >
< small class = "condition-always-desc" > $ { t ( 'automation s.condition.time_of_day.overnight_hint' ) } < / s m a l l >
< / d i v > ` ;
return ;
}
@@ -426,14 +426,14 @@ function addProfileConditionRow(condition) {
container . innerHTML = `
< div class = "condition-fields" >
< div class = "condition-field" >
< label > $ { t ( 'profile s.condition.system_idle.idle_minutes' ) } < / l a b e l >
< label > $ { t ( 'automation s.condition.system_idle.idle_minutes' ) } < / l a b e l >
< input type = "number" class = "condition-idle-minutes" min = "1" max = "999" value = "${idleMinutes}" >
< / d i v >
< div class = "condition-field" >
< label > $ { t ( 'profile s.condition.system_idle.mode' ) } < / l a b e l >
< label > $ { t ( 'automation s.condition.system_idle.mode' ) } < / l a b e l >
< select class = "condition-when-idle" >
< option value = "true" $ { whenIdle ? 'selected' : '' } > $ { t ( 'profile s.condition.system_idle.when_idle' ) } < / o p t i o n >
< option value = "false" $ { ! whenIdle ? 'selected' : '' } > $ { t ( 'profile s.condition.system_idle.when_active' ) } < / o p t i o n >
< option value = "true" $ { whenIdle ? 'selected' : '' } > $ { t ( 'automation s.condition.system_idle.when_idle' ) } < / o p t i o n >
< option value = "false" $ { ! whenIdle ? 'selected' : '' } > $ { t ( 'automation s.condition.system_idle.when_active' ) } < / o p t i o n >
< / s e l e c t >
< / d i v >
< / d i v > ` ;
@@ -444,10 +444,10 @@ function addProfileConditionRow(condition) {
container . innerHTML = `
< div class = "condition-fields" >
< div class = "condition-field" >
< label > $ { t ( 'profile s.condition.display_state.state' ) } < / l a b e l >
< label > $ { t ( 'automation s.condition.display_state.state' ) } < / l a b e l >
< select class = "condition-display-state" >
< option value = "on" $ { dState === 'on' ? 'selected' : '' } > $ { t ( 'profile s.condition.display_state.on' ) } < / o p t i o n >
< option value = "off" $ { dState === 'off' ? 'selected' : '' } > $ { t ( 'profile s.condition.display_state.off' ) } < / o p t i o n >
< option value = "on" $ { dState === 'on' ? 'selected' : '' } > $ { t ( 'automation s.condition.display_state.on' ) } < / o p t i o n >
< option value = "off" $ { dState === 'off' ? 'selected' : '' } > $ { t ( 'automation s.condition.display_state.off' ) } < / o p t i o n >
< / s e l e c t >
< / d i v >
< / d i v > ` ;
@@ -460,19 +460,19 @@ function addProfileConditionRow(condition) {
container . innerHTML = `
< div class = "condition-fields" >
< div class = "condition-field" >
< label > $ { t ( 'profile s.condition.mqtt.topic' ) } < / l a b e l >
< label > $ { t ( 'automation s.condition.mqtt.topic' ) } < / l a b e l >
< input type = "text" class = "condition-mqtt-topic" value = "${escapeHtml(topic)}" placeholder = "home/status/power" >
< / d i v >
< div class = "condition-field" >
< label > $ { t ( 'profile s.condition.mqtt.payload' ) } < / l a b e l >
< label > $ { t ( 'automation s.condition.mqtt.payload' ) } < / l a b e l >
< input type = "text" class = "condition-mqtt-payload" value = "${escapeHtml(payload)}" placeholder = "ON" >
< / d i v >
< div class = "condition-field" >
< label > $ { t ( 'profile s.condition.mqtt.match_mode' ) } < / l a b e l >
< label > $ { t ( 'automation s.condition.mqtt.match_mode' ) } < / l a b e l >
< select class = "condition-mqtt-match-mode" >
< option value = "exact" $ { matchMode === 'exact' ? 'selected' : '' } > $ { t ( 'profile s.condition.mqtt.match_mode.exact' ) } < / o p t i o n >
< option value = "contains" $ { matchMode === 'contains' ? 'selected' : '' } > $ { t ( 'profile s.condition.mqtt.match_mode.contains' ) } < / o p t i o n >
< option value = "regex" $ { matchMode === 'regex' ? 'selected' : '' } > $ { t ( 'profile s.condition.mqtt.match_mode.regex' ) } < / o p t i o n >
< option value = "exact" $ { matchMode === 'exact' ? 'selected' : '' } > $ { t ( 'automation s.condition.mqtt.match_mode.exact' ) } < / o p t i o n >
< option value = "contains" $ { matchMode === 'contains' ? 'selected' : '' } > $ { t ( 'automation s.condition.mqtt.match_mode.contains' ) } < / o p t i o n >
< option value = "regex" $ { matchMode === 'regex' ? 'selected' : '' } > $ { t ( 'automation s.condition.mqtt.match_mode.regex' ) } < / o p t i o n >
< / s e l e c t >
< / d i v >
< / d i v > ` ;
@@ -483,22 +483,22 @@ function addProfileConditionRow(condition) {
container . innerHTML = `
< div class = "condition-fields" >
< div class = "condition-field" >
< label > $ { t ( 'profile s.condition.application.match_type' ) } < / l a b e l >
< label > $ { t ( 'automation s.condition.application.match_type' ) } < / l a b e l >
< select class = "condition-match-type" >
< option value = "running" $ { matchType === 'running' ? 'selected' : '' } > $ { t ( 'profile s.condition.application.match_type.running' ) } < / o p t i o n >
< option value = "topmost" $ { matchType === 'topmost' ? 'selected' : '' } > $ { t ( 'profile s.condition.application.match_type.topmost' ) } < / o p t i o n >
< option value = "topmost_fullscreen" $ { matchType === 'topmost_fullscreen' ? 'selected' : '' } > $ { t ( 'profile s.condition.application.match_type.topmost_fullscreen' ) } < / o p t i o n >
< option value = "fullscreen" $ { matchType === 'fullscreen' ? 'selected' : '' } > $ { t ( 'profile s.condition.application.match_type.fullscreen' ) } < / o p t i o n >
< option value = "running" $ { matchType === 'running' ? 'selected' : '' } > $ { t ( 'automation s.condition.application.match_type.running' ) } < / o p t i o n >
< option value = "topmost" $ { matchType === 'topmost' ? 'selected' : '' } > $ { t ( 'automation s.condition.application.match_type.topmost' ) } < / o p t i o n >
< option value = "topmost_fullscreen" $ { matchType === 'topmost_fullscreen' ? 'selected' : '' } > $ { t ( 'automation s.condition.application.match_type.topmost_fullscreen' ) } < / o p t i o n >
< option value = "fullscreen" $ { matchType === 'fullscreen' ? 'selected' : '' } > $ { t ( 'automation s.condition.application.match_type.fullscreen' ) } < / o p t i o n >
< / s e l e c t >
< / d i v >
< div class = "condition-field" >
< div class = "condition-apps-header" >
< label > $ { t ( 'profile s.condition.application.apps' ) } < / l a b e l >
< button type = "button" class = "btn-browse-apps" title = "${t('profile s.condition.application.browse')}" > $ { t ( 'profile s.condition.application.browse' ) } < / b u t t o n >
< label > $ { t ( 'automation s.condition.application.apps' ) } < / l a b e l >
< button type = "button" class = "btn-browse-apps" title = "${t('automation s.condition.application.browse')}" > $ { t ( 'automation s.condition.application.browse' ) } < / b u t t o n >
< / d i v >
< textarea class = "condition-apps" rows = "3" placeholder = "firefox.exe chrome.exe" > $ { escapeHtml ( appsValue ) } < / t e x t a r e a >
< div class = "process-picker" style = "display:none" >
< input type = "text" class = "process-picker-search" placeholder = "${t('profile s.condition.application.search')}" autocomplete = "off" >
< input type = "text" class = "process-picker-search" placeholder = "${t('automation s.condition.application.search')}" autocomplete = "off" >
< div class = "process-picker-list" > < / d i v >
< / d i v >
< / d i v >
@@ -551,7 +551,7 @@ async function toggleProcessPicker(picker, row) {
function renderProcessPicker ( picker , processes , existing ) {
const listEl = picker . querySelector ( '.process-picker-list' ) ;
if ( processes . length === 0 ) {
listEl . innerHTML = ` <div class="process-picker-loading"> ${ t ( 'profile s.condition.application.no_processes' ) } </div> ` ;
listEl . innerHTML = ` <div class="process-picker-loading"> ${ t ( 'automation s.condition.application.no_processes' ) } </div> ` ;
return ;
}
listEl . innerHTML = processes . map ( p => {
@@ -562,7 +562,7 @@ function renderProcessPicker(picker, processes, existing) {
listEl . querySelectorAll ( '.process-picker-item:not(.added)' ) . forEach ( item => {
item . addEventListener ( 'click' , ( ) => {
const proc = item . dataset . process ;
const row = picker . closest ( '.profile -condition-row' ) ;
const row = picker . closest ( '.automation -condition-row' ) ;
const textarea = row . querySelector ( '.condition-apps' ) ;
const current = textarea . value . trim ( ) ;
textarea . value = current ? current + '\n' + proc : proc ;
@@ -579,8 +579,8 @@ function filterProcessPicker(picker) {
renderProcessPicker ( picker , filtered , picker . _existing || new Set ( ) ) ;
}
function getProfile EditorConditions ( ) {
const rows = document . querySelectorAll ( '#profile -conditions-list .profile -condition-row' ) ;
function getAutomation EditorConditions ( ) {
const rows = document . querySelectorAll ( '#automation -conditions-list .automation -condition-row' ) ;
const conditions = [ ] ;
rows . forEach ( row => {
const typeSelect = row . querySelector ( '.condition-type-select' ) ;
@@ -621,15 +621,15 @@ function getProfileEditorConditions() {
return conditions ;
}
export async function saveProfile Editor ( ) {
const idInput = document . getElementById ( 'profile -editor-id' ) ;
const nameInput = document . getElementById ( 'profile -editor-name' ) ;
const enabledInput = document . getElementById ( 'profile -editor-enabled' ) ;
const logicSelect = document . getElementById ( 'profile -editor-logic' ) ;
export async function saveAutomation Editor ( ) {
const idInput = document . getElementById ( 'automation -editor-id' ) ;
const nameInput = document . getElementById ( 'automation -editor-name' ) ;
const enabledInput = document . getElementById ( 'automation -editor-enabled' ) ;
const logicSelect = document . getElementById ( 'automation -editor-logic' ) ;
const name = nameInput . value . trim ( ) ;
if ( ! name ) {
profile Modal. showError ( t ( 'profile s.error.name_required' ) ) ;
automation Modal. showError ( t ( 'automation s.error.name_required' ) ) ;
return ;
}
@@ -637,61 +637,61 @@ export async function saveProfileEditor() {
name ,
enabled : enabledInput . checked ,
condition _logic : logicSelect . value ,
conditions : getProfile EditorConditions ( ) ,
scene _preset _id : document . getElementById ( 'profile -scene-id' ) . value || null ,
deactivation _mode : document . getElementById ( 'profile -deactivation-mode' ) . value ,
deactivation _scene _preset _id : document . getElementById ( 'profile -fallback-scene-id' ) . value || null ,
conditions : getAutomation EditorConditions ( ) ,
scene _preset _id : document . getElementById ( 'automation -scene-id' ) . value || null ,
deactivation _mode : document . getElementById ( 'automation -deactivation-mode' ) . value ,
deactivation _scene _preset _id : document . getElementById ( 'automation -fallback-scene-id' ) . value || null ,
} ;
const profile Id = idInput . value ;
const isEdit = ! ! profile Id;
const automation Id = idInput . value ;
const isEdit = ! ! automation Id;
try {
const url = isEdit ? ` /profiles/ ${ profile Id } ` : '/profile s' ;
const url = isEdit ? ` /automations/ ${ automation Id } ` : '/automation s' ;
const resp = await fetchWithAuth ( url , {
method : isEdit ? 'PUT' : 'POST' ,
body : JSON . stringify ( body ) ,
} ) ;
if ( ! resp . ok ) {
const err = await resp . json ( ) . catch ( ( ) => ( { } ) ) ;
throw new Error ( err . detail || 'Failed to save profile ' ) ;
throw new Error ( err . detail || 'Failed to save automation ' ) ;
}
profile Modal. forceClose ( ) ;
showToast ( isEdit ? t ( 'profile s.updated' ) : t ( 'profile s.created' ) , 'success' ) ;
loadProfile s ( ) ;
automation Modal. forceClose ( ) ;
showToast ( isEdit ? t ( 'automation s.updated' ) : t ( 'automation s.created' ) , 'success' ) ;
loadAutomation s ( ) ;
} catch ( e ) {
if ( e . isAuth ) return ;
profile Modal. showError ( e . message ) ;
automation Modal. showError ( e . message ) ;
}
}
export async function toggleProfile Enabled ( profile Id, enable ) {
export async function toggleAutomation Enabled ( automation Id, enable ) {
try {
const action = enable ? 'enable' : 'disable' ;
const resp = await fetchWithAuth ( ` /profiles/ ${ profile Id } / ${ action } ` , {
const resp = await fetchWithAuth ( ` /automations/ ${ automation Id } / ${ action } ` , {
method : 'POST' ,
} ) ;
if ( ! resp . ok ) throw new Error ( ` Failed to ${ action } profile ` ) ;
loadProfile s ( ) ;
if ( ! resp . ok ) throw new Error ( ` Failed to ${ action } automation ` ) ;
loadAutomation s ( ) ;
} catch ( e ) {
if ( e . isAuth ) return ;
showToast ( e . message , 'error' ) ;
}
}
export async function deleteProfile ( profileId , profile Name ) {
const msg = t ( 'profile s.delete.confirm' ) . replace ( '{name}' , profile Name) ;
export async function deleteAutomation ( automationId , automation Name ) {
const msg = t ( 'automation s.delete.confirm' ) . replace ( '{name}' , automation Name) ;
const confirmed = await showConfirm ( msg ) ;
if ( ! confirmed ) return ;
try {
const resp = await fetchWithAuth ( ` /profiles/ ${ profile Id } ` , {
const resp = await fetchWithAuth ( ` /automations/ ${ automation Id } ` , {
method : 'DELETE' ,
} ) ;
if ( ! resp . ok ) throw new Error ( 'Failed to delete profile ' ) ;
showToast ( t ( 'profile s.deleted' ) , 'success' ) ;
loadProfile s ( ) ;
if ( ! resp . ok ) throw new Error ( 'Failed to delete automation ' ) ;
showToast ( t ( 'automation s.deleted' ) , 'success' ) ;
loadAutomation s ( ) ;
} catch ( e ) {
if ( e . isAuth ) return ;
showToast ( e . message , 'error' ) ;