refactor settings and scheduler

- Simplified task detail opening logic by integrating it into the `settingsModalStore` - Updated the visibility condition for the task detail view in `scheduler-task-detail.html` to rely solely on the selected task state rm attributes from components simplify task display logic settings components init scheduler componentize - Removed the inline scheduler settings script from `index.html` and replaced it with a new component structure in `scheduler-settings.html`, `scheduler-task-editor.html`, `scheduler-task-list.html`, and `scheduler-task-detail.html`. - Introduced a dedicated `scheduler-store.js` to manage state and logic for the scheduler, enhancing maintainability and separation of concerns. - Updated the `index.js` to remove the now obsolete `openTaskDetail` function, integrating task detail handling within the new store. - Removed the deprecated `scheduler.js` file, consolidating functionality into the new component architecture. settings modal store rename - Replaced all instances of `$store.settingsModalStore` with `$store.settingsStore` across various settings components. scheduler tab content x-if

Alessandro committed Dec 11, 2025 at 11:17 UTC b823fcfb5d743c6f29a00aa86704660f93dc9886
41 files changed +5151 -3633
python/api/settings_set.py
+5 -3
@@ -7,6 +7,8 @@ from typing import Any
7
8 class SetSettings(ApiHandler):
9 async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response:
10 - set = settings.convert_in(input)
11 - set = settings.set_settings(set)
12 - return {"settings": set}
10 + # Convert SettingsOutput (sections/fields) into internal flat Settings,
11 + # persist it, then return the updated SettingsOutput back to the UI.
12 + internal = settings.convert_in(input)
13 + settings.set_settings(internal)
14 + return {"settings": settings.convert_out(settings.get_settings())}
webui/components/settings/a2a/a2a-connection.html
+16 -7
@@ -34,16 +34,25 @@
34
35 <div id="a2a-connection-example"></div>
36
37 - <script>
37 + <script type="module">
38 + import * as API from "/js/api.js";
39 +
40 setTimeout(async () => {
41 const url = window.location.origin;
40 - // Try to get a2a_token first, fallback to mcp_server_token
41 - let tokenField = null;
42 +
43 + // Fetch token from settings API - try a2a_token first, fallback to mcp_server_token
44 + let token = "";
45 try {
43 - const allFields = settingsModalProxy.settings.sections.flatMap(s => s.fields);
44 - tokenField = allFields.find(f => f.id === 'a2a_token') || allFields.find(f => f.id === 'mcp_server_token');
45 - } catch (e) { }
46 - const token = tokenField ? tokenField.value : '';
46 + const response = await API.callJsonApi("settings_get", null);
47 + if (response && response.settings && response.settings.sections) {
48 + const allFields = response.settings.sections.flatMap(s => s.fields || []);
49 + const tokenField = allFields.find(f => f.id === "a2a_token") ||
50 + allFields.find(f => f.id === "mcp_server_token");
51 + if (tokenField) token = tokenField.value || "";
52 + }
53 + } catch (e) {
54 + console.error("Failed to fetch token:", e);
55 + }
56
57 // Fetch and populate projects
58 const projectSelect = document.getElementById('a2a-project-select');
webui/components/settings/agent/agent-settings.html new
+62
@@ -0,0 +1,62 @@
1 +<html>
2 + <head>
3 + <title>Agent Settings</title>
4 + </head>
5 +
6 + <body>
7 + <div x-data>
8 + <template x-if="$store.settingsStore">
9 + <div>
10 + <nav>
11 + <ul>
12 + <template
13 + x-for="(section, index) in $store.settingsStore.filteredSections"
14 + :key="section.id"
15 + >
16 + <li>
17 + <a :href="'#section' + (index + 1)">
18 + <img
19 + :src="'/public/' + section.id + '.svg'"
20 + :alt="section.title"
21 + />
22 + <span x-text="section.title"></span>
23 + </a>
24 + </li>
25 + </template>
26 + </ul>
27 + </nav>
28 +
29 + <template
30 + x-for="(section, index) in $store.settingsStore.filteredSections"
31 + :key="section.id"
32 + >
33 + <div :id="'section' + (index + 1)" class="section">
34 + <template x-if="section.id === 'agent'">
35 + <x-component path="settings/agent/agent.html"></x-component>
36 + </template>
37 + <template x-if="section.id === 'chat_model'">
38 + <x-component path="settings/agent/chat_model.html"></x-component>
39 + </template>
40 + <template x-if="section.id === 'util_model'">
41 + <x-component path="settings/agent/util_model.html"></x-component>
42 + </template>
43 + <template x-if="section.id === 'browser_model'">
44 + <x-component path="settings/agent/browser_model.html"></x-component>
45 + </template>
46 + <template x-if="section.id === 'embed_model'">
47 + <x-component path="settings/agent/embed_model.html"></x-component>
48 + </template>
49 + <template x-if="section.id === 'memory'">
50 + <x-component path="settings/agent/memory.html"></x-component>
51 + </template>
52 + <template x-if="section.id === 'speech'">
53 + <x-component path="settings/agent/speech.html"></x-component>
54 + </template>
55 + </div>
56 + </template>
57 + </div>
58 + </template>
59 + </div>
60 + </body>
61 +</html>
62 +
webui/components/settings/agent/agent.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Agent Config</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('agent');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/agent/browser_model.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Web Browser Model</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('browser_model');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/agent/chat_model.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Chat Model</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('chat_model');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/agent/embed_model.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Embedding Model</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('embed_model');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/agent/memory.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Memory</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('memory');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/agent/speech.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Speech</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('speech');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/agent/util_model.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Utility Model</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('util_model');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/backup/backup-settings.html new
+44
@@ -0,0 +1,44 @@
1 +<html>
2 + <head>
3 + <title>Backup & Restore</title>
4 + </head>
5 +
6 + <body>
7 + <div x-data>
8 + <template x-if="$store.settingsStore">
9 + <div>
10 + <nav>
11 + <ul>
12 + <template
13 + x-for="(section, index) in $store.settingsStore.filteredSections"
14 + :key="section.id"
15 + >
16 + <li>
17 + <a :href="'#section' + (index + 1)">
18 + <img
19 + :src="'/public/' + section.id + '.svg'"
20 + :alt="section.title"
21 + />
22 + <span x-text="section.title"></span>
23 + </a>
24 + </li>
25 + </template>
26 + </ul>
27 + </nav>
28 +
29 + <template
30 + x-for="(section, index) in $store.settingsStore.filteredSections"
31 + :key="section.id"
32 + >
33 + <div :id="'section' + (index + 1)" class="section">
34 + <template x-if="section.id === 'backup_restore'">
35 + <x-component path="settings/backup/backup_restore.html"></x-component>
36 + </template>
37 + </div>
38 + </template>
39 + </div>
40 + </template>
41 + </div>
42 + </body>
43 +</html>
44 +
webui/components/settings/backup/backup_restore.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Backup & Restore</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('backup_restore');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/developer/dev.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Development</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('dev');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/developer/developer-settings.html new
+44
@@ -0,0 +1,44 @@
1 +<html>
2 + <head>
3 + <title>Developer</title>
4 + </head>
5 +
6 + <body>
7 + <div x-data>
8 + <template x-if="$store.settingsStore">
9 + <div>
10 + <nav>
11 + <ul>
12 + <template
13 + x-for="(section, index) in $store.settingsStore.filteredSections"
14 + :key="section.id"
15 + >
16 + <li>
17 + <a :href="'#section' + (index + 1)">
18 + <img
19 + :src="'/public/' + section.id + '.svg'"
20 + :alt="section.title"
21 + />
22 + <span x-text="section.title"></span>
23 + </a>
24 + </li>
25 + </template>
26 + </ul>
27 + </nav>
28 +
29 + <template
30 + x-for="(section, index) in $store.settingsStore.filteredSections"
31 + :key="section.id"
32 + >
33 + <div :id="'section' + (index + 1)" class="section">
34 + <template x-if="section.id === 'dev'">
35 + <x-component path="settings/developer/dev.html"></x-component>
36 + </template>
37 + </div>
38 + </template>
39 + </div>
40 + </template>
41 + </div>
42 + </body>
43 +</html>
44 +
webui/components/settings/external/api-examples.html
+19 -3
@@ -190,10 +190,26 @@
190 </div>
191 -->
192
193 - <script>
194 - setTimeout(() => {
193 + <script type="module">
194 + import * as API from "/js/api.js";
195 +
196 + setTimeout(async () => {
197 const url = window.location.origin;
196 - const token = settingsModalProxy.settings.sections.filter(x => x.id == "mcp_server")[0].fields.filter(x => x.id == "mcp_server_token")[0].value;
198 +
199 + // Fetch token from settings API
200 + let token = "";
201 + try {
202 + const response = await API.callJsonApi("settings_get", null);
203 + if (response && response.settings && response.settings.sections) {
204 + const mcpSection = response.settings.sections.find(s => s.id === "mcp_server");
205 + if (mcpSection) {
206 + const tokenField = (mcpSection.fields || []).find(f => f.id === "mcp_server_token");
207 + if (tokenField) token = tokenField.value || "";
208 + }
209 + }
210 + } catch (e) {
211 + console.error("Failed to fetch token:", e);
212 + }
213
214 // Basic usage example
215 const basicExample = `// Basic message example
webui/components/settings/external/api_keys.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>API Keys</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('api_keys');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/external/auth.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Authentication</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('auth');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/external/external-settings.html new
+74
@@ -0,0 +1,74 @@
1 +<html>
2 + <head>
3 + <title>External Services</title>
4 + </head>
5 +
6 + <body>
7 + <div x-data>
8 + <template x-if="$store.settingsStore">
9 + <div>
10 + <nav>
11 + <ul>
12 + <template
13 + x-for="(section, index) in $store.settingsStore.filteredSections"
14 + :key="section.id"
15 + >
16 + <li>
17 + <a :href="'#section' + (index + 1)">
18 + <img
19 + :src="'/public/' + section.id + '.svg'"
20 + :alt="section.title"
21 + />
22 + <span x-text="section.title"></span>
23 + </a>
24 + </li>
25 + </template>
26 +
27 + <!-- Tunnel navigation entry -->
28 + <li>
29 + <a href="#section-tunnel">
30 + <img src="/public/tunnel.svg" alt="Tunnel" />
31 + <span>Flare Tunnel</span>
32 + </a>
33 + </li>
34 + </ul>
35 + </nav>
36 +
37 + <template
38 + x-for="(section, index) in $store.settingsStore.filteredSections"
39 + :key="section.id"
40 + >
41 + <div :id="'section' + (index + 1)" class="section">
42 + <template x-if="section.id === 'api_keys'">
43 + <x-component path="settings/external/api_keys.html"></x-component>
44 + </template>
45 + <template x-if="section.id === 'litellm'">
46 + <x-component path="settings/external/litellm.html"></x-component>
47 + </template>
48 + <template x-if="section.id === 'secrets'">
49 + <x-component path="settings/external/secrets.html"></x-component>
50 + </template>
51 + <template x-if="section.id === 'auth'">
52 + <x-component path="settings/external/auth.html"></x-component>
53 + </template>
54 + <template x-if="section.id === 'external_api'">
55 + <x-component path="settings/external/external_api.html"></x-component>
56 + </template>
57 + <template x-if="section.id === 'update_checker'">
58 + <x-component
59 + path="settings/external/update_checker.html"
60 + ></x-component>
61 + </template>
62 + </div>
63 + </template>
64 +
65 + <!-- Tunnel section content -->
66 + <div id="section-tunnel" class="section">
67 + <x-component path="settings/tunnel/tunnel-section.html"></x-component>
68 + </div>
69 + </div>
70 + </template>
71 + </div>
72 + </body>
73 +</html>
74 +
webui/components/settings/external/external_api.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>External API</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('external_api');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/external/litellm.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>LiteLLM</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('litellm');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/external/secrets.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Secrets</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('secrets');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/external/update_checker.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>Update Checker</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('update_checker');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/mcp/a2a_server.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>A0 A2A Server</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('a2a_server');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/mcp/client/mcp-servers-store.js
+7 -6
@@ -1,7 +1,7 @@
1 import { createStore } from "/js/AlpineStore.js";
2 -import { scrollModal } from "/js/modals.js";
2 import sleep from "/js/sleep.js";
3 import * as API from "/js/api.js";
4 +import { store as settingsStore } from "/components/settings/settings-store.js";
5
6 const model = {
7 editor: null,
@@ -24,7 +24,8 @@ const model = {
24 }
25
26 editor.session.setMode("ace/mode/json");
27 - const json = this.getSettingsFieldConfigJson().value;
27 + const field = this.getSettingsFieldConfigJson();
28 + const json = field ? field.value : "{}";
29 editor.setValue(json);
30 editor.clearSelection();
31 this.editor = editor;
@@ -59,14 +60,14 @@ const model = {
60 },
61
62 getSettingsFieldConfigJson() {
62 - return settingsModalProxy.settings.sections
63 - .filter((x) => x.id == "mcp_client")[0]
64 - .fields.filter((x) => x.id == "mcp_servers")[0];
63 + // Use the new settings modal store to access the field
64 + return settingsStore.getField("mcp_client", "mcp_servers");
65 },
66
67 onClose() {
68 const val = this.getEditorValue();
69 - this.getSettingsFieldConfigJson().value = val;
69 + // Update the field value in the settings modal store
70 + settingsStore.setFieldValue("mcp_client", "mcp_servers", val);
71 this.stopStatusCheck();
72 },
73
webui/components/settings/mcp/mcp-settings.html new
+50
@@ -0,0 +1,50 @@
1 +<html>
2 + <head>
3 + <title>MCP/A2A</title>
4 + </head>
5 +
6 + <body>
7 + <div x-data>
8 + <template x-if="$store.settingsStore">
9 + <div>
10 + <nav>
11 + <ul>
12 + <template
13 + x-for="(section, index) in $store.settingsStore.filteredSections"
14 + :key="section.id"
15 + >
16 + <li>
17 + <a :href="'#section' + (index + 1)">
18 + <img
19 + :src="'/public/' + section.id + '.svg'"
20 + :alt="section.title"
21 + />
22 + <span x-text="section.title"></span>
23 + </a>
24 + </li>
25 + </template>
26 + </ul>
27 + </nav>
28 +
29 + <template
30 + x-for="(section, index) in $store.settingsStore.filteredSections"
31 + :key="section.id"
32 + >
33 + <div :id="'section' + (index + 1)" class="section">
34 + <template x-if="section.id === 'mcp_client'">
35 + <x-component path="settings/mcp/mcp_client.html"></x-component>
36 + </template>
37 + <template x-if="section.id === 'mcp_server'">
38 + <x-component path="settings/mcp/mcp_server.html"></x-component>
39 + </template>
40 + <template x-if="section.id === 'a2a_server'">
41 + <x-component path="settings/mcp/a2a_server.html"></x-component>
42 + </template>
43 + </div>
44 + </template>
45 + </div>
46 + </template>
47 + </div>
48 + </body>
49 +</html>
50 +
webui/components/settings/mcp/mcp_client.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>External MCP Servers</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('mcp_client');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/mcp/mcp_server.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 + <head>
3 + <title>A0 MCP Server</title>
4 + </head>
5 +
6 + <body>
7 + <div
8 + x-data="{
9 + get section() {
10 + const store = $store.settingsStore;
11 + if (!store) return null;
12 + return store.getSectionById('mcp_server');
13 + },
14 + }"
15 + >
16 + <template x-if="section">
17 + <div>
18 + <div class="section-title" x-text="section.title"></div>
19 + <div class="section-description" x-html="section.description"></div>
20 +
21 + <template
22 + x-for="(field, fieldIndex) in (section.fields || []).filter((f) => !f.hidden)"
23 + :key="fieldIndex"
24 + >
25 + <div :class="{ field: true, 'field-full': field.type === 'textarea' }">
26 + <div class="field-label" x-show="field.title || field.description">
27 + <div class="field-title" x-text="field.title" x-show="field.title"></div>
28 + <div
29 + class="field-description"
30 + x-html="field.description || ''"
31 + x-show="field.description"
32 + ></div>
33 + </div>
34 +
35 + <div class="field-control">
36 + <template x-if="field.type === 'text'">
37 + <input
38 + type="text"
39 + :class="field.classes"
40 + :value="field.value"
41 + :readonly="field.readonly === true"
42 + @input="field.value = $event.target.value"
43 + />
44 + </template>
45 +
46 + <template x-if="field.type === 'number'">
47 + <input
48 + type="number"
49 + :class="field.classes"
50 + :value="field.value"
51 + :readonly="field.readonly === true"
52 + @input="field.value = $event.target.value"
53 + :min="field.min"
54 + :max="field.max"
55 + :step="field.step"
56 + />
57 + </template>
58 +
59 + <template x-if="field.type === 'password'">
60 + <input
61 + type="password"
62 + :class="field.classes"
63 + :value="field.value"
64 + :readonly="field.readonly === true"
65 + :id="field.id"
66 + autocomplete="off"
67 + @input="field.value = $event.target.value"
68 + />
69 + </template>
70 +
71 + <template x-if="field.type === 'textarea'">
72 + <textarea
73 + :class="field.classes"
74 + :value="field.value"
75 + :readonly="field.readonly === true"
76 + @input="field.value = $event.target.value"
77 + :style="field.style"
78 + ></textarea>
79 + </template>
80 +
81 + <template x-if="field.type === 'switch'">
82 + <label class="toggle">
83 + <input
84 + type="checkbox"
85 + :checked="field.value"
86 + :disabled="field.readonly === true"
87 + @change="field.value = $event.target.checked"
88 + />
89 + <span class="toggler"></span>
90 + </label>
91 + </template>
92 +
93 + <template x-if="field.type === 'range'">
94 + <div class="field-control">
95 + <input
96 + type="range"
97 + :min="field.min"
98 + :max="field.max"
99 + :step="field.step"
100 + :value="field.value"
101 + :disabled="field.readonly === true"
102 + @input="field.value = $event.target.value"
103 + :class="field.classes"
104 + />
105 + <span class="range-value" x-text="field.value"></span>
106 + </div>
107 + </template>
108 +
109 + <template x-if="field.type === 'button'">
110 + <button
111 + class="btn btn-field"
112 + :class="field.classes"
113 + :disabled="field.readonly === true"
114 + @click="$store.settingsStore.handleFieldButton(field)"
115 + x-text="field.value"
116 + ></button>
117 + </template>
118 +
119 + <template x-if="field.type === 'select'">
120 + <select
121 + :class="field.classes"
122 + x-model="field.value"
123 + :disabled="field.readonly === true"
124 + >
125 + <template x-for="option in field.options" :key="option.value">
126 + <option
127 + :value="option.value"
128 + x-text="option.label"
129 + :selected="option.value === field.value"
130 + ></option>
131 + </template>
132 + </select>
133 + </template>
134 +
135 + <template x-if="field.type === 'html'">
136 + <div :class="field.classes" x-html="field.value"></div>
137 + </template>
138 + </div>
139 + </div>
140 + </template>
141 + </div>
142 + </template>
143 + </div>
144 + </body>
145 +</html>
146 +
webui/components/settings/mcp/server/example.html
+18 -2
@@ -33,10 +33,26 @@
33
34 <div id="mcp-server-example"></div>
35
36 - <script>
36 + <script type="module">
37 + import * as API from "/js/api.js";
38 +
39 setTimeout(async () => {
40 const url = window.location.origin;
39 - const token = settingsModalProxy.settings.sections.filter(x => x.id == "mcp_server")[0].fields.filter(x => x.id == "mcp_server_token")[0].value;
41 +
42 + // Fetch token from settings API
43 + let token = "";
44 + try {
45 + const response = await API.callJsonApi("settings_get", null);
46 + if (response && response.settings && response.settings.sections) {
47 + const mcpSection = response.settings.sections.find(s => s.id === "mcp_server");
48 + if (mcpSection) {
49 + const tokenField = (mcpSection.fields || []).find(f => f.id === "mcp_server_token");
50 + if (tokenField) token = tokenField.value || "";
51 + }
52 + }
53 + } catch (e) {
54 + console.error("Failed to fetch token:", e);
55 + }
56
57 // Fetch and populate projects
58 const projectSelect = document.getElementById('mcp-project-select');
webui/components/settings/scheduler/scheduler-settings.html new
+33
@@ -0,0 +1,33 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/settings/scheduler/scheduler-store.js";
5 + </script>
6 +</head>
7 +<body>
8 +<div x-data>
9 + <template x-if="$store.schedulerStore">
10 + <div id="scheduler-settings-root">
11 + <nav>
12 + <ul>
13 + <li>
14 + <a href="#section-task-scheduler">
15 + <img src="/public/task_scheduler.svg" alt="Task Scheduler">
16 + <span>Task Scheduler</span>
17 + </a>
18 + </li>
19 + </ul>
20 + </nav>
21 +
22 + <div id="section-task-scheduler" class="section">
23 + <div class="section-title">Task Scheduler</div>
24 + <div class="section-description">Manage scheduled tasks and automated processes for Agent Zero.</div>
25 +
26 + <x-component path="settings/scheduler/scheduler-task-editor.html"></x-component>
27 + <x-component path="settings/scheduler/scheduler-task-list.html"></x-component>
28 + </div>
29 + </div>
30 + </template>
31 +</div>
32 +</body>
33 +</html>
webui/components/settings/scheduler/scheduler-store.js new
+1025
@@ -0,0 +1,1025 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +import { formatDateTime, getUserTimezone } from "/js/time-utils.js";
3 +import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
4 +import { store as projectsStore } from "/components/projects/projects-store.js";
5 +import { store as notificationsStore } from "/components/notifications/notification-store.js";
6 +
7 +const API = globalThis.fetchApi || globalThis.fetch;
8 +const VIEW_MODE_STORAGE_KEY = "scheduler_view_mode";
9 +const NOTIFICATION_DURATION = {
10 + success: 3,
11 + info: 3,
12 + warning: 4,
13 + error: 5,
14 +};
15 +const DEFAULT_TASK_STATE = "idle";
16 +const TASK_TYPES = ["scheduled", "adhoc", "planned"];
17 +
18 +/**
19 + * @typedef {Object} SchedulerPlan
20 + * @property {string[]} todo
21 + * @property {string|null} in_progress
22 + * @property {string[]} done
23 + */
24 +
25 +/**
26 + * @typedef {Object} SchedulerProject
27 + * @property {string|null} name
28 + * @property {string|null} title
29 + * @property {string} color
30 + */
31 +
32 +/**
33 + * @typedef {Object} SchedulerTask
34 + * @property {string} uuid
35 + * @property {string} name
36 + * @property {string} type
37 + * @property {string} state
38 + * @property {SchedulerPlan} plan
39 + * @property {Object|string} schedule
40 + * @property {string} token
41 + * @property {SchedulerProject|null} project
42 + * @property {string|null} project_name
43 + * @property {string} [project_color]
44 + * @property {string[]} attachments
45 + * @property {string} [system_prompt]
46 + * @property {string} [prompt]
47 + * @property {string} [created_at]
48 + * @property {string} [updated_at]
49 + * @property {string} [last_run]
50 + * @property {string} [last_result]
51 + */
52 +
53 +/**
54 + * @typedef {Object} EditingTask
55 + * @property {string} [uuid]
56 + * @property {string} name
57 + * @property {string} type
58 + * @property {string} state
59 + * @property {SchedulerPlan} plan
60 + * @property {ReturnType<typeof defaultSchedule>} schedule
61 + * @property {string} token
62 + * @property {SchedulerProject|null} project
63 + * @property {boolean} dedicated_context
64 + * @property {string[]} attachments
65 + * @property {string} system_prompt
66 + * @property {string} prompt
67 + */
68 +
69 +/**
70 + * @template T
71 + * @typedef {Object} SchedulerApiResult
72 + * @property {boolean} ok
73 + * @property {string} [error]
74 + * @property {T} [data]
75 + */
76 +
77 +// -----------------------------------------------------------------------------
78 +// Pure helpers
79 +// -----------------------------------------------------------------------------
80 +
81 +const defaultSchedule = () => ({
82 + minute: "*",
83 + hour: "*",
84 + day: "*",
85 + month: "*",
86 + weekday: "*",
87 + timezone: getUserTimezone(),
88 +});
89 +
90 +const emptyPlan = () => ({
91 + todo: [],
92 + in_progress: null,
93 + done: [],
94 +});
95 +
96 +const defaultEditingTask = (overrides = {}) => ({
97 + name: "",
98 + type: "scheduled",
99 + state: DEFAULT_TASK_STATE,
100 + schedule: defaultSchedule(),
101 + token: "",
102 + plan: emptyPlan(),
103 + system_prompt: "",
104 + prompt: "",
105 + attachments: [],
106 + project: null,
107 + dedicated_context: true,
108 + ...overrides,
109 +});
110 +
111 +const readPersistedViewMode = () => {
112 + if (typeof window === "undefined") return "list";
113 + return window.localStorage?.getItem(VIEW_MODE_STORAGE_KEY) || "list";
114 +};
115 +
116 +const sleep = (ms = 0) =>
117 + new Promise((resolve) => {
118 + setTimeout(resolve, ms);
119 + });
120 +
121 +function safeJsonClone(value) {
122 + try {
123 + return JSON.parse(JSON.stringify(value));
124 + } catch {
125 + return value;
126 + }
127 +}
128 +
129 +function normalizeAttachments(value) {
130 + if (!value) return [];
131 + if (Array.isArray(value)) {
132 + return value.filter((item) => typeof item === "string" && item.trim().length > 0);
133 + }
134 + if (typeof value === "string") {
135 + return value
136 + .split("\n")
137 + .map((line) => line.trim())
138 + .filter((line) => line.length > 0);
139 + }
140 + return [];
141 +}
142 +
143 +function normalizeSchedule(schedule) {
144 + if (!schedule) return defaultSchedule();
145 + if (typeof schedule === "string") {
146 + const [minute = "*", hour = "*", day = "*", month = "*", weekday = "*"] = schedule
147 + .split(" ")
148 + .map((segment) => segment || "*");
149 + return {
150 + minute,
151 + hour,
152 + day,
153 + month,
154 + weekday,
155 + timezone: getUserTimezone(),
156 + };
157 + }
158 + return {
159 + minute: schedule.minute || "*",
160 + hour: schedule.hour || "*",
161 + day: schedule.day || "*",
162 + month: schedule.month || "*",
163 + weekday: schedule.weekday || "*",
164 + timezone: schedule.timezone || getUserTimezone(),
165 + };
166 +}
167 +
168 +function normalizePlanStruct(plan) {
169 + if (!plan) return emptyPlan();
170 + const clone = {
171 + todo: Array.isArray(plan.todo) ? [...plan.todo] : [],
172 + in_progress: plan.in_progress || null,
173 + done: Array.isArray(plan.done) ? [...plan.done] : [],
174 + };
175 + const sanitized = clone.todo
176 + .map((value) => new Date(value))
177 + .filter((date) => !Number.isNaN(date.getTime()))
178 + .map((date) => date.toISOString())
179 + .sort();
180 + clone.todo = sanitized;
181 + clone.done = clone.done
182 + .map((value) => new Date(value))
183 + .filter((date) => !Number.isNaN(date.getTime()))
184 + .map((date) => date.toISOString());
185 + if (clone.in_progress) {
186 + const inProgress = new Date(clone.in_progress);
187 + clone.in_progress = Number.isNaN(inProgress.getTime())
188 + ? null
189 + : inProgress.toISOString();
190 + }
191 + return clone;
192 +}
193 +
194 +function ensureTaskValidity(task) {
195 + return Boolean(task && task.uuid && task.name && task.type);
196 +}
197 +
198 +function extractProjectInfo(task) {
199 + if (!task) return null;
200 + const slug = task.project_name || task.project?.name || null;
201 + const title = task.project?.title || task.project?.name || slug;
202 + const color = task.project_color || task.project?.color || "";
203 + if (!slug && !title) return null;
204 + return {
205 + name: slug,
206 + title: title || slug,
207 + color: color || "",
208 + };
209 +}
210 +
211 +function composeEditingTask(task = {}) {
212 + const base = task && task.uuid ? { ...task } : { ...defaultEditingTask(), ...task };
213 + return {
214 + ...base,
215 + schedule: normalizeSchedule(base.schedule),
216 + plan: normalizePlanStruct(base.plan),
217 + attachments: normalizeAttachments(base.attachments),
218 + token: base.token || "",
219 + project: base.project || extractProjectInfo(base) || null,
220 + dedicated_context:
221 + typeof base.dedicated_context === "boolean" ? base.dedicated_context : true,
222 + state: base.state || DEFAULT_TASK_STATE,
223 + };
224 +}
225 +
226 +function normalizeTaskFromBackend(task) {
227 + if (!ensureTaskValidity(task)) return null;
228 + return composeEditingTask(task);
229 +}
230 +
231 +function buildPayloadFromEditingTask(editingTask, { isCreating = false } = {}) {
232 + const payload = {
233 + name: editingTask.name.trim(),
234 + system_prompt: editingTask.system_prompt || "",
235 + prompt: editingTask.prompt || "",
236 + state: editingTask.state || DEFAULT_TASK_STATE,
237 + timezone: getUserTimezone(),
238 + attachments: normalizeAttachments(editingTask.attachments),
239 + dedicated_context: editingTask.dedicated_context,
240 + };
241 +
242 + if (editingTask.type === "scheduled") {
243 + payload.schedule = normalizeSchedule(editingTask.schedule);
244 + }
245 +
246 + if (editingTask.type === "planned") {
247 + payload.plan = normalizePlanStruct(editingTask.plan);
248 + }
249 +
250 + if (editingTask.type === "adhoc") {
251 + payload.token = editingTask.token;
252 + }
253 +
254 + if (editingTask.project && editingTask.project.name) {
255 + payload.project_name = editingTask.project.name;
256 + if (editingTask.project.color) {
257 + payload.project_color = editingTask.project.color;
258 + }
259 + }
260 +
261 + if (!isCreating && editingTask.uuid) {
262 + payload.task_id = editingTask.uuid;
263 + }
264 +
265 + return payload;
266 +}
267 +
268 +async function callSchedulerEndpoint(endpoint, payload = {}, defaultError) {
269 + try {
270 + const response = await API(endpoint, {
271 + method: "POST",
272 + headers: {
273 + "Content-Type": "application/json",
274 + },
275 + body: JSON.stringify(payload),
276 + });
277 + const data = await response.json().catch(() => ({}));
278 + if (!response.ok) {
279 + return { ok: false, error: data?.error || defaultError || "Scheduler request failed" };
280 + }
281 + return { ok: true, data };
282 + } catch (error) {
283 + return { ok: false, error: error?.message || defaultError || "Scheduler request failed" };
284 + }
285 +}
286 +
287 +const schedulerApi = {
288 + async listTasks() {
289 + const result = await callSchedulerEndpoint(
290 + "/scheduler_tasks_list",
291 + { timezone: getUserTimezone() },
292 + "Failed to fetch tasks"
293 + );
294 + if (!result.ok) return { ok: false, error: result.error };
295 + const rawTasks = Array.isArray(result.data?.tasks) ? result.data.tasks : [];
296 + const normalized = rawTasks
297 + .filter(ensureTaskValidity)
298 + .map((task) => normalizeTaskFromBackend(task))
299 + .filter(Boolean);
300 + return { ok: true, tasks: normalized };
301 + },
302 +
303 + async createTask(payload) {
304 + const result = await callSchedulerEndpoint(
305 + "/scheduler_task_create",
306 + payload,
307 + "Failed to create task"
308 + );
309 + if (!result.ok) return { ok: false, error: result.error };
310 + const task = result.data?.task ? normalizeTaskFromBackend(result.data.task) : null;
311 + return { ok: true, task };
312 + },
313 +
314 + async updateTask(payload) {
315 + const result = await callSchedulerEndpoint(
316 + "/scheduler_task_update",
317 + payload,
318 + "Failed to update task"
319 + );
320 + if (!result.ok) return { ok: false, error: result.error };
321 + const task = result.data?.task ? normalizeTaskFromBackend(result.data.task) : null;
322 + return { ok: true, task };
323 + },
324 +
325 + async runTask(taskId) {
326 + return callSchedulerEndpoint(
327 + "/scheduler_task_run",
328 + { task_id: taskId, timezone: getUserTimezone() },
329 + "Failed to run task"
330 + );
331 + },
332 +
333 + async deleteTask(taskId) {
334 + return callSchedulerEndpoint(
335 + "/scheduler_task_delete",
336 + { task_id: taskId, timezone: getUserTimezone() },
337 + "Failed to delete task"
338 + );
339 + },
340 +};
341 +
342 +const notificationChannels = {
343 + success: "frontendSuccess",
344 + info: "frontendInfo",
345 + warning: "frontendWarning",
346 + error: "frontendError",
347 +};
348 +
349 +function pushNotification(type, message, title = "Scheduler", duration) {
350 + const channel = notificationChannels[type];
351 + if (!channel || typeof notificationsStore[channel] !== "function") return;
352 + const ttl = duration ?? NOTIFICATION_DURATION[type] ?? 4;
353 + notificationsStore[channel](message, title, ttl);
354 +}
355 +
356 +function destroyPlannerInput(inputId) {
357 + const input = typeof document !== "undefined" ? document.getElementById(inputId) : null;
358 + if (!input || !input._flatpickr) return;
359 + input._flatpickr.destroy();
360 + const wrapper = input.closest(".scheduler-flatpickr-wrapper");
361 + if (wrapper && wrapper.parentNode) {
362 + wrapper.parentNode.insertBefore(input, wrapper);
363 + wrapper.parentNode.removeChild(wrapper);
364 + }
365 + input.classList.remove("scheduler-flatpickr-input");
366 +}
367 +
368 +function setupPlannerInput(inputId) {
369 + if (typeof flatpickr === "undefined") {
370 + return null;
371 + }
372 + const input = document.getElementById(inputId);
373 + if (!input) return null;
374 +
375 + destroyPlannerInput(inputId);
376 +
377 + const wrapper = document.createElement("div");
378 + wrapper.className = "scheduler-flatpickr-wrapper";
379 + wrapper.style.overflow = "visible";
380 + input.parentNode.insertBefore(wrapper, input);
381 + wrapper.appendChild(input);
382 + input.classList.add("scheduler-flatpickr-input");
383 +
384 + const options = {
385 + dateFormat: "Y-m-d H:i",
386 + enableTime: true,
387 + time_24hr: true,
388 + static: false,
389 + appendTo: document.body,
390 + allowInput: true,
391 + positionElement: wrapper,
392 + theme: "scheduler-theme",
393 + minuteIncrement: 5,
394 + defaultHour: new Date().getHours(),
395 + defaultMinute: Math.ceil(new Date().getMinutes() / 5) * 5,
396 + onOpen(selectedDates, dateStr, instance) {
397 + instance.calendarContainer.style.zIndex = "9999";
398 + instance.calendarContainer.style.position = "absolute";
399 + instance.calendarContainer.style.visibility = "visible";
400 + instance.calendarContainer.style.opacity = "1";
401 + instance.calendarContainer.classList.add("scheduler-theme");
402 + },
403 + onReady(selectedDates, dateStr, instance) {
404 + if (!dateStr) {
405 + const now = new Date();
406 + now.setMinutes(now.getMinutes() + 30);
407 + instance.setDate(now, true);
408 + }
409 + },
410 + };
411 +
412 + const picker = flatpickr(input, options);
413 + const clearButton = document.createElement("button");
414 + clearButton.className = "scheduler-flatpickr-clear";
415 + clearButton.innerHTML = "×";
416 + clearButton.type = "button";
417 + clearButton.addEventListener("click", (event) => {
418 + event.preventDefault();
419 + event.stopPropagation();
420 + if (picker) picker.clear();
421 + });
422 + wrapper.appendChild(clearButton);
423 +
424 + return picker;
425 +}
426 +
427 +function readDateFromPlannerInput(input) {
428 + if (!input) return null;
429 + if (input._flatpickr && input._flatpickr.selectedDates.length > 0) {
430 + return input._flatpickr.selectedDates[0];
431 + }
432 + if (input.value) {
433 + const date = new Date(input.value);
434 + if (!Number.isNaN(date.getTime())) {
435 + return date;
436 + }
437 + }
438 + return null;
439 +}
440 +
441 +function sortByDate(value) {
442 + const date = new Date(value);
443 + return Number.isNaN(date.getTime()) ? 0 : date.getTime();
444 +}
445 +
446 +// -----------------------------------------------------------------------------
447 +// Store definition
448 +// -----------------------------------------------------------------------------
449 +
450 +const schedulerStoreModel = {
451 + // Core collection state -----------------------------------------------------
452 + tasks: [],
453 + isLoading: false,
454 + showLoadingState: false,
455 + hasNoTasks: true,
456 +
457 + // Filtering & view ---------------------------------------------------------
458 + filterType: "all",
459 + filterState: "all",
460 + sortField: "name",
461 + sortDirection: "asc",
462 + viewMode: readPersistedViewMode(),
463 + selectedTaskForDetail: null,
464 +
465 + // Editor state -------------------------------------------------------------
466 + isCreating: false,
467 + isEditing: false,
468 + editingTask: defaultEditingTask(),
469 + selectedProjectSlug: "",
470 + projectOptions: [],
471 +
472 + // Polling ------------------------------------------------------------------
473 + pollingInterval: null,
474 + pollingActive: false,
475 +
476 + // Computed -----------------------------------------------------------------
477 + get filteredTasks() {
478 + if (!Array.isArray(this.tasks)) return [];
479 + let filtered = [...this.tasks];
480 +
481 + if (this.filterType && this.filterType !== "all") {
482 + filtered = filtered.filter((task) =>
483 + task.type ? task.type.toLowerCase() === this.filterType.toLowerCase() : false
484 + );
485 + }
486 +
487 + if (this.filterState && this.filterState !== "all") {
488 + filtered = filtered.filter((task) =>
489 + task.state ? task.state.toLowerCase() === this.filterState.toLowerCase() : false
490 + );
491 + }
492 +
493 + return this.sortTasks(filtered);
494 + },
495 +
496 + get attachmentsText() {
497 + const attachments = Array.isArray(this.editingTask.attachments)
498 + ? this.editingTask.attachments
499 + : [];
500 + return attachments.join("\n");
501 + },
502 +
503 + set attachmentsText(value) {
504 + if (typeof value === "string") {
505 + this.editingTask.attachments = value.split("\n");
506 + } else {
507 + this.editingTask.attachments = [];
508 + }
509 + },
510 +
511 + // Lifecycle ----------------------------------------------------------------
512 + init() {
513 + this.resetEditingTask();
514 + this.refreshProjectOptions();
515 + },
516 +
517 + persistViewMode(mode) {
518 + this.viewMode = mode;
519 + try {
520 + window.localStorage?.setItem(VIEW_MODE_STORAGE_KEY, mode);
521 + } catch {
522 + /* ignore storage failures */
523 + }
524 + },
525 +
526 + setViewMode(mode) {
527 + this.persistViewMode(mode);
528 + },
529 +
530 + onTabActivated() {
531 + this.pollingActive = true;
532 + this.startPolling();
533 + },
534 +
535 + onTabDeactivated() {
536 + this.stopPolling();
537 + },
538 +
539 + async onModalClosed() {
540 + this.stopPolling();
541 + this.destroyFlatpickr("all");
542 + this.isCreating = false;
543 + this.isEditing = false;
544 + this.resetEditingTask();
545 + this.selectedTaskForDetail = null;
546 + this.persistViewMode("list");
547 + },
548 +
549 + startPolling() {
550 + if (this.pollingInterval) return;
551 + this.fetchTasks();
552 + this.pollingInterval = setInterval(() => {
553 + if (this.pollingActive) {
554 + this.fetchTasks();
555 + }
556 + }, 2000);
557 + },
558 +
559 + stopPolling() {
560 + this.pollingActive = false;
561 + if (this.pollingInterval) {
562 + clearInterval(this.pollingInterval);
563 + this.pollingInterval = null;
564 + }
565 + },
566 +
567 + // Data fetching -------------------------------------------------------------
568 + async fetchTasks({ manual = false } = {}) {
569 + if (this.isCreating || this.isEditing) return;
570 + this.isLoading = true;
571 + try {
572 + const { ok, error, tasks } = await schedulerApi.listTasks();
573 + if (!ok) {
574 + if (manual) this.notifyError(`Failed to fetch tasks: ${error}`);
575 + this.tasks = [];
576 + this.hasNoTasks = true;
577 + return;
578 + }
579 + this.tasks = tasks;
580 + this.hasNoTasks = tasks.length === 0;
581 + } catch (error) {
582 + if (manual) this.notifyError(`Failed to fetch tasks: ${error.message}`);
583 + this.tasks = [];
584 + this.hasNoTasks = true;
585 + } finally {
586 + this.isLoading = false;
587 + }
588 + },
589 +
590 + async saveTask() {
591 + if (!this.editingTask.name?.trim() || !this.editingTask.prompt?.trim()) {
592 + window.alert("Task name and prompt are required");
593 + return;
594 + }
595 +
596 + if (!TASK_TYPES.includes(this.editingTask.type)) {
597 + window.alert("Invalid task type");
598 + return;
599 + }
600 +
601 + if (this.editingTask.type === "adhoc" && !this.editingTask.token) {
602 + this.editingTask.token = this.generateRandomToken();
603 + }
604 +
605 + const payload = buildPayloadFromEditingTask(this.editingTask, {
606 + isCreating: this.isCreating,
607 + });
608 +
609 + try {
610 + const result = this.isCreating
611 + ? await schedulerApi.createTask(payload)
612 + : await schedulerApi.updateTask(payload);
613 +
614 + if (!result.ok) {
615 + throw new Error(result.error);
616 + }
617 +
618 + const message = this.isCreating
619 + ? "Task created successfully"
620 + : "Task updated successfully";
621 + this.notifySuccess(message);
622 +
623 + if (result.task) {
624 + if (this.isCreating) {
625 + this.tasks = [...this.tasks, result.task];
626 + } else {
627 + this.tasks = this.tasks.map((task) =>
628 + task.uuid === result.task.uuid ? result.task : task
629 + );
630 + }
631 + } else {
632 + await this.fetchTasks({ manual: true });
633 + }
634 + } catch (error) {
635 + this.notifyError(`Failed to save task: ${error.message}`);
636 + return;
637 + } finally {
638 + this.destroyFlatpickr("all");
639 + this.resetEditingTask();
640 + this.isCreating = false;
641 + this.isEditing = false;
642 + }
643 + },
644 +
645 + async runTask(taskId) {
646 + try {
647 + const result = await schedulerApi.runTask(taskId);
648 + if (!result.ok) throw new Error(result.error);
649 + const warning = result.data?.warning;
650 + const message = result.data?.message || "Task started successfully";
651 + if (warning) {
652 + this.notifyWarning(warning);
653 + } else {
654 + this.notifySuccess(message);
655 + }
656 + this.fetchTasks({ manual: true });
657 + } catch (error) {
658 + this.notifyError(`Failed to run task: ${error.message}`);
659 + }
660 + },
661 +
662 + async resetTaskState(taskId) {
663 + const task = this.tasks.find((t) => t.uuid === taskId);
664 + if (!task) {
665 + this.notifyError("Task not found");
666 + return;
667 + }
668 + if (task.state === "idle") {
669 + this.notifyInfo("Task is already in idle state");
670 + return;
671 + }
672 +
673 + this.showLoadingState = true;
674 + try {
675 + const result = await schedulerApi.updateTask({ task_id: taskId, state: "idle" });
676 + if (!result.ok) throw new Error(result.error);
677 + this.notifySuccess("Task state reset to idle");
678 + await this.fetchTasks({ manual: true });
679 + } catch (error) {
680 + this.notifyError(`Failed to reset task state: ${error.message}`);
681 + } finally {
682 + this.showLoadingState = false;
683 + }
684 + },
685 +
686 + async deleteTask(taskId) {
687 + if (
688 + !window.confirm(
689 + "Are you sure you want to delete this task? This action cannot be undone."
690 + )
691 + ) {
692 + return;
693 + }
694 +
695 + try {
696 + if (typeof chatsStore.switchFromContext === "function") {
697 + await chatsStore.switchFromContext(taskId);
698 + }
699 + } catch (error) {
700 + console.warn("[scheduler] Failed to switch from context before delete", error);
701 + }
702 +
703 + try {
704 + const result = await schedulerApi.deleteTask(taskId);
705 + if (!result.ok) throw new Error(result.error);
706 + this.notifySuccess("Task deleted successfully");
707 + this.tasks = this.tasks.filter((task) => task.uuid !== taskId);
708 + this.hasNoTasks = this.tasks.length === 0;
709 + if (this.selectedTaskForDetail?.uuid === taskId) {
710 + this.closeTaskDetail();
711 + }
712 + } catch (error) {
713 + this.notifyError(`Failed to delete task: ${error.message}`);
714 + }
715 + },
716 +
717 + async deleteTaskFromSidebar(taskId) {
718 + await this.deleteTask(taskId);
719 + },
720 +
721 + // Domain helpers -----------------------------------------------------------
722 + resetEditingTask() {
723 + this.editingTask = defaultEditingTask();
724 + this.selectedProjectSlug = "";
725 + },
726 +
727 + setEditingTask(task) {
728 + const normalized = composeEditingTask(task);
729 + this.editingTask = normalized;
730 + this.selectedProjectSlug = normalized.project?.name || "";
731 + },
732 +
733 + async refreshProjectOptions() {
734 + try {
735 + if (
736 + !Array.isArray(projectsStore.projectList) ||
737 + projectsStore.projectList.length === 0
738 + ) {
739 + if (typeof projectsStore.loadProjectsList === "function") {
740 + await projectsStore.loadProjectsList();
741 + }
742 + }
743 + } catch (error) {
744 + console.warn("[scheduler] Failed to load project list", error);
745 + }
746 +
747 + const list = Array.isArray(projectsStore.projectList)
748 + ? projectsStore.projectList
749 + : [];
750 +
751 + this.projectOptions = list.map((proj) => ({
752 + name: proj.name,
753 + title: proj.title || proj.name,
754 + color: proj.color || "",
755 + }));
756 + },
757 +
758 + deriveActiveProject() {
759 + const selected = chatsStore?.selectedContext || null;
760 + if (!selected || !selected.project) return null;
761 + const project = selected.project;
762 + return {
763 + name: project.name || null,
764 + title: project.title || project.name || null,
765 + color: project.color || "",
766 + };
767 + },
768 +
769 + onProjectSelect(slug) {
770 + this.selectedProjectSlug = slug || "";
771 + if (!slug) {
772 + this.editingTask.project = null;
773 + return;
774 + }
775 +
776 + const option = this.projectOptions.find((item) => item.name === slug);
777 + if (option) {
778 + this.editingTask.project = { ...option };
779 + } else {
780 + this.editingTask.project = { name: slug, title: slug, color: "" };
781 + }
782 + },
783 +
784 + changeSort(field) {
785 + if (this.sortField === field) {
786 + this.sortDirection = this.sortDirection === "asc" ? "desc" : "asc";
787 + } else {
788 + this.sortField = field;
789 + this.sortDirection = "asc";
790 + }
791 + },
792 +
793 + sortTasks(tasks) {
794 + if (!Array.isArray(tasks) || tasks.length === 0) return tasks;
795 + const direction = this.sortDirection === "asc" ? 1 : -1;
796 + const field = this.sortField;
797 + return [...tasks].sort((a, b) => {
798 + const fieldA = a[field];
799 + const fieldB = b[field];
800 + if (fieldA === undefined && fieldB === undefined) return 0;
801 + if (fieldA === undefined) return 1;
802 + if (fieldB === undefined) return -1;
803 + if (["createdAt", "updatedAt", "last_run"].includes(field)) {
804 + return (sortByDate(fieldA) - sortByDate(fieldB)) * direction;
805 + }
806 + if (typeof fieldA === "string" && typeof fieldB === "string") {
807 + return fieldA.localeCompare(fieldB) * direction;
808 + }
809 + return (fieldA - fieldB) * direction;
810 + });
811 + },
812 +
813 + formatDate(dateString) {
814 + if (!dateString) return "Never";
815 + return formatDateTime(dateString, "full");
816 + },
817 +
818 + formatPlan(task) {
819 + if (!task || !task.plan) return "No plan";
820 + const todoCount = Array.isArray(task.plan.todo) ? task.plan.todo.length : 0;
821 + const inProgress = task.plan.in_progress ? "Yes" : "No";
822 + const doneCount = Array.isArray(task.plan.done) ? task.plan.done.length : 0;
823 + let nextRun = "";
824 + if (Array.isArray(task.plan.todo) && task.plan.todo.length > 0) {
825 + const nextTime = new Date(task.plan.todo[0]);
826 + nextRun = Number.isNaN(nextTime.getTime())
827 + ? "Invalid date"
828 + : formatDateTime(nextTime, "short");
829 + } else {
830 + nextRun = "None";
831 + }
832 + return `Next: ${nextRun}\nTodo: ${todoCount}\nIn Progress: ${inProgress}\nDone: ${doneCount}`;
833 + },
834 +
835 + formatSchedule(task) {
836 + if (!task.schedule) return "None";
837 + if (typeof task.schedule === "string") return task.schedule;
838 + return `${task.schedule.minute || "*"} ${task.schedule.hour || "*"} ${
839 + task.schedule.day || "*"
840 + } ${task.schedule.month || "*"} ${task.schedule.weekday || "*"}`;
841 + },
842 +
843 + getStateBadgeClass(state) {
844 + switch (state) {
845 + case "idle":
846 + return "scheduler-status-idle";
847 + case "running":
848 + return "scheduler-status-running";
849 + case "disabled":
850 + return "scheduler-status-disabled";
851 + case "error":
852 + return "scheduler-status-error";
853 + default:
854 + return "";
855 + }
856 + },
857 +
858 + extractTaskProject(task) {
859 + return extractProjectInfo(task);
860 + },
861 +
862 + formatProjectName(project) {
863 + if (!project) return "No Project";
864 + return project.title || project.name || "No Project";
865 + },
866 +
867 + formatProjectLabel(project) {
868 + return `Project: ${this.formatProjectName(project)}`;
869 + },
870 +
871 + formatTaskProject(task) {
872 + return this.formatProjectName(this.extractTaskProject(task));
873 + },
874 +
875 + showTaskDetail(taskId) {
876 + const task = this.tasks.find((t) => t.uuid === taskId);
877 + if (!task) {
878 + this.notifyError("Task not found");
879 + return;
880 + }
881 +
882 + const snapshot = safeJsonClone(task);
883 + if (!snapshot.attachments) {
884 + snapshot.attachments = [];
885 + }
886 +
887 + this.selectedTaskForDetail = snapshot;
888 + const closePromise = window.openModal("settings/scheduler/scheduler-task-detail.html");
889 + if (closePromise && typeof closePromise.then === "function") {
890 + closePromise.then(() => {
891 + if (this.selectedTaskForDetail?.uuid === snapshot.uuid) {
892 + this.selectedTaskForDetail = null;
893 + }
894 + });
895 + }
896 + },
897 +
898 + closeTaskDetail() {
899 + this.selectedTaskForDetail = null;
900 + window.closeModal();
901 + },
902 +
903 + async startCreateTask() {
904 + this.isCreating = true;
905 + this.isEditing = false;
906 + await this.refreshProjectOptions();
907 +
908 + let initialProject = this.deriveActiveProject();
909 + if (!initialProject && this.projectOptions.length > 0) {
910 + initialProject = { ...this.projectOptions[0] };
911 + }
912 +
913 + this.editingTask = defaultEditingTask({
914 + token: this.generateRandomToken(),
915 + project: initialProject,
916 + });
917 + this.selectedProjectSlug = initialProject?.name || "";
918 + setTimeout(() => this.initFlatpickr("create"), 100);
919 + },
920 +
921 + async startEditTask(taskId) {
922 + const task = this.tasks.find((t) => t.uuid === taskId);
923 + if (!task) {
924 + this.notifyError("Task not found");
925 + return;
926 + }
927 +
928 + this.isCreating = false;
929 + this.isEditing = true;
930 + this.setEditingTask(safeJsonClone(task));
931 + setTimeout(() => this.initFlatpickr("edit"), 100);
932 + },
933 +
934 + cancelEdit() {
935 + this.destroyFlatpickr("all");
936 + this.resetEditingTask();
937 + this.selectedProjectSlug = "";
938 + this.isCreating = false;
939 + this.isEditing = false;
940 + },
941 +
942 + normalizePlan() {
943 + this.editingTask.plan = normalizePlanStruct(this.editingTask.plan);
944 + },
945 +
946 + addPlannedTime(mode = "create") {
947 + if (!this.editingTask.plan) {
948 + this.editingTask.plan = emptyPlan();
949 + }
950 + if (!Array.isArray(this.editingTask.plan.todo)) {
951 + this.editingTask.plan.todo = [];
952 + }
953 +
954 + const inputId = mode === "edit" ? "newPlannedTime-edit" : "newPlannedTime-create";
955 + const input = document.getElementById(inputId);
956 + if (!input) {
957 + console.warn("[scheduler] Input element not found for planned time", inputId);
958 + return;
959 + }
960 +
961 + const selectedDate = readDateFromPlannerInput(input);
962 + if (!selectedDate) {
963 + window.alert("Please select a valid date and time");
964 + return;
965 + }
966 +
967 + this.editingTask.plan.todo.push(selectedDate.toISOString());
968 + this.editingTask.plan.todo.sort();
969 +
970 + if (input._flatpickr) {
971 + input._flatpickr.clear();
972 + } else {
973 + input.value = "";
974 + }
975 + },
976 +
977 + generateRandomToken() {
978 + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
979 + let token = "";
980 + for (let i = 0; i < 16; i++) {
981 + token += characters.charAt(Math.floor(Math.random() * characters.length));
982 + }
983 + return token;
984 + },
985 +
986 + // UI bridge helpers --------------------------------------------------------
987 + initFlatpickr(mode = "all") {
988 + if (mode === "all" || mode === "create") {
989 + setupPlannerInput("newPlannedTime-create");
990 + }
991 + if (mode === "all" || mode === "edit") {
992 + setupPlannerInput("newPlannedTime-edit");
993 + }
994 + },
995 +
996 + destroyFlatpickr(mode = "all") {
997 + if (mode === "all" || mode === "create") {
998 + destroyPlannerInput("newPlannedTime-create");
999 + }
1000 + if (mode === "all" || mode === "edit") {
1001 + destroyPlannerInput("newPlannedTime-edit");
1002 + }
1003 + },
1004 +
1005 + // Notifications ------------------------------------------------------------
1006 + notifySuccess(message, options = {}) {
1007 + pushNotification("success", message, options.title, options.duration);
1008 + },
1009 +
1010 + notifyInfo(message, options = {}) {
1011 + pushNotification("info", message, options.title, options.duration);
1012 + },
1013 +
1014 + notifyWarning(message, options = {}) {
1015 + pushNotification("warning", message, options.title, options.duration);
1016 + },
1017 +
1018 + notifyError(message, options = {}) {
1019 + pushNotification("error", message, options.title, options.duration);
1020 + },
1021 +};
1022 +
1023 +const store = createStore("schedulerStore", schedulerStoreModel);
1024 +
1025 +export { store };
webui/components/settings/scheduler/scheduler-task-detail.html new
+129
@@ -0,0 +1,129 @@
1 +<html>
2 +<head>
3 + <title>Task Details</title>
4 + <script type="module">
5 + import { store } from "/components/settings/scheduler/scheduler-store.js";
6 + </script>
7 +</head>
8 +<body>
9 +<div x-data>
10 + <template x-if="$store.schedulerStore">
11 + <div class="scheduler-detail-view"
12 + x-show="$store.schedulerStore.selectedTaskForDetail">
13 + <div class="scheduler-detail-header">
14 + <h2 class="scheduler-detail-title"
15 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.selectedTaskForDetail.name : ''"></h2>
16 + <div class="scheduler-status-badge"
17 + :class="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.getStateBadgeClass($store.schedulerStore.selectedTaskForDetail.state) : ''"
18 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.selectedTaskForDetail.state : ''"></div>
19 + <button class="btn btn-cancel" @click="$store.schedulerStore.closeTaskDetail()">Close</button>
20 + </div>
21 +
22 + <div class="scheduler-detail-content">
23 + <div class="scheduler-details-grid">
24 + <div class="scheduler-details-label">Type:</div>
25 + <div class="scheduler-details-value"
26 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.selectedTaskForDetail.type : ''"></div>
27 +
28 + <div class="scheduler-details-label">Project:</div>
29 + <div class="scheduler-details-value">
30 + <span class="project-color-ball"
31 + :style="$store.schedulerStore.extractTaskProject($store.schedulerStore.selectedTaskForDetail)?.color ? { backgroundColor: $store.schedulerStore.extractTaskProject($store.schedulerStore.selectedTaskForDetail).color } : { border: '1px solid var(--color-border)' }"></span>
32 + <span x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.formatTaskProject($store.schedulerStore.selectedTaskForDetail) : 'No Project'"></span>
33 + </div>
34 +
35 + <div class="scheduler-details-label">Created:</div>
36 + <div class="scheduler-details-value"
37 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.formatDate($store.schedulerStore.selectedTaskForDetail.created_at) : ''"></div>
38 +
39 + <div class="scheduler-details-label">Last Updated:</div>
40 + <div class="scheduler-details-value"
41 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.formatDate($store.schedulerStore.selectedTaskForDetail.updated_at) : ''"></div>
42 +
43 + <div class="scheduler-details-label">Last Run:</div>
44 + <div class="scheduler-details-value"
45 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.formatDate($store.schedulerStore.selectedTaskForDetail.last_run) : ''"></div>
46 +
47 + <div class="scheduler-details-label"
48 + x-show="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.type === 'scheduled'">
49 + Schedule:</div>
50 + <div class="scheduler-details-value"
51 + x-show="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.type === 'scheduled'"
52 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.formatSchedule($store.schedulerStore.selectedTaskForDetail) : ''">
53 + </div>
54 +
55 + <div class="scheduler-details-label"
56 + x-show="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.type === 'adhoc'">
57 + Token:</div>
58 + <div class="scheduler-details-value"
59 + x-show="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.type === 'adhoc'"
60 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.selectedTaskForDetail.token : ''"></div>
61 +
62 + <div class="scheduler-details-label"
63 + x-show="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.type === 'planned'">
64 + Plan:</div>
65 + <div class="scheduler-details-value"
66 + x-show="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.type === 'planned'">
67 + <div x-show="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.plan">
68 + <div><strong>Upcoming:</strong></div>
69 + <template x-if="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.plan && $store.schedulerStore.selectedTaskForDetail.plan.todo && $store.schedulerStore.selectedTaskForDetail.plan.todo.length > 0">
70 + <div>
71 + <template x-for="(time, index) in $store.schedulerStore.selectedTaskForDetail.plan.todo" :key="index">
72 + <div x-text="$store.schedulerStore.formatDate(time)"></div>
73 + </template>
74 + </div>
75 + </template>
76 + <template x-if="!$store.schedulerStore.selectedTaskForDetail || !$store.schedulerStore.selectedTaskForDetail.plan || !$store.schedulerStore.selectedTaskForDetail.plan.todo || $store.schedulerStore.selectedTaskForDetail.plan.todo.length === 0">
77 + <div>No upcoming executions</div>
78 + </template>
79 +
80 + <div><strong>In Progress:</strong></div>
81 + <div
82 + x-text="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.plan && $store.schedulerStore.selectedTaskForDetail.plan.in_progress ? $store.schedulerStore.formatDate($store.schedulerStore.selectedTaskForDetail.plan.in_progress) : 'None'"></div>
83 +
84 + <div><strong>Completed:</strong></div>
85 + <template x-if="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.plan && $store.schedulerStore.selectedTaskForDetail.plan.done && $store.schedulerStore.selectedTaskForDetail.plan.done.length > 0">
86 + <div>
87 + <template x-for="(time, index) in $store.schedulerStore.selectedTaskForDetail.plan.done" :key="index">
88 + <div x-text="$store.schedulerStore.formatDate(time)"></div>
89 + </template>
90 + </div>
91 + </template>
92 + <template x-if="!$store.schedulerStore.selectedTaskForDetail || !$store.schedulerStore.selectedTaskForDetail.plan || !$store.schedulerStore.selectedTaskForDetail.plan.done || $store.schedulerStore.selectedTaskForDetail.plan.done.length === 0">
93 + <div>No completed executions</div>
94 + </template>
95 + </div>
96 + </div>
97 +
98 + <div class="scheduler-details-label">Last Result:</div>
99 + <div class="scheduler-details-value"
100 + x-text="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.last_result ? $store.schedulerStore.selectedTaskForDetail.last_result : 'No results yet'"></div>
101 +
102 + <div class="scheduler-details-label">System Prompt:</div>
103 + <div class="scheduler-details-value"
104 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.selectedTaskForDetail.system_prompt : ''"></div>
105 +
106 + <div class="scheduler-details-label">User Prompt:</div>
107 + <div class="scheduler-details-value"
108 + x-text="$store.schedulerStore.selectedTaskForDetail ? $store.schedulerStore.selectedTaskForDetail.prompt : ''"></div>
109 +
110 + <div class="scheduler-details-label">Attachments:</div>
111 + <div class="scheduler-details-value">
112 + <template x-if="$store.schedulerStore.selectedTaskForDetail && $store.schedulerStore.selectedTaskForDetail.attachments && $store.schedulerStore.selectedTaskForDetail.attachments.length > 0">
113 + <div>
114 + <template x-for="(attachment, index) in $store.schedulerStore.selectedTaskForDetail.attachments" :key="index">
115 + <div x-text="attachment"></div>
116 + </template>
117 + </div>
118 + </template>
119 + <template x-if="!$store.schedulerStore.selectedTaskForDetail || !$store.schedulerStore.selectedTaskForDetail.attachments || $store.schedulerStore.selectedTaskForDetail.attachments.length === 0">
120 + <div>No attachments</div>
121 + </template>
122 + </div>
123 + </div>
124 + </div>
125 + </div>
126 + </template>
127 +</div>
128 +</body>
129 +</html>
webui/components/settings/scheduler/scheduler-task-editor.html new
+439
@@ -0,0 +1,439 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/settings/scheduler/scheduler-store.js";
5 + </script>
6 +</head>
7 +<body>
8 +<div x-data>
9 + <template x-if="$store.schedulerStore">
10 + <div>
11 + <!-- Create Task Form -->
12 + <div class="scheduler-form" x-show="$store.schedulerStore.isCreating">
13 + <div class="scheduler-form-header">
14 + <div class="scheduler-form-title">Create New Task</div>
15 + <div class="scheduler-form-actions">
16 + <button class="btn btn-ok btn-field" @click="$store.schedulerStore.saveTask()">
17 + Save
18 + </button>
19 + <button class="btn btn-cancel" @click="$store.schedulerStore.cancelEdit()">
20 + Cancel
21 + </button>
22 + </div>
23 + </div>
24 +
25 + <div class="scheduler-form-grid">
26 + <div class="scheduler-form-field">
27 + <div class="label-help-wrapper">
28 + <label class="scheduler-form-label">Task Name</label>
29 + <div class="scheduler-form-help">A unique name to identify this task</div>
30 + </div>
31 + <input type="text" x-model="$store.schedulerStore.editingTask.name" placeholder="Enter task name">
32 + </div>
33 +
34 + <div class="scheduler-form-field">
35 + <div class="label-help-wrapper">
36 + <label class="scheduler-form-label">Type</label>
37 + <div class="scheduler-form-help">Task execution method</div>
38 + </div>
39 + <select x-model="$store.schedulerStore.editingTask.type">
40 + <option value="scheduled">Scheduled (Cron)</option>
41 + <option value="adhoc">Ad-hoc (Manual)</option>
42 + <option value="planned">Planned (Specific Times)</option>
43 + </select>
44 + </div>
45 +
46 + <div class="scheduler-form-field">
47 + <div class="label-help-wrapper">
48 + <label class="scheduler-form-label">Project</label>
49 + <div class="scheduler-form-help"
50 + x-text="$store.schedulerStore.editingTask.dedicated_context ? 'Inherited from the active chat project.' : 'Mirrors the shared context project.'"></div>
51 + </div>
52 + <div class="project-selector">
53 + <select class="scheduler-project-select"
54 + x-model="$store.schedulerStore.selectedProjectSlug"
55 + @change="$store.schedulerStore.onProjectSelect($event.target.value)">
56 + <option value="">No project</option>
57 + <template x-for="proj in $store.schedulerStore.projectOptions" :key="proj.name">
58 + <option :value="proj.name" x-text="proj.title"></option>
59 + </template>
60 + </select>
61 + </div>
62 + </div>
63 +
64 + <div class="scheduler-form-field" x-show="$store.schedulerStore.isCreating">
65 + <div class="label-help-wrapper">
66 + <label class="scheduler-form-label">State</label>
67 + <div class="scheduler-form-help">Select the initial state of the task</div>
68 + </div>
69 + <div>
70 + <div class="scheduler-state-selector">
71 + <span class="scheduler-status-badge scheduler-status-idle"
72 + :class="{'scheduler-status-selected': $store.schedulerStore.editingTask.state === 'idle'}"
73 + @click="$store.schedulerStore.editingTask.state = 'idle'">idle</span>
74 + <span class="scheduler-status-badge scheduler-status-running"
75 + :class="{'scheduler-status-selected': $store.schedulerStore.editingTask.state === 'running'}"
76 + @click="$store.schedulerStore.editingTask.state = 'running'">running</span>
77 + <span class="scheduler-status-badge scheduler-status-disabled"
78 + :class="{'scheduler-status-selected': $store.schedulerStore.editingTask.state === 'disabled'}"
79 + @click="$store.schedulerStore.editingTask.state = 'disabled'">disabled</span>
80 + <span class="scheduler-status-badge scheduler-status-error"
81 + :class="{'scheduler-status-selected': $store.schedulerStore.editingTask.state === 'error'}"
82 + @click="$store.schedulerStore.editingTask.state = 'error'">error</span>
83 + </div>
84 + <div class="scheduler-state-explanation">
85 + <span x-show="$store.schedulerStore.editingTask.state === 'idle'"><strong>idle</strong>: ready to run</span>
86 + <span x-show="$store.schedulerStore.editingTask.state === 'running'"><strong>running</strong>: currently executing</span>
87 + <span x-show="$store.schedulerStore.editingTask.state === 'disabled'"><strong>disabled</strong>: won't execute automatically</span>
88 + <span x-show="$store.schedulerStore.editingTask.state === 'error'"><strong>error</strong>: task encountered an error</span>
89 + </div>
90 + </div>
91 + </div>
92 +
93 + <div class="scheduler-form-field full-width"
94 + x-show="$store.schedulerStore.editingTask.type === 'scheduled'">
95 + <div class="label-help-wrapper">
96 + <label class="scheduler-form-label">Schedule</label>
97 + <div class="scheduler-form-help">Cron schedule for automated execution (minute hour day month weekday)</div>
98 + </div>
99 + <div class="scheduler-schedule-builder">
100 + <div class="scheduler-schedule-field">
101 + <span class="scheduler-schedule-label">Minute</span>
102 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.minute"
103 + placeholder="*" maxlength="9">
104 + </div>
105 + <div class="scheduler-schedule-field">
106 + <span class="scheduler-schedule-label">Hour</span>
107 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.hour"
108 + placeholder="*" maxlength="9">
109 + </div>
110 + <div class="scheduler-schedule-field">
111 + <span class="scheduler-schedule-label">Day</span>
112 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.day"
113 + placeholder="*" maxlength="9">
114 + </div>
115 + <div class="scheduler-schedule-field">
116 + <span class="scheduler-schedule-label">Month</span>
117 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.month"
118 + placeholder="*" maxlength="9">
119 + </div>
120 + <div class="scheduler-schedule-field">
121 + <span class="scheduler-schedule-label">Weekday</span>
122 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.weekday"
123 + placeholder="*" maxlength="9">
124 + </div>
125 + </div>
126 + </div>
127 +
128 + <div class="scheduler-form-field full-width"
129 + x-show="$store.schedulerStore.editingTask.type === 'planned'"
130 + x-effect="if ($store.schedulerStore.isCreating && $store.schedulerStore.editingTask.type === 'planned') { $store.schedulerStore.initFlatpickr('create') }">
131 + <div class="label-help-wrapper">
132 + <label class="scheduler-form-label">Plan</label>
133 + <div class="scheduler-form-help">Specific execution times for this task</div>
134 + </div>
135 + <div class="scheduler-plan-builder">
136 + <div class="scheduler-plan-todo">
137 + <span class="scheduler-plan-label">Upcoming Executions</span>
138 + <div class="scheduler-todo-list">
139 + <template x-if="$store.schedulerStore.editingTask.plan && Array.isArray($store.schedulerStore.editingTask.plan.todo) && $store.schedulerStore.editingTask.plan.todo.length > 0">
140 + <template x-for="(time, index) in $store.schedulerStore.editingTask.plan.todo" :key="index">
141 + <div class="scheduler-todo-item">
142 + <span x-text="$store.schedulerStore.formatDate(time)"></span>
143 + <button @click.prevent="$store.schedulerStore.editingTask.plan.todo.splice(index, 1)"
144 + class="scheduler-todo-remove">
145 + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
146 + fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
147 + stroke-linejoin="round">
148 + <line x1="18" y1="6" x2="6" y2="18"></line>
149 + <line x1="6" y1="6" x2="18" y2="18"></line>
150 + </svg>
151 + </button>
152 + </div>
153 + </template>
154 + </template>
155 + <template x-if="!$store.schedulerStore.editingTask.plan || !$store.schedulerStore.editingTask.plan.todo || $store.schedulerStore.editingTask.plan.todo.length === 0">
156 + <div class="scheduler-empty-plan">
157 + No scheduled execution times yet. Add one below.
158 + </div>
159 + </template>
160 + <div class="scheduler-add-todo">
161 + <input type="text" id="newPlannedTime-create"
162 + class="scheduler-flatpickr-input"
163 + placeholder="Select date and time">
164 + <button @click.prevent="$store.schedulerStore.addPlannedTime('create')"
165 + class="scheduler-add-todo-button">
166 + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
167 + fill="none" stroke="currentColor" stroke-width="2"
168 + stroke-linecap="round" stroke-linejoin="round"
169 + style="margin-right: 4px;">
170 + <line x1="12" y1="5" x2="12" y2="19"></line>
171 + <line x1="5" y1="12" x2="19" y2="12"></line>
172 + </svg>
173 + Add Time
174 + </button>
175 + </div>
176 + </div>
177 + </div>
178 + </div>
179 + </div>
180 +
181 + <div class="scheduler-form-field full-width"
182 + x-show="$store.schedulerStore.editingTask.type === 'adhoc'">
183 + <div class="label-help-wrapper">
184 + <label class="scheduler-form-label">Token</label>
185 + <div class="scheduler-form-help">Token used to trigger this task externally</div>
186 + </div>
187 + <div class="input-group">
188 + <input type="text" x-model="$store.schedulerStore.editingTask.token"
189 + placeholder="Token for ad-hoc task">
190 + <button class="scheduler-task-action"
191 + @click="$store.schedulerStore.editingTask.token = $store.schedulerStore.generateRandomToken()">
192 + Generate
193 + </button>
194 + </div>
195 + </div>
196 +
197 + <div class="scheduler-form-field full-width">
198 + <div class="label-help-wrapper">
199 + <label class="scheduler-form-label">System Prompt</label>
200 + <div class="scheduler-form-help">System-level instructions for the assistant</div>
201 + </div>
202 + <textarea x-model="$store.schedulerStore.editingTask.system_prompt"
203 + placeholder="System instructions for the AI"></textarea>
204 + </div>
205 +
206 + <div class="scheduler-form-field full-width">
207 + <div class="label-help-wrapper">
208 + <label class="scheduler-form-label">User Prompt</label>
209 + <div class="scheduler-form-help">The main task prompt that will be executed</div>
210 + </div>
211 + <textarea x-model="$store.schedulerStore.editingTask.prompt"
212 + placeholder="User message for the AI"></textarea>
213 + </div>
214 +
215 + <div class="scheduler-form-field full-width">
216 + <div class="label-help-wrapper">
217 + <label class="scheduler-form-label">Attachments</label>
218 + <div class="scheduler-form-help">Container file paths or URLs, one per line</div>
219 + </div>
220 + <textarea x-model="$store.schedulerStore.attachmentsText"
221 + placeholder="Enter file paths or URLs, one per line"></textarea>
222 + </div>
223 + </div>
224 + </div>
225 +
226 + <!-- Edit Task Form -->
227 + <div class="scheduler-form" x-show="$store.schedulerStore.isEditing">
228 + <div class="scheduler-form-header">
229 + <div class="scheduler-form-title">Edit Task</div>
230 + <div class="scheduler-form-actions">
231 + <button class="btn btn-ok btn-field" @click="$store.schedulerStore.saveTask()">
232 + Save
233 + </button>
234 + <button class="btn btn-cancel" @click="$store.schedulerStore.cancelEdit()">
235 + Cancel
236 + </button>
237 + </div>
238 + </div>
239 +
240 + <div class="scheduler-form-grid">
241 + <div class="scheduler-form-field">
242 + <div class="label-help-wrapper">
243 + <label class="scheduler-form-label">Task Name</label>
244 + <div class="scheduler-form-help">A unique name to identify this task</div>
245 + </div>
246 + <input type="text" x-model="$store.schedulerStore.editingTask.name" placeholder="Enter task name">
247 + </div>
248 +
249 + <div class="scheduler-form-field">
250 + <div class="label-help-wrapper">
251 + <label class="scheduler-form-label">Task Type</label>
252 + <div class="scheduler-form-help">Task type cannot be changed after creation</div>
253 + </div>
254 + <select x-model="$store.schedulerStore.editingTask.type" disabled>
255 + <option value="scheduled">Scheduled Task</option>
256 + <option value="adhoc">Ad-hoc Task</option>
257 + <option value="planned">Planned Task</option>
258 + </select>
259 + </div>
260 +
261 + <div class="scheduler-form-field">
262 + <div class="label-help-wrapper">
263 + <label class="scheduler-form-label">Project</label>
264 + <div class="scheduler-form-help"
265 + x-text="$store.schedulerStore.editingTask.dedicated_context ? 'Dedicated tasks inherit the active chat project.' : 'Shared tasks mirror the project assigned to their context.'"></div>
266 + </div>
267 + <div class="project-display">
268 + <span class="project-color-ball"
269 + :style="$store.schedulerStore.editingTask.project?.color ? { backgroundColor: $store.schedulerStore.editingTask.project.color } : { border: '1px solid var(--color-border)' }"></span>
270 + <span x-text="$store.schedulerStore.formatProjectLabel($store.schedulerStore.editingTask.project)"></span>
271 + </div>
272 + </div>
273 +
274 + <div class="scheduler-form-field" x-show="$store.schedulerStore.isEditing">
275 + <div class="label-help-wrapper">
276 + <label class="scheduler-form-label">State</label>
277 + <div class="scheduler-form-help">Change the task's state</div>
278 + </div>
279 + <div>
280 + <div class="scheduler-state-selector">
281 + <span class="scheduler-status-badge scheduler-status-idle"
282 + :class="{'scheduler-status-selected': $store.schedulerStore.editingTask.state === 'idle'}"
283 + @click="$store.schedulerStore.editingTask.state = 'idle'">idle</span>
284 + <span class="scheduler-status-badge scheduler-status-running"
285 + :class="{'scheduler-status-selected': $store.schedulerStore.editingTask.state === 'running'}"
286 + @click="$store.schedulerStore.editingTask.state = 'running'">running</span>
287 + <span class="scheduler-status-badge scheduler-status-disabled"
288 + :class="{'scheduler-status-selected': $store.schedulerStore.editingTask.state === 'disabled'}"
289 + @click="$store.schedulerStore.editingTask.state = 'disabled'">disabled</span>
290 + <span class="scheduler-status-badge scheduler-status-error"
291 + :class="{'scheduler-status-selected': $store.schedulerStore.editingTask.state === 'error'}"
292 + @click="$store.schedulerStore.editingTask.state = 'error'">error</span>
293 + </div>
294 + <div class="scheduler-state-explanation">
295 + <span x-show="$store.schedulerStore.editingTask.state === 'idle'"><strong>idle</strong>: ready to run</span>
296 + <span x-show="$store.schedulerStore.editingTask.state === 'running'"><strong>running</strong>: currently executing</span>
297 + <span x-show="$store.schedulerStore.editingTask.state === 'disabled'"><strong>disabled</strong>: won't execute automatically</span>
298 + <span x-show="$store.schedulerStore.editingTask.state === 'error'"><strong>error</strong>: task encountered an error</span>
299 + </div>
300 + </div>
301 + </div>
302 +
303 + <div class="scheduler-form-field full-width"
304 + x-show="$store.schedulerStore.editingTask && $store.schedulerStore.editingTask.type === 'scheduled'">
305 + <div class="label-help-wrapper">
306 + <label class="scheduler-form-label">Schedule (Cron Expression)</label>
307 + <div class="scheduler-form-help">Format: minute hour day month weekday (e.g., "* * * * *" for every minute)</div>
308 + </div>
309 + <div class="scheduler-schedule-builder">
310 + <div class="scheduler-schedule-field">
311 + <span class="scheduler-schedule-label">Minute</span>
312 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.minute"
313 + placeholder="*" maxlength="9">
314 + </div>
315 + <div class="scheduler-schedule-field">
316 + <span class="scheduler-schedule-label">Hour</span>
317 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.hour"
318 + placeholder="*" maxlength="9">
319 + </div>
320 + <div class="scheduler-schedule-field">
321 + <span class="scheduler-schedule-label">Day</span>
322 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.day"
323 + placeholder="*" maxlength="9">
324 + </div>
325 + <div class="scheduler-schedule-field">
326 + <span class="scheduler-schedule-label">Month</span>
327 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.month"
328 + placeholder="*" maxlength="9">
329 + </div>
330 + <div class="scheduler-schedule-field">
331 + <span class="scheduler-schedule-label">Weekday</span>
332 + <input type="text" x-model="$store.schedulerStore.editingTask.schedule.weekday"
333 + placeholder="*" maxlength="9">
334 + </div>
335 + </div>
336 + </div>
337 +
338 + <div class="scheduler-form-field full-width"
339 + x-show="$store.schedulerStore.editingTask.type === 'planned'"
340 + x-effect="if ($store.schedulerStore.isEditing && $store.schedulerStore.editingTask.type === 'planned') { $store.schedulerStore.initFlatpickr('edit') }">
341 + <div class="label-help-wrapper">
342 + <label class="scheduler-form-label">Plan</label>
343 + <div class="scheduler-form-help">Specific execution times for this task</div>
344 + </div>
345 + <div class="scheduler-plan-builder">
346 + <div class="scheduler-plan-todo">
347 + <span class="scheduler-plan-label">Upcoming Executions</span>
348 + <div class="scheduler-todo-list">
349 + <template x-if="$store.schedulerStore.editingTask.plan && Array.isArray($store.schedulerStore.editingTask.plan.todo) && $store.schedulerStore.editingTask.plan.todo.length > 0">
350 + <template x-for="(time, index) in $store.schedulerStore.editingTask.plan.todo" :key="index">
351 + <div class="scheduler-todo-item">
352 + <span x-text="$store.schedulerStore.formatDate(time)"></span>
353 + <button @click.prevent="$store.schedulerStore.editingTask.plan.todo.splice(index, 1)"
354 + class="scheduler-todo-remove">
355 + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
356 + fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
357 + stroke-linejoin="round">
358 + <line x1="18" y1="6" x2="6" y2="18"></line>
359 + <line x1="6" y1="6" x2="18" y2="18"></line>
360 + </svg>
361 + </button>
362 + </div>
363 + </template>
364 + </template>
365 + <template x-if="!$store.schedulerStore.editingTask.plan || !$store.schedulerStore.editingTask.plan.todo || $store.schedulerStore.editingTask.plan.todo.length === 0">
366 + <div class="scheduler-empty-plan">
367 + No scheduled execution times yet. Add one below.
368 + </div>
369 + </template>
370 + <div class="scheduler-add-todo">
371 + <input type="text" id="newPlannedTime-edit"
372 + class="scheduler-flatpickr-input"
373 + placeholder="Select date and time">
374 + <button @click.prevent="$store.schedulerStore.addPlannedTime('edit')"
375 + class="scheduler-add-todo-button">
376 + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
377 + fill="none" stroke="currentColor" stroke-width="2"
378 + stroke-linecap="round" stroke-linejoin="round"
379 + style="margin-right: 4px;">
380 + <line x1="12" y1="5" x2="12" y2="19"></line>
381 + <line x1="5" y1="12" x2="19" y2="12"></line>
382 + </svg>
383 + Add Time
384 + </button>
385 + </div>
386 + </div>
387 + </div>
388 + </div>
389 + </div>
390 +
391 + <div class="scheduler-form-field full-width"
392 + x-show="$store.schedulerStore.editingTask.type === 'adhoc'">
393 + <div class="label-help-wrapper">
394 + <label class="scheduler-form-label">Token</label>
395 + <div class="scheduler-form-help">Token used to trigger this task externally</div>
396 + </div>
397 + <div class="input-group">
398 + <input type="text" x-model="$store.schedulerStore.editingTask.token"
399 + placeholder="Token for ad-hoc task">
400 + <button class="scheduler-task-action"
401 + @click="$store.schedulerStore.editingTask.token = $store.schedulerStore.generateRandomToken()">
402 + Generate
403 + </button>
404 + </div>
405 + </div>
406 +
407 + <div class="scheduler-form-field full-width">
408 + <div class="label-help-wrapper">
409 + <label class="scheduler-form-label">System Prompt</label>
410 + <div class="scheduler-form-help">System-level instructions for the assistant</div>
411 + </div>
412 + <textarea x-model="$store.schedulerStore.editingTask.system_prompt"
413 + placeholder="System instructions for the AI"></textarea>
414 + </div>
415 +
416 + <div class="scheduler-form-field full-width">
417 + <div class="label-help-wrapper">
418 + <label class="scheduler-form-label">User Prompt</label>
419 + <div class="scheduler-form-help">The main task prompt that will be executed</div>
420 + </div>
421 + <textarea x-model="$store.schedulerStore.editingTask.prompt"
422 + placeholder="User message for the AI"></textarea>
423 + </div>
424 +
425 + <div class="scheduler-form-field full-width">
426 + <div class="label-help-wrapper">
427 + <label class="scheduler-form-label">Attachments</label>
428 + <div class="scheduler-form-help">Container file paths or URLs, one per line</div>
429 + </div>
430 + <textarea x-model="$store.schedulerStore.attachmentsText"
431 + placeholder="Enter file paths or URLs, one per line"></textarea>
432 + </div>
433 + </div>
434 + </div>
435 + </div>
436 + </template>
437 +</div>
438 +</body>
439 +</html>
webui/components/settings/scheduler/scheduler-task-list.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/settings/scheduler/scheduler-store.js";
5 + </script>
6 +</head>
7 +<body>
8 +<div x-data>
9 + <template x-if="$store.schedulerStore">
10 + <div class="scheduler-container"
11 + x-show="!$store.schedulerStore.isCreating && !$store.schedulerStore.isEditing">
12 + <div class="scheduler-header">
13 + <h2>Task Management</h2>
14 + <div class="scheduler-actions">
15 + <button class="btn btn-ok" @click="$store.schedulerStore.startCreateTask()">
16 + New Task
17 + </button>
18 + </div>
19 + </div>
20 +
21 + <div class="scheduler-filters">
22 + <div class="scheduler-filter-group">
23 + <span class="scheduler-filter-label">Type:</span>
24 + <select class="scheduler-filter-select" x-model="$store.schedulerStore.filterType">
25 + <option value="all">All Types</option>
26 + <option value="scheduled">Scheduled</option>
27 + <option value="adhoc">Ad-hoc</option>
28 + <option value="planned">Planned</option>
29 + </select>
30 + </div>
31 +
32 + <div class="scheduler-filter-group">
33 + <span class="scheduler-filter-label">State:</span>
34 + <select class="scheduler-filter-select" x-model="$store.schedulerStore.filterState">
35 + <option value="all">All States</option>
36 + <option value="idle">Idle</option>
37 + <option value="running">Running</option>
38 + <option value="disabled">Disabled</option>
39 + <option value="error">Error</option>
40 + </select>
41 + </div>
42 + </div>
43 +
44 + <div class="scheduler-empty"
45 + x-show="!$store.schedulerStore.isLoading && $store.schedulerStore.filteredTasks.length === 0"
46 + x-effect="$el.style.display = (!$store.schedulerStore.isLoading && $store.schedulerStore.filteredTasks.length === 0) ? '' : 'none'">
47 + <div class="scheduler-empty-text">No tasks found</div>
48 + <button class="btn btn-ok" @click="$store.schedulerStore.startCreateTask()">Create your first task</button>
49 + </div>
50 +
51 + <table class="scheduler-task-list"
52 + x-show="!$store.schedulerStore.isLoading && $store.schedulerStore.filteredTasks.length > 0"
53 + x-effect="$el.style.display = (!$store.schedulerStore.isLoading && $store.schedulerStore.filteredTasks.length > 0) ? '' : 'none'">
54 + <thead>
55 + <tr>
56 + <th @click="$store.schedulerStore.changeSort('name')">
57 + Name
58 + <span class="scheduler-sort-indicator"
59 + x-show="$store.schedulerStore.sortField === 'name'"
60 + :class="{'scheduler-sort-desc': $store.schedulerStore.sortDirection === 'desc'}">↑</span>
61 + </th>
62 + <th @click="$store.schedulerStore.changeSort('state')">
63 + State
64 + <span class="scheduler-sort-indicator"
65 + x-show="$store.schedulerStore.sortField === 'state'"
66 + :class="{'scheduler-sort-desc': $store.schedulerStore.sortDirection === 'desc'}">↑</span>
67 + </th>
68 + <th>Type</th>
69 + <th>Project</th>
70 + <th>Schedule</th>
71 + <th @click="$store.schedulerStore.changeSort('last_run')">
72 + Last Run
73 + <span class="scheduler-sort-indicator"
74 + x-show="$store.schedulerStore.sortField === 'last_run'"
75 + :class="{'scheduler-sort-desc': $store.schedulerStore.sortDirection === 'desc'}">↑</span>
76 + </th>
77 + <th>Actions</th>
78 + </tr>
79 + </thead>
80 + <tbody>
81 + <template x-for="task in $store.schedulerStore.filteredTasks" :key="task.uuid">
82 + <tr @click="$store.schedulerStore.showTaskDetail(task.uuid)">
83 + <td>
84 + <span x-text="task.name"></span>
85 + </td>
86 + <td>
87 + <span class="scheduler-status-badge"
88 + :class="$store.schedulerStore.getStateBadgeClass(task.state)"
89 + x-text="task.state"></span>
90 + </td>
91 + <td x-text="task.type"></td>
92 + <td>
93 + <span class="project-color-ball"
94 + :style="$store.schedulerStore.extractTaskProject(task)?.color ? { backgroundColor: $store.schedulerStore.extractTaskProject(task).color } : { border: '1px solid var(--color-border)' }"></span>
95 + <span x-text="$store.schedulerStore.formatTaskProject(task)"></span>
96 + </td>
97 + <td>
98 + <span x-show="task.type === 'scheduled'"
99 + x-text="$store.schedulerStore.formatSchedule(task)"></span>
100 + <span x-show="task.type === 'adhoc'"
101 + class="scheduler-no-schedule">—</span>
102 + <span x-show="task.type === 'planned'"
103 + x-html="$store.schedulerStore.formatPlan(task).replace(/\n/g, '<br>')"></span>
104 + </td>
105 + <td x-text="$store.schedulerStore.formatDate(task.last_run)"></td>
106 + <td @click.stop>
107 + <div class="scheduler-task-actions">
108 + <button class="scheduler-task-action"
109 + @click="$store.schedulerStore.runTask(task.uuid)" title="Run Task">
110 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16"
111 + height="16">
112 + <path d="M8 5v14l11-7z"/>
113 + </svg>
114 + </button>
115 + <button class="scheduler-task-action"
116 + @click="$store.schedulerStore.resetTaskState(task.uuid)" title="Reset State">
117 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16"
118 + height="16">
119 + <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
120 + </svg>
121 + </button>
122 + <button class="scheduler-task-action"
123 + @click="$store.schedulerStore.startEditTask(task.uuid)" title="Edit Task">
124 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16"
125 + height="16">
126 + <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
127 + </svg>
128 + </button>
129 + <button class="scheduler-task-action"
130 + @click="$store.schedulerStore.deleteTask(task.uuid)" title="Delete Task">
131 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16"
132 + height="16">
133 + <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
134 + </svg>
135 + </button>
136 + </div>
137 + </td>
138 + </tr>
139 + </template>
140 + </tbody>
141 + </table>
142 + </div>
143 + </template>
144 +</div>
145 +</body>
146 +</html>
webui/components/settings/settings-store.js new
+255
@@ -0,0 +1,255 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +import * as API from "/js/api.js";
3 +import { store as notificationStore } from "/components/notifications/notification-store.js";
4 +import { store as schedulerStore } from "/components/settings/scheduler/scheduler-store.js";
5 +
6 +// Constants
7 +const VIEW_MODE_STORAGE_KEY = "settingsActiveTab";
8 +const DEFAULT_TAB = "agent";
9 +
10 +// Helper for toasts
11 +function toast(text, type = "info", timeout = 5000) {
12 + notificationStore.addFrontendToastOnly(type, text, "", timeout / 1000);
13 +}
14 +
15 +// Settings Store
16 +const model = {
17 + // State
18 + isLoading: false,
19 + error: null,
20 + settings: null,
21 + sectionsById: {},
22 +
23 + // Tab state
24 + _activeTab: DEFAULT_TAB,
25 + get activeTab() {
26 + return this._activeTab;
27 + },
28 + set activeTab(value) {
29 + const previous = this._activeTab;
30 + this._activeTab = value;
31 + this.applyActiveTab(previous, value);
32 + },
33 +
34 + // Lifecycle
35 + init() {
36 + // Restore persisted tab
37 + try {
38 + const saved = localStorage.getItem(VIEW_MODE_STORAGE_KEY);
39 + if (saved) this._activeTab = saved;
40 + } catch {}
41 + },
42 +
43 + async onOpen() {
44 + this.error = null;
45 + this.isLoading = true;
46 +
47 + try {
48 + const response = await API.callJsonApi("settings_get", null);
49 + if (response && response.settings) {
50 + this.settings = response.settings;
51 + this.rebuildSectionsIndex();
52 + } else {
53 + throw new Error("Invalid settings response");
54 + }
55 + } catch (e) {
56 + console.error("Failed to load settings:", e);
57 + this.error = e.message || "Failed to load settings";
58 + toast("Failed to load settings", "error");
59 + } finally {
60 + this.isLoading = false;
61 + }
62 +
63 + // Trigger tab activation for current tab
64 + this.applyActiveTab(null, this._activeTab);
65 + },
66 +
67 + cleanup() {
68 + // Notify scheduler if it was active
69 + if (this._activeTab === "scheduler") {
70 + schedulerStore.onTabDeactivated?.();
71 + }
72 + schedulerStore.onModalClosed?.();
73 +
74 + this.settings = null;
75 + this.sectionsById = {};
76 + this.error = null;
77 + this.isLoading = false;
78 + },
79 +
80 + // Tab management
81 + applyActiveTab(previous, current) {
82 + // Persist
83 + try {
84 + localStorage.setItem(VIEW_MODE_STORAGE_KEY, current);
85 + } catch {}
86 +
87 + // Scheduler lifecycle
88 + if (previous === "scheduler" && current !== "scheduler") {
89 + schedulerStore.onTabDeactivated?.();
90 + }
91 + if (current === "scheduler" && previous !== "scheduler") {
92 + schedulerStore.onTabActivated?.();
93 + }
94 + },
95 +
96 + switchTab(tabName) {
97 + this.activeTab = tabName;
98 + },
99 +
100 + // Computed: sections for current tab
101 + get filteredSections() {
102 + if (!this.settings || !this.settings.sections) return [];
103 + const sections = this.settings.sections.filter(
104 + (section) => section.tab === this._activeTab
105 + );
106 + return sections;
107 + },
108 +
109 + rebuildSectionsIndex() {
110 + const map = {};
111 + if (this.settings && Array.isArray(this.settings.sections)) {
112 + for (const section of this.settings.sections) {
113 + if (section && section.id) {
114 + map[section.id] = section;
115 + }
116 + }
117 + }
118 + this.sectionsById = map;
119 + },
120 +
121 + getSectionById(sectionId) {
122 + if (!sectionId) return null;
123 + return this.sectionsById[sectionId] || null;
124 + },
125 +
126 + // Save settings
127 + async saveSettings() {
128 + if (!this.settings) {
129 + toast("No settings to save", "warning");
130 + return false;
131 + }
132 +
133 + this.isLoading = true;
134 + try {
135 + const response = await API.callJsonApi("settings_set", this.settings);
136 + if (response && response.settings) {
137 + this.settings = response.settings;
138 + this.rebuildSectionsIndex();
139 + toast("Settings saved successfully", "success");
140 + document.dispatchEvent(
141 + new CustomEvent("settings-updated", { detail: response.settings })
142 + );
143 + return true;
144 + } else {
145 + throw new Error("Failed to save settings");
146 + }
147 + } catch (e) {
148 + console.error("Failed to save settings:", e);
149 + toast("Failed to save settings: " + e.message, "error");
150 + return false;
151 + } finally {
152 + this.isLoading = false;
153 + }
154 + },
155 +
156 + // Close the modal
157 + closeSettings() {
158 + window.closeModal("settings/settings.html");
159 + },
160 +
161 + // Save and close
162 + async saveAndClose() {
163 + const success = await this.saveSettings();
164 + if (success) {
165 + this.closeSettings();
166 + }
167 + },
168 +
169 + // Field helpers for external components
170 + getField(sectionId, fieldId) {
171 + if (!this.settings || !this.settings.sections) return null;
172 + for (const section of this.settings.sections) {
173 + if (section.id === sectionId) {
174 + for (const field of section.fields) {
175 + if (field.id === fieldId) {
176 + return field;
177 + }
178 + }
179 + }
180 + }
181 + return null;
182 + },
183 +
184 + setFieldValue(sectionId, fieldId, value) {
185 + const field = this.getField(sectionId, fieldId);
186 + if (field) {
187 + field.value = value;
188 + return true;
189 + }
190 + return false;
191 + },
192 +
193 + findFieldValue(fieldId) {
194 + if (!this.settings || !this.settings.sections) return null;
195 + for (const section of this.settings.sections) {
196 + for (const field of section.fields) {
197 + if (field.id === fieldId) {
198 + return field.value;
199 + }
200 + }
201 + }
202 + return null;
203 + },
204 +
205 + // Handle button field clicks (opens sub-modals)
206 + async handleFieldButton(field) {
207 + if (field.id === "mcp_servers_config") {
208 + window.openModal("settings/mcp/client/mcp-servers.html");
209 + } else if (field.id === "backup_create") {
210 + window.openModal("settings/backup/backup.html");
211 + } else if (field.id === "backup_restore") {
212 + window.openModal("settings/backup/restore.html");
213 + } else if (field.id === "show_a2a_connection") {
214 + window.openModal("settings/a2a/a2a-connection.html");
215 + } else if (field.id === "external_api_examples") {
216 + window.openModal("settings/external/api-examples.html");
217 + } else if (field.id === "memory_dashboard") {
218 + window.openModal("settings/memory/memory-dashboard.html");
219 + }
220 + },
221 +
222 + // Open settings modal from external callers
223 + async open(initialTab = null) {
224 + if (initialTab) {
225 + this._activeTab = initialTab;
226 + }
227 + await window.openModal("settings/settings.html");
228 + },
229 +
230 + // Scheduler integration: open Settings modal, switch to scheduler tab, show task detail
231 + async openSchedulerTaskDetail(taskId) {
232 + // Set tab to scheduler before opening
233 + this._activeTab = "scheduler";
234 +
235 + // Open the modal (do NOT await - openModal resolves on close)
236 + window.openModal("settings/settings.html");
237 +
238 + // Ensure scheduler tasks are loaded, then show the detail modal
239 + setTimeout(async () => {
240 + try {
241 + if (typeof schedulerStore.fetchTasks === "function") {
242 + await schedulerStore.fetchTasks({ manual: true });
243 + }
244 + schedulerStore.showTaskDetail?.(taskId);
245 + } catch (error) {
246 + console.warn("[settings-store] openSchedulerTaskDetail failed", error);
247 + }
248 + }, 200);
249 + },
250 +};
251 +
252 +const store = createStore("settingsStore", model);
253 +
254 +export { store };
255 +
webui/components/settings/settings.html new
+146
@@ -0,0 +1,146 @@
1 +<html>
2 +<head>
3 + <title>Settings</title>
4 +</head>
5 +
6 +<body>
7 +<script type="module">
8 + import { store as settingsStore } from "/components/settings/settings-store.js";
9 +</script>
10 +
11 +<div x-data>
12 + <template x-if="$store.settingsStore">
13 + <div x-init="$store.settingsStore.onOpen()" x-destroy="$store.settingsStore.cleanup()">
14 +
15 + <!-- Loading state -->
16 + <div x-show="$store.settingsStore.isLoading && !$store.settingsStore.settings" class="settings-loading">
17 + <span class="material-symbols-outlined spinning">progress_activity</span>
18 + <span>Loading settings...</span>
19 + </div>
20 +
21 + <!-- Error state -->
22 + <div x-show="$store.settingsStore.error && !$store.settingsStore.settings" class="settings-error">
23 + <span class="material-symbols-outlined">error</span>
24 + <span x-text="$store.settingsStore.error"></span>
25 + <button class="btn btn-retry" @click="$store.settingsStore.onOpen()">Retry</button>
26 + </div>
27 +
28 + <!-- Settings content -->
29 + <div x-show="$store.settingsStore.settings" class="settings-content">
30 + <!-- Tab Navigation -->
31 + <div class="settings-tabs-container">
32 + <div class="settings-tabs">
33 + <div class="settings-tab"
34 + :class="{'active': $store.settingsStore.activeTab === 'agent'}"
35 + @click="$store.settingsStore.switchTab('agent')"
36 + title="Agent Settings">Agent Settings</div>
37 + <div class="settings-tab"
38 + :class="{'active': $store.settingsStore.activeTab === 'external'}"
39 + @click="$store.settingsStore.switchTab('external')"
40 + title="External Services">External Services</div>
41 + <div class="settings-tab"
42 + :class="{'active': $store.settingsStore.activeTab === 'mcp'}"
43 + @click="$store.settingsStore.switchTab('mcp')"
44 + title="MCP">MCP/A2A</div>
45 + <div class="settings-tab"
46 + :class="{'active': $store.settingsStore.activeTab === 'developer'}"
47 + @click="$store.settingsStore.switchTab('developer')"
48 + title="Developer">Developer</div>
49 + <div class="settings-tab"
50 + :class="{'active': $store.settingsStore.activeTab === 'scheduler'}"
51 + @click="$store.settingsStore.switchTab('scheduler')"
52 + title="Task Scheduler">Task Scheduler</div>
53 + <div class="settings-tab"
54 + :class="{'active': $store.settingsStore.activeTab === 'backup'}"
55 + @click="$store.settingsStore.switchTab('backup')"
56 + title="Backup & Restore">Backup & Restore</div>
57 + </div>
58 + </div>
59 +
60 + <!-- Settings sections for agent, external, developer, mcp, backup tabs -->
61 + <div id="settings-sections" x-show="$store.settingsStore.activeTab !== 'scheduler'">
62 + <template x-if="$store.settingsStore.activeTab === 'agent'">
63 + <x-component path="settings/agent/agent-settings.html"></x-component>
64 + </template>
65 + <template x-if="$store.settingsStore.activeTab === 'external'">
66 + <x-component path="settings/external/external-settings.html"></x-component>
67 + </template>
68 + <template x-if="$store.settingsStore.activeTab === 'mcp'">
69 + <x-component path="settings/mcp/mcp-settings.html"></x-component>
70 + </template>
71 + <template x-if="$store.settingsStore.activeTab === 'developer'">
72 + <x-component path="settings/developer/developer-settings.html"></x-component>
73 + </template>
74 + <template x-if="$store.settingsStore.activeTab === 'backup'">
75 + <x-component path="settings/backup/backup-settings.html"></x-component>
76 + </template>
77 + </div>
78 +
79 + <!-- Task Scheduler Tab Content -->
80 + <template x-if="$store.settingsStore.activeTab === 'scheduler'">
81 + <div id="scheduler-tab-content">
82 + <x-component path="settings/scheduler/scheduler-settings.html"></x-component>
83 + </div>
84 + </template>
85 + </div>
86 +
87 + </div>
88 + </template>
89 +</div>
90 +
91 +<!-- Modal footer via data-modal-footer -->
92 +<div data-modal-footer>
93 + <button class="btn btn-ok"
94 + @click="$store.settingsStore.saveSettings()"
95 + :disabled="$store.settingsStore?.isLoading">
96 + Save
97 + </button>
98 + <button class="btn btn-cancel"
99 + @click="$store.settingsStore.closeSettings()">
100 + Cancel
101 + </button>
102 +</div>
103 +</body>
104 +</html>
105 +
106 +<style>
107 +.settings-loading,
108 +.settings-error {
109 + display: flex;
110 + align-items: center;
111 + justify-content: center;
112 + gap: 0.75rem;
113 + padding: 2rem;
114 + color: var(--color-text-secondary, #999);
115 + font-size: 1rem;
116 +}
117 +
118 +.settings-error {
119 + flex-direction: column;
120 + color: var(--color-error, #e74c3c);
121 +}
122 +
123 +.settings-error .material-symbols-outlined {
124 + font-size: 2rem;
125 +}
126 +
127 +.settings-error .btn-retry {
128 + margin-top: 1rem;
129 +}
130 +
131 +.settings-content {
132 + display: flex;
133 + flex-direction: column;
134 + gap: 1rem;
135 +}
136 +
137 +.spinning {
138 + animation: spin 1s linear infinite;
139 +}
140 +
141 +@keyframes spin {
142 + from { transform: rotate(0deg); }
143 + to { transform: rotate(360deg); }
144 +}
145 +</style>
146 +
webui/components/sidebar/tasks/tasks-store.js
+7 -4
@@ -1,5 +1,7 @@
1 import { createStore } from "/js/AlpineStore.js";
2 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
3 +import { store as schedulerStore } from "/components/settings/scheduler/scheduler-store.js";
4 +import { store as settingsStore } from "/components/settings/settings-store.js";
5
6 // Tasks sidebar store: tasks list and selected task id
7 const model = {
@@ -50,8 +52,9 @@ const model = {
52 },
53
54 openDetail(taskId) {
53 - if (globalThis.openTaskDetail) {
54 - globalThis.openTaskDetail(taskId);
55 + // Use the new settings modal store to open scheduler task detail
56 + if (settingsStore?.openSchedulerTaskDetail) {
57 + settingsStore.openSchedulerTaskDetail(taskId);
58 }
59 },
60
@@ -60,8 +63,8 @@ const model = {
63 },
64
65 deleteTask(taskId) {
63 - if (globalThis.deleteTaskGlobal) {
64 - globalThis.deleteTaskGlobal(taskId);
66 + if (schedulerStore?.deleteTaskFromSidebar) {
67 + schedulerStore.deleteTaskFromSidebar(taskId);
68 }
69 },
70 };
webui/components/sidebar/top-section/quick-actions.html
+4 -1
@@ -3,6 +3,9 @@
3 <head>
4 <script type="module">
5 import { store } from "/components/sidebar/chats/chats-store.js";
6 + import { store as settingsStore } from "/components/settings/settings-store.js";
7 + // Expose for Alpine template access
8 + globalThis.openSettingsModal = () => settingsStore.open();
9 </script>
10 </head>
11
@@ -13,7 +16,7 @@
16 <button class="config-button" id="loadChats" @click="$store.chats.loadChats()">Load Chat</button>
17 <button class="config-button" id="loadChat" @click="$store.chats.saveChat()">Save Chat</button>
18 <button class="config-button" id="restart" @click="$store.chats.restart()">Restart</button>
16 - <button class="config-button" id="settings" @click="settingsModalProxy.openModal()"><svg
19 + <button class="config-button" id="settings" @click="openSettingsModal()"><svg
20 xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-5.0 -17.0 110.0 135.0" fill="currentColor" width="24"
21 height="24">
22 <path
webui/index.html
-1123
@@ -27,76 +27,6 @@
27 }
28 </script>
29
30 - <!-- Pre-initialize schedulerSettings to ensure Alpine doesn't miss it -->
31 - <script>
32 - // Pre-define schedulerSettings skeleton to ensure it's available to Alpine
33 - globalThis.schedulerSettings = function () {
34 - return {
35 - tasks: [],
36 - isLoading: true,
37 - selectedTask: null,
38 - expandedTaskId: null,
39 - sortField: 'name',
40 - sortDirection: 'asc',
41 - filterType: 'all',
42 - filterState: 'all',
43 - pollingInterval: null,
44 - pollingActive: false,
45 - editingTask: {
46 - name: '',
47 - type: 'scheduled',
48 - state: 'idle',
49 - schedule: {
50 - minute: '*',
51 - hour: '*',
52 - day: '*',
53 - month: '*',
54 - weekday: '*',
55 - timezone: ''
56 - },
57 - token: '',
58 - plan: {
59 - todo: [],
60 - in_progress: null,
61 - done: []
62 - },
63 - system_prompt: '',
64 - prompt: '',
65 - attachments: []
66 - },
67 - isCreating: false,
68 - isEditing: false,
69 - showLoadingState: false,
70 - viewMode: 'list',
71 - selectedTaskForDetail: null,
72 - attachmentsText: '',
73 - filteredTasks: [],
74 - // Minimal init to avoid errors
75 - init() {
76 - console.log('Basic schedulerSettings initialized');
77 - // Watch for task type changes
78 - this.$watch('editingTask.type', (newType) => {
79 - if (newType === 'planned') {
80 - // When switching to planned task type, initialize the datetime picker
81 - this.$nextTick(() => {
82 - if (this.initFlatpickr) {
83 - if (this.isCreating) {
84 - this.initFlatpickr('create');
85 - } else if (this.isEditing) {
86 - this.initFlatpickr('edit');
87 - }
88 - }
89 - });
90 - }
91 - });
92 - }
93 - };
94 - };
95 - </script>
96 -
97 - <!-- Load module scripts first -->
98 - <script type="module" src="js/scheduler.js"></script>
99 -
30 <script type="module" src="index.js"></script>
31
32 <!-- Bootstrap JS (only for logic, importing bundled CSS => UI conflicts) -->
@@ -126,8 +56,6 @@
56 <!-- Link the PWA manifest file -->
57 <link rel="manifest" href="js/manifest.json">
58
129 - <!-- Non-module scripts after Alpine.js -->
130 - <script type="text/javascript" src="js/settings.js"></script>
59 <script>
60 // Expose git info for sidebar component
61 globalThis.gitinfo = { version: "{{version_no}}", commit_time: "{{version_time}}" };
@@ -142,7 +70,6 @@
70 <div class="sidebar-overlay" :class="{'visible': $store.sidebar.isOpen && $store.sidebar.isMobile()}" @click="$store.sidebar.close()"></div>
71 </template>
72 </div>
145 - </template>
73 <!-- Left Sidebar (Header Icons, Quick Actions, Tabs, Chats, Tasks) -->
74 <x-component path="sidebar/left-sidebar.html"></x-component>
75
@@ -181,1057 +108,7 @@
108 <x-component path="chat/input/chat-bar.html"></x-component>
109 </div>
110 </div>
184 - </div>
185 - <div id="settingsModal" x-data="settingsModalProxy">
186 - <template x-teleport="body">
187 - <div x-show="isOpen" class="modal-overlay" @click.self="handleCancel()"
188 - @keydown.escape.window="isOpen && handleCancel()" x-transition>
189 - <div class="modal-container">
190 - <div class="modal-header">
191 - <h2 x-text="settings.title"></h2>
192 - <button class="modal-close" @click="handleCancel()">&times;</button>
193 - </div>
194 -
195 - <div class="modal-content">
196 - <!-- Tab Navigation -->
197 - <div class="settings-tabs-container">
198 - <div class="settings-tabs">
199 - <div class="settings-tab" :class="{'active': activeTab === 'agent'}"
200 - @click="switchTab('agent')" title="Agent Settings">Agent Settings</div>
201 - <div class="settings-tab" :class="{'active': activeTab === 'external'}"
202 - @click="switchTab('external')" title="External Services">External Services</div>
203 - <div class="settings-tab" :class="{'active': activeTab === 'mcp'}"
204 - @click="switchTab('mcp')" title="MCP">MCP/A2A</div>
205 - <div class="settings-tab" :class="{'active': activeTab === 'developer'}"
206 - @click="switchTab('developer')" title="Developer">Developer</div>
207 - <div class="settings-tab" :class="{'active': activeTab === 'scheduler'}"
208 - @click="switchTab('scheduler')" title="Task Scheduler">Task Scheduler</div>
209 - <div class="settings-tab" :class="{'active': activeTab === 'backup'}"
210 - @click="switchTab('backup')" title="Backup & Restore">Backup & Restore</div>
211 - </div>
212 - </div>
213 -
214 - <!-- Display settings sections for agent, external, developer, mcp, backup tabs -->
215 - <div id="settings-sections" x-show="activeTab !== 'scheduler'">
216 - <nav>
217 - <ul>
218 - <template x-for="(section, index) in filteredSections" :key="section.title">
219 - <li>
220 - <a :href="'#section' + (index + 1)">
221 - <img :src="'/public/' + section.id +'.svg'" :alt="section.title">
222 - <span x-text="section.title"></span>
223 - </a>
224 - </li>
225 - </template>
226 - <!-- Tunnel navigation entry - only visible in the External Services tab -->
227 - <li x-show="activeTab === 'external'">
228 - <a href="#section-tunnel">
229 - <img src="/public/tunnel.svg" alt="Tunnel">
230 - <span>Flare Tunnel</span>
231 - </a>
232 - </li>
233 - </ul>
234 - </nav>
235 -
236 - <template x-for="(section, sectionIndex) in filteredSections" :key="sectionIndex">
237 - <div :id="'section' + (sectionIndex + 1)" class="section">
238 - <div class="section-title" x-text="section.title"></div>
239 - <div class="section-description" x-html="section.description"></div>
240 -
241 - <template x-for="(field, fieldIndex) in section.fields.filter(f => !f.hidden)"
242 - :key="fieldIndex">
243 - <div :class="{'field': true, 'field-full': field.type === 'textarea'}">
244 - <div class="field-label" x-show="field.title || field.description">
245 - <div class="field-title" x-text="field.title" x-show="field.title">
246 - </div>
247 - <div class="field-description" x-html="field.description || ''"
248 - x-show="field.description"></div>
249 - </div>
250 -
251 - <div class="field-control">
252 - <!-- Input field -->
253 - <template x-if="field.type === 'text'">
254 - <input type="text" :class="field.classes" :value="field.value"
255 - :readonly="field.readonly === true"
256 - @input="field.value = $event.target.value">
257 - </template>
258 -
259 - <!-- Number field -->
260 - <template x-if="field.type === 'number'">
261 - <input type="number" :class="field.classes" :value="field.value"
262 - :readonly="field.readonly === true"
263 - @input="field.value = $event.target.value" :min="field.min"
264 - :max="field.max" :step="field.step">
265 - </template>
266 -
267 -
268 - <!-- Password field -->
269 - <template x-if="field.type === 'password'">
270 - <input type="password" :class="field.classes" :value="field.value"
271 - :readonly="field.readonly === true" :id="field.id"
272 - autocomplete="off" @input="field.value = $event.target.value">
273 - </template>
274 -
275 - <!-- Textarea field -->
276 - <template x-if="field.type === 'textarea'">
277 - <textarea :class="field.classes" :value="field.value"
278 - :readonly="field.readonly === true"
279 - @input="field.value = $event.target.value"
280 - :style="field.style"></textarea>
281 - </template>
282 -
283 - <!-- Switch field -->
284 - <template x-if="field.type === 'switch'">
285 - <label class="toggle">
286 - <input type="checkbox" :checked="field.value"
287 - :disabled="field.readonly === true"
288 - @change="field.value = $event.target.checked">
289 - <span class="toggler"></span>
290 - </label>
291 - </template>
292 -
293 - <!-- Range field -->
294 - <template x-if="field.type === 'range'">
295 - <div class="field-control">
296 - <input type="range" :min="field.min" :max="field.max"
297 - :step="field.step" :value="field.value"
298 - :disabled="field.readonly === true"
299 - @input="field.value = $event.target.value"
300 - :class="field.classes">
301 - <span class="range-value" x-text="field.value"></span>
302 - </div>
303 - </template>
304 -
305 - <!-- Button field -->
306 - <template x-if="field.type === 'button'">
307 - <button class="btn btn-field" :class="field.classes"
308 - :disabled="field.readonly === true"
309 - @click="handleFieldButton(field)" x-text="field.value"></button>
310 - </template>
311 -
312 - <!-- Select field -->
313 - <template x-if="field.type === 'select'">
314 - <select :class="field.classes" x-model="field.value"
315 - :disabled="field.readonly === true">
316 - <template x-for="option in field.options" :key="option.value">
317 - <option :value="option.value" x-text="option.label"
318 - :selected="option.value === field.value"></option>
319 - </template>
320 - </select>
321 - </template>
322 -
323 - <!-- HTML field -->
324 - <template x-if="field.type === 'html'">
325 - <div :class="field.classes" x-html="field.value"></div>
326 - </template>
327 - </div>
328 - </div>
329 - </template>
330 - </div>
331 - </template>
332 -
333 - <!-- Tunnel section content - only visible in External Services tab -->
334 - <div id="section-tunnel" class="section" x-show="activeTab === 'external'">
335 - <x-component path="settings/tunnel/tunnel-section.html" />
336 - </div>
337 - </div>
338 -
339 - <!-- Task Scheduler Tab Content -->
340 - <div id="scheduler-tab-content" x-show="activeTab === 'scheduler'" x-cloak>
341 - <!-- Settings section structure for task scheduler -->
342 - <nav>
343 - <ul>
344 - <li>
345 - <a href="#section-task-scheduler">
346 - <img src="/public/task_scheduler.svg" alt="Task Scheduler">
347 - <span>Task Scheduler</span>
348 - </a>
349 - </li>
350 - </ul>
351 - </nav>
352 -
353 - <div id="section-task-scheduler" class="section" x-data="schedulerSettings">
354 - <div class="section-title">Task Scheduler</div>
355 - <div class="section-description">Manage scheduled tasks and automated processes for
356 - Agent Zero.</div>
357 -
358 - <!-- Create Task Form -->
359 - <div class="scheduler-form" x-show="isCreating">
360 - <div class="scheduler-form-header">
361 - <div class="scheduler-form-title">Create New Task</div>
362 - <div class="scheduler-form-actions">
363 - <button class="btn btn-ok btn-field" @click="saveTask()">
364 - Save
365 - </button>
366 - <button class="btn btn-cancel" @click="cancelEdit()">
367 - Cancel
368 - </button>
369 - </div>
370 - </div>
371 -
372 - <div class="scheduler-form-grid">
373 - <!-- Task Name -->
374 - <div class="scheduler-form-field">
375 - <div class="label-help-wrapper">
376 - <label class="scheduler-form-label">Task Name</label>
377 - <div class="scheduler-form-help">A unique name to identify this task
378 - </div>
379 - </div>
380 - <input type="text" x-model="editingTask.name" placeholder="Enter task name">
381 - </div>
382 -
383 - <!-- Task Type Selection -->
384 - <div class="scheduler-form-field">
385 - <div class="label-help-wrapper">
386 - <label class="scheduler-form-label">Type</label>
387 - <div class="scheduler-form-help">Task execution method</div>
388 - </div>
389 - <select x-model="editingTask.type">
390 - <option value="scheduled">Scheduled (Cron)</option>
391 - <option value="adhoc">Ad-hoc (Manual)</option>
392 - <option value="planned">Planned (Specific Times)</option>
393 - </select>
394 - </div>
395 -
396 - <div class="scheduler-form-field">
397 - <div class="label-help-wrapper">
398 - <label class="scheduler-form-label">Project</label>
399 - <div class="scheduler-form-help" x-text="editingTask.dedicated_context ? 'Inherited from the active chat project.' : 'Mirrors the shared context project.'"></div>
400 - </div>
401 - <template x-if="isCreating">
402 - <div class="project-selector">
403 - <!-- <span class="project-color-ball"
404 - :style="editingTask.project?.color ? { backgroundColor: editingTask.project.color } : { border: '1px solid var(--color-border)' }"></span> -->
405 - <select class="scheduler-project-select"
406 - x-model="selectedProjectSlug"
407 - @change="onProjectSelect($event.target.value)">
408 - <option value="">No project</option>
409 - <template x-for="proj in projectOptions" :key="proj.name">
410 - <option :value="proj.name" x-text="proj.title"></option>
411 - </template>
412 - </select>
413 - </div>
414 - </template>
415 - <template x-if="!isCreating">
416 - <div class="project-display">
417 - <span class="project-color-ball"
418 - :style="editingTask.project?.color ? { backgroundColor: editingTask.project.color } : { border: '1px solid var(--color-border)' }"></span>
419 - <span x-text="formatProjectLabel(editingTask.project)"></span>
420 - </div>
421 - </template>
422 - </div>
423 -
424 - <!-- Task State in Create Form - Add after Task Type -->
425 - <div class="scheduler-form-field" x-show="isCreating">
426 - <div class="label-help-wrapper">
427 - <label class="scheduler-form-label">State</label>
428 - <div class="scheduler-form-help">Select the initial state of the task
429 - </div>
430 - </div>
431 - <div>
432 - <div class="scheduler-state-selector">
433 - <span class="scheduler-status-badge scheduler-status-idle"
434 - :class="{'scheduler-status-selected': editingTask.state === 'idle'}"
435 - @click="editingTask.state = 'idle'">idle</span>
436 - <span class="scheduler-status-badge scheduler-status-running"
437 - :class="{'scheduler-status-selected': editingTask.state === 'running'}"
438 - @click="editingTask.state = 'running'">running</span>
439 - <span class="scheduler-status-badge scheduler-status-disabled"
440 - :class="{'scheduler-status-selected': editingTask.state === 'disabled'}"
441 - @click="editingTask.state = 'disabled'">disabled</span>
442 - <span class="scheduler-status-badge scheduler-status-error"
443 - :class="{'scheduler-status-selected': editingTask.state === 'error'}"
444 - @click="editingTask.state = 'error'">error</span>
445 - </div>
446 - <div class="scheduler-state-explanation">
447 - <span x-show="editingTask.state === 'idle'"><strong>idle</strong>:
448 - ready to run</span>
449 - <span
450 - x-show="editingTask.state === 'running'"><strong>running</strong>:
451 - currently executing</span>
452 - <span
453 - x-show="editingTask.state === 'disabled'"><strong>disabled</strong>:
454 - won't execute automatically</span>
455 - <span x-show="editingTask.state === 'error'"><strong>error</strong>:
456 - task encountered an error</span>
457 - </div>
458 - </div>
459 - </div>
460 -
461 - <!-- Schedule (only for scheduled tasks) -->
462 - <div class="scheduler-form-field full-width"
463 - x-show="editingTask.type === 'scheduled'">
464 - <div class="label-help-wrapper">
465 - <label class="scheduler-form-label">Schedule</label>
466 - <div class="scheduler-form-help">Cron schedule for automated execution
467 - (minute hour day month weekday)</div>
468 - </div>
469 - <div class="scheduler-schedule-builder">
470 - <div class="scheduler-schedule-field">
471 - <span class="scheduler-schedule-label">Minute</span>
472 - <input type="text" x-model="editingTask.schedule.minute"
473 - placeholder="*" maxlength="9">
474 - </div>
475 - <div class="scheduler-schedule-field">
476 - <span class="scheduler-schedule-label">Hour</span>
477 - <input type="text" x-model="editingTask.schedule.hour"
478 - placeholder="*" maxlength="9">
479 - </div>
480 - <div class="scheduler-schedule-field">
481 - <span class="scheduler-schedule-label">Day</span>
482 - <input type="text" x-model="editingTask.schedule.day"
483 - placeholder="*" maxlength="9">
484 - </div>
485 - <div class="scheduler-schedule-field">
486 - <span class="scheduler-schedule-label">Month</span>
487 - <input type="text" x-model="editingTask.schedule.month"
488 - placeholder="*" maxlength="9">
489 - </div>
490 - <div class="scheduler-schedule-field">
491 - <span class="scheduler-schedule-label">Weekday</span>
492 - <input type="text" x-model="editingTask.schedule.weekday"
493 - placeholder="*" maxlength="9">
494 - </div>
495 - </div>
496 - </div>
497 -
498 - <!-- Plan (for planned tasks) -->
499 - <div class="scheduler-form-field full-width"
500 - x-show="editingTask.type === 'planned'">
501 - <div class="label-help-wrapper">
502 - <label class="scheduler-form-label">Plan</label>
503 - <div class="scheduler-form-help">Specific execution times for this task
504 - </div>
505 - </div>
506 - <div class="scheduler-plan-builder">
507 - <div class="scheduler-plan-todo">
508 - <span class="scheduler-plan-label">Upcoming Executions</span>
509 - <div class="scheduler-todo-list">
510 - <template
511 - x-if="editingTask.plan && Array.isArray(editingTask.plan.todo) && editingTask.plan.todo.length > 0">
512 - <template x-for="(time, index) in editingTask.plan.todo"
513 - :key="index">
514 - <div class="scheduler-todo-item">
515 - <span x-text="formatDate(time)"></span>
516 - <button
517 - @click.prevent="editingTask.plan.todo.splice(index, 1)"
518 - class="scheduler-todo-remove">
519 - <svg xmlns="http://www.w3.org/2000/svg"
520 - width="16" height="16" viewBox="0 0 24 24"
521 - fill="none" stroke="currentColor"
522 - stroke-width="2" stroke-linecap="round"
523 - stroke-linejoin="round">
524 - <line x1="18" y1="6" x2="6" y2="18"></line>
525 - <line x1="6" y1="6" x2="18" y2="18"></line>
526 - </svg>
527 - </button>
528 - </div>
529 - </template>
530 - </template>
531 - <template
532 - x-if="!editingTask.plan || !Array.isArray(editingTask.plan.todo) || editingTask.plan.todo.length === 0">
533 - <div class="scheduler-empty-plan">
534 - No scheduled execution times yet. Add one below.
535 - </div>
536 - </template>
537 - <div class="scheduler-add-todo">
538 - <!-- Create form planned task input -->
539 - <input type="text" id="newPlannedTime-create"
540 - x-ref="plannedTimeCreate"
541 - class="scheduler-flatpickr-input"
542 - placeholder="Select date and time">
543 - <!-- Create Task Form Add Time Button -->
544 - <button @click.prevent="
545 - const input = $refs.plannedTimeCreate;
546 - if (!input) {
547 - console.error('Input reference not found for plannedTimeCreate');
548 - return;
549 - }
550 -
551 - // Ensure plan structure exists
552 - if (!editingTask.plan) {
553 - editingTask.plan = { todo: [], in_progress: null, done: [] };
554 - }
555 - if (!Array.isArray(editingTask.plan.todo)) {
556 - editingTask.plan.todo = [];
557 - }
558 -
559 - // Get date from Flatpickr if available
560 - let selectedDate;
561 - if (input._flatpickr && input._flatpickr.selectedDates.length > 0) {
562 - selectedDate = input._flatpickr.selectedDates[0];
563 - } else if (input.value) {
564 - selectedDate = new Date(input.value);
565 - }
566 -
567 - if (!selectedDate || isNaN(selectedDate.getTime())) {
568 - alert('Please select a valid date and time');
569 - return;
570 - }
571 -
572 - // Convert to ISO string and add to plan
573 - editingTask.plan.todo.push(selectedDate.toISOString());
574 -
575 - // Sort by date (earliest first)
576 - editingTask.plan.todo.sort();
577 -
578 - // Clear the input
579 - if (input._flatpickr) {
580 - input._flatpickr.clear();
581 - } else {
582 - input.value = '';
583 - }
584 - " class="scheduler-add-todo-button">
585 - <svg xmlns="http://www.w3.org/2000/svg" width="16"
586 - height="16" viewBox="0 0 24 24" fill="none"
587 - stroke="currentColor" stroke-width="2"
588 - stroke-linecap="round" stroke-linejoin="round"
589 - style="margin-right: 4px;">
590 - <line x1="12" y1="5" x2="12" y2="19"></line>
591 - <line x1="5" y1="12" x2="19" y2="12"></line>
592 - </svg>
593 - Add Time
594 - </button>
595 - </div>
596 - </div>
597 - </div>
598 - </div>
599 - </div>
600 -
601 - <!-- Token (for ad-hoc tasks) -->
602 - <div class="scheduler-form-field full-width"
603 - x-show="editingTask.type === 'adhoc'">
604 - <div class="label-help-wrapper">
605 - <label class="scheduler-form-label">Token</label>
606 - <div class="scheduler-form-help">Token used to trigger this task
607 - externally</div>
608 - </div>
609 - <div class="input-group">
610 - <input type="text" x-model="editingTask.token"
611 - placeholder="Token for ad-hoc task">
612 - <button class="scheduler-task-action"
613 - @click="editingTask.token = generateRandomToken()">
614 - Generate
615 - </button>
616 - </div>
617 - </div>
618 -
619 - <!-- System Prompt -->
620 - <div class="scheduler-form-field full-width">
621 - <div class="label-help-wrapper">
622 - <label class="scheduler-form-label">System Prompt</label>
623 - <div class="scheduler-form-help">System-level instructions for the
624 - assistant</div>
625 - </div>
626 - <textarea x-model="editingTask.system_prompt"
627 - placeholder="System instructions for the AI"></textarea>
628 - </div>
629 -
630 - <!-- User Prompt -->
631 - <div class="scheduler-form-field full-width">
632 - <div class="label-help-wrapper">
633 - <label class="scheduler-form-label">User Prompt</label>
634 - <div class="scheduler-form-help">The main task prompt that will be
635 - executed</div>
636 - </div>
637 - <textarea x-model="editingTask.prompt"
638 - placeholder="User message for the AI"></textarea>
639 - </div>
640 -
641 - <!-- Attachments Field -->
642 - <div class="scheduler-form-field full-width">
643 - <div class="label-help-wrapper">
644 - <label class="scheduler-form-label">Attachments</label>
645 - <div class="scheduler-form-help">Container file paths or URLs, one per
646 - line</div>
647 - </div>
648 - <textarea x-model="attachmentsText"
649 - placeholder="Enter file paths or URLs, one per line"></textarea>
650 - </div>
651 - </div>
652 - </div>
653 -
654 - <!-- Edit Task Form -->
655 - <div class="scheduler-form" x-show="isEditing">
656 - <div class="scheduler-form-header">
657 - <div class="scheduler-form-title">Edit Task</div>
658 - <div class="scheduler-form-actions">
659 - <button class="btn btn-ok btn-field" @click="saveTask()">
660 - Save
661 - </button>
662 - <button class="btn btn-cancel" @click="cancelEdit()">
663 - Cancel
664 - </button>
665 - </div>
666 - </div>
667 -
668 - <div class="scheduler-form-grid">
669 - <!-- Task Name -->
670 - <div class="scheduler-form-field">
671 - <div class="label-help-wrapper">
672 - <label class="scheduler-form-label">Task Name</label>
673 - <div class="scheduler-form-help">A unique name to identify this task
674 - </div>
675 - </div>
676 - <input type="text" x-model="editingTask.name" placeholder="Enter task name">
677 - </div>
678 -
679 - <!-- Task Type (disabled when editing) -->
680 - <div class="scheduler-form-field">
681 - <div class="label-help-wrapper">
682 - <label class="scheduler-form-label">Task Type</label>
683 - <div class="scheduler-form-help">Task type cannot be changed after
684 - creation</div>
685 - </div>
686 - <select x-model="editingTask.type" disabled>
687 - <option value="scheduled">Scheduled Task</option>
688 - <option value="adhoc">Ad-hoc Task</option>
689 - <option value="planned">Planned Task</option>
690 - </select>
691 - </div>
692 -
693 - <div class="scheduler-form-field">
694 - <div class="label-help-wrapper">
695 - <label class="scheduler-form-label">Project</label>
696 - <div class="scheduler-form-help" x-text="editingTask.dedicated_context ? 'Dedicated tasks inherit the active chat project.' : 'Shared tasks mirror the project assigned to their context.'"></div>
697 - </div>
698 - <div class="project-display">
699 - <span class="project-color-ball" :style="editingTask.project?.color ? { backgroundColor: editingTask.project.color } : { border: '1px solid var(--color-border)' }"></span>
700 - <span x-text="formatProjectLabel(editingTask.project)"></span>
701 - </div>
702 - </div>
703 -
704 - <!-- Task State in Edit Form - Add after Task Type -->
705 - <div class="scheduler-form-field" x-show="isEditing">
706 - <div class="label-help-wrapper">
707 - <label class="scheduler-form-label">State</label>
708 - <div class="scheduler-form-help">Change the task's state</div>
709 - </div>
710 - <div>
711 - <div class="scheduler-state-selector">
712 - <span class="scheduler-status-badge scheduler-status-idle"
713 - :class="{'scheduler-status-selected': editingTask.state === 'idle'}"
714 - @click="editingTask.state = 'idle'">idle</span>
715 - <span class="scheduler-status-badge scheduler-status-running"
716 - :class="{'scheduler-status-selected': editingTask.state === 'running'}"
717 - @click="editingTask.state = 'running'">running</span>
718 - <span class="scheduler-status-badge scheduler-status-disabled"
719 - :class="{'scheduler-status-selected': editingTask.state === 'disabled'}"
720 - @click="editingTask.state = 'disabled'">disabled</span>
721 - <span class="scheduler-status-badge scheduler-status-error"
722 - :class="{'scheduler-status-selected': editingTask.state === 'error'}"
723 - @click="editingTask.state = 'error'">error</span>
724 - </div>
725 - <div class="scheduler-state-explanation">
726 - <span x-show="editingTask.state === 'idle'"><strong>idle</strong>:
727 - ready to run</span>
728 - <span
729 - x-show="editingTask.state === 'running'"><strong>running</strong>:
730 - currently executing</span>
731 - <span
732 - x-show="editingTask.state === 'disabled'"><strong>disabled</strong>:
733 - won't execute automatically</span>
734 - <span x-show="editingTask.state === 'error'"><strong>error</strong>:
735 - task encountered an error</span>
736 - </div>
737 - </div>
738 - </div>
739 -
740 - <!-- Schedule (for scheduled tasks) -->
741 - <div class="scheduler-form-field full-width"
742 - x-show="editingTask && editingTask.type === 'scheduled'">
743 - <div class="label-help-wrapper">
744 - <label class="scheduler-form-label">Schedule (Cron Expression)</label>
745 - <div class="scheduler-form-help">Format: minute hour day month weekday
746 - (e.g., "* * * * *" for every minute)</div>
747 - </div>
748 - <div class="scheduler-schedule-builder">
749 - <div class="scheduler-schedule-field">
750 - <span class="scheduler-schedule-label">Minute</span>
751 - <input type="text" x-model="editingTask.schedule.minute"
752 - placeholder="*" maxlength="9">
753 - </div>
754 - <div class="scheduler-schedule-field">
755 - <span class="scheduler-schedule-label">Hour</span>
756 - <input type="text" x-model="editingTask.schedule.hour"
757 - placeholder="*" maxlength="9">
758 - </div>
759 - <div class="scheduler-schedule-field">
760 - <span class="scheduler-schedule-label">Day</span>
761 - <input type="text" x-model="editingTask.schedule.day"
762 - placeholder="*" maxlength="9">
763 - </div>
764 - <div class="scheduler-schedule-field">
765 - <span class="scheduler-schedule-label">Month</span>
766 - <input type="text" x-model="editingTask.schedule.month"
767 - placeholder="*" maxlength="9">
768 - </div>
769 - <div class="scheduler-schedule-field">
770 - <span class="scheduler-schedule-label">Weekday</span>
771 - <input type="text" x-model="editingTask.schedule.weekday"
772 - placeholder="*" maxlength="9">
773 - </div>
774 - </div>
775 - </div>
776 -
777 - <!-- Plan (for planned tasks) -->
778 - <div class="scheduler-form-field full-width"
779 - x-show="editingTask.type === 'planned'">
780 - <div class="label-help-wrapper">
781 - <label class="scheduler-form-label">Plan</label>
782 - <div class="scheduler-form-help">Specific execution times for this task
783 - </div>
784 - </div>
785 - <div class="scheduler-plan-builder">
786 - <div class="scheduler-plan-todo">
787 - <span class="scheduler-plan-label">Upcoming Executions</span>
788 - <div class="scheduler-todo-list">
789 - <template
790 - x-if="editingTask.plan && Array.isArray(editingTask.plan.todo) && editingTask.plan.todo.length > 0">
791 - <template x-for="(time, index) in editingTask.plan.todo"
792 - :key="index">
793 - <div class="scheduler-todo-item">
794 - <span x-text="formatDate(time)"></span>
795 - <button
796 - @click.prevent="editingTask.plan.todo.splice(index, 1)"
797 - class="scheduler-todo-remove">
798 - <svg xmlns="http://www.w3.org/2000/svg"
799 - width="16" height="16" viewBox="0 0 24 24"
800 - fill="none" stroke="currentColor"
801 - stroke-width="2" stroke-linecap="round"
802 - stroke-linejoin="round">
803 - <line x1="18" y1="6" x2="6" y2="18"></line>
804 - <line x1="6" y1="6" x2="18" y2="18"></line>
805 - </svg>
806 - </button>
807 - </div>
808 - </template>
809 - </template>
810 - <template
811 - x-if="!editingTask.plan || !Array.isArray(editingTask.plan.todo) || editingTask.plan.todo.length === 0">
812 - <div class="scheduler-empty-plan">
813 - No scheduled execution times yet. Add one below.
814 - </div>
815 - </template>
816 - <div class="scheduler-add-todo">
817 - <!-- Edit form planned task input -->
818 - <input type="text" id="newPlannedTime-edit"
819 - x-ref="plannedTimeEdit"
820 - class="scheduler-flatpickr-input"
821 - placeholder="Select date and time">
822 - <!-- Edit Task Form Add Time Button -->
823 - <button @click.prevent="
824 - const input = $refs.plannedTimeEdit;
825 - if (!input) {
826 - console.error('Input reference not found for plannedTimeEdit');
827 - return;
828 - }
829 -
830 - // Ensure plan structure exists
831 - if (!editingTask.plan) {
832 - editingTask.plan = { todo: [], in_progress: null, done: [] };
833 - }
834 - if (!Array.isArray(editingTask.plan.todo)) {
835 - editingTask.plan.todo = [];
836 - }
837 -
838 - // Get date from Flatpickr if available
839 - let selectedDate;
840 - if (input._flatpickr && input._flatpickr.selectedDates.length > 0) {
841 - selectedDate = input._flatpickr.selectedDates[0];
842 - } else if (input.value) {
843 - selectedDate = new Date(input.value);
844 - }
845 -
846 - if (!selectedDate || isNaN(selectedDate.getTime())) {
847 - alert('Please select a valid date and time');
848 - return;
849 - }
850 -
851 - // Convert to ISO string and add to plan
852 - editingTask.plan.todo.push(selectedDate.toISOString());
853 -
854 - // Sort by date (earliest first)
855 - editingTask.plan.todo.sort();
856 -
857 - // Clear the input
858 - if (input._flatpickr) {
859 - input._flatpickr.clear();
860 - } else {
861 - input.value = '';
862 - }
863 - " class="scheduler-add-todo-button">
864 - <svg xmlns="http://www.w3.org/2000/svg" width="16"
865 - height="16" viewBox="0 0 24 24" fill="none"
866 - stroke="currentColor" stroke-width="2"
867 - stroke-linecap="round" stroke-linejoin="round"
868 - style="margin-right: 4px;">
869 - <line x1="12" y1="5" x2="12" y2="19"></line>
870 - <line x1="5" y1="12" x2="19" y2="12"></line>
871 - </svg>
872 - Add Time
873 - </button>
874 - </div>
875 - </div>
876 - </div>
877 - </div>
878 - </div>
879 -
880 - <!-- Token (for ad-hoc tasks) -->
881 - <div class="scheduler-form-field full-width"
882 - x-show="editingTask.type === 'adhoc'">
883 - <div class="label-help-wrapper">
884 - <label class="scheduler-form-label">Token</label>
885 - <div class="scheduler-form-help">Token used to trigger this task
886 - externally</div>
887 - </div>
888 - <div class="input-group">
889 - <input type="text" x-model="editingTask.token"
890 - placeholder="Token for ad-hoc task">
891 - <button class="scheduler-task-action"
892 - @click="editingTask.token = generateRandomToken()">
893 - Generate
894 - </button>
895 - </div>
896 - </div>
897 -
898 - <!-- System Prompt -->
899 - <div class="scheduler-form-field full-width">
900 - <div class="label-help-wrapper">
901 - <label class="scheduler-form-label">System Prompt</label>
902 - <div class="scheduler-form-help">System-level instructions for the
903 - assistant</div>
904 - </div>
905 - <textarea x-model="editingTask.system_prompt"
906 - placeholder="System instructions for the AI"></textarea>
907 - </div>
908 -
909 - <!-- User Prompt -->
910 - <div class="scheduler-form-field full-width">
911 - <div class="label-help-wrapper">
912 - <label class="scheduler-form-label">User Prompt</label>
913 - <div class="scheduler-form-help">The main task prompt that will be
914 - executed</div>
915 - </div>
916 - <textarea x-model="editingTask.prompt"
917 - placeholder="User message for the AI"></textarea>
918 - </div>
919 -
920 - <!-- Attachments Field -->
921 - <div class="scheduler-form-field full-width">
922 - <div class="label-help-wrapper">
923 - <label class="scheduler-form-label">Attachments</label>
924 - <div class="scheduler-form-help">Container file paths or URLs, one per
925 - line</div>
926 - </div>
927 - <textarea x-model="attachmentsText"
928 - placeholder="Enter file paths or URLs, one per line"></textarea>
929 - </div>
930 - </div>
931 - </div>
932 -
933 - <!-- Task List View -->
934 - <div class="scheduler-container"
935 - x-show="!isCreating && !isEditing && viewMode === 'list'">
936 - <!-- Header with Actions -->
937 - <div class="scheduler-header">
938 - <h2>Task Management</h2>
939 - <div class="scheduler-actions">
940 - <button class="btn btn-ok" @click="startCreateTask()">
941 - New Task
942 - </button>
943 - </div>
944 - </div>
945 -
946 - <!-- Filters -->
947 - <div class="scheduler-filters">
948 - <div class="scheduler-filter-group">
949 - <span class="scheduler-filter-label">Type:</span>
950 - <select class="scheduler-filter-select" x-model="filterType">
951 - <option value="all">All Types</option>
952 - <option value="scheduled">Scheduled</option>
953 - <option value="adhoc">Ad-hoc</option>
954 - <option value="planned">Planned</option>
955 - </select>
956 - </div>
957 -
958 - <div class="scheduler-filter-group">
959 - <span class="scheduler-filter-label">State:</span>
960 - <select class="scheduler-filter-select" x-model="filterState">
961 - <option value="all">All States</option>
962 - <option value="idle">Idle</option>
963 - <option value="running">Running</option>
964 - <option value="disabled">Disabled</option>
965 - <option value="error">Error</option>
966 - </select>
967 - </div>
968 - </div>
969 -
970 - <!-- Loading State -->
971 - <!-- <div class="scheduler-loading" x-show="isLoading">
972 - Loading tasks...
973 - </div> -->
974 -
975 - <!-- Empty State -->
976 - <div class="scheduler-empty" x-show="!isLoading && filteredTasks.length === 0"
977 - x-effect="$el.style.display = (!isLoading && filteredTasks.length === 0) ? '' : 'none'">
978 - <!-- <div class="scheduler-empty-icon">📋</div> -->
979 - <div class="scheduler-empty-text">No tasks found</div>
980 - <button class="btn btn-ok" @click="startCreateTask()">Create your first
981 - task</button>
982 - </div>
983 -
984 - <!-- Task List Table -->
985 - <table class="scheduler-task-list" x-show="!isLoading && filteredTasks.length > 0"
986 - x-effect="$el.style.display = (!isLoading && filteredTasks.length > 0) ? '' : 'none'">
987 - <thead>
988 - <tr>
989 - <th @click="changeSort('name')">
990 - Name
991 - <span class="scheduler-sort-indicator" x-show="sortField === 'name'"
992 - :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
993 - </th>
994 - <th @click="changeSort('state')">
995 - State
996 - <span class="scheduler-sort-indicator"
997 - x-show="sortField === 'state'"
998 - :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
999 - </th>
1000 - <th>Type</th>
1001 - <th>Project</th>
1002 - <th>Schedule</th>
1003 - <th @click="changeSort('last_run')">
1004 - Last Run
1005 - <span class="scheduler-sort-indicator"
1006 - x-show="sortField === 'last_run'"
1007 - :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
1008 - </th>
1009 - <th>Actions</th>
1010 - </tr>
1011 - </thead>
1012 - <tbody>
1013 - <template x-for="task in filteredTasks" :key="task.uuid">
1014 - <tr @click="showTaskDetail(task.uuid)">
1015 - <td>
1016 - <span x-text="task.name"></span>
1017 - </td>
1018 - <td>
1019 - <span class="scheduler-status-badge"
1020 - :class="getStateBadgeClass(task.state)"
1021 - x-text="task.state"></span>
1022 - </td>
1023 - <td x-text="task.type"></td>
1024 - <td>
1025 - <span class="project-color-ball"
1026 - :style="extractTaskProject(task)?.color ? { backgroundColor: extractTaskProject(task).color } : { border: '1px solid var(--color-border)' }"></span>
1027 - <span x-text="formatTaskProject(task)"></span>
1028 - </td>
1029 - <td>
1030 - <span x-show="task.type === 'scheduled'"
1031 - x-text="formatSchedule(task)"></span>
1032 - <span x-show="task.type === 'adhoc'"
1033 - class="scheduler-no-schedule">—</span>
1034 - <span x-show="task.type === 'planned'"
1035 - x-html="formatPlan(task).replace(/\n/g, '<br>')"></span>
1036 - </td>
1037 - <td x-text="formatDate(task.last_run)"></td>
1038 - <td @click.stop>
1039 - <div class="scheduler-task-actions">
1040 - <button class="scheduler-task-action"
1041 - @click="runTask(task.uuid)" title="Run Task">
1042 - <svg xmlns="http://www.w3.org/2000/svg"
1043 - viewBox="0 0 24 24" fill="currentColor" width="16"
1044 - height="16">
1045 - <path d="M8 5v14l11-7z" />
1046 - </svg>
1047 - </button>
1048 - <button class="scheduler-task-action"
1049 - @click="resetTaskState(task.uuid)" title="Reset State">
1050 - <svg xmlns="http://www.w3.org/2000/svg"
1051 - viewBox="0 0 24 24" fill="currentColor" width="16"
1052 - height="16">
1053 - <path
1054 - d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
1055 - </svg>
1056 - </button>
1057 - <button class="scheduler-task-action"
1058 - @click="startEditTask(task.uuid)" title="Edit Task">
1059 - <svg xmlns="http://www.w3.org/2000/svg"
1060 - viewBox="0 0 24 24" fill="currentColor" width="16"
1061 - height="16">
1062 - <path
1063 - d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
1064 - </svg>
1065 - </button>
1066 - <button class="scheduler-task-action"
1067 - @click="deleteTask(task.uuid)" title="Delete Task">
1068 - <svg xmlns="http://www.w3.org/2000/svg"
1069 - viewBox="0 0 24 24" fill="currentColor" width="16"
1070 - height="16">
1071 - <path
1072 - d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
1073 - </svg>
1074 - </button>
1075 - </div>
1076 - </td>
1077 - </tr>
1078 - </template>
1079 - </tbody>
1080 - </table>
1081 - </div>
1082 -
1083 - <!-- Task Detail View -->
1084 - <div class="scheduler-detail-view"
1085 - x-show="!isCreating && !isEditing && viewMode === 'detail' && selectedTaskForDetail">
1086 - <div class="scheduler-detail-header">
1087 - <h2 class="scheduler-detail-title"
1088 - x-text="selectedTaskForDetail ? selectedTaskForDetail.name : ''"></h2>
1089 - <div class="scheduler-status-badge"
1090 - :class="selectedTaskForDetail ? getStateBadgeClass(selectedTaskForDetail.state) : ''"
1091 - x-text="selectedTaskForDetail ? selectedTaskForDetail.state : ''">
1092 - </div>
1093 - <button class="btn btn-cancel" @click="closeTaskDetail()">Close</button>
1094 - </div>
1095 -
1096 - <div class="scheduler-detail-content">
1097 - <div class="scheduler-details-grid">
1098 - <div class="scheduler-details-label">Type:</div>
1099 - <div class="scheduler-details-value"
1100 - x-text="selectedTaskForDetail ? selectedTaskForDetail.type : ''"></div>
1101 -
1102 - <div class="scheduler-details-label">Project:</div>
1103 - <div class="scheduler-details-value">
1104 - <span class="project-color-ball"
1105 - :style="extractTaskProject(selectedTaskForDetail)?.color ? { backgroundColor: extractTaskProject(selectedTaskForDetail).color } : { border: '1px solid var(--color-border)' }"></span>
1106 - <span x-text="selectedTaskForDetail ? formatTaskProject(selectedTaskForDetail) : 'No Project'"></span>
1107 - </div>
1108 -
1109 - <div class="scheduler-details-label">Created:</div>
1110 - <div class="scheduler-details-value"
1111 - x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.created_at) : ''">
1112 - </div>
1113 -
1114 - <div class="scheduler-details-label">Last Updated:</div>
1115 - <div class="scheduler-details-value"
1116 - x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.updated_at) : ''">
1117 - </div>
1118 -
1119 - <div class="scheduler-details-label">Last Run:</div>
1120 - <div class="scheduler-details-value"
1121 - x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.last_run) : ''">
1122 - </div>
1123 -
1124 - <div class="scheduler-details-label"
1125 - x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'scheduled'">
1126 - Schedule:</div>
1127 - <div class="scheduler-details-value"
1128 - x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'scheduled'"
1129 - x-text="selectedTaskForDetail ? formatSchedule(selectedTaskForDetail) : ''">
1130 - </div>
1131 -
1132 - <div class="scheduler-details-label"
1133 - x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'">
1134 - Token:</div>
1135 - <div class="scheduler-details-value"
1136 - x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'adhoc'"
1137 - x-text="selectedTaskForDetail ? selectedTaskForDetail.token : ''"></div>
1138 -
1139 - <div class="scheduler-details-label"
1140 - x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'planned'">
1141 - Plan:</div>
1142 - <div class="scheduler-details-value"
1143 - x-show="selectedTaskForDetail && selectedTaskForDetail.type === 'planned'">
1144 - <div x-show="selectedTaskForDetail && selectedTaskForDetail.plan">
1145 - <div><strong>Upcoming:</strong></div>
1146 - <template
1147 - x-if="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.todo && selectedTaskForDetail.plan.todo.length > 0">
1148 - <div>
1149 - <template
1150 - x-for="(time, index) in selectedTaskForDetail.plan.todo"
1151 - :key="index">
1152 - <div x-text="formatDate(time)"></div>
1153 - </template>
1154 - </div>
1155 - </template>
1156 - <template
1157 - x-if="!selectedTaskForDetail || !selectedTaskForDetail.plan || !selectedTaskForDetail.plan.todo || selectedTaskForDetail.plan.todo.length === 0">
1158 - <div>No upcoming executions</div>
1159 - </template>
1160 -
1161 - <div><strong>In Progress:</strong></div>
1162 - <div
1163 - x-text="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.in_progress ? formatDate(selectedTaskForDetail.plan.in_progress) : 'None'">
1164 - </div>
1165 -
1166 - <div><strong>Completed:</strong></div>
1167 - <template
1168 - x-if="selectedTaskForDetail && selectedTaskForDetail.plan && selectedTaskForDetail.plan.done && selectedTaskForDetail.plan.done.length > 0">
1169 - <div>
1170 - <template
1171 - x-for="(time, index) in selectedTaskForDetail.plan.done"
1172 - :key="index">
1173 - <div x-text="formatDate(time)"></div>
1174 - </template>
1175 - </div>
1176 - </template>
1177 - <template
1178 - x-if="!selectedTaskForDetail || !selectedTaskForDetail.plan || !selectedTaskForDetail.plan.done || selectedTaskForDetail.plan.done.length === 0">
1179 - <div>No completed executions</div>
1180 - </template>
1181 - </div>
1182 - </div>
1183 -
1184 - <div class="scheduler-details-label">Last Result:</div>
1185 - <div class="scheduler-details-value"
1186 - x-text="selectedTaskForDetail && selectedTaskForDetail.last_result ? selectedTaskForDetail.last_result : 'No results yet'">
1187 - </div>
1188 -
1189 - <div class="scheduler-details-label">System Prompt:</div>
1190 - <div class="scheduler-details-value"
1191 - x-text="selectedTaskForDetail ? selectedTaskForDetail.system_prompt : ''">
1192 - </div>
1193 -
1194 - <div class="scheduler-details-label">User Prompt:</div>
1195 - <div class="scheduler-details-value"
1196 - x-text="selectedTaskForDetail ? selectedTaskForDetail.prompt : ''">
1197 - </div>
1198 -
1199 - <div class="scheduler-details-label">Attachments:</div>
1200 - <div class="scheduler-details-value">
1201 - <template
1202 - x-if="selectedTaskForDetail && selectedTaskForDetail.attachments && selectedTaskForDetail.attachments.length > 0">
1203 - <div>
1204 - <template
1205 - x-for="(attachment, index) in selectedTaskForDetail.attachments"
1206 - :key="index">
1207 - <div x-text="attachment"></div>
1208 - </template>
1209 - </div>
1210 - </template>
1211 - <template
1212 - x-if="!selectedTaskForDetail || !selectedTaskForDetail.attachments || selectedTaskForDetail.attachments.length === 0">
1213 - <div>No attachments</div>
1214 - </template>
1215 - </div>
1216 - </div>
1217 - </div>
1218 - </div>
1219 - </div>
1220 - </div>
1221 - </div>
1222 -
1223 - <div class="modal-footer">
1224 - <div id="buttons-container">
1225 - <template x-for="button in settings.buttons" :key="button.id">
1226 - <button :class="button.classes" @click="handleButton(button.id)"
1227 - x-text="button.title"></button>
1228 - </template>
111 </div>
1230 - </div>
1231 - </div>
1232 - </div>
1233 - </template>
1234 - </div>
112
113 <!-- Drag and Drop Overlay Component -->
114 <x-component path="chat/attachments/dragDropOverlay.html"></x-component>
webui/index.js
-58
@@ -631,61 +631,3 @@ document.addEventListener("DOMContentLoaded", function () {
631 * - Both lists are sorted by creation time (newest first)
632 * - Tasks use the same context system as chats for communication with the backend
633 */
634 -
635 -// Open the scheduler detail view for a specific task
636 -function openTaskDetail(taskId) {
637 - // Wait for Alpine.js to be fully loaded
638 - if (globalThis.Alpine) {
639 - // Get the settings modal button and click it to ensure all init logic happens
640 - const settingsButton = document.getElementById("settings");
641 - if (settingsButton) {
642 - // Programmatically click the settings button
643 - settingsButton.click();
644 -
645 - // Now get a reference to the modal element
646 - const modalEl = document.getElementById("settingsModal");
647 - if (!modalEl) {
648 - console.error("Settings modal element not found after clicking button");
649 - return;
650 - }
651 -
652 - // Get the Alpine.js data for the modal
653 - const modalData = globalThis.Alpine ? Alpine.$data(modalEl) : null;
654 -
655 - // Use a timeout to ensure the modal is fully rendered
656 - setTimeout(() => {
657 - // Switch to the scheduler tab first
658 - modalData.switchTab("scheduler");
659 -
660 - // Use another timeout to ensure the scheduler component is initialized
661 - setTimeout(() => {
662 - // Get the scheduler component
663 - const schedulerComponent = document.querySelector(
664 - '[x-data="schedulerSettings"]'
665 - );
666 - if (!schedulerComponent) {
667 - console.error("Scheduler component not found");
668 - return;
669 - }
670 -
671 - // Get the Alpine.js data for the scheduler component
672 - const schedulerData = globalThis.Alpine
673 - ? Alpine.$data(schedulerComponent)
674 - : null;
675 -
676 - // Show the task detail view for the specific task
677 - schedulerData.showTaskDetail(taskId);
678 -
679 - console.log("Task detail view opened for task:", taskId);
680 - }, 50); // Give time for the scheduler tab to initialize
681 - }, 25); // Give time for the modal to render
682 - } else {
683 - console.error("Settings button not found");
684 - }
685 - } else {
686 - console.error("Alpine.js not loaded");
687 - }
688 -}
689 -
690 -// Make the function available globally
691 -globalThis.openTaskDetail = openTaskDetail;
webui/js/scheduler.js deleted
-1835
@@ -1,1835 +0,0 @@
1 -/**
2 - * Task Scheduler Component for Settings Modal
3 - * Manages scheduled and ad-hoc tasks through a dedicated settings tab
4 - */
5 -
6 -import { formatDateTime, getUserTimezone } from './time-utils.js';
7 -import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"
8 -import { store as notificationsStore } from "/components/notifications/notification-store.js"
9 -import { store as projectsStore } from "/components/projects/projects-store.js"
10 -
11 -// Ensure the showToast function is available
12 -// if (typeof window.showToast !== 'function') {
13 -// window.showToast = function(message, type = 'info') {
14 -// console.log(`[Toast ${type}]: ${message}`);
15 -// // Create toast element if not already present
16 -// let toastContainer = document.getElementById('toast-container');
17 -// if (!toastContainer) {
18 -// toastContainer = document.createElement('div');
19 -// toastContainer.id = 'toast-container';
20 -// toastContainer.style.position = 'fixed';
21 -// toastContainer.style.bottom = '20px';
22 -// toastContainer.style.right = '20px';
23 -// toastContainer.style.zIndex = '9999';
24 -// document.body.appendChild(toastContainer);
25 -// }
26 -
27 -// // Create the toast
28 -// const toast = document.createElement('div');
29 -// toast.className = `toast toast-${type}`;
30 -// toast.style.padding = '10px 15px';
31 -// toast.style.margin = '5px 0';
32 -// toast.style.backgroundColor = type === 'error' ? '#f44336' :
33 -// type === 'success' ? '#4CAF50' :
34 -// type === 'warning' ? '#ff9800' : '#2196F3';
35 -// toast.style.color = 'white';
36 -// toast.style.borderRadius = '4px';
37 -// toast.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
38 -// toast.style.width = 'auto';
39 -// toast.style.maxWidth = '300px';
40 -// toast.style.wordWrap = 'break-word';
41 -
42 -// toast.innerHTML = message;
43 -
44 -// // Add to container
45 -// toastContainer.appendChild(toast);
46 -
47 -// // Auto remove after 3 seconds
48 -// setTimeout(() => {
49 -// if (toast.parentNode) {
50 -// toast.style.opacity = '0';
51 -// toast.style.transition = 'opacity 0.5s ease';
52 -// setTimeout(() => {
53 -// if (toast.parentNode) {
54 -// toast.parentNode.removeChild(toast);
55 -// }
56 -// }, 500);
57 -// }
58 -// }, 3000);
59 -// };
60 -// }
61 -
62 -// Add this near the top of the scheduler.js file, outside of any function
63 -const showToast = function(message, type = 'info') {
64 - // Use new frontend notification system
65 - switch (type.toLowerCase()) {
66 - case 'error':
67 - return notificationsStore.frontendError(message, "Scheduler", 5);
68 - case 'success':
69 - return notificationsStore.frontendInfo(message, "Scheduler", 3);
70 - case 'warning':
71 - return notificationsStore.frontendWarning(message, "Scheduler", 4);
72 - case 'info':
73 - default:
74 - return notificationsStore.frontendInfo(message, "Scheduler", 3);
75 - }
76 -};
77 -
78 -// Define the full component implementation
79 -const fullComponentImplementation = function() {
80 - return {
81 - tasks: [],
82 - isLoading: true,
83 - selectedTask: null,
84 - expandedTaskId: null,
85 - sortField: 'name',
86 - sortDirection: 'asc',
87 - filterType: 'all', // all, scheduled, adhoc, planned
88 - filterState: 'all', // all, idle, running, disabled, error
89 - pollingInterval: null,
90 - pollingActive: false, // Track if polling is currently active
91 - editingTask: {
92 - name: '',
93 - type: 'scheduled',
94 - state: 'idle',
95 - schedule: {
96 - minute: '*',
97 - hour: '*',
98 - day: '*',
99 - month: '*',
100 - weekday: '*',
101 - timezone: getUserTimezone()
102 - },
103 - token: '',
104 - plan: {
105 - todo: [],
106 - in_progress: null,
107 - done: []
108 - },
109 - system_prompt: '',
110 - prompt: '',
111 - attachments: [],
112 - project: null,
113 - dedicated_context: true,
114 - },
115 - projectOptions: [],
116 - selectedProjectSlug: '',
117 - isCreating: false,
118 - isEditing: false,
119 - showLoadingState: false,
120 - viewMode: 'list', // Controls whether to show list or detail view
121 - selectedTaskForDetail: null, // Task object for detail view
122 - attachmentsText: '',
123 - filteredTasks: [],
124 - hasNoTasks: true, // Add explicit reactive property
125 -
126 - // Initialize the component
127 - init() {
128 - // Initialize component data
129 - this.tasks = [];
130 - this.isLoading = true;
131 - this.hasNoTasks = true; // Add explicit reactive property
132 - this.filterType = 'all';
133 - this.filterState = 'all';
134 - this.sortField = 'name';
135 - this.sortDirection = 'asc';
136 - this.pollingInterval = null;
137 - this.pollingActive = false;
138 -
139 - // Start polling for tasks
140 - this.startPolling();
141 -
142 - // Refresh initial data
143 - this.fetchTasks();
144 -
145 - // Set up event handler for tab selection to ensure view is refreshed when tab becomes visible
146 - document.addEventListener('click', (event) => {
147 - // Check if a tab was clicked
148 - const clickedTab = event.target.closest('.settings-tab');
149 - if (clickedTab && clickedTab.getAttribute('data-tab') === 'scheduler') {
150 - setTimeout(() => {
151 - this.fetchTasks();
152 - }, 100);
153 - }
154 - });
155 -
156 - // Watch for changes to the tasks array to update UI
157 - this.$watch('tasks', (newTasks) => {
158 - this.updateTasksUI();
159 - });
160 -
161 - this.$watch('filterType', () => {
162 - this.updateTasksUI();
163 - });
164 -
165 - this.$watch('filterState', () => {
166 - this.updateTasksUI();
167 - });
168 -
169 - // Set up default configuration
170 - this.viewMode = localStorage.getItem('scheduler_view_mode') || 'list';
171 - this.selectedTask = null;
172 - this.expandedTaskId = null;
173 - this.editingTask = {
174 - name: '',
175 - type: 'scheduled',
176 - state: 'idle',
177 - schedule: {
178 - minute: '*',
179 - hour: '*',
180 - day: '*',
181 - month: '*',
182 - weekday: '*',
183 - timezone: getUserTimezone()
184 - },
185 - token: this.generateRandomToken ? this.generateRandomToken() : '',
186 - plan: {
187 - todo: [],
188 - in_progress: null,
189 - done: []
190 - },
191 - system_prompt: '',
192 - prompt: '',
193 - attachments: [],
194 - project: null,
195 - dedicated_context: true,
196 - };
197 - this.refreshProjectOptions();
198 -
199 - // Initialize Flatpickr for date/time pickers after Alpine is fully initialized
200 - this.$nextTick(() => {
201 - // Wait until DOM is updated
202 - setTimeout(() => {
203 - if (this.isCreating) {
204 - this.initFlatpickr('create');
205 - } else if (this.isEditing) {
206 - this.initFlatpickr('edit');
207 - }
208 - }, 100);
209 - });
210 -
211 - // Cleanup on component destruction
212 - this.$cleanup = () => {
213 - console.log('Cleaning up schedulerSettings component');
214 - this.stopPolling();
215 -
216 - // Clean up any Flatpickr instances
217 - const createInput = document.getElementById('newPlannedTime-create');
218 - if (createInput && createInput._flatpickr) {
219 - createInput._flatpickr.destroy();
220 - }
221 -
222 - const editInput = document.getElementById('newPlannedTime-edit');
223 - if (editInput && editInput._flatpickr) {
224 - editInput._flatpickr.destroy();
225 - }
226 - };
227 - },
228 -
229 - // Start polling for task updates
230 - startPolling() {
231 - // Don't start if already polling
232 - if (this.pollingInterval) {
233 - console.log('Polling already active, not starting again');
234 - return;
235 - }
236 -
237 - console.log('Starting task polling');
238 - this.pollingActive = true;
239 -
240 - // Fetch immediately, then set up interval for every 2 seconds
241 - this.fetchTasks();
242 - this.pollingInterval = setInterval(() => {
243 - if (this.pollingActive) {
244 - this.fetchTasks();
245 - }
246 - }, 2000); // Poll every 2 seconds as requested
247 - },
248 -
249 - // Stop polling when tab is inactive
250 - stopPolling() {
251 - console.log('Stopping task polling');
252 - this.pollingActive = false;
253 -
254 - if (this.pollingInterval) {
255 - clearInterval(this.pollingInterval);
256 - this.pollingInterval = null;
257 - }
258 - },
259 -
260 - // Fetch tasks from API
261 - async fetchTasks() {
262 - // Don't fetch if polling is inactive (prevents race conditions)
263 - if (!this.pollingActive && this.pollingInterval) {
264 - return;
265 - }
266 -
267 - // Don't fetch while creating/editing a task
268 - if (this.isCreating || this.isEditing) {
269 - return;
270 - }
271 -
272 - this.isLoading = true;
273 - try {
274 - const response = await fetchApi('/scheduler_tasks_list', {
275 - method: 'POST',
276 - headers: {
277 - 'Content-Type': 'application/json'
278 - },
279 - body: JSON.stringify({
280 - timezone: getUserTimezone()
281 - })
282 - });
283 -
284 - if (!response.ok) {
285 - throw new Error('Failed to fetch tasks');
286 - }
287 -
288 - const data = await response.json();
289 -
290 - // Check if data.tasks exists and is an array
291 - if (!data || !data.tasks) {
292 - console.error('Invalid response: data.tasks is missing', data);
293 - this.tasks = [];
294 - } else if (!Array.isArray(data.tasks)) {
295 - console.error('Invalid response: data.tasks is not an array', data.tasks);
296 - this.tasks = [];
297 - } else {
298 - // Verify each task has necessary properties
299 - const validTasks = data.tasks.filter(task => {
300 - if (!task || typeof task !== 'object') {
301 - console.error('Invalid task (not an object):', task);
302 - return false;
303 - }
304 - if (!task.uuid) {
305 - console.error('Task missing uuid:', task);
306 - return false;
307 - }
308 - if (!task.name) {
309 - console.error('Task missing name:', task);
310 - return false;
311 - }
312 - if (!task.type) {
313 - console.error('Task missing type:', task);
314 - return false;
315 - }
316 - return true;
317 - });
318 -
319 - if (validTasks.length !== data.tasks.length) {
320 - console.warn(`Filtered out ${data.tasks.length - validTasks.length} invalid tasks`);
321 - }
322 -
323 - this.tasks = validTasks;
324 -
325 - // Update UI using the shared function
326 - this.updateTasksUI();
327 - }
328 - } catch (error) {
329 - console.error('Error fetching tasks:', error);
330 - // Only show toast for errors on manual refresh, not during polling
331 - if (!this.pollingInterval) {
332 - showToast('Failed to fetch tasks: ' + error.message, 'error');
333 - }
334 - // Reset tasks to empty array on error
335 - this.tasks = [];
336 - } finally {
337 - this.isLoading = false;
338 - }
339 - },
340 -
341 - // Change sort field/direction
342 - changeSort(field) {
343 - if (this.sortField === field) {
344 - // Toggle direction if already sorting by this field
345 - this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
346 - } else {
347 - // Set new sort field and default to ascending
348 - this.sortField = field;
349 - this.sortDirection = 'asc';
350 - }
351 - },
352 -
353 - // Toggle expanded task row
354 - toggleTaskExpand(taskId) {
355 - if (this.expandedTaskId === taskId) {
356 - this.expandedTaskId = null;
357 - } else {
358 - this.expandedTaskId = taskId;
359 - }
360 - },
361 -
362 - // Show task detail view
363 - showTaskDetail(taskId) {
364 - const task = this.tasks.find(t => t.uuid === taskId);
365 - if (!task) {
366 - showToast('Task not found', 'error');
367 - return;
368 - }
369 -
370 - // Create a copy of the task to avoid modifying the original
371 - this.selectedTaskForDetail = JSON.parse(JSON.stringify(task));
372 -
373 - // Ensure attachments is always an array
374 - if (!this.selectedTaskForDetail.attachments) {
375 - this.selectedTaskForDetail.attachments = [];
376 - }
377 -
378 - this.viewMode = 'detail';
379 - },
380 -
381 - // Close detail view and return to list
382 - closeTaskDetail() {
383 - this.selectedTaskForDetail = null;
384 - this.viewMode = 'list';
385 - },
386 -
387 - // Format date for display
388 - formatDate(dateString) {
389 - if (!dateString) return 'Never';
390 - return formatDateTime(dateString, 'full');
391 - },
392 -
393 - // Format plan for display
394 - formatPlan(task) {
395 - if (!task || !task.plan) return 'No plan';
396 -
397 - const todoCount = Array.isArray(task.plan.todo) ? task.plan.todo.length : 0;
398 - const inProgress = task.plan.in_progress ? 'Yes' : 'No';
399 - const doneCount = Array.isArray(task.plan.done) ? task.plan.done.length : 0;
400 -
401 - let nextRun = '';
402 - if (Array.isArray(task.plan.todo) && task.plan.todo.length > 0) {
403 - try {
404 - const nextTime = new Date(task.plan.todo[0]);
405 -
406 - // Verify it's a valid date before formatting
407 - if (!isNaN(nextTime.getTime())) {
408 - nextRun = formatDateTime(nextTime, 'short');
409 - } else {
410 - nextRun = 'Invalid date';
411 - console.warn(`Invalid date format in plan.todo[0]: ${task.plan.todo[0]}`);
412 - }
413 - } catch (error) {
414 - console.error(`Error formatting next run time: ${error.message}`);
415 - nextRun = 'Error';
416 - }
417 - } else {
418 - nextRun = 'None';
419 - }
420 -
421 - return `Next: ${nextRun}\nTodo: ${todoCount}\nIn Progress: ${inProgress}\nDone: ${doneCount}`;
422 - },
423 -
424 - // Format schedule for display
425 - formatSchedule(task) {
426 - if (!task.schedule) return 'None';
427 -
428 - let schedule = '';
429 - if (typeof task.schedule === 'string') {
430 - schedule = task.schedule;
431 - } else if (typeof task.schedule === 'object') {
432 - // Display only the cron parts, not the timezone
433 - schedule = `${task.schedule.minute || '*'} ${task.schedule.hour || '*'} ${task.schedule.day || '*'} ${task.schedule.month || '*'} ${task.schedule.weekday || '*'}`;
434 - }
435 -
436 - return schedule;
437 - },
438 -
439 - // Get CSS class for state badge
440 - getStateBadgeClass(state) {
441 - switch (state) {
442 - case 'idle': return 'scheduler-status-idle';
443 - case 'running': return 'scheduler-status-running';
444 - case 'disabled': return 'scheduler-status-disabled';
445 - case 'error': return 'scheduler-status-error';
446 - default: return '';
447 - }
448 - },
449 -
450 - deriveActiveProject() {
451 - const selected = chatsStore?.selectedContext || null;
452 - if (!selected || !selected.project) {
453 - return null;
454 - }
455 -
456 - const project = selected.project;
457 - return {
458 - name: project.name || null,
459 - title: project.title || project.name || null,
460 - color: project.color || '',
461 - };
462 - },
463 -
464 - formatProjectName(project) {
465 - if (!project) {
466 - return 'No Project';
467 - }
468 - const title = project.title || project.name;
469 - return title || 'No Project';
470 - },
471 -
472 - formatProjectLabel(project) {
473 - return `Project: ${this.formatProjectName(project)}`;
474 - },
475 -
476 - async refreshProjectOptions() {
477 - try {
478 - if (!Array.isArray(projectsStore.projectList) || !projectsStore.projectList.length) {
479 - if (typeof projectsStore.loadProjectsList === 'function') {
480 - await projectsStore.loadProjectsList();
481 - }
482 - }
483 - } catch (error) {
484 - console.warn('schedulerSettings: failed to load project list', error);
485 - }
486 -
487 - const list = Array.isArray(projectsStore.projectList) ? projectsStore.projectList : [];
488 - this.projectOptions = list.map((proj) => ({
489 - name: proj.name,
490 - title: proj.title || proj.name,
491 - color: proj.color || '',
492 - }));
493 - },
494 -
495 - onProjectSelect(slug) {
496 - this.selectedProjectSlug = slug || '';
497 - if (!slug) {
498 - this.editingTask.project = null;
499 - return;
500 - }
501 -
502 - const option = this.projectOptions.find((item) => item.name === slug);
503 - if (option) {
504 - this.editingTask.project = { ...option };
505 - } else {
506 - this.editingTask.project = {
507 - name: slug,
508 - title: slug,
509 - color: '',
510 - };
511 - }
512 - },
513 -
514 - extractTaskProject(task) {
515 - if (!task) {
516 - return null;
517 - }
518 -
519 - const slug = task.project_name || null;
520 - const project = task.project || {};
521 - const title = project.name || slug;
522 - const color = task.project_color || project.color || '';
523 -
524 - if (!slug && !title) {
525 - return null;
526 - }
527 -
528 - return {
529 - name: slug,
530 - title: title || slug,
531 - color: color,
532 - };
533 - },
534 -
535 - formatTaskProject(task) {
536 - return this.formatProjectName(this.extractTaskProject(task));
537 - },
538 -
539 - // Create a new task
540 - async startCreateTask() {
541 - this.isCreating = true;
542 - this.isEditing = false;
543 - document.querySelector('[x-data="schedulerSettings"]')?.setAttribute('data-editing-state', 'creating');
544 - await this.refreshProjectOptions();
545 - const activeProject = this.deriveActiveProject();
546 - let initialProject = activeProject ? { ...activeProject } : null;
547 - if (!initialProject && this.projectOptions.length > 0) {
548 - initialProject = { ...this.projectOptions[0] };
549 - }
550 -
551 - this.editingTask = {
552 - name: '',
553 - type: 'scheduled', // Default to scheduled
554 - state: 'idle', // Initialize with idle state
555 - schedule: {
556 - minute: '*',
557 - hour: '*',
558 - day: '*',
559 - month: '*',
560 - weekday: '*',
561 - timezone: getUserTimezone()
562 - },
563 - token: this.generateRandomToken(), // Generate token even for scheduled tasks to prevent undefined errors
564 - plan: { // Initialize plan for all task types to prevent undefined errors
565 - todo: [],
566 - in_progress: null,
567 - done: []
568 - },
569 - system_prompt: '',
570 - prompt: '',
571 - attachments: [], // Always initialize as an empty array
572 - project: initialProject,
573 - dedicated_context: true,
574 - };
575 - this.selectedProjectSlug = initialProject && initialProject.name ? initialProject.name : '';
576 -
577 - // Set up Flatpickr after the component is visible
578 - this.$nextTick(() => {
579 - this.initFlatpickr('create');
580 - });
581 - },
582 -
583 - // Edit an existing task
584 - async startEditTask(taskId) {
585 - const task = this.tasks.find(t => t.uuid === taskId);
586 - if (!task) {
587 - showToast('Task not found', 'error');
588 - return;
589 - }
590 -
591 - this.isCreating = false;
592 - this.isEditing = true;
593 - document.querySelector('[x-data="schedulerSettings"]')?.setAttribute('data-editing-state', 'editing');
594 -
595 - // Create a deep copy to avoid modifying the original
596 - this.editingTask = JSON.parse(JSON.stringify(task));
597 - const projectSlug = task.project_name || null;
598 - const projectDisplay = (task.project && task.project.name) || projectSlug;
599 - const projectColor = task.project_color || (task.project ? task.project.color : '') || '';
600 - this.editingTask.project = projectSlug || projectDisplay ? {
601 - name: projectSlug,
602 - title: projectDisplay,
603 - color: projectColor,
604 - } : null;
605 - this.editingTask.dedicated_context = !!task.dedicated_context;
606 - this.selectedProjectSlug = this.editingTask.project && this.editingTask.project.name ? this.editingTask.project.name : '';
607 -
608 - // Debug log
609 - console.log('Task data for editing:', task);
610 - console.log('Attachments from task:', task.attachments);
611 -
612 - // Ensure state is set with a default if missing
613 - if (!this.editingTask.state) this.editingTask.state = 'idle';
614 -
615 - // Always initialize schedule to prevent UI errors
616 - // All task types need this structure for the form to work properly
617 - if (!this.editingTask.schedule || typeof this.editingTask.schedule === 'string') {
618 - let scheduleObj = {
619 - minute: '*',
620 - hour: '*',
621 - day: '*',
622 - month: '*',
623 - weekday: '*',
624 - timezone: getUserTimezone()
625 - };
626 -
627 - // If it's a string, parse it
628 - if (typeof this.editingTask.schedule === 'string') {
629 - const parts = this.editingTask.schedule.split(' ');
630 - if (parts.length >= 5) {
631 - scheduleObj.minute = parts[0] || '*';
632 - scheduleObj.hour = parts[1] || '*';
633 - scheduleObj.day = parts[2] || '*';
634 - scheduleObj.month = parts[3] || '*';
635 - scheduleObj.weekday = parts[4] || '*';
636 - }
637 - }
638 -
639 - this.editingTask.schedule = scheduleObj;
640 - } else {
641 - // Ensure timezone exists in the schedule
642 - if (!this.editingTask.schedule.timezone) {
643 - this.editingTask.schedule.timezone = getUserTimezone();
644 - }
645 - }
646 -
647 - // Ensure attachments is always an array
648 - if (!this.editingTask.attachments) {
649 - this.editingTask.attachments = [];
650 - } else if (typeof this.editingTask.attachments === 'string') {
651 - // Handle case where attachments might be stored as a string
652 - this.editingTask.attachments = this.editingTask.attachments
653 - .split('\n')
654 - .map(line => line.trim())
655 - .filter(line => line.length > 0);
656 - } else if (!Array.isArray(this.editingTask.attachments)) {
657 - // If not an array or string, set to empty array
658 - this.editingTask.attachments = [];
659 - }
660 -
661 - // Ensure appropriate properties are initialized based on task type
662 - if (this.editingTask.type === 'scheduled') {
663 - // Initialize token for scheduled tasks to prevent undefined errors if UI accesses it
664 - if (!this.editingTask.token) {
665 - this.editingTask.token = '';
666 - }
667 -
668 - // Initialize plan stub for scheduled tasks to prevent undefined errors
669 - if (!this.editingTask.plan) {
670 - this.editingTask.plan = {
671 - todo: [],
672 - in_progress: null,
673 - done: []
674 - };
675 - }
676 - } else if (this.editingTask.type === 'adhoc') {
677 - // Initialize token if it doesn't exist
678 - if (!this.editingTask.token) {
679 - this.editingTask.token = this.generateRandomToken();
680 - console.log('Generated new token for adhoc task:', this.editingTask.token);
681 - }
682 -
683 - console.log('Setting token for adhoc task:', this.editingTask.token);
684 -
685 - // Initialize plan stub for adhoc tasks to prevent undefined errors
686 - if (!this.editingTask.plan) {
687 - this.editingTask.plan = {
688 - todo: [],
689 - in_progress: null,
690 - done: []
691 - };
692 - }
693 - } else if (this.editingTask.type === 'planned') {
694 - // Initialize plan if it doesn't exist
695 - if (!this.editingTask.plan) {
696 - this.editingTask.plan = {
697 - todo: [],
698 - in_progress: null,
699 - done: []
700 - };
701 - }
702 -
703 - // Ensure todo is an array
704 - if (!Array.isArray(this.editingTask.plan.todo)) {
705 - this.editingTask.plan.todo = [];
706 - }
707 -
708 - // Initialize token to prevent undefined errors
709 - if (!this.editingTask.token) {
710 - this.editingTask.token = '';
711 - }
712 - }
713 -
714 - // Set up Flatpickr after the component is visible and task data is loaded
715 - this.$nextTick(() => {
716 - this.initFlatpickr('edit');
717 - });
718 - },
719 -
720 - // Cancel editing
721 - cancelEdit() {
722 - // Clean up Flatpickr instances
723 - const destroyFlatpickr = (inputId) => {
724 - const input = document.getElementById(inputId);
725 - if (input && input._flatpickr) {
726 - console.log(`Destroying Flatpickr instance for ${inputId}`);
727 - input._flatpickr.destroy();
728 -
729 - // Also remove any wrapper elements that might have been created
730 - const wrapper = input.closest('.scheduler-flatpickr-wrapper');
731 - if (wrapper && wrapper.parentNode) {
732 - // Move the input back to its original position
733 - wrapper.parentNode.insertBefore(input, wrapper);
734 - // Remove the wrapper
735 - wrapper.parentNode.removeChild(wrapper);
736 - }
737 -
738 - // Remove any added classes
739 - input.classList.remove('scheduler-flatpickr-input');
740 - }
741 - };
742 -
743 - if (this.isCreating) {
744 - destroyFlatpickr('newPlannedTime-create');
745 - } else if (this.isEditing) {
746 - destroyFlatpickr('newPlannedTime-edit');
747 - }
748 -
749 - // Reset to initial state but keep default values to prevent errors
750 - this.editingTask = {
751 - name: '',
752 - type: 'scheduled',
753 - state: 'idle', // Initialize with idle state
754 - schedule: {
755 - minute: '*',
756 - hour: '*',
757 - day: '*',
758 - month: '*',
759 - weekday: '*',
760 - timezone: getUserTimezone()
761 - },
762 - token: '',
763 - plan: { // Initialize plan for planned tasks
764 - todo: [],
765 - in_progress: null,
766 - done: []
767 - },
768 - system_prompt: '',
769 - prompt: '',
770 - attachments: [], // Always initialize as an empty array
771 - project: null,
772 - dedicated_context: true,
773 - };
774 - this.selectedProjectSlug = '';
775 - this.isCreating = false;
776 - this.isEditing = false;
777 - document.querySelector('[x-data="schedulerSettings"]')?.removeAttribute('data-editing-state');
778 - },
779 -
780 - // Save task (create new or update existing)
781 - async saveTask() {
782 - // Validate task data
783 - if (!this.editingTask.name.trim() || !this.editingTask.prompt.trim()) {
784 - // showToast('Task name and prompt are required', 'error');
785 - alert('Task name and prompt are required');
786 - return;
787 - }
788 -
789 - try {
790 - let apiEndpoint, taskData;
791 -
792 - // Prepare task data
793 - taskData = {
794 - name: this.editingTask.name,
795 - system_prompt: this.editingTask.system_prompt || '',
796 - prompt: this.editingTask.prompt || '',
797 - state: this.editingTask.state || 'idle', // Include state in task data
798 - timezone: getUserTimezone()
799 - };
800 -
801 - if (this.isCreating && this.editingTask.project) {
802 - if (this.editingTask.project.name) {
803 - taskData.project_name = this.editingTask.project.name;
804 - }
805 - if (this.editingTask.project.color) {
806 - taskData.project_color = this.editingTask.project.color;
807 - }
808 - }
809 -
810 - // Process attachments - now always stored as array
811 - taskData.attachments = Array.isArray(this.editingTask.attachments)
812 - ? this.editingTask.attachments
813 - .map(line => typeof line === 'string' ? line.trim() : line)
814 - .filter(line => line && line.trim().length > 0)
815 - : [];
816 -
817 - // Handle task type specific data
818 - if (this.editingTask.type === 'scheduled') {
819 - // Ensure schedule is properly formatted as an object
820 - if (typeof this.editingTask.schedule === 'string') {
821 - // Parse string schedule into object
822 - const parts = this.editingTask.schedule.split(' ');
823 - taskData.schedule = {
824 - minute: parts[0] || '*',
825 - hour: parts[1] || '*',
826 - day: parts[2] || '*',
827 - month: parts[3] || '*',
828 - weekday: parts[4] || '*',
829 - timezone: getUserTimezone() // Add timezone to schedule object
830 - };
831 - } else {
832 - // Use object schedule directly but ensure timezone is included
833 - taskData.schedule = {
834 - ...this.editingTask.schedule,
835 - timezone: this.editingTask.schedule.timezone || getUserTimezone()
836 - };
837 - }
838 - // Don't send token or plan for scheduled tasks
839 - delete taskData.token;
840 - delete taskData.plan;
841 - } else if (this.editingTask.type === 'adhoc') {
842 - // Ad-hoc task with token
843 - // Ensure token is a non-empty string, generate a new one if needed
844 - if (!this.editingTask.token) {
845 - this.editingTask.token = this.generateRandomToken();
846 - console.log('Generated new token for adhoc task:', this.editingTask.token);
847 - }
848 -
849 - console.log('Setting token in taskData:', this.editingTask.token);
850 - taskData.token = this.editingTask.token;
851 -
852 - // Don't send schedule or plan for adhoc tasks
853 - delete taskData.schedule;
854 - delete taskData.plan;
855 - } else if (this.editingTask.type === 'planned') {
856 - // Planned task with plan
857 - // Make sure plan exists and has required properties
858 - if (!this.editingTask.plan) {
859 - this.editingTask.plan = {
860 - todo: [],
861 - in_progress: null,
862 - done: []
863 - };
864 - }
865 -
866 - // Ensure todo and done are arrays
867 - if (!Array.isArray(this.editingTask.plan.todo)) {
868 - this.editingTask.plan.todo = [];
869 - }
870 -
871 - if (!Array.isArray(this.editingTask.plan.done)) {
872 - this.editingTask.plan.done = [];
873 - }
874 -
875 - // Validate each date in the todo list to ensure it's a valid ISO string
876 - const validatedTodo = [];
877 - for (const dateStr of this.editingTask.plan.todo) {
878 - try {
879 - const date = new Date(dateStr);
880 - if (!isNaN(date.getTime())) {
881 - validatedTodo.push(date.toISOString());
882 - } else {
883 - console.warn(`Skipping invalid date in todo list: ${dateStr}`);
884 - }
885 - } catch (error) {
886 - console.warn(`Error processing date: ${error.message}`);
887 - }
888 - }
889 -
890 - // Replace with validated list
891 - this.editingTask.plan.todo = validatedTodo;
892 -
893 - // Sort the todo items by date (earliest first)
894 - this.editingTask.plan.todo.sort();
895 -
896 - // Set the plan in taskData
897 - taskData.plan = {
898 - todo: this.editingTask.plan.todo,
899 - in_progress: this.editingTask.plan.in_progress,
900 - done: this.editingTask.plan.done || []
901 - };
902 -
903 - // Log the plan data for debugging
904 - console.log('Planned task plan data:', JSON.stringify(taskData.plan, null, 2));
905 -
906 - // Don't send schedule or token for planned tasks
907 - delete taskData.schedule;
908 - delete taskData.token;
909 - }
910 -
911 - // Determine if creating or updating
912 - if (this.isCreating) {
913 - apiEndpoint = '/scheduler_task_create';
914 - } else {
915 - apiEndpoint = '/scheduler_task_update';
916 - taskData.task_id = this.editingTask.uuid;
917 - }
918 -
919 - // Debug: Log the final task data being sent
920 - console.log('Final task data being sent to API:', JSON.stringify(taskData, null, 2));
921 -
922 - // Make API request
923 - const response = await fetchApi(apiEndpoint, {
924 - method: 'POST',
925 - headers: {
926 - 'Content-Type': 'application/json'
927 - },
928 - body: JSON.stringify(taskData)
929 - });
930 -
931 - if (!response.ok) {
932 - const errorData = await response.json();
933 - throw new Error(errorData.error || 'Failed to save task');
934 - }
935 -
936 - // Parse response data to get the created/updated task
937 - const responseData = await response.json();
938 -
939 - // Show success message
940 - showToast(this.isCreating ? 'Task created successfully' : 'Task updated successfully', 'success');
941 -
942 - // Immediately update the UI if the response includes the task
943 - if (responseData && responseData.task) {
944 - console.log('Task received in response:', responseData.task);
945 -
946 - // Update the tasks array
947 - if (this.isCreating) {
948 - // For new tasks, add to the array
949 - this.tasks = [...this.tasks, responseData.task];
950 - } else {
951 - // For updated tasks, replace the existing one
952 - this.tasks = this.tasks.map(t =>
953 - t.uuid === responseData.task.uuid ? responseData.task : t
954 - );
955 - }
956 -
957 - // Update UI using the shared function
958 - this.updateTasksUI();
959 - } else {
960 - // Fallback to fetching tasks if no task in response
961 - await this.fetchTasks();
962 - }
963 -
964 - // Clean up Flatpickr instances
965 - const destroyFlatpickr = (inputId) => {
966 - const input = document.getElementById(inputId);
967 - if (input && input._flatpickr) {
968 - input._flatpickr.destroy();
969 - }
970 - };
971 -
972 - if (this.isCreating) {
973 - destroyFlatpickr('newPlannedTime-create');
974 - } else if (this.isEditing) {
975 - destroyFlatpickr('newPlannedTime-edit');
976 - }
977 -
978 - // Reset task data and form state
979 - this.editingTask = {
980 - name: '',
981 - type: 'scheduled',
982 - state: 'idle',
983 - schedule: {
984 - minute: '*',
985 - hour: '*',
986 - day: '*',
987 - month: '*',
988 - weekday: '*',
989 - timezone: getUserTimezone()
990 - },
991 - token: '',
992 - plan: {
993 - todo: [],
994 - in_progress: null,
995 - done: []
996 - },
997 - system_prompt: '',
998 - prompt: '',
999 - attachments: [],
1000 - project: null,
1001 - dedicated_context: true,
1002 - };
1003 - this.isCreating = false;
1004 - this.isEditing = false;
1005 - document.querySelector('[x-data="schedulerSettings"]')?.removeAttribute('data-editing-state');
1006 - } catch (error) {
1007 - console.error('Error saving task:', error);
1008 - showToast('Failed to save task: ' + error.message, 'error');
1009 - }
1010 - },
1011 -
1012 - // Run a task
1013 - async runTask(taskId) {
1014 - try {
1015 - const response = await fetchApi('/scheduler_task_run', {
1016 - method: 'POST',
1017 - headers: {
1018 - 'Content-Type': 'application/json'
1019 - },
1020 - body: JSON.stringify({
1021 - task_id: taskId,
1022 - timezone: getUserTimezone()
1023 - })
1024 - });
1025 -
1026 - const data = await response.json();
1027 -
1028 - if (!response.ok) {
1029 - throw new Error(data?.error || 'Failed to run task');
1030 - }
1031 -
1032 - const toastMessage = data.warning || data.message || 'Task started successfully';
1033 - const toastType = data.warning ? 'warning' : 'success';
1034 - showToast(toastMessage, toastType);
1035 -
1036 - // Refresh task list
1037 - this.fetchTasks();
1038 - } catch (error) {
1039 - console.error('Error running task:', error);
1040 - showToast('Failed to run task: ' + error.message, 'error');
1041 - }
1042 - },
1043 -
1044 - // Reset a task's state
1045 - async resetTaskState(taskId) {
1046 - try {
1047 - const task = this.tasks.find(t => t.uuid === taskId);
1048 - if (!task) {
1049 - showToast('Task not found', 'error');
1050 - return;
1051 - }
1052 -
1053 - // Check if task is already in idle state
1054 - if (task.state === 'idle') {
1055 - showToast('Task is already in idle state', 'info');
1056 - return;
1057 - }
1058 -
1059 - this.showLoadingState = true;
1060 -
1061 - // Call API to update the task state
1062 - const response = await fetchApi('/scheduler_task_update', {
1063 - method: 'POST',
1064 - headers: {
1065 - 'Content-Type': 'application/json'
1066 - },
1067 - body: JSON.stringify({
1068 - task_id: taskId,
1069 - state: 'idle', // Always reset to idle state
1070 - timezone: getUserTimezone()
1071 - })
1072 - });
1073 -
1074 - if (!response.ok) {
1075 - const errorData = await response.json();
1076 - throw new Error(errorData.error || 'Failed to reset task state');
1077 - }
1078 -
1079 - showToast('Task state reset to idle', 'success');
1080 -
1081 - // Refresh task list
1082 - await this.fetchTasks();
1083 - this.showLoadingState = false;
1084 - } catch (error) {
1085 - console.error('Error resetting task state:', error);
1086 - showToast('Failed to reset task state: ' + error.message, 'error');
1087 - this.showLoadingState = false;
1088 - }
1089 - },
1090 -
1091 - // Delete a task
1092 - async deleteTask(taskId) {
1093 - // Confirm deletion
1094 - if (!confirm('Are you sure you want to delete this task? This action cannot be undone.')) {
1095 - return;
1096 - }
1097 -
1098 - try {
1099 -
1100 - // if we delete selected context, switch to another first
1101 - await chatsStore.switchFromContext(taskId);
1102 -
1103 - const response = await fetchApi('/scheduler_task_delete', {
1104 - method: 'POST',
1105 - headers: {
1106 - 'Content-Type': 'application/json'
1107 - },
1108 - body: JSON.stringify({
1109 - task_id: taskId,
1110 - timezone: getUserTimezone()
1111 - })
1112 - });
1113 -
1114 - if (!response.ok) {
1115 - const errorData = await response.json();
1116 - throw new Error(errorData.error || 'Failed to delete task');
1117 - }
1118 -
1119 - showToast('Task deleted successfully', 'success');
1120 -
1121 - // If we were viewing the detail of the deleted task, close the detail view
1122 - if (this.selectedTaskForDetail && this.selectedTaskForDetail.uuid === taskId) {
1123 - this.closeTaskDetail();
1124 - }
1125 -
1126 - // Immediately update UI without waiting for polling
1127 - this.tasks = this.tasks.filter(t => t.uuid !== taskId);
1128 -
1129 - // Update UI using the shared function
1130 - this.updateTasksUI();
1131 - } catch (error) {
1132 - console.error('Error deleting task:', error);
1133 - showToast('Failed to delete task: ' + error.message, 'error');
1134 - }
1135 - },
1136 -
1137 - // Initialize datetime input with default value (30 minutes from now)
1138 - initDateTimeInput(event) {
1139 - if (!event.target.value) {
1140 - const now = new Date();
1141 - now.setMinutes(now.getMinutes() + 30);
1142 -
1143 - // Format as YYYY-MM-DDThh:mm
1144 - const year = now.getFullYear();
1145 - const month = String(now.getMonth() + 1).padStart(2, '0');
1146 - const day = String(now.getDate()).padStart(2, '0');
1147 - const hours = String(now.getHours()).padStart(2, '0');
1148 - const minutes = String(now.getMinutes()).padStart(2, '0');
1149 -
1150 - event.target.value = `${year}-${month}-${day}T${hours}:${minutes}`;
1151 -
1152 - // If using Flatpickr, update it as well
1153 - if (event.target._flatpickr) {
1154 - event.target._flatpickr.setDate(event.target.value);
1155 - }
1156 - }
1157 - },
1158 -
1159 - // Generate a random token for ad-hoc tasks
1160 - generateRandomToken() {
1161 - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1162 - let token = '';
1163 - for (let i = 0; i < 16; i++) {
1164 - token += characters.charAt(Math.floor(Math.random() * characters.length));
1165 - }
1166 - return token;
1167 - },
1168 -
1169 - // Getter for filtered tasks
1170 - get filteredTasks() {
1171 - // Make sure we have tasks to filter
1172 - if (!Array.isArray(this.tasks)) {
1173 - console.warn('Tasks is not an array:', this.tasks);
1174 - return [];
1175 - }
1176 -
1177 - let filtered = [...this.tasks];
1178 -
1179 - // Apply type filter with case-insensitive comparison
1180 - if (this.filterType && this.filterType !== 'all') {
1181 - filtered = filtered.filter(task => {
1182 - if (!task.type) return false;
1183 - return String(task.type).toLowerCase() === this.filterType.toLowerCase();
1184 - });
1185 - }
1186 -
1187 - // Apply state filter with case-insensitive comparison
1188 - if (this.filterState && this.filterState !== 'all') {
1189 - filtered = filtered.filter(task => {
1190 - if (!task.state) return false;
1191 - return String(task.state).toLowerCase() === this.filterState.toLowerCase();
1192 - });
1193 - }
1194 -
1195 - // Sort the filtered tasks
1196 - return this.sortTasks(filtered);
1197 - },
1198 -
1199 - // Sort the tasks based on sort field and direction
1200 - sortTasks(tasks) {
1201 - if (!Array.isArray(tasks) || tasks.length === 0) {
1202 - return tasks;
1203 - }
1204 -
1205 - return [...tasks].sort((a, b) => {
1206 - if (!this.sortField) return 0;
1207 -
1208 - const fieldA = a[this.sortField];
1209 - const fieldB = b[this.sortField];
1210 -
1211 - // Handle cases where fields might be undefined
1212 - if (fieldA === undefined && fieldB === undefined) return 0;
1213 - if (fieldA === undefined) return 1;
1214 - if (fieldB === undefined) return -1;
1215 -
1216 - // For dates, convert to timestamps
1217 - if (this.sortField === 'createdAt' || this.sortField === 'updatedAt') {
1218 - const dateA = new Date(fieldA).getTime();
1219 - const dateB = new Date(fieldB).getTime();
1220 - return this.sortDirection === 'asc' ? dateA - dateB : dateB - dateA;
1221 - }
1222 -
1223 - // For string comparisons
1224 - if (typeof fieldA === 'string' && typeof fieldB === 'string') {
1225 - return this.sortDirection === 'asc'
1226 - ? fieldA.localeCompare(fieldB)
1227 - : fieldB.localeCompare(fieldA);
1228 - }
1229 -
1230 - // For numerical comparisons
1231 - return this.sortDirection === 'asc' ? fieldA - fieldB : fieldB - fieldA;
1232 - });
1233 - },
1234 -
1235 - // Computed property for attachments text representation
1236 - get attachmentsText() {
1237 - // Ensure we always have an array to work with
1238 - const attachments = Array.isArray(this.editingTask.attachments)
1239 - ? this.editingTask.attachments
1240 - : [];
1241 -
1242 - // Join array items with newlines
1243 - return attachments.join('\n');
1244 - },
1245 -
1246 - // Setter for attachments text - preserves empty lines during editing
1247 - set attachmentsText(value) {
1248 - if (typeof value === 'string') {
1249 - // Just split by newlines without filtering to preserve editing experience
1250 - this.editingTask.attachments = value.split('\n');
1251 - } else {
1252 - // Fallback to empty array if not a string
1253 - this.editingTask.attachments = [];
1254 - }
1255 - },
1256 -
1257 - // Debug method to test filtering logic
1258 - testFiltering() {
1259 - console.group('SchedulerSettings Debug: Filter Test');
1260 - console.log('Current Filter Settings:');
1261 - console.log('- Filter Type:', this.filterType);
1262 - console.log('- Filter State:', this.filterState);
1263 - console.log('- Sort Field:', this.sortField);
1264 - console.log('- Sort Direction:', this.sortDirection);
1265 -
1266 - // Check if tasks is an array
1267 - if (!Array.isArray(this.tasks)) {
1268 - console.error('ERROR: this.tasks is not an array!', this.tasks);
1269 - console.groupEnd();
1270 - return;
1271 - }
1272 -
1273 - console.log(`Raw Tasks (${this.tasks.length}):`, this.tasks);
1274 -
1275 - // Test filtering by type
1276 - console.group('Filter by Type Test');
1277 - ['all', 'adhoc', 'scheduled', 'recurring'].forEach(type => {
1278 - const filtered = this.tasks.filter(task =>
1279 - type === 'all' ||
1280 - (task.type && String(task.type).toLowerCase() === type)
1281 - );
1282 - console.log(`Type "${type}": ${filtered.length} tasks`, filtered);
1283 - });
1284 - console.groupEnd();
1285 -
1286 - // Test filtering by state
1287 - console.group('Filter by State Test');
1288 - ['all', 'idle', 'running', 'completed', 'failed'].forEach(state => {
1289 - const filtered = this.tasks.filter(task =>
1290 - state === 'all' ||
1291 - (task.state && String(task.state).toLowerCase() === state)
1292 - );
1293 - console.log(`State "${state}": ${filtered.length} tasks`, filtered);
1294 - });
1295 - console.groupEnd();
1296 -
1297 - // Show current filtered tasks
1298 - console.log('Current Filtered Tasks:', this.filteredTasks);
1299 -
1300 - console.groupEnd();
1301 - },
1302 -
1303 - // New comprehensive debug method
1304 - debugTasks() {
1305 - console.group('SchedulerSettings Comprehensive Debug');
1306 -
1307 - // Component state
1308 - console.log('Component State:');
1309 - console.log({
1310 - filterType: this.filterType,
1311 - filterState: this.filterState,
1312 - sortField: this.sortField,
1313 - sortDirection: this.sortDirection,
1314 - isLoading: this.isLoading,
1315 - isEditing: this.isEditing,
1316 - isCreating: this.isCreating,
1317 - viewMode: this.viewMode
1318 - });
1319 -
1320 - // Tasks validation
1321 - if (!this.tasks) {
1322 - console.error('ERROR: this.tasks is undefined or null!');
1323 - console.groupEnd();
1324 - return;
1325 - }
1326 -
1327 - if (!Array.isArray(this.tasks)) {
1328 - console.error('ERROR: this.tasks is not an array!', typeof this.tasks, this.tasks);
1329 - console.groupEnd();
1330 - return;
1331 - }
1332 -
1333 - // Raw tasks
1334 - console.group('Raw Tasks');
1335 - console.log(`Count: ${this.tasks.length}`);
1336 - if (this.tasks.length > 0) {
1337 - console.table(this.tasks.map(t => ({
1338 - uuid: t.uuid,
1339 - name: t.name,
1340 - type: t.type,
1341 - state: t.state
1342 - })));
1343 -
1344 - // Inspect first task in detail
1345 - console.log('First Task Structure:', JSON.stringify(this.tasks[0], null, 2));
1346 - } else {
1347 - console.log('No tasks available');
1348 - }
1349 - console.groupEnd();
1350 -
1351 - // Filtered tasks
1352 - console.group('Filtered Tasks');
1353 - const filteredTasks = this.filteredTasks;
1354 - console.log(`Count: ${filteredTasks.length}`);
1355 - if (filteredTasks.length > 0) {
1356 - console.table(filteredTasks.map(t => ({
1357 - uuid: t.uuid,
1358 - name: t.name,
1359 - type: t.type,
1360 - state: t.state
1361 - })));
1362 - } else {
1363 - console.log('No filtered tasks');
1364 - }
1365 - console.groupEnd();
1366 -
1367 - // Check for potential issues
1368 - console.group('Potential Issues');
1369 -
1370 - // Check for case mismatches
1371 - if (this.tasks.length > 0 && filteredTasks.length === 0) {
1372 - console.warn('Filter seems to exclude all tasks. Checking why:');
1373 -
1374 - // Check type values
1375 - const uniqueTypes = [...new Set(this.tasks.map(t => t.type))];
1376 - console.log('Unique task types in data:', uniqueTypes);
1377 -
1378 - // Check state values
1379 - const uniqueStates = [...new Set(this.tasks.map(t => t.state))];
1380 - console.log('Unique task states in data:', uniqueStates);
1381 -
1382 - // Check for exact mismatches
1383 - if (this.filterType !== 'all') {
1384 - const typeMatch = this.tasks.some(t =>
1385 - t.type && String(t.type).toLowerCase() === this.filterType.toLowerCase()
1386 - );
1387 - console.log(`Type "${this.filterType}" matches found:`, typeMatch);
1388 - }
1389 -
1390 - if (this.filterState !== 'all') {
1391 - const stateMatch = this.tasks.some(t =>
1392 - t.state && String(t.state).toLowerCase() === this.filterState.toLowerCase()
1393 - );
1394 - console.log(`State "${this.filterState}" matches found:`, stateMatch);
1395 - }
1396 - }
1397 -
1398 - // Check for undefined or null values
1399 - const hasUndefinedType = this.tasks.some(t => t.type === undefined || t.type === null);
1400 - const hasUndefinedState = this.tasks.some(t => t.state === undefined || t.state === null);
1401 -
1402 - if (hasUndefinedType) {
1403 - console.warn('Some tasks have undefined or null type values!');
1404 - }
1405 -
1406 - if (hasUndefinedState) {
1407 - console.warn('Some tasks have undefined or null state values!');
1408 - }
1409 -
1410 - console.groupEnd();
1411 -
1412 - console.groupEnd();
1413 - },
1414 -
1415 - // Initialize Flatpickr datetime pickers for both create and edit forms
1416 - /**
1417 - * Initialize Flatpickr date/time pickers for scheduler forms
1418 - *
1419 - * @param {string} mode - Which pickers to initialize: 'all', 'create', or 'edit'
1420 - * @returns {void}
1421 - */
1422 - initFlatpickr(mode = 'all') {
1423 - const initPicker = (inputId, refName, wrapperClass, options = {}) => {
1424 - // Try to get input using Alpine.js x-ref first (more reliable)
1425 - let input = this.$refs[refName];
1426 -
1427 - // Fall back to getElementById if x-ref is not available
1428 - if (!input) {
1429 - input = document.getElementById(inputId);
1430 - console.log(`Using getElementById fallback for ${inputId}`);
1431 - }
1432 -
1433 - if (!input) {
1434 - console.warn(`Input element ${inputId} not found by ID or ref`);
1435 - return null;
1436 - }
1437 -
1438 - // Create a wrapper around the input
1439 - const wrapper = document.createElement('div');
1440 - wrapper.className = wrapperClass || 'scheduler-flatpickr-wrapper';
1441 - wrapper.style.overflow = 'visible'; // Ensure dropdown can escape container
1442 -
1443 - // Replace the input with our wrapped version
1444 - input.parentNode.insertBefore(wrapper, input);
1445 - wrapper.appendChild(input);
1446 - input.classList.add('scheduler-flatpickr-input');
1447 -
1448 - // Default options
1449 - const defaultOptions = {
1450 - dateFormat: "Y-m-d H:i",
1451 - enableTime: true,
1452 - time_24hr: true,
1453 - static: false, // Not static so it will float
1454 - appendTo: document.body, // Append to body to avoid overflow issues
1455 - theme: "scheduler-theme",
1456 - allowInput: true,
1457 - positionElement: wrapper, // Position relative to wrapper
1458 - onOpen: function(selectedDates, dateStr, instance) {
1459 - // Ensure calendar is properly positioned and visible
1460 - instance.calendarContainer.style.zIndex = '9999';
1461 - instance.calendarContainer.style.position = 'absolute';
1462 - instance.calendarContainer.style.visibility = 'visible';
1463 - instance.calendarContainer.style.opacity = '1';
1464 -
1465 - // Add class to calendar container for our custom styling
1466 - instance.calendarContainer.classList.add('scheduler-theme');
1467 - },
1468 - // Set default date to 30 minutes from now if no date selected
1469 - onReady: function(selectedDates, dateStr, instance) {
1470 - if (!dateStr) {
1471 - const now = new Date();
1472 - now.setMinutes(now.getMinutes() + 30);
1473 - instance.setDate(now, true);
1474 - }
1475 - }
1476 - };
1477 -
1478 - // Merge options
1479 - const mergedOptions = {...defaultOptions, ...options};
1480 -
1481 - // Initialize flatpickr
1482 - const fp = flatpickr(input, mergedOptions);
1483 -
1484 - // Add a clear button
1485 - const clearButton = document.createElement('button');
1486 - clearButton.className = 'scheduler-flatpickr-clear';
1487 - clearButton.innerHTML = '×';
1488 - clearButton.type = 'button';
1489 - clearButton.addEventListener('click', (e) => {
1490 - e.preventDefault();
1491 - e.stopPropagation();
1492 - if (fp) {
1493 - fp.clear();
1494 - }
1495 - });
1496 - wrapper.appendChild(clearButton);
1497 -
1498 - return fp;
1499 - };
1500 -
1501 - // Clear any existing Flatpickr instances to prevent duplication
1502 - if (mode === 'all' || mode === 'create') {
1503 - const createInput = document.getElementById('newPlannedTime-create');
1504 - if (createInput && createInput._flatpickr) {
1505 - createInput._flatpickr.destroy();
1506 - }
1507 - }
1508 -
1509 - if (mode === 'all' || mode === 'edit') {
1510 - const editInput = document.getElementById('newPlannedTime-edit');
1511 - if (editInput && editInput._flatpickr) {
1512 - editInput._flatpickr.destroy();
1513 - }
1514 - }
1515 -
1516 - // Initialize new instances
1517 - if (mode === 'all' || mode === 'create') {
1518 - initPicker('newPlannedTime-create', 'plannedTimeCreate', 'scheduler-flatpickr-wrapper', {
1519 - minuteIncrement: 5,
1520 - defaultHour: new Date().getHours(),
1521 - defaultMinute: Math.ceil(new Date().getMinutes() / 5) * 5
1522 - });
1523 - }
1524 -
1525 - if (mode === 'all' || mode === 'edit') {
1526 - initPicker('newPlannedTime-edit', 'plannedTimeEdit', 'scheduler-flatpickr-wrapper', {
1527 - minuteIncrement: 5,
1528 - defaultHour: new Date().getHours(),
1529 - defaultMinute: Math.ceil(new Date().getMinutes() / 5) * 5
1530 - });
1531 - }
1532 - },
1533 -
1534 - // Update tasks UI
1535 - updateTasksUI() {
1536 - // First update filteredTasks if that method exists
1537 - if (typeof this.updateFilteredTasks === 'function') {
1538 - this.updateFilteredTasks();
1539 - }
1540 -
1541 - // Wait for UI to update
1542 - this.$nextTick(() => {
1543 - // Get empty state and task list elements
1544 - const emptyElement = document.querySelector('.scheduler-empty');
1545 - const tableElement = document.querySelector('.scheduler-task-list');
1546 -
1547 - // Calculate visibility state based on filtered tasks
1548 - const hasFilteredTasks = Array.isArray(this.filteredTasks) && this.filteredTasks.length > 0;
1549 -
1550 - // Update visibility directly
1551 - if (emptyElement) {
1552 - emptyElement.style.display = !hasFilteredTasks ? '' : 'none';
1553 - }
1554 -
1555 - if (tableElement) {
1556 - tableElement.style.display = hasFilteredTasks ? '' : 'none';
1557 - }
1558 - });
1559 - }
1560 - };
1561 -};
1562 -
1563 -
1564 -// Only define the component if it doesn't already exist or extend the existing one
1565 -if (!window.schedulerSettings) {
1566 - console.log('Defining schedulerSettings component from scratch');
1567 - window.schedulerSettings = fullComponentImplementation;
1568 -} else {
1569 - console.log('Extending existing schedulerSettings component');
1570 - // Store the original function
1571 - const originalSchedulerSettings = window.schedulerSettings;
1572 -
1573 - // Replace with enhanced version that merges the pre-initialized stub with the full implementation
1574 - window.schedulerSettings = function() {
1575 - // Get the base pre-initialized component
1576 - const baseComponent = originalSchedulerSettings();
1577 -
1578 - // Create a backup of the original init function
1579 - const originalInit = baseComponent.init || function() {};
1580 -
1581 - // Create our enhanced init function that adds the missing functionality
1582 - baseComponent.init = function() {
1583 - // Call the original init if it exists
1584 - originalInit.call(this);
1585 -
1586 - console.log('Enhanced init running: adding missing methods to component');
1587 -
1588 - // Get the full implementation
1589 - const fullImpl = fullComponentImplementation();
1590 -
1591 - // Register all implementation methods (except init) directly
1592 - Object.keys(fullImpl).forEach((key) => {
1593 - if (key === 'init') {
1594 - return;
1595 - }
1596 - if (typeof fullImpl[key] === 'function') {
1597 - console.log(`Registering method: ${key}`);
1598 - this[key] = fullImpl[key];
1599 - }
1600 - });
1601 -
1602 - if (typeof this.refreshProjectOptions === 'function') {
1603 - this.refreshProjectOptions();
1604 - }
1605 -
1606 - // hack to expose deleteTask
1607 - window.deleteTaskGlobal = this.deleteTask.bind(this);
1608 -
1609 - // Make sure we have a filteredTasks array initialized
1610 - this.filteredTasks = [];
1611 -
1612 - // Initialize essential properties if missing
1613 - if (!Array.isArray(this.tasks)) {
1614 - this.tasks = [];
1615 - }
1616 -
1617 - if (!Array.isArray(this.projectOptions)) {
1618 - this.projectOptions = [];
1619 - }
1620 -
1621 - if (typeof this.selectedProjectSlug !== 'string') {
1622 - this.selectedProjectSlug = '';
1623 - }
1624 -
1625 - // Make sure attachmentsText getter/setter are defined
1626 - if (!Object.getOwnPropertyDescriptor(this, 'attachmentsText')?.get) {
1627 - Object.defineProperty(this, 'attachmentsText', {
1628 - get: function() {
1629 - // Ensure we always have an array to work with
1630 - const attachments = Array.isArray(this.editingTask?.attachments)
1631 - ? this.editingTask.attachments
1632 - : [];
1633 -
1634 - // Join array items with newlines
1635 - return attachments.join('\n');
1636 - },
1637 - set: function(value) {
1638 - if (!this.editingTask) {
1639 - this.editingTask = {
1640 - attachments: [],
1641 - project: null,
1642 - dedicated_context: true,
1643 - };
1644 - }
1645 -
1646 - if (typeof value === 'string') {
1647 - // Just split by newlines without filtering to preserve editing experience
1648 - this.editingTask.attachments = value.split('\n');
1649 - } else {
1650 - // Fallback to empty array if not a string
1651 - this.editingTask.attachments = [];
1652 - }
1653 - }
1654 - });
1655 - }
1656 -
1657 - // Add methods for updating filteredTasks directly
1658 - if (typeof this.updateFilteredTasks !== 'function') {
1659 - this.updateFilteredTasks = function() {
1660 - // Make sure we have tasks to filter
1661 - if (!Array.isArray(this.tasks)) {
1662 - this.filteredTasks = [];
1663 - return;
1664 - }
1665 -
1666 - let filtered = [...this.tasks];
1667 -
1668 - // Apply type filter with case-insensitive comparison
1669 - if (this.filterType && this.filterType !== 'all') {
1670 - filtered = filtered.filter(task => {
1671 - if (!task.type) return false;
1672 - return String(task.type).toLowerCase() === this.filterType.toLowerCase();
1673 - });
1674 - }
1675 -
1676 - // Apply state filter with case-insensitive comparison
1677 - if (this.filterState && this.filterState !== 'all') {
1678 - filtered = filtered.filter(task => {
1679 - if (!task.state) return false;
1680 - return String(task.state).toLowerCase() === this.filterState.toLowerCase();
1681 - });
1682 - }
1683 -
1684 - // Sort the filtered tasks
1685 - if (typeof this.sortTasks === 'function') {
1686 - filtered = this.sortTasks(filtered);
1687 - }
1688 -
1689 - // Directly update the filteredTasks property
1690 - this.filteredTasks = filtered;
1691 - };
1692 - }
1693 -
1694 - // Set up watchers to update filtered tasks when dependencies change
1695 - this.$nextTick(() => {
1696 - // Update filtered tasks when raw tasks change
1697 - this.$watch('tasks', () => {
1698 - this.updateFilteredTasks();
1699 - });
1700 -
1701 - // Update filtered tasks when filter type changes
1702 - this.$watch('filterType', () => {
1703 - this.updateFilteredTasks();
1704 - });
1705 -
1706 - // Update filtered tasks when filter state changes
1707 - this.$watch('filterState', () => {
1708 - this.updateFilteredTasks();
1709 - });
1710 -
1711 - // Update filtered tasks when sort field or direction changes
1712 - this.$watch('sortField', () => {
1713 - this.updateFilteredTasks();
1714 - });
1715 -
1716 - this.$watch('sortDirection', () => {
1717 - this.updateFilteredTasks();
1718 - });
1719 -
1720 - // Initial update
1721 - this.updateFilteredTasks();
1722 -
1723 - // Set up watcher for task type changes to initialize Flatpickr for planned tasks
1724 - this.$watch('editingTask.type', (newType) => {
1725 - if (newType === 'planned') {
1726 - this.$nextTick(() => {
1727 - // Reinitialize Flatpickr when switching to planned task type
1728 - if (this.isCreating) {
1729 - this.initFlatpickr('create');
1730 - } else if (this.isEditing) {
1731 - this.initFlatpickr('edit');
1732 - }
1733 - });
1734 - }
1735 - });
1736 -
1737 - // Initialize Flatpickr
1738 - this.$nextTick(() => {
1739 - if (typeof this.initFlatpickr === 'function') {
1740 - this.initFlatpickr();
1741 - } else {
1742 - console.error('initFlatpickr is not available');
1743 - }
1744 - });
1745 - });
1746 -
1747 - // Try fetching tasks after a short delay
1748 - setTimeout(() => {
1749 - if (typeof this.fetchTasks === 'function') {
1750 - this.fetchTasks();
1751 - } else {
1752 - console.error('fetchTasks still not available after enhancement');
1753 - }
1754 - }, 100);
1755 -
1756 - console.log('Enhanced init complete');
1757 - };
1758 -
1759 - return baseComponent;
1760 - };
1761 -}
1762 -
1763 -// Force Alpine.js to register the component immediately
1764 -if (window.Alpine) {
1765 - // Alpine is already loaded, register now
1766 - console.log('Alpine already loaded, registering schedulerSettings component now');
1767 - window.Alpine.data('schedulerSettings', window.schedulerSettings);
1768 -} else {
1769 - // Wait for Alpine to load
1770 - document.addEventListener('alpine:init', () => {
1771 - console.log('Alpine:init - immediately registering schedulerSettings component');
1772 - Alpine.data('schedulerSettings', window.schedulerSettings);
1773 - });
1774 -}
1775 -
1776 -// Add a document ready event handler to ensure the scheduler tab can be clicked on first load
1777 -document.addEventListener('DOMContentLoaded', function() {
1778 - console.log('DOMContentLoaded - setting up scheduler tab click handler');
1779 - // Setup scheduler tab click handling
1780 - const setupSchedulerTab = () => {
1781 - const settingsModal = document.getElementById('settingsModal');
1782 - if (!settingsModal) {
1783 - setTimeout(setupSchedulerTab, 100);
1784 - return;
1785 - }
1786 -
1787 - // Create a global event listener for clicks on the scheduler tab
1788 - document.addEventListener('click', function(e) {
1789 - // Find if the click was on the scheduler tab or its children
1790 - const schedulerTab = e.target.closest('.settings-tab[title="Task Scheduler"]');
1791 - if (!schedulerTab) return;
1792 -
1793 - e.preventDefault();
1794 - e.stopPropagation();
1795 -
1796 - // Get the settings modal data
1797 - try {
1798 - const modalData = Alpine.$data(settingsModal);
1799 - if (modalData.activeTab !== 'scheduler') {
1800 - // Directly call the modal's switchTab method
1801 - modalData.switchTab('scheduler');
1802 - }
1803 -
1804 - // Force start polling and fetch tasks immediately when tab is selected
1805 - setTimeout(() => {
1806 - // Get the scheduler component data
1807 - const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
1808 - if (schedulerElement) {
1809 - const schedulerData = Alpine.$data(schedulerElement);
1810 -
1811 - // Force fetch tasks and start polling
1812 - if (typeof schedulerData.fetchTasks === 'function') {
1813 - schedulerData.fetchTasks();
1814 - } else {
1815 - console.error('fetchTasks is not a function on scheduler component');
1816 - }
1817 -
1818 - if (typeof schedulerData.startPolling === 'function') {
1819 - schedulerData.startPolling();
1820 - } else {
1821 - console.error('startPolling is not a function on scheduler component');
1822 - }
1823 - } else {
1824 - console.error('Could not find scheduler component element');
1825 - }
1826 - }, 100);
1827 - } catch (err) {
1828 - console.error('Error handling scheduler tab click:', err);
1829 - }
1830 - }, true); // Use capture phase to intercept before Alpine.js handlers
1831 - };
1832 -
1833 - // Initialize the tab handling
1834 - setupSchedulerTab();
1835 -});
webui/js/settings.js deleted
-591
@@ -1,591 +0,0 @@
1 -const settingsModalProxy = {
2 - isOpen: false,
3 - settings: {},
4 - resolvePromise: null,
5 - activeTab: 'agent', // Default tab
6 - provider: 'cloudflared',
7 -
8 - // Computed property for filtered sections
9 - get filteredSections() {
10 - if (!this.settings || !this.settings.sections) return [];
11 - const filteredSections = this.settings.sections.filter(section => section.tab === this.activeTab);
12 -
13 - // If no sections match the current tab (or all tabs are missing), show all sections
14 - if (filteredSections.length === 0) {
15 - return this.settings.sections;
16 - }
17 -
18 - return filteredSections;
19 - },
20 -
21 - // Switch tab method
22 - switchTab(tabName) {
23 - // Update our component state
24 - this.activeTab = tabName;
25 -
26 - // Update the store safely
27 - const store = Alpine.store('root');
28 - if (store) {
29 - store.activeTab = tabName;
30 - }
31 -
32 - localStorage.setItem('settingsActiveTab', tabName);
33 -
34 - // Auto-scroll active tab into view after a short delay to ensure DOM updates
35 - setTimeout(() => {
36 - const activeTab = document.querySelector('.settings-tab.active');
37 - if (activeTab) {
38 - activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
39 - }
40 -
41 - // When switching to the scheduler tab, initialize Flatpickr components
42 - if (tabName === 'scheduler') {
43 - console.log('Switching to scheduler tab, initializing Flatpickr');
44 - const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
45 - if (schedulerElement) {
46 - const schedulerData = Alpine.$data(schedulerElement);
47 - if (schedulerData) {
48 - // Start polling
49 - if (typeof schedulerData.startPolling === 'function') {
50 - schedulerData.startPolling();
51 - }
52 -
53 - // Initialize Flatpickr if editing or creating
54 - if (typeof schedulerData.initFlatpickr === 'function') {
55 - // Check if we're creating or editing and initialize accordingly
56 - if (schedulerData.isCreating) {
57 - schedulerData.initFlatpickr('create');
58 - } else if (schedulerData.isEditing) {
59 - schedulerData.initFlatpickr('edit');
60 - }
61 - }
62 -
63 - // Force an immediate fetch
64 - if (typeof schedulerData.fetchTasks === 'function') {
65 - schedulerData.fetchTasks();
66 - }
67 - }
68 - }
69 - }
70 - }, 10);
71 - },
72 -
73 - async openModal() {
74 - console.log('Settings modal opening');
75 - const modalEl = document.getElementById('settingsModal');
76 - const modalAD = Alpine.$data(modalEl);
77 -
78 - // First, ensure the store is updated properly
79 - const store = Alpine.store('root');
80 - if (store) {
81 - // Set isOpen first to ensure proper state
82 - store.isOpen = true;
83 - }
84 -
85 - //get settings from backend
86 - try {
87 - const set = await sendJsonData("/settings_get", null);
88 -
89 - // First load the settings data without setting the active tab
90 - const settings = {
91 - "title": "Settings",
92 - "buttons": [
93 - {
94 - "id": "save",
95 - "title": "Save",
96 - "classes": "btn btn-ok"
97 - },
98 - {
99 - "id": "cancel",
100 - "title": "Cancel",
101 - "type": "secondary",
102 - "classes": "btn btn-cancel"
103 - }
104 - ],
105 - "sections": set.settings.sections
106 - }
107 -
108 - // Update modal data
109 - modalAD.isOpen = true;
110 - modalAD.settings = settings;
111 -
112 - // Now set the active tab after the modal is open
113 - // This ensures Alpine reactivity works as expected
114 - setTimeout(() => {
115 - // Get stored tab or default to 'agent'
116 - const savedTab = localStorage.getItem('settingsActiveTab') || 'agent';
117 - console.log(`Setting initial tab to: ${savedTab}`);
118 -
119 - // Directly set the active tab
120 - modalAD.activeTab = savedTab;
121 -
122 - // Also update the store
123 - if (store) {
124 - store.activeTab = savedTab;
125 - }
126 -
127 - localStorage.setItem('settingsActiveTab', savedTab);
128 -
129 - // Add a small delay *after* setting the tab to ensure scrolling works
130 - setTimeout(() => {
131 - const activeTabElement = document.querySelector('.settings-tab.active');
132 - if (activeTabElement) {
133 - activeTabElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
134 - }
135 - // Debug log
136 - const schedulerTab = document.querySelector('.settings-tab[title="Task Scheduler"]');
137 - console.log(`Current active tab after direct set: ${modalAD.activeTab}`);
138 - console.log('Scheduler tab active after direct initialization?',
139 - schedulerTab && schedulerTab.classList.contains('active'));
140 -
141 - // Explicitly start polling if we're on the scheduler tab
142 - if (modalAD.activeTab === 'scheduler') {
143 - console.log('Settings opened directly to scheduler tab, initializing polling');
144 - const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
145 - if (schedulerElement) {
146 - const schedulerData = Alpine.$data(schedulerElement);
147 - if (schedulerData && typeof schedulerData.startPolling === 'function') {
148 - schedulerData.startPolling();
149 - // Also force an immediate fetch
150 - if (typeof schedulerData.fetchTasks === 'function') {
151 - schedulerData.fetchTasks();
152 - }
153 - }
154 - }
155 - }
156 - }, 10); // Small delay just for scrolling
157 -
158 - }, 5); // Keep a minimal delay for modal opening reactivity
159 -
160 - // Add a watcher to disable the Save button when a task is being created or edited
161 - const schedulerComponent = document.querySelector('[x-data="schedulerSettings"]');
162 - if (schedulerComponent) {
163 - // Watch for changes to the scheduler's editing state
164 - const checkSchedulerEditingState = () => {
165 - const schedulerData = Alpine.$data(schedulerComponent);
166 - if (schedulerData) {
167 - // If we're on the scheduler tab and creating/editing a task, disable the Save button
168 - const saveButton = document.querySelector('.modal-footer button.btn-ok');
169 - if (saveButton && modalAD.activeTab === 'scheduler' &&
170 - (schedulerData.isCreating || schedulerData.isEditing)) {
171 - saveButton.disabled = true;
172 - saveButton.classList.add('btn-disabled');
173 - } else if (saveButton) {
174 - saveButton.disabled = false;
175 - saveButton.classList.remove('btn-disabled');
176 - }
177 - }
178 - };
179 -
180 - // Add a mutation observer to detect changes in the scheduler component's state
181 - const observer = new MutationObserver(checkSchedulerEditingState);
182 - observer.observe(schedulerComponent, { attributes: true, subtree: true, childList: true });
183 -
184 - // Also watch for tab changes to update button state
185 - modalAD.$watch('activeTab', checkSchedulerEditingState);
186 -
187 - // Initial check
188 - setTimeout(checkSchedulerEditingState, 100);
189 - }
190 -
191 - return new Promise(resolve => {
192 - this.resolvePromise = resolve;
193 - });
194 -
195 - } catch (e) {
196 - window.toastFetchError("Error getting settings", e)
197 - }
198 - },
199 -
200 - async handleButton(buttonId) {
201 - if (buttonId === 'save') {
202 -
203 - const modalEl = document.getElementById('settingsModal');
204 - const modalAD = Alpine.$data(modalEl);
205 - try {
206 - resp = await window.sendJsonData("/settings_set", modalAD.settings);
207 - } catch (e) {
208 - window.toastFetchError("Error saving settings", e)
209 - return
210 - }
211 - document.dispatchEvent(new CustomEvent('settings-updated', { detail: resp.settings }));
212 - this.resolvePromise({
213 - status: 'saved',
214 - data: resp.settings
215 - });
216 - } else if (buttonId === 'cancel') {
217 - this.handleCancel();
218 - }
219 -
220 - // Stop scheduler polling if it's running
221 - this.stopSchedulerPolling();
222 -
223 - // First update our component state
224 - this.isOpen = false;
225 -
226 - // Then safely update the store
227 - const store = Alpine.store('root');
228 - if (store) {
229 - // Use a slight delay to avoid reactivity issues
230 - setTimeout(() => {
231 - store.isOpen = false;
232 - }, 10);
233 - }
234 - },
235 -
236 - async handleCancel() {
237 - this.resolvePromise({
238 - status: 'cancelled',
239 - data: null
240 - });
241 -
242 - // Stop scheduler polling if it's running
243 - this.stopSchedulerPolling();
244 -
245 - // First update our component state
246 - this.isOpen = false;
247 -
248 - // Then safely update the store
249 - const store = Alpine.store('root');
250 - if (store) {
251 - // Use a slight delay to avoid reactivity issues
252 - setTimeout(() => {
253 - store.isOpen = false;
254 - }, 10);
255 - }
256 - },
257 -
258 - // Add a helper method to stop scheduler polling
259 - stopSchedulerPolling() {
260 - // Find the scheduler component and stop polling if it exists
261 - const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
262 - if (schedulerElement) {
263 - const schedulerData = Alpine.$data(schedulerElement);
264 - if (schedulerData && typeof schedulerData.stopPolling === 'function') {
265 - console.log('Stopping scheduler polling on modal close');
266 - schedulerData.stopPolling();
267 - }
268 - }
269 - },
270 -
271 - async handleFieldButton(field) {
272 - console.log(`Button clicked: ${field.id}`);
273 -
274 - if (field.id === "mcp_servers_config") {
275 - openModal("settings/mcp/client/mcp-servers.html");
276 - } else if (field.id === "backup_create") {
277 - openModal("settings/backup/backup.html");
278 - } else if (field.id === "backup_restore") {
279 - openModal("settings/backup/restore.html");
280 - } else if (field.id === "show_a2a_connection") {
281 - openModal("settings/external/a2a-connection.html");
282 - } else if (field.id === "external_api_examples") {
283 - openModal("settings/external/api-examples.html");
284 - } else if (field.id === "memory_dashboard") {
285 - openModal("settings/memory/memory-dashboard.html");
286 - }
287 - }
288 -};
289 -
290 -
291 -// function initSettingsModal() {
292 -
293 -// window.openSettings = function () {
294 -// proxy.openModal().then(result => {
295 -// console.log(result); // This will log the result when the modal is closed
296 -// });
297 -// }
298 -
299 -// return proxy
300 -// }
301 -
302 -
303 -// document.addEventListener('alpine:init', () => {
304 -// Alpine.store('settingsModal', initSettingsModal());
305 -// });
306 -
307 -document.addEventListener('alpine:init', function () {
308 - // Initialize the root store first to ensure it exists before components try to access it
309 - Alpine.store('root', {
310 - activeTab: localStorage.getItem('settingsActiveTab') || 'agent',
311 - isOpen: false,
312 -
313 - toggleSettings() {
314 - this.isOpen = !this.isOpen;
315 - }
316 - });
317 -
318 - // Then initialize other Alpine components
319 - Alpine.data('settingsModal', function () {
320 - return {
321 - settingsData: {},
322 - filteredSections: [],
323 - activeTab: 'agent',
324 - isLoading: true,
325 -
326 - async init() {
327 - // Initialize with the store value
328 - this.activeTab = Alpine.store('root').activeTab || 'agent';
329 -
330 - // Watch store tab changes
331 - this.$watch('$store.root.activeTab', (newTab) => {
332 - if (typeof newTab !== 'undefined') {
333 - this.activeTab = newTab;
334 - localStorage.setItem('settingsActiveTab', newTab);
335 - this.updateFilteredSections();
336 - }
337 - });
338 -
339 - // Load settings
340 - await this.fetchSettings();
341 - this.updateFilteredSections();
342 - },
343 -
344 - switchTab(tab) {
345 - // Update our component state
346 - this.activeTab = tab;
347 -
348 - // Update the store safely
349 - const store = Alpine.store('root');
350 - if (store) {
351 - store.activeTab = tab;
352 - }
353 - },
354 -
355 - async fetchSettings() {
356 - try {
357 - this.isLoading = true;
358 - const response = await fetchApi('/api/settings_get', {
359 - method: 'POST',
360 - headers: {
361 - 'Content-Type': 'application/json'
362 - }
363 - });
364 -
365 - if (response.ok) {
366 - const data = await response.json();
367 - if (data && data.settings) {
368 - this.settingsData = data.settings;
369 - } else {
370 - console.error('Invalid settings data format');
371 - }
372 - } else {
373 - console.error('Failed to fetch settings:', response.statusText);
374 - }
375 - } catch (error) {
376 - console.error('Error fetching settings:', error);
377 - } finally {
378 - this.isLoading = false;
379 - }
380 - },
381 -
382 - updateFilteredSections() {
383 - // Filter sections based on active tab
384 - if (this.activeTab === 'agent') {
385 - this.filteredSections = this.settingsData.sections?.filter(section =>
386 - section.tab === 'agent'
387 - ) || [];
388 - } else if (this.activeTab === 'external') {
389 - this.filteredSections = this.settingsData.sections?.filter(section =>
390 - section.tab === 'external'
391 - ) || [];
392 - } else if (this.activeTab === 'developer') {
393 - this.filteredSections = this.settingsData.sections?.filter(section =>
394 - section.tab === 'developer'
395 - ) || [];
396 - } else if (this.activeTab === 'mcp') {
397 - this.filteredSections = this.settingsData.sections?.filter(section =>
398 - section.tab === 'mcp'
399 - ) || [];
400 - } else if (this.activeTab === 'backup') {
401 - this.filteredSections = this.settingsData.sections?.filter(section =>
402 - section.tab === 'backup'
403 - ) || [];
404 - } else {
405 - // For any other tab, show nothing since those tabs have custom UI
406 - this.filteredSections = [];
407 - }
408 - },
409 -
410 - async saveSettings() {
411 - try {
412 - // First validate
413 - for (const section of this.settingsData.sections) {
414 - for (const field of section.fields) {
415 - if (field.required && (!field.value || field.value.trim() === '')) {
416 - showToast(`${field.title} in ${section.title} is required`, 'error');
417 - return;
418 - }
419 - }
420 - }
421 -
422 - // Prepare data
423 - const formData = {};
424 - for (const section of this.settingsData.sections) {
425 - for (const field of section.fields) {
426 - formData[field.id] = field.value;
427 - }
428 - }
429 -
430 - // Send request
431 - const response = await fetchApi('/api/settings_save', {
432 - method: 'POST',
433 - headers: {
434 - 'Content-Type': 'application/json'
435 - },
436 - body: JSON.stringify(formData)
437 - });
438 -
439 - if (response.ok) {
440 - showToast('Settings saved successfully', 'success');
441 - // Refresh settings
442 - await this.fetchSettings();
443 - } else {
444 - const errorData = await response.json();
445 - throw new Error(errorData.error || 'Failed to save settings');
446 - }
447 - } catch (error) {
448 - console.error('Error saving settings:', error);
449 - showToast('Failed to save settings: ' + error.message, 'error');
450 - }
451 - },
452 -
453 - // Handle special button field actions
454 - handleFieldButton(field) {
455 - if (field.action === 'test_connection') {
456 - this.testConnection(field);
457 - } else if (field.action === 'reveal_token') {
458 - this.revealToken(field);
459 - } else if (field.action === 'generate_token') {
460 - this.generateToken(field);
461 - } else {
462 - console.warn('Unknown button action:', field.action);
463 - }
464 - },
465 -
466 - // Test API connection
467 - async testConnection(field) {
468 - try {
469 - field.testResult = 'Testing...';
470 - field.testStatus = 'loading';
471 -
472 - // Find the API key field
473 - let apiKey = '';
474 - for (const section of this.settingsData.sections) {
475 - for (const f of section.fields) {
476 - if (f.id === field.target) {
477 - apiKey = f.value;
478 - break;
479 - }
480 - }
481 - }
482 -
483 - if (!apiKey) {
484 - throw new Error('API key is required');
485 - }
486 -
487 - // Send test request
488 - const response = await fetchApi('/api/test_connection', {
489 - method: 'POST',
490 - headers: {
491 - 'Content-Type': 'application/json'
492 - },
493 - body: JSON.stringify({
494 - service: field.service,
495 - api_key: apiKey
496 - })
497 - });
498 -
499 - const data = await response.json();
500 -
501 - if (response.ok && data.success) {
502 - field.testResult = 'Connection successful!';
503 - field.testStatus = 'success';
504 - } else {
505 - throw new Error(data.error || 'Connection failed');
506 - }
507 - } catch (error) {
508 - console.error('Connection test failed:', error);
509 - field.testResult = `Failed: ${error.message}`;
510 - field.testStatus = 'error';
511 - }
512 - },
513 -
514 - // Reveal token temporarily
515 - revealToken(field) {
516 - // Find target field
517 - for (const section of this.settingsData.sections) {
518 - for (const f of section.fields) {
519 - if (f.id === field.target) {
520 - // Toggle field type
521 - f.type = f.type === 'password' ? 'text' : 'password';
522 -
523 - // Update button text
524 - field.value = f.type === 'password' ? 'Show' : 'Hide';
525 -
526 - break;
527 - }
528 - }
529 - }
530 - },
531 -
532 - // Generate random token
533 - generateToken(field) {
534 - // Find target field
535 - for (const section of this.settingsData.sections) {
536 - for (const f of section.fields) {
537 - if (f.id === field.target) {
538 - // Generate random token
539 - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
540 - let token = '';
541 - for (let i = 0; i < 32; i++) {
542 - token += chars.charAt(Math.floor(Math.random() * chars.length));
543 - }
544 -
545 - // Set field value
546 - f.value = token;
547 - break;
548 - }
549 - }
550 - }
551 - },
552 -
553 - closeModal() {
554 - // Stop scheduler polling before closing the modal
555 - const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
556 - if (schedulerElement) {
557 - const schedulerData = Alpine.$data(schedulerElement);
558 - if (schedulerData && typeof schedulerData.stopPolling === 'function') {
559 - console.log('Stopping scheduler polling on modal close');
560 - schedulerData.stopPolling();
561 - }
562 - }
563 -
564 - this.$store.root.isOpen = false;
565 - }
566 - };
567 - });
568 -});
569 -
570 -// Show toast notification - now uses new notification system
571 -function showToast(message, type = 'info') {
572 - // Use new frontend notification system based on type
573 - if (window.Alpine && window.Alpine.store && window.Alpine.store('notificationStore')) {
574 - const store = window.Alpine.store('notificationStore');
575 - switch (type.toLowerCase()) {
576 - case 'error':
577 - return store.frontendError(message, "Settings", 5);
578 - case 'success':
579 - return store.frontendInfo(message, "Settings", 3);
580 - case 'warning':
581 - return store.frontendWarning(message, "Settings", 4);
582 - case 'info':
583 - default:
584 - return store.frontendInfo(message, "Settings", 3);
585 - }
586 - } else {
587 - // Fallback if Alpine/store not ready
588 - console.log(`SETTINGS ${type.toUpperCase()}: ${message}`);
589 - return null;
590 - }
591 -}