| 1 | import { createStore } from "/js/AlpineStore.js"; |
| 2 | import { fetchApi } from "/js/api.js"; |
| 3 | import { store as settingsStore } from "/components/plugins/plugin-settings-store.js"; |
| 4 | |
| 5 | const model = { |
| 6 | pluginName: null, |
| 7 | |
| 8 | // Context selectors |
| 9 | projects: [], |
| 10 | agentProfiles: [], |
| 11 | projectName: "", |
| 12 | agentProfileKey: "", |
| 13 | |
| 14 | // State |
| 15 | isLoading: false, |
| 16 | isSaving: false, |
| 17 | error: null, |
| 18 | |
| 19 | // Status: 'enabled' | 'disabled' |
| 20 | status: 'enabled', |
| 21 | alwaysEnabled: false, |
| 22 | perProjectConfig: true, |
| 23 | perAgentConfig: true, |
| 24 | hasExplicitRuleForScope: false, |
| 25 | hasConfigScreen: false, |
| 26 | |
| 27 | // Where the effective toggle was actually resolved (mirrors plugin-settings-store loadedXxx fields) |
| 28 | loadedPath: "", |
| 29 | loadedProjectName: "", |
| 30 | loadedAgentProfile: "", |
| 31 | |
| 32 | configs: [], |
| 33 | |
| 34 | async open(plugin, { projectName = "", agentProfileKey = "" } = {}) { |
| 35 | this.isLoading = true; |
| 36 | this.error = null; |
| 37 | this.projects = []; |
| 38 | this.agentProfiles = []; |
| 39 | this.projectName = projectName || ""; |
| 40 | this.agentProfileKey = agentProfileKey || ""; |
| 41 | this.configs = []; |
| 42 | this.status = 'enabled'; |
| 43 | this.hasExplicitRuleForScope = false; |
| 44 | this.loadedPath = ""; |
| 45 | this.loadedProjectName = ""; |
| 46 | this.loadedAgentProfile = ""; |
| 47 | |
| 48 | const pluginName = typeof plugin === 'string' ? plugin : plugin?.name; |
| 49 | this.pluginName = pluginName; |
| 50 | this.alwaysEnabled = typeof plugin === 'object' ? !!plugin.always_enabled : false; |
| 51 | this.perProjectConfig = typeof plugin === 'object' ? !!plugin.per_project_config : true; |
| 52 | this.perAgentConfig = typeof plugin === 'object' ? !!plugin.per_agent_config : true; |
| 53 | this.hasConfigScreen = typeof plugin === 'object' ? !!plugin.has_config_screen : false; |
| 54 | |
| 55 | try { |
| 56 | await Promise.all([this.loadProjects(), this.loadAgentProfiles()]); |
| 57 | await this.loadConfigs(); |
| 58 | } finally { |
| 59 | this.isLoading = false; |
| 60 | } |
| 61 | }, |
| 62 | |
| 63 | cleanup() { |
| 64 | this.pluginName = null; |
| 65 | this.projectName = ""; |
| 66 | this.agentProfileKey = ""; |
| 67 | this.error = null; |
| 68 | this.configs = []; |
| 69 | this.perProjectConfig = true; |
| 70 | this.perAgentConfig = true; |
| 71 | this.alwaysEnabled = false; |
| 72 | this.hasConfigScreen = false; |
| 73 | this.hasExplicitRuleForScope = false; |
| 74 | this.loadedPath = ""; |
| 75 | this.loadedProjectName = ""; |
| 76 | this.loadedAgentProfile = ""; |
| 77 | this.isLoading = false; |
| 78 | this.isSaving = false; |
| 79 | }, |
| 80 | |
| 81 | async loadProjects() { |
| 82 | try { |
| 83 | const response = await fetchApi("/projects", { |
| 84 | method: "POST", |
| 85 | headers: { "Content-Type": "application/json" }, |
| 86 | body: JSON.stringify({ action: "list_options" }), |
| 87 | }); |
| 88 | const data = await response.json().catch(() => ({})); |
| 89 | this.projects = data.ok ? (data.data || []) : []; |
| 90 | } catch { |
| 91 | this.projects = []; |
| 92 | } |
| 93 | }, |
| 94 | |
| 95 | async loadAgentProfiles() { |
| 96 | try { |
| 97 | const response = await fetchApi("/agents", { |
| 98 | method: "POST", |
| 99 | headers: { "Content-Type": "application/json" }, |
| 100 | body: JSON.stringify({ action: "list" }), |
| 101 | }); |
| 102 | const data = await response.json().catch(() => ({})); |
| 103 | this.agentProfiles = data.ok ? (data.data || []) : []; |
| 104 | } catch { |
| 105 | this.agentProfiles = []; |
| 106 | } |
| 107 | }, |
| 108 | |
| 109 | async loadConfigs() { |
| 110 | if (!this.pluginName) return; |
| 111 | this.isLoading = true; |
| 112 | try { |
| 113 | const response = await fetchApi("/plugins", { |
| 114 | method: "POST", |
| 115 | headers: { "Content-Type": "application/json" }, |
| 116 | body: JSON.stringify({ |
| 117 | action: "list_configs", |
| 118 | plugin_name: this.pluginName, |
| 119 | asset_type: "toggle" |
| 120 | }), |
| 121 | }); |
| 122 | const result = await response.json().catch(() => ({})); |
| 123 | this.configs = result.ok ? (result.data || []) : []; |
| 124 | await this.loadToggleStatus(); |
| 125 | } catch (e) { |
| 126 | this.error = e?.message || "Failed to load configurations"; |
| 127 | } finally { |
| 128 | this.isLoading = false; |
| 129 | } |
| 130 | }, |
| 131 | |
| 132 | async loadToggleStatus() { |
| 133 | if (!this.pluginName) return; |
| 134 | |
| 135 | if (this.alwaysEnabled) { |
| 136 | this.status = 'enabled'; |
| 137 | this.hasExplicitRuleForScope = true; |
| 138 | this.loadedPath = ""; |
| 139 | this.loadedProjectName = this.projectName || ""; |
| 140 | this.loadedAgentProfile = this.agentProfileKey || ""; |
| 141 | return; |
| 142 | } |
| 143 | |
| 144 | try { |
| 145 | const response = await fetchApi("/plugins", { |
| 146 | method: "POST", |
| 147 | headers: { "Content-Type": "application/json" }, |
| 148 | body: JSON.stringify({ |
| 149 | action: "get_toggle_status", |
| 150 | plugin_name: this.pluginName, |
| 151 | project_name: this.projectName || "", |
| 152 | agent_profile: this.agentProfileKey || "", |
| 153 | }), |
| 154 | }); |
| 155 | const result = await response.json().catch(() => ({})); |
| 156 | if (result.ok) { |
| 157 | this.status = result.status || 'enabled'; |
| 158 | this.loadedPath = result.loaded_path || ""; |
| 159 | this.loadedProjectName = result.loaded_project_name || ""; |
| 160 | this.loadedAgentProfile = result.loaded_agent_profile || ""; |
| 161 | const p = this.projectName || ""; |
| 162 | const a = this.agentProfileKey || ""; |
| 163 | this.hasExplicitRuleForScope = !!(this.loadedPath) && |
| 164 | this.loadedProjectName === p && |
| 165 | this.loadedAgentProfile === a; |
| 166 | } |
| 167 | } catch (e) { |
| 168 | this.error = e?.message || "Failed to load toggle status"; |
| 169 | } |
| 170 | }, |
| 171 | |
| 172 | async switchToConfig(projectName, agentProfile) { |
| 173 | this.projectName = projectName || ""; |
| 174 | this.agentProfileKey = agentProfile || ""; |
| 175 | await this.onScopeChanged(); |
| 176 | await window.closeModal?.(); |
| 177 | }, |
| 178 | |
| 179 | async deleteConfig(path) { |
| 180 | if (!this.pluginName || !path) return; |
| 181 | this.isLoading = true; |
| 182 | try { |
| 183 | const response = await fetchApi("/plugins", { |
| 184 | method: "POST", |
| 185 | headers: { "Content-Type": "application/json" }, |
| 186 | body: JSON.stringify({ |
| 187 | action: "delete_config", |
| 188 | plugin_name: this.pluginName, |
| 189 | path: path |
| 190 | }), |
| 191 | }); |
| 192 | const result = await response.json(); |
| 193 | if (!result.ok) throw new Error(result.error); |
| 194 | await this.loadConfigs(); |
| 195 | } catch (e) { |
| 196 | this.error = e.message || "Delete failed"; |
| 197 | } finally { |
| 198 | this.isLoading = false; |
| 199 | } |
| 200 | }, |
| 201 | |
| 202 | async setEnabled(enabled, { projectName = this.projectName, agentProfileKey = this.agentProfileKey } = {}) { |
| 203 | if (!this.pluginName || this.alwaysEnabled) return; |
| 204 | const previousStatus = this.status; |
| 205 | const previousProjectName = this.projectName; |
| 206 | const previousAgentProfileKey = this.agentProfileKey; |
| 207 | this.projectName = projectName || ""; |
| 208 | this.agentProfileKey = agentProfileKey || ""; |
| 209 | this.status = enabled ? 'enabled' : 'disabled'; |
| 210 | this.isSaving = true; |
| 211 | try { |
| 212 | const response = await fetchApi("/plugins", { |
| 213 | method: "POST", |
| 214 | headers: { "Content-Type": "application/json" }, |
| 215 | body: JSON.stringify({ |
| 216 | action: "toggle_plugin", |
| 217 | plugin_name: this.pluginName, |
| 218 | project_name: this.projectName || "", |
| 219 | agent_profile: this.agentProfileKey || "", |
| 220 | enabled: enabled |
| 221 | }), |
| 222 | }); |
| 223 | const result = await response.json(); |
| 224 | if (!result.ok) throw new Error(result.error); |
| 225 | await new Promise(r => setTimeout(r, 100)); |
| 226 | await this.loadConfigs(); |
| 227 | } catch (e) { |
| 228 | this.status = previousStatus; |
| 229 | this.projectName = previousProjectName; |
| 230 | this.agentProfileKey = previousAgentProfileKey; |
| 231 | this.error = e.message || "Failed to save"; |
| 232 | throw e; |
| 233 | } finally { |
| 234 | this.isSaving = false; |
| 235 | } |
| 236 | }, |
| 237 | |
| 238 | async onScopeChanged() { |
| 239 | await this.loadToggleStatus(); |
| 240 | |
| 241 | // Sync scope with settings store so its loadSettings picks up the right context |
| 242 | settingsStore.projectName = this.projectName || ""; |
| 243 | settingsStore.agentProfileKey = this.agentProfileKey || ""; |
| 244 | await settingsStore.loadSettings(); |
| 245 | }, |
| 246 | |
| 247 | async addRule() { |
| 248 | await this.setEnabled(this.status === 'enabled'); |
| 249 | }, |
| 250 | |
| 251 | projectLabel(key) { |
| 252 | if (!key) return "Global"; |
| 253 | const found = (this.projects || []).find(p => p.key === key); |
| 254 | return found?.label || key; |
| 255 | }, |
| 256 | |
| 257 | agentProfileLabel(key) { |
| 258 | if (!key) return "All profiles"; |
| 259 | const found = (this.agentProfiles || []).find(p => p.key === key); |
| 260 | return found?.label || key; |
| 261 | }, |
| 262 | |
| 263 | get statusLabel() { |
| 264 | return this.status === 'enabled' ? 'ON' : 'OFF'; |
| 265 | }, |
| 266 | |
| 267 | get noScopeRuleMessage() { |
| 268 | if (this.alwaysEnabled || this.isLoading) return ""; |
| 269 | if (this.hasExplicitRuleForScope) return ""; |
| 270 | |
| 271 | if (!this.loadedPath) { |
| 272 | return this.configs.length === 0 |
| 273 | ? "No activation rule exists yet. This plugin is currently ON by default." |
| 274 | : "No activation rule exists for this scope yet. This scope is currently ON by default."; |
| 275 | } |
| 276 | |
| 277 | // Inherited from a parent scope - mirrors plugin-settings-store scopeMismatchMessage |
| 278 | const pLabel = this.projectLabel(this.loadedProjectName || ""); |
| 279 | const aLabel = this.agentProfileLabel(this.loadedAgentProfile || ""); |
| 280 | const state = this.status === 'enabled' ? 'ON' : 'OFF'; |
| 281 | return `No rule for this scope. Inheriting from ${pLabel}, ${aLabel}: ${state}.`; |
| 282 | } |
| 283 | }; |
| 284 | |
| 285 | export const store = createStore("pluginToggle", model); |