@goodandready/dsh-cron 0.1.24 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -3,10 +3,14 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  var module = { exports: {} }
5
5
  var exports = module.exports
6
- const React = window.React || require('react');
6
+ const React = window.React || require('react');
7
7
  const ReactDOM = window.ReactDOM || require('react-dom/client');
8
8
 
9
- const NS = 'cron';
9
+ // Settings namespace, locale namespace and plugin marker share the same
10
+ // canonical identifier — the settings.plugin.item slot key MUST match the
11
+ // namespace registered on the server ('dsh-cron', Issue #85).
12
+ const PLUGIN_ID = '@goodandready/dsh-cron';
13
+ const NS = 'dsh-cron';
10
14
  const PANEL = 'cron';
11
15
  const ACTIVATE_EVENT = 'dsh-panel-activate';
12
16
  const ACTIVE_ATTR = 'data-dsh-cron-active';
@@ -14,23 +18,173 @@ const React = window.React || require('react');
14
18
  const VIEW_ATTR = 'data-dsh-cron-view';
15
19
  const COLUMN_SELECTOR = '[data-pane=conversation], [class*=centerCol]';
16
20
  const SESSION_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="newSession"]';
21
+ const SCRIPT_CONFIRM_HEADER = 'x-dsh-cron-confirm';
17
22
 
18
23
  const BACK_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 3.5 5.5 8l4.5 4.5"/></svg>';
19
- const ICON_TIMER = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>';
20
- const ICON_PLAY = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 21"></polygon></svg>';
21
- const ICON_PAUSE = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
22
- const ICON_TRASH = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
23
- const ICON_SEARCH = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';
24
- const ICON_CHEVRON_DOWN = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';
25
- const ICON_BELL = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>';
26
- const ICON_CLIPBOARD = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>';
27
- const ICON_ACTIVITY = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>';
28
- const ICON_EDIT = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
29
- const ICON_SEND = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
30
- const ICON_SETTINGS = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>';
31
- const ICON_TELEGRAM = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8-1.7 8.01c-.13.58-.47.72-.95.45l-2.6-1.92-1.25 1.21c-.14.14-.26.26-.53.26l.19-2.64 4.81-4.35c.21-.19-.05-.29-.32-.11L8.34 13.5 5.78 12.7c-.56-.17-.57-.56.12-.83l10-3.86c.46-.17.87.11.74.79z"/></svg>';
24
+ const ICON_TIMER = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>';
25
+ const ICON_PLAY = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="5 3 19 12 5 21 5 21"></polygon></svg>';
26
+ const ICON_PAUSE = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
27
+ const ICON_TRASH = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
28
+ const ICON_SEARCH = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';
29
+ const ICON_CHEVRON_DOWN = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg>';
30
+ const ICON_BELL = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>';
31
+ const ICON_CLIPBOARD = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>';
32
+ const ICON_ACTIVITY = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>';
33
+ const ICON_EDIT = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
34
+ const ICON_SEND = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
35
+ const ICON_TELEGRAM = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8-1.7 8.01c-.13.58-.47.72-.95.45l-2.6-1.92-1.25 1.21c-.14.14-.26.26-.53.26l.19-2.64 4.81-4.35c.21-.19-.05-.29-.32-.11L8.34 13.5 5.78 12.7c-.56-.17-.57-.56.12-.83l10-3.86c.46-.17.87.11.74.79z"/></svg>';
36
+
37
+ // Locale dictionary: English is the canonical source language (Issue #87).
38
+ // Russian and other languages are provided at runtime by the translation
39
+ // plugin; no per-language duplicates are stored here.
40
+ const STRINGS = {
41
+ en: {
42
+ 'sidebar.label': 'Scheduled tasks',
43
+ 'chip.title': 'Scheduled tasks (Cron)',
44
+ 'panel.back': 'Back to chat',
45
+ 'panel.title': 'Scheduled tasks',
46
+ 'panel.subtitle': 'Ask DSH to plan tasks, set reminders or track updates',
47
+ 'actions.create': 'Create',
48
+ 'actions.createWithDsh': '💬 Create with DSH',
49
+ 'actions.setupManually': '✏️ Set up manually',
50
+ 'actions.runNow': 'Run now',
51
+ 'actions.runNowShort': 'Run',
52
+ 'actions.edit': 'Edit / Details',
53
+ 'actions.delete': 'Delete task',
54
+ 'actions.pause': 'Pause',
55
+ 'actions.resume': 'Resume',
56
+ 'actions.settings': 'Telegram & Notifications',
57
+ 'search.placeholder': 'Search scheduled tasks',
58
+ 'stats.activeTasks': 'Active tasks',
59
+ 'stats.totalRuns': 'Total runs',
60
+ 'stats.totalTokens': 'Tokens spent',
61
+ 'stats.totalCost': 'Estimated cost',
62
+ 'tabs.all': 'All',
63
+ 'tabs.active': 'Active',
64
+ 'tabs.paused': 'Paused',
65
+ 'tabs.completed': 'Completed',
66
+ 'banner.loadError': 'Failed to load tasks: {error}',
67
+ 'banner.retry': 'Retry',
68
+ 'list.loading': 'Loading scheduled tasks...',
69
+ 'list.empty': 'No scheduled tasks yet',
70
+ 'list.typeShell': 'Shell',
71
+ 'list.typeLlm': 'LLM',
72
+ 'list.oneShot': 'One-shot',
73
+ 'list.next': ' · next: {time}',
74
+ 'recs.title': 'Recommended tasks',
75
+ 'modal.editTitle': 'Edit task',
76
+ 'modal.newTitle': 'New scheduled task',
77
+ 'modal.statusActive': '● Active',
78
+ 'modal.statusPaused': '○ Paused',
79
+ 'modal.params': 'Parameters',
80
+ 'modal.history': 'Run history ({count})',
81
+ 'modal.cancel': 'Cancel',
82
+ 'modal.save': 'Save changes',
83
+ 'modal.create': 'Create task',
84
+ 'modal.close': 'Close',
85
+ 'history.loading': 'Loading history...',
86
+ 'history.empty': 'No runs recorded yet',
87
+ 'history.refresh': 'Refresh history',
88
+ 'history.statusSuccess': 'Success',
89
+ 'history.statusSkipped': 'Skipped',
90
+ 'history.statusMissed': 'Missed',
91
+ 'history.statusTimeout': 'Timeout',
92
+ 'history.statusFailed': 'Failed',
93
+ 'form.nameLabel': 'Task name',
94
+ 'form.namePlaceholder': 'For example: dsh-tags-watch',
95
+ 'form.typeLabel': 'Execution type',
96
+ 'form.typeLlm': 'LLM (AI agent)',
97
+ 'form.typeScript': 'NO-LLM (Shell command/script)',
98
+ 'form.scheduleLabel': 'Schedule (cron or interval)',
99
+ 'form.schedulePlaceholder': '0 */6 * * * or every 2h or in 30m or at: 2026-09-05T15:00:00Z',
100
+ 'form.timeoutLabel': 'Execution timeout (seconds)',
101
+ 'form.overlapLabel': 'Overlap policy',
102
+ 'form.overlapSkip': 'Skip the new run (skip)',
103
+ 'form.overlapQueue': 'Queue the next run (queue)',
104
+ 'form.overlapReplace': 'Cancel current run and restart (replace)',
105
+ 'form.kanbanLabel': 'Kanban card creation (dsh-kanban)',
106
+ 'form.kanbanNone': 'Do not create cards (disabled)',
107
+ 'form.kanbanOnFailure': 'Create a ticket on failure (into Backlog)',
108
+ 'form.kanbanAlways': 'Create a card on every completion (Done / Backlog)',
109
+ 'form.providerLabel': 'LLM provider',
110
+ 'form.providerDefault': 'Default (DSH)',
111
+ 'form.modelLabel': 'LLM model',
112
+ 'form.modelDefault': 'Default',
113
+ 'form.promptLabelScript': 'Shell command or script',
114
+ 'form.promptLabelLlm': 'Instruction / Prompt for the agent',
115
+ 'form.promptPlaceholderScript': 'curl -fsSL https://... || exit 1',
116
+ 'form.promptPlaceholderLlm': 'Describe exactly what the agent should do...',
117
+ 'form.notifyTitle': 'Telegram notifications',
118
+ 'form.notifyTelegram': 'Send the run report to Telegram',
119
+ 'form.notifyOnlyFailure': 'Only on failures (onlyOnFailure / silent on success)',
120
+ 'form.nextRunLabel': 'Next run: ',
121
+ 'form.notScheduled': 'not scheduled',
122
+ 'settings.sectionLabel': 'Cron tasks',
123
+ 'settings.cardTitle': '⏰ Task scheduler (Cron)',
124
+ 'settings.cardDesc': 'Scheduled task execution (cron/intervals), background agent scenarios and Telegram/Kanban integration.',
125
+ 'settings.openPanel': 'Open task panel',
126
+ 'settings.modalTitle': 'Telegram & notification settings',
127
+ 'settings.tokenLabel': 'Telegram Bot Token',
128
+ 'settings.tokenPlaceholder': '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
129
+ 'settings.tokenPlaceholderDefault': 'Using the bot configured in DSH settings (or enter your own)',
130
+ 'settings.defaultHint': '✓ A configured bot was found in the DSH profile',
131
+ 'settings.chatIdLabel': 'Telegram Chat ID',
132
+ 'settings.chatIdPlaceholder': 'For example: 345678901',
133
+ 'settings.kanbanUrlLabel': 'Kanban Base URL',
134
+ 'settings.globalNotify': 'Deliver reports of all tasks to Telegram globally',
135
+ 'settings.globalOnlyFailure': 'Global mode: only on failures (silent on success)',
136
+ 'settings.testTelegram': '🔔 Test Telegram delivery',
137
+ 'settings.testKanban': '📋 Test Kanban',
138
+ 'settings.sending': 'Sending...',
139
+ 'settings.checkingKanban': 'Checking Kanban...',
140
+ 'settings.testOk': '✅ Test message delivered to Telegram!',
141
+ 'settings.testFail': '❌ {error}',
142
+ 'settings.kanbanOk': '✅ Kanban card created',
143
+ 'settings.kanbanFail': '❌ {error}',
144
+ 'settings.save': 'Save settings',
145
+ 'settings.saved': '✓ Saved',
146
+ 'settings.saveError': 'Save failed: {error}',
147
+ 'settings.saveErrorUnknown': 'unknown error',
148
+ 'dsh.modalTitle': 'Schedule a task with the DSH agent',
149
+ 'dsh.descLabel': 'Describe the task and the desired frequency:',
150
+ 'dsh.placeholder': 'For example: check free disk space and failed systemd services every 2 hours, and alert only if something is wrong...',
151
+ 'dsh.sendHint': 'Ctrl+Enter to send',
152
+ 'dsh.sendTitle': 'Send to agent',
153
+ 'dsh.startFailed': 'Failed to start the agent dialog: {error}',
154
+ 'dsh.launchError': 'Launch failed: {error}',
155
+ 'confirm.delete': 'Delete this task?',
156
+ 'list.loadErrorPrefix': 'Failed to load tasks'
157
+ }
158
+ };
159
+
160
+ // Module-level translator. apply() rebinds it to the DSH locale service so
161
+ // the active language and live language switches are honored.
162
+ let translate = (key, vars) => {
163
+ let s = (STRINGS.en && STRINGS.en[key]) || key;
164
+ if (vars) {
165
+ for (const k of Object.keys(vars)) {
166
+ s = s.split('{' + k + '}').join(String(vars[k]));
167
+ }
168
+ }
169
+ return s;
170
+ };
171
+ const T = (key, vars) => translate(key, vars);
32
172
 
173
+ // Status colors: core semantic tokens first, plugin fallbacks as a single
174
+ // point of change (Issue #91 — no scattered hardcoded hex).
33
175
  const STYLES = `
176
+ :root {
177
+ --dsh-cron-success: var(--dsw-alias-label-success, #10b981);
178
+ --dsh-cron-success-bg: var(--dsw-alias-bg-success, rgba(16, 185, 129, 0.15));
179
+ --dsh-cron-danger: var(--dsw-alias-label-danger, #ef4444);
180
+ --dsh-cron-danger-bg: var(--dsw-alias-bg-danger, rgba(239, 68, 68, 0.15));
181
+ --dsh-cron-info: var(--dsw-alias-label-info, #60a5fa);
182
+ --dsh-cron-info-bg: var(--dsw-alias-bg-info, rgba(59, 130, 246, 0.15));
183
+ --dsh-cron-accent: var(--dsw-alias-label-accent, #c084fc);
184
+ --dsh-cron-accent-bg: var(--dsw-alias-bg-accent, rgba(168, 85, 247, 0.15));
185
+ --dsh-cron-warning: var(--dsw-alias-label-warning, #eab308);
186
+ --dsh-cron-warning-bg: var(--dsw-alias-bg-warning, rgba(234, 179, 8, 0.15));
187
+ }
34
188
  [data-pane=conversation], [class*=centerCol] {
35
189
  position: relative !important;
36
190
  }
@@ -56,7 +210,7 @@ const React = window.React || require('react');
56
210
 
57
211
  .dsh-cron-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--dsw-alias-bg-base, #171717); color: var(--dsw-alias-label-primary, #ededed); overflow-y: auto; padding: 32px 48px; z-index: 100; box-sizing: border-box; }
58
212
  .dsh-cron-container { max-width: 860px; width: 100%; margin: 0 auto; }
59
-
213
+
60
214
  .dsh-cron-top-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
61
215
  .dsh-cron-back-btn { appearance: none; font: inherit; cursor: pointer; color: var(--dsw-alias-label-secondary, #9ca3af); background: 0 0; border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 8px; align-items: center; gap: 6px; padding: 4px 12px; font-size: 13px; display: inline-flex; flex: none; height: 32px; box-sizing: border-box; }
62
216
  .dsh-cron-back-btn:hover { color: var(--dsw-alias-label-primary, #ededed); background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06)); }
@@ -64,61 +218,57 @@ const React = window.React || require('react');
64
218
  .dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }
65
219
  .dsh-cron-title { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 6px; }
66
220
  .dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary, #9ca3af); }
67
- .dsh-cron-create-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-4, #262626); color: #fff; border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 9999px; padding: 8px 16px; font-size: 14px; font-weight: 500; cursor: pointer; position: relative; }
68
- .dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, #333); }
221
+ .dsh-cron-create-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-4, #262626); color: var(--dsw-alias-label-primary, #fff); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 9999px; padding: 8px 16px; font-size: 14px; font-weight: 500; cursor: pointer; position: relative; }
222
+ .dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, var(--dsw-alias-interactive-bg-hover, #333)); }
69
223
  .dsh-cron-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 200px; background: var(--dsw-alias-bg-layer-3, #212121); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 12px; padding: 6px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 200; }
70
- .dsh-cron-dropdown-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; background: transparent; border: none; border-radius: 8px; color: #ededed; font-size: 13.5px; text-align: left; cursor: pointer; }
71
- .dsh-cron-dropdown-item:hover { background: var(--dsw-alias-bg-layer-4, #2a2a2a); }
224
+ .dsh-cron-dropdown-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; background: transparent; border: none; border-radius: 8px; color: var(--dsw-alias-label-primary, #ededed); font-size: 13.5px; text-align: left; cursor: pointer; }
225
+ .dsh-cron-dropdown-item:hover { background: var(--dsw-alias-interactive-bg-hover, #2a2a2a); }
72
226
  .dsh-cron-search-bar { position: relative; margin-bottom: 20px; }
73
- .dsh-cron-search-input { width: 100%; background: var(--dsw-alias-bg-layer-3, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 10px; padding: 10px 14px 10px 38px; color: inherit; font-size: 14px; outline: none; box-sizing: border-box; }
74
- .dsh-cron-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: #666; }
227
+ .dsh-cron-search-input { width: 100%; background: var(--dsw-alias-bg-layer-3, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 10px; padding: 10px 14px 10px 38px; color: var(--dsw-alias-label-primary, inherit); font-size: 14px; outline: none; box-sizing: border-box; }
228
+ .dsh-cron-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--dsw-alias-label-tertiary, #666); }
75
229
  .dsh-cron-tabs { display: flex; gap: 8px; margin-bottom: 24px; }
76
- .dsh-cron-tab { appearance: none; background: transparent; border: none; padding: 6px 14px; border-radius: 9999px; color: #9ca3af; font-size: 13.5px; cursor: pointer; }
77
- .dsh-cron-tab[data-active="true"] { background: var(--dsw-alias-bg-layer-4, #2a2a2a); color: #fff; font-weight: 500; }
230
+ .dsh-cron-tab { appearance: none; background: transparent; border: none; padding: 6px 14px; border-radius: 9999px; color: var(--dsw-alias-label-secondary, #9ca3af); font-size: 13.5px; cursor: pointer; }
231
+ .dsh-cron-tab[data-active="true"] { background: var(--dsw-alias-bg-layer-4, #2a2a2a); color: var(--dsw-alias-label-primary, #fff); font-weight: 500; }
78
232
  .dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 40px; }
79
233
  .dsh-cron-task-item { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1e1e1e); border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 12px; cursor: pointer; transition: background 0.15s, border-color 0.15s; }
80
- .dsh-cron-task-item:hover { background: var(--dsw-alias-bg-layer-4, #242424); border-color: #444; }
81
- .dsh-cron-task-prompt-preview { font-size: 12.5px; color: #888; margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 520px; line-height: 1.3; }
234
+ .dsh-cron-task-item:hover, .dsh-cron-task-item:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-border-l1, #444); outline: none; }
235
+ .dsh-cron-task-prompt-preview { font-size: 12.5px; color: var(--dsw-alias-label-tertiary, #888); margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 520px; line-height: 1.3; }
82
236
  .dsh-cron-type-tag { display: inline-block; font-size: 10.5px; padding: 1px 6px; border-radius: 4px; text-transform: uppercase; font-weight: 600; margin-left: 8px; vertical-align: middle; }
83
- .dsh-cron-task-item:hover { border-color: #444; }
84
- .dsh-cron-task-status-btn { background: none; border: 1px solid #444; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; color: #888; cursor: pointer; flex-shrink: 0; }
85
- .dsh-cron-task-status-btn[data-active="true"] { color: #10b981; border-color: #10b981; }
86
- .dsh-cron-task-info { flex: 1; min-width: 0; }
87
- .dsh-cron-task-title { font-size: 15px; font-weight: 500; margin-bottom: 3px; }
88
- .dsh-cron-task-sched { font-size: 13px; color: #9ca3af; }
89
- .dsh-cron-task-model-tag { display: inline-block; font-size: 11px; padding: 2px 7px; border-radius: 4px; background: rgba(255,255,255,0.06); color: #aaa; margin-left: 8px; vertical-align: middle; }
237
+ .dsh-cron-task-model-tag { display: inline-block; font-size: 11px; padding: 2px 7px; border-radius: 4px; background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06)); color: var(--dsw-alias-label-secondary, #aaa); margin-left: 8px; vertical-align: middle; }
90
238
  .dsh-cron-task-actions { display: flex; gap: 8px; }
91
- .dsh-cron-icon-btn { background: transparent; border: 1px solid #333; border-radius: 8px; padding: 6px 12px; font-size: 12.5px; color: #aaa; cursor: pointer; }
92
- .dsh-cron-icon-btn:hover { background: #2a2a2a; color: #fff; }
239
+ .dsh-cron-icon-btn { background: transparent; border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 6px 12px; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #aaa); cursor: pointer; }
240
+ .dsh-cron-icon-btn:hover { background: var(--dsw-alias-interactive-bg-hover, #2a2a2a); color: var(--dsw-alias-label-primary, #fff); }
241
+ .dsh-cron-task-status-btn { background: none; border: 1px solid var(--dsw-alias-border-l1, #444); border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; color: var(--dsw-alias-label-tertiary, #888); cursor: pointer; flex-shrink: 0; }
242
+ .dsh-cron-task-status-btn[data-active="true"] { color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
93
243
  .dsh-cron-recs-title { font-size: 17px; font-weight: 600; margin-bottom: 14px; }
94
244
  .dsh-cron-recs-list { display: flex; flex-direction: column; gap: 10px; }
95
245
  .dsh-cron-rec-card { display: flex; align-items: center; gap: 16px; padding: 16px 20px; background: var(--dsw-alias-bg-layer-3, #1c1c1c); border: 1px solid var(--dsw-alias-border-l1, #282828); border-radius: 12px; cursor: pointer; transition: background 0.15s; }
96
- .dsh-cron-rec-card:hover { background: var(--dsw-alias-bg-layer-4, #242424); border-color: #383838; }
97
- .dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: rgba(255,255,255,0.03); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
246
+ .dsh-cron-rec-card:hover, .dsh-cron-rec-card:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-border-l2, #383838); outline: none; }
247
+ .dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.03)); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
98
248
  .dsh-cron-rec-content { flex: 1; min-width: 0; }
99
249
  .dsh-cron-rec-title { font-size: 14.5px; font-weight: 500; margin-bottom: 4px; }
100
- .dsh-cron-rec-time { font-size: 13px; color: #9ca3af; font-weight: normal; margin-left: 8px; }
101
- .dsh-cron-rec-desc { font-size: 13px; color: #71717a; }
102
-
250
+ .dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary, #9ca3af); font-weight: normal; margin-left: 8px; }
251
+ .dsh-cron-rec-desc { font-size: 13px; color: var(--dsw-alias-label-tertiary, #71717a); }
252
+
103
253
  .dsh-cron-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; }
104
254
  .dsh-cron-modal { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 16px; width: 100%; max-width: 540px; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); box-sizing: border-box; }
105
255
  .dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
106
256
  .dsh-cron-form-group { margin-bottom: 16px; }
107
- .dsh-cron-form-group label { display: block; font-size: 13px; color: #aaa; margin-bottom: 6px; }
108
- .dsh-cron-form-group input, .dsh-cron-form-group textarea, .dsh-cron-form-group select { width: 100%; background: #141414; border: 1px solid #333; border-radius: 8px; padding: 9px 12px; color: #fff; font-size: 13.5px; outline: none; box-sizing: border-box; }
257
+ .dsh-cron-form-group label { display: block; font-size: 13px; color: var(--dsw-alias-label-secondary, #aaa); margin-bottom: 6px; }
258
+ .dsh-cron-form-group input, .dsh-cron-form-group textarea, .dsh-cron-form-group select { width: 100%; background: var(--dsw-alias-bg-base, #141414); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 9px 12px; color: var(--dsw-alias-label-primary, #fff); font-size: 13.5px; outline: none; box-sizing: border-box; }
109
259
  .dsh-cron-form-group select { cursor: pointer; }
110
260
  .dsh-cron-form-group textarea { min-height: 80px; resize: vertical; }
111
261
  .dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
112
262
  .dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; }
113
- .dsh-cron-btn-primary { background: #ededed; color: #111; border: none; border-radius: 8px; padding: 7px 16px; font-size: 13.5px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
263
+ .dsh-cron-btn-primary { background: var(--dsw-alias-label-primary, #ededed); color: var(--dsw-alias-bg-base, #111); border: none; border-radius: 8px; padding: 7px 16px; font-size: 13.5px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
114
264
  .dsh-cron-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
115
- .dsh-cron-btn-secondary { background: transparent; color: #999; border: 1px solid #383838; border-radius: 8px; padding: 7px 16px; font-size: 13.5px; cursor: pointer; }
116
- .dsh-cron-chat-box { position: relative; background: #141414; border: 1px solid #333; border-radius: 12px; padding: 12px; }
117
- .dsh-cron-chat-input { width: 100%; background: transparent; border: none; color: #fff; font-size: 14px; min-height: 100px; resize: vertical; outline: none; box-sizing: border-box; line-height: 1.5; }
118
- .dsh-cron-chat-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-top: 8px; border-top: 1px solid #222; }
119
- .dsh-cron-chat-hint { font-size: 12px; color: #666; }
120
- .dsh-cron-send-btn { width: 34px; height: 34px; border-radius: 8px; background: #ededed; color: #111; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.1s, background 0.15s; }
121
- .dsh-cron-send-btn:hover:not(:disabled) { background: #fff; transform: translateY(-1px); }
265
+ .dsh-cron-btn-secondary { background: transparent; color: var(--dsw-alias-label-secondary, #999); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 8px; padding: 7px 16px; font-size: 13.5px; cursor: pointer; }
266
+ .dsh-cron-chat-box { position: relative; background: var(--dsw-alias-bg-base, #141414); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 12px; padding: 12px; }
267
+ .dsh-cron-chat-input { width: 100%; background: transparent; border: none; color: var(--dsw-alias-label-primary, #fff); font-size: 14px; min-height: 100px; resize: vertical; outline: none; box-sizing: border-box; line-height: 1.5; }
268
+ .dsh-cron-chat-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l1, #222); }
269
+ .dsh-cron-chat-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary, #666); }
270
+ .dsh-cron-send-btn { width: 34px; height: 34px; border-radius: 8px; background: var(--dsw-alias-label-primary, #ededed); color: var(--dsw-alias-bg-base, #111); border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.1s, background 0.15s; }
271
+ .dsh-cron-send-btn:hover:not(:disabled) { transform: translateY(-1px); }
122
272
  .dsh-cron-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
123
273
 
124
274
  .dsh-cron-card { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l1, #333); border-radius: 12px; padding: 18px 20px; margin-bottom: 16px; }
@@ -126,35 +276,35 @@ const React = window.React || require('react');
126
276
  .dsh-cron-card-title { font-size: 15px; font-weight: 600; color: var(--dsw-alias-label-primary, #fff); }
127
277
  .dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #888); margin-bottom: 16px; line-height: 1.4; }
128
278
 
129
- .dsh-cron-modal-nav { display: flex; gap: 4px; border-bottom: 1px solid #2e2e2e; margin-bottom: 18px; padding-bottom: 2px; }
130
- .dsh-cron-modal-tab { appearance: none; background: transparent; border: none; border-bottom: 2px solid transparent; padding: 8px 14px; font-size: 13.5px; color: #888; cursor: pointer; font-weight: 500; transition: color 0.15s, border-color 0.15s; }
131
- .dsh-cron-modal-tab:hover { color: #ccc; }
132
- .dsh-cron-modal-tab[data-active="true"] { color: #fff; border-bottom-color: #ededed; }
279
+ .dsh-cron-modal-nav { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l2, #2e2e2e); margin-bottom: 18px; padding-bottom: 2px; }
280
+ .dsh-cron-modal-tab { appearance: none; background: transparent; border: none; border-bottom: 2px solid transparent; padding: 8px 14px; font-size: 13.5px; color: var(--dsw-alias-label-tertiary, #888); cursor: pointer; font-weight: 500; transition: color 0.15s, border-color 0.15s; }
281
+ .dsh-cron-modal-tab:hover { color: var(--dsw-alias-label-secondary, #ccc); }
282
+ .dsh-cron-modal-tab[data-active="true"] { color: var(--dsw-alias-label-primary, #fff); border-bottom-color: var(--dsw-alias-label-primary, #ededed); }
133
283
  .dsh-cron-history-list { display: flex; flex-direction: column; gap: 10px; max-height: 420px; overflow-y: auto; padding-right: 4px; }
134
- .dsh-cron-history-item { background: #161616; border: 1px solid #2a2a2a; border-radius: 8px; padding: 12px 14px; font-size: 12.5px; }
284
+ .dsh-cron-history-item { background: var(--dsw-alias-bg-base, #161616); border: 1px solid var(--dsw-alias-border-l1, #2a2a2a); border-radius: 8px; padding: 12px 14px; font-size: 12.5px; }
135
285
  .dsh-cron-history-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
136
286
  .dsh-cron-history-status { font-weight: 600; font-size: 11.5px; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; }
137
- .dsh-cron-history-status[data-status="success"] { background: rgba(16, 185, 129, 0.15); color: #10b981; }
138
- .dsh-cron-history-status[data-status="error"] { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
139
- .dsh-cron-history-status[data-status="skipped"] { background: rgba(234, 179, 8, 0.15); color: #eab308; }
140
- .dsh-cron-history-time { color: #777; font-size: 12px; }
141
- .dsh-cron-history-output { background: #0c0c0c; border: 1px solid #222; border-radius: 6px; padding: 8px 10px; font-family: monospace; font-size: 12px; color: #bbb; max-height: 140px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; margin-top: 6px; }
142
- .dsh-cron-history-empty { text-align: center; padding: 36px 0; color: #666; font-size: 13.5px; }
287
+ .dsh-cron-history-status[data-status="success"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); }
288
+ .dsh-cron-history-status[data-status="error"], .dsh-cron-history-status[data-status="timeout"] { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); }
289
+ .dsh-cron-history-status[data-status="skipped"], .dsh-cron-history-status[data-status="missed"] { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); }
290
+ .dsh-cron-history-time { color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; }
291
+ .dsh-cron-history-output { background: var(--dsw-alias-bg-base, #0c0c0c); border: 1px solid var(--dsw-alias-border-l1, #222); border-radius: 6px; padding: 8px 10px; font-family: monospace; font-size: 12px; color: var(--dsw-alias-label-secondary, #bbb); max-height: 140px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; margin-top: 6px; }
292
+ .dsh-cron-history-empty { text-align: center; padding: 36px 0; color: var(--dsw-alias-label-tertiary, #666); font-size: 13.5px; }
143
293
 
144
294
  .dsh-cron-stats-bar { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 18px; }
145
- .dsh-cron-stat-card { background: rgba(255, 255, 255, 0.02); border: 1px solid #222; border-radius: 8px; padding: 10px 14px; }
146
- .dsh-cron-stat-val { font-size: 16px; font-weight: 600; color: #fff; }
147
- .dsh-cron-stat-lbl { font-size: 11.5px; color: #777; margin-top: 2px; }
148
- .dsh-cron-oneshot-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: rgba(168, 85, 247, 0.15); color: #c084fc; font-weight: 500; }
149
- .dsh-cron-cost-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: rgba(16, 185, 129, 0.12); color: #10b981; font-weight: 500; }
150
- .dsh-cron-tokens-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: rgba(59, 130, 246, 0.12); color: #60a5fa; }
151
- .dsh-cron-checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #ccc; cursor: pointer; user-select: none; }
152
- .dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: #ededed; }
153
- .dsh-cron-hint { font-size: 12px; color: #888; margin-top: 4px; }
295
+ .dsh-cron-stat-card { background: var(--dsw-alias-bg-layer-3, rgba(255, 255, 255, 0.02)); border: 1px solid var(--dsw-alias-border-l1, #222); border-radius: 8px; padding: 10px 14px; }
296
+ .dsh-cron-stat-val { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, #fff); }
297
+ .dsh-cron-stat-lbl { font-size: 11.5px; color: var(--dsw-alias-label-tertiary, #777); margin-top: 2px; }
298
+ .dsh-cron-oneshot-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsh-cron-accent-bg); color: var(--dsh-cron-accent); font-weight: 500; }
299
+ .dsh-cron-cost-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); font-weight: 500; }
300
+ .dsh-cron-tokens-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); }
301
+ .dsh-cron-checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--dsw-alias-label-secondary, #ccc); cursor: pointer; user-select: none; }
302
+ .dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: var(--dsw-alias-label-primary, #ededed); }
303
+ .dsh-cron-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); margin-top: 4px; }
154
304
  .dsh-cron-status-banner { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-top: 12px; }
155
- .dsh-cron-status-banner[data-type="success"] { background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.3); color: #10b981; }
156
- .dsh-cron-status-banner[data-type="error"] { background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); color: #ef4444; }
157
- .dsh-cron-status-banner[data-type="info"] { background: rgba(59,130,246,0.15); border: 1px solid rgba(59,130,246,0.3); color: #60a5fa; }
305
+ .dsh-cron-status-banner[data-type="success"] { background: var(--dsh-cron-success-bg); border: 1px solid var(--dsh-cron-success); color: var(--dsh-cron-success); }
306
+ .dsh-cron-status-banner[data-type="error"] { background: var(--dsh-cron-danger-bg); border: 1px solid var(--dsh-cron-danger); color: var(--dsh-cron-danger); }
307
+ .dsh-cron-status-banner[data-type="info"] { background: var(--dsh-cron-info-bg); border: 1px solid var(--dsh-cron-info); color: var(--dsh-cron-info); }
158
308
  `;
159
309
 
160
310
  function ensureStyles() {
@@ -163,6 +313,8 @@ const React = window.React || require('react');
163
313
  if (!el) {
164
314
  el = document.createElement('style');
165
315
  el.id = 'dsh-cron-styles';
316
+ // Mark ownership before insertion so neighbor cleanups leave it alone (#91)
317
+ el.dataset.dshPlugin = 'dsh-cron';
166
318
  document.head.appendChild(el);
167
319
  }
168
320
  el.textContent = STYLES;
@@ -194,7 +346,7 @@ const React = window.React || require('react');
194
346
  const getSessions = () => (ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : (ctx && ctx.sessions));
195
347
  let sessions = getSessions();
196
348
 
197
- // 1. Попытка вызвать sessions.open(sessionId) немедленно
349
+ // 1. Try sessions.open(sessionId) immediately
198
350
  if (sessions && typeof sessions.open === 'function') {
199
351
  try {
200
352
  sessions.open(sessionId);
@@ -202,7 +354,7 @@ const React = window.React || require('react');
202
354
  } catch (e) {}
203
355
  }
204
356
 
205
- // 2. Если сессия еще не попала в manager.summaries, обновляем список с бэкенда
357
+ // 2. If the session is not in manager.summaries yet, refresh from the backend
206
358
  if (sessions && typeof sessions.refresh === 'function') {
207
359
  try {
208
360
  await sessions.refresh();
@@ -211,7 +363,7 @@ const React = window.React || require('react');
211
363
  } catch (e) {}
212
364
  }
213
365
 
214
- // 3. Асинхронные ретраи с интервалом (пока WebSocket/SSE событие добавляет сессию в список)
366
+ // 3. Async retries until the WebSocket/SSE event adds the session to the list
215
367
  for (let i = 0; i < 15; i++) {
216
368
  await new Promise(resolve => setTimeout(resolve, 120));
217
369
  sessions = getSessions();
@@ -221,7 +373,7 @@ const React = window.React || require('react');
221
373
  return true;
222
374
  } catch (e) {}
223
375
  }
224
- // DOM fallback: клик по сессии в боковой панели, если она уже отрендерилась
376
+ // DOM fallback: click the session in the sidebar if it has rendered
225
377
  if (typeof document !== 'undefined') {
226
378
  const domItem = document.querySelector('[data-session-id="' + sessionId + '"], [role="treeitem"][data-id="' + sessionId + '"], [data-id="' + sessionId + '"]');
227
379
  if (domItem) {
@@ -233,8 +385,20 @@ const React = window.React || require('react');
233
385
  return false;
234
386
  }
235
387
 
388
+ function statusLabel(t, status) {
389
+ if (status === 'success') return T_KEY(t, 'history.statusSuccess');
390
+ if (status === 'skipped') return T_KEY(t, 'history.statusSkipped');
391
+ if (status === 'missed') return T_KEY(t, 'history.statusMissed');
392
+ if (status === 'timeout') return T_KEY(t, 'history.statusTimeout');
393
+ return T_KEY(t, 'history.statusFailed');
394
+ }
395
+ function T_KEY(t, key, vars) {
396
+ return (typeof t === 'function' ? t : translate)(key, vars);
397
+ }
398
+
236
399
  function CronScreen(props) {
237
400
  const { ctx, toggle, onClose } = props;
401
+ const t = props.t || translate;
238
402
  const [tab, setTab] = React.useState('all');
239
403
  const [query, setQuery] = React.useState('');
240
404
  const [tasks, setTasks] = React.useState([]);
@@ -311,11 +475,11 @@ const React = window.React || require('react');
311
475
  setRecs(data.recommendations || []);
312
476
  setStats(data.stats || null);
313
477
  } else {
314
- setFetchError(data && data.error ? data.error : 'Не удалось загрузить задачи');
478
+ setFetchError(data && data.error ? data.error : T_KEY(t, 'list.loadErrorPrefix'));
315
479
  }
316
480
  } catch (err) {
317
481
  console.error('[dsh-cron] fetch tasks error:', err);
318
- setFetchError(err.message || 'Ошибка сети при загрузке задач');
482
+ setFetchError(err.message || T_KEY(t, 'list.loadErrorPrefix'));
319
483
  } finally {
320
484
  setLoading(false);
321
485
  }
@@ -329,7 +493,7 @@ const React = window.React || require('react');
329
493
  if (data && data.ok) {
330
494
  if (!providerName) {
331
495
  setProviders(data.providers || []);
332
- setDefaultModelInfo(data.defaultModel || null);
496
+ setDefaultModelInfo(data.defaultModel || data.current || null);
333
497
  if (data.defaultModel?.models) {
334
498
  setModels(data.defaultModel.models);
335
499
  }
@@ -370,7 +534,7 @@ const React = window.React || require('react');
370
534
 
371
535
  const handleRunNow = async (id) => {
372
536
  try {
373
- setTasks(prev => prev.map(t => t.id === id ? { ...t, lastStatus: 'running' } : t));
537
+ setTasks(prev => prev.map(t2 => t2.id === id ? { ...t2, lastStatus: 'running' } : t2));
374
538
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id) + '/run', { method: 'POST' });
375
539
  setTimeout(() => fetchTasks(), 1500);
376
540
  } catch (err) {
@@ -379,7 +543,7 @@ const React = window.React || require('react');
379
543
  };
380
544
 
381
545
  const handleDelete = async (id) => {
382
- if (!confirm('Удалить эту задачу?')) return;
546
+ if (!confirm(T_KEY(t, 'confirm.delete'))) return;
383
547
  try {
384
548
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
385
549
  fetchTasks();
@@ -422,15 +586,15 @@ const React = window.React || require('react');
422
586
  if (data && data.ok) {
423
587
  setSettingsModalOpen(false);
424
588
  } else {
425
- alert('Ошибка сохранения: ' + (data?.error || 'неизвестная ошибка'));
589
+ alert(T_KEY(t, 'settings.saveError', { error: (data && data.error) || T_KEY(t, 'settings.saveErrorUnknown') }));
426
590
  }
427
591
  } catch (err) {
428
- alert('Ошибка сохранения: ' + err.message);
592
+ alert(T_KEY(t, 'settings.saveError', { error: err.message }));
429
593
  }
430
594
  };
431
595
 
432
596
  const handleTestTelegram = async () => {
433
- setTestStatus({ loading: true, message: 'Отправка тестового сообщения...' });
597
+ setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
434
598
  try {
435
599
  const res = await fetch('/dsh-cron/telegram/test', {
436
600
  method: 'POST',
@@ -442,12 +606,12 @@ const React = window.React || require('react');
442
606
  });
443
607
  const data = await res.json();
444
608
  if (data && data.ok) {
445
- setTestStatus({ loading: false, success: true, message: '✅ Тестовое сообщение успешно отправлено в Telegram!' });
609
+ setTestStatus({ loading: false, success: true, message: T_KEY(t, 'settings.testOk') });
446
610
  } else {
447
- setTestStatus({ loading: false, success: false, message: '❌ ' + (data?.error || 'Не удалось отправить сообщение') });
611
+ setTestStatus({ loading: false, success: false, message: T_KEY(t, 'settings.testFail', { error: (data && data.error) || '' }) });
448
612
  }
449
613
  } catch (err) {
450
- setTestStatus({ loading: false, success: false, message: '❌ ' + err.message });
614
+ setTestStatus({ loading: false, success: false, message: T_KEY(t, 'settings.testFail', { error: err.message }) });
451
615
  }
452
616
  };
453
617
 
@@ -544,13 +708,17 @@ const React = window.React || require('react');
544
708
  overlapPolicy: formOverlapPolicy,
545
709
  kanbanMode: formKanbanMode,
546
710
  };
711
+ // Cross-origin hardening: script tasks need the explicit confirm
712
+ // header, which browsers cannot attach to forged cross-site posts (#86)
713
+ const headers = { 'Content-Type': 'application/json' };
714
+ if (formType === 'script') headers[SCRIPT_CONFIRM_HEADER] = 'script';
547
715
  const res = await fetch('/dsh-cron/tasks', {
548
716
  method: 'POST',
549
- headers: { 'Content-Type': 'application/json' },
717
+ headers,
550
718
  body: JSON.stringify(payload)
551
719
  });
552
720
  const data = await res.json();
553
- if (!data.ok) { alert(data.error || 'Ошибка сохранения'); return; }
721
+ if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
554
722
  setManualModalOpen(false);
555
723
  fetchTasks();
556
724
  } catch (err) { alert(err.message); }
@@ -575,7 +743,7 @@ const React = window.React || require('react');
575
743
  });
576
744
  const data = await res.json();
577
745
  if (!data.ok || !data.sessionId) {
578
- alert(data.error || 'Не удалось запустить диалог с агентом');
746
+ alert(T_KEY(t, 'dsh.startFailed', { error: data.error || '' }));
579
747
  setDshLoading(false);
580
748
  return;
581
749
  }
@@ -586,61 +754,63 @@ const React = window.React || require('react');
586
754
 
587
755
  await openSession(ctx, data.sessionId);
588
756
  } catch (err) {
589
- alert('Ошибка запуска: ' + err.message);
757
+ alert(T_KEY(t, 'dsh.launchError', { error: err.message }));
590
758
  } finally {
591
759
  setDshLoading(false);
592
760
  }
593
761
  };
594
762
 
763
+ const iconHtml = (html) => ({ dangerouslySetInnerHTML: { __html: html } });
764
+ const svgProps = (html) => ({ style: { display: 'flex', alignItems: 'center' }, ...iconHtml(html) });
765
+
595
766
  return React.createElement('div', { className: 'dsh-cron-overlay' },
596
767
  React.createElement('div', { className: 'dsh-cron-container' },
597
768
  React.createElement('div', { className: 'dsh-cron-top-bar' },
598
769
  React.createElement('button', {
599
770
  type: 'button',
600
771
  className: 'dsh-cron-back-btn',
601
- title: 'Назад к чату',
772
+ title: T_KEY(t, 'panel.back'),
602
773
  onClick: () => { if (onClose) onClose(); else toggle.set(false); }
603
774
  },
604
- React.createElement('span', {
605
- style: { display: 'flex', alignItems: 'center' },
606
- dangerouslySetInnerHTML: { __html: BACK_ICON }
607
- }),
608
- React.createElement('span', null, 'Назад к чату')
775
+ React.createElement('span', svgProps(BACK_ICON)),
776
+ React.createElement('span', null, T_KEY(t, 'panel.back'))
609
777
  )
610
778
  ),
611
779
  React.createElement('div', { className: 'dsh-cron-header' },
612
780
  React.createElement('div', null,
613
- React.createElement('div', { className: 'dsh-cron-title' }, 'Запланированные задачи'),
614
- React.createElement('div', { className: 'dsh-cron-subtitle' }, 'Попросите DSH планировать задачи, ставить напоминания или отслеживать обновления')
781
+ React.createElement('div', { className: 'dsh-cron-title' }, T_KEY(t, 'panel.title')),
782
+ React.createElement('div', { className: 'dsh-cron-subtitle' }, T_KEY(t, 'panel.subtitle'))
615
783
  ),
616
784
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
617
785
  React.createElement('button', {
618
786
  type: 'button',
619
787
  className: 'dsh-cron-btn-secondary',
620
788
  style: { display: 'flex', alignItems: 'center', gap: '6px', height: '36px', padding: '0 14px' },
621
- title: 'Настройки Telegram и уведомлений',
789
+ title: T_KEY(t, 'actions.settings'),
622
790
  onClick: openSettingsModal
623
791
  },
624
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
625
- 'Telegram & Оповещения'
792
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
793
+ T_KEY(t, 'actions.settings')
626
794
  ),
627
795
  React.createElement('div', { style: { position: 'relative' } },
628
796
  React.createElement('button', { className: 'dsh-cron-create-btn', onClick: () => setDropdownOpen(!dropdownOpen) },
629
- 'Создать',
630
- React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } })
797
+ T_KEY(t, 'actions.create'),
798
+ React.createElement('span', iconHtml(ICON_CHEVRON_DOWN))
631
799
  ),
632
800
  dropdownOpen && React.createElement('div', { className: 'dsh-cron-dropdown' },
633
- React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: openDshModal }, '💬 Создать с DSH'),
634
- React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, '✏️ Настроить вручную')
801
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: openDshModal }, T_KEY(t, 'actions.createWithDsh')),
802
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, T_KEY(t, 'actions.setupManually'))
635
803
  )
636
804
  )
637
805
  )
638
806
  ),
639
807
  React.createElement('div', { className: 'dsh-cron-search-bar' },
640
- React.createElement('span', { className: 'dsh-cron-search-icon', dangerouslySetInnerHTML: { __html: ICON_SEARCH } }),
808
+ React.createElement('span', { className: 'dsh-cron-search-icon', ...iconHtml(ICON_SEARCH) }),
641
809
  React.createElement('input', {
642
810
  className: 'dsh-cron-search-input',
643
- placeholder: 'Поиск запланированных задач',
811
+ type: 'search',
812
+ 'aria-label': T_KEY(t, 'search.placeholder'),
813
+ placeholder: T_KEY(t, 'search.placeholder'),
644
814
  value: query,
645
815
  onChange: (e) => setQuery(e.target.value)
646
816
  })
@@ -648,29 +818,33 @@ const React = window.React || require('react');
648
818
  stats && React.createElement('div', { className: 'dsh-cron-stats-bar' },
649
819
  React.createElement('div', { className: 'dsh-cron-stat-card' },
650
820
  React.createElement('div', { className: 'dsh-cron-stat-val' }, stats.activeTasks || 0),
651
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Активных задач')
821
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.activeTasks'))
652
822
  ),
653
823
  React.createElement('div', { className: 'dsh-cron-stat-card' },
654
824
  React.createElement('div', { className: 'dsh-cron-stat-val' }, stats.totalRuns || 0),
655
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Всего запусков')
825
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalRuns'))
656
826
  ),
657
827
  React.createElement('div', { className: 'dsh-cron-stat-card' },
658
828
  React.createElement('div', { className: 'dsh-cron-stat-val' }, (stats.totalTokens || 0).toLocaleString()),
659
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Токенов израсходовано')
829
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalTokens'))
660
830
  ),
661
831
  React.createElement('div', { className: 'dsh-cron-stat-card' },
662
- React.createElement('div', { className: 'dsh-cron-stat-val', style: { color: '#10b981' } }, '$' + (stats.totalCostUsd || 0).toFixed(4)),
663
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Оценочная стоимость')
832
+ React.createElement('div', { className: 'dsh-cron-stat-val', style: { color: 'var(--dsh-cron-success)' } }, '$' + (stats.totalCostUsd || 0).toFixed(4)),
833
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalCost'))
664
834
  )
665
835
  ),
666
836
  React.createElement('div', { className: 'dsh-cron-tabs' },
667
- ['all', 'active', 'paused', 'completed'].map((t) =>
837
+ ['all', 'active', 'paused', 'completed'].map((tabName) =>
668
838
  React.createElement('button', {
669
- key: t,
839
+ key: tabName,
670
840
  className: 'dsh-cron-tab',
671
- 'data-active': tab === t ? 'true' : undefined,
672
- onClick: () => setTab(t)
673
- }, t === 'all' ? 'Все' : t === 'active' ? 'Активные' : t === 'paused' ? 'На паузе' : 'Завершенные')
841
+ 'data-active': tab === tabName ? 'true' : undefined,
842
+ onClick: () => setTab(tabName)
843
+ },
844
+ tabName === 'all' ? T_KEY(t, 'tabs.all')
845
+ : tabName === 'active' ? T_KEY(t, 'tabs.active')
846
+ : tabName === 'paused' ? T_KEY(t, 'tabs.paused')
847
+ : T_KEY(t, 'tabs.completed'))
674
848
  )
675
849
  ),
676
850
  fetchError ? React.createElement('div', {
@@ -678,50 +852,59 @@ const React = window.React || require('react');
678
852
  'data-type': 'error',
679
853
  style: { margin: '8px 0', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }
680
854
  },
681
- React.createElement('span', null, 'Ошибка загрузки задач: ' + fetchError),
855
+ React.createElement('span', null, T_KEY(t, 'banner.loadError', { error: fetchError })),
682
856
  React.createElement('button', {
683
857
  type: 'button',
684
- className: 'dsh-cron-btn',
858
+ className: 'dsh-cron-btn-secondary',
685
859
  style: { marginLeft: '8px', padding: '2px 8px', fontSize: '12px' },
686
860
  onClick: () => fetchTasks()
687
- }, 'Повторить')
861
+ }, T_KEY(t, 'banner.retry'))
688
862
  ) : null,
689
863
  React.createElement('div', { className: 'dsh-cron-task-list' },
690
864
  loading && tasks.length === 0
691
- ? React.createElement('div', { style: { color: '#888', padding: '16px 0', fontSize: '14px' } }, 'Загрузка запланированных задач...')
865
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #888)', padding: '16px 0', fontSize: '14px' } }, T_KEY(t, 'list.loading'))
692
866
  : tasks.length === 0
693
- ? React.createElement('div', { style: { color: '#777', padding: '16px 0', fontSize: '14px' } }, 'Нет запланированных задач')
867
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #777)', padding: '16px 0', fontSize: '14px' } }, T_KEY(t, 'list.empty'))
694
868
  : tasks.map((task) =>
695
869
  React.createElement('div', {
696
870
  key: task.id,
697
871
  className: 'dsh-cron-task-item',
698
- onClick: () => openEditModal(task)
872
+ role: 'button',
873
+ tabIndex: 0,
874
+ 'aria-label': task.title,
875
+ onClick: () => openEditModal(task),
876
+ onKeyDown: (e) => {
877
+ if (e.key === 'Enter' || e.key === ' ') {
878
+ e.preventDefault();
879
+ openEditModal(task);
880
+ }
881
+ }
699
882
  },
700
883
  React.createElement('button', {
701
884
  type: 'button',
702
885
  className: 'dsh-cron-task-status-btn',
703
886
  'data-active': task.status === 'active' ? 'true' : undefined,
704
- title: task.status === 'active' ? 'Приостановить' : 'Возобновить',
887
+ title: task.status === 'active' ? T_KEY(t, 'actions.pause') : T_KEY(t, 'actions.resume'),
888
+ 'aria-label': task.status === 'active' ? T_KEY(t, 'actions.pause') : T_KEY(t, 'actions.resume'),
705
889
  onClick: (e) => { e.stopPropagation(); handleToggleTask(task.id); }
706
- }, React.createElement('span', { dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PAUSE : ICON_PLAY } })),
890
+ }, React.createElement('span', svgProps(task.status === 'active' ? ICON_PAUSE : ICON_PLAY))),
707
891
  React.createElement('div', { className: 'dsh-cron-task-info' },
708
892
  React.createElement('div', { className: 'dsh-cron-task-title' },
709
893
  task.title,
710
894
  React.createElement('span', {
711
895
  className: 'dsh-cron-type-tag',
712
- style: {
713
- background: task.type === 'script' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(16, 185, 129, 0.15)',
714
- color: task.type === 'script' ? '#60a5fa' : '#34d399'
715
- }
716
- }, task.type === 'script' ? 'Shell' : 'LLM'),
896
+ style: task.type === 'script'
897
+ ? { background: 'var(--dsh-cron-info-bg)', color: 'var(--dsh-cron-info)' }
898
+ : { background: 'var(--dsh-cron-success-bg)', color: 'var(--dsh-cron-success)' }
899
+ }, task.type === 'script' ? T_KEY(t, 'list.typeShell') : T_KEY(t, 'list.typeLlm')),
717
900
  task.model ? React.createElement('span', { className: 'dsh-cron-task-model-tag' }, task.model.split('/').pop()) : null,
718
- task.oneShot ? React.createElement('span', { className: 'dsh-cron-oneshot-tag' }, 'Разовая') : null,
901
+ task.oneShot ? React.createElement('span', { className: 'dsh-cron-oneshot-tag' }, T_KEY(t, 'list.oneShot')) : null,
719
902
  task.totalCostUsd > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + task.totalCostUsd.toFixed(4)) : null,
720
903
  task.totalTokens > 0 ? React.createElement('span', { className: 'dsh-cron-tokens-tag' }, (task.totalTokens > 1000 ? Math.round(task.totalTokens / 1000) + 'k' : task.totalTokens) + ' tok') : null
721
904
  ),
722
905
  React.createElement('div', { className: 'dsh-cron-task-sched' },
723
906
  task.scheduleText || task.schedule,
724
- task.nextRunAt ? ` · след: ${new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''
907
+ task.nextRunAt ? T_KEY(t, 'list.next', { time: new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }) : ''
725
908
  ),
726
909
  task.prompt ? React.createElement('div', {
727
910
  className: 'dsh-cron-task-prompt-preview',
@@ -735,37 +918,45 @@ const React = window.React || require('react');
735
918
  React.createElement('button', {
736
919
  type: 'button',
737
920
  className: 'dsh-cron-icon-btn',
738
- title: 'Запустить немедленно',
921
+ title: T_KEY(t, 'actions.runNow'),
739
922
  onClick: () => handleRunNow(task.id)
740
- }, 'Запустить'),
923
+ }, T_KEY(t, 'actions.runNowShort')),
741
924
  React.createElement('button', {
742
925
  type: 'button',
743
926
  className: 'dsh-cron-icon-btn',
744
- title: 'Редактировать / Подробнее',
927
+ title: T_KEY(t, 'actions.edit'),
928
+ 'aria-label': T_KEY(t, 'actions.edit'),
745
929
  onClick: () => openEditModal(task)
746
- }, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_EDIT } })),
930
+ }, React.createElement('span', svgProps(ICON_EDIT))),
747
931
  React.createElement('button', {
748
932
  type: 'button',
749
933
  className: 'dsh-cron-icon-btn',
750
- title: 'Удалить задачу',
934
+ title: T_KEY(t, 'actions.delete'),
935
+ 'aria-label': T_KEY(t, 'actions.delete'),
751
936
  onClick: () => handleDelete(task.id)
752
- }, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_TRASH } }))
937
+ }, React.createElement('span', svgProps(ICON_TRASH)))
753
938
  )
754
939
  )
755
940
  )
756
941
  ),
757
- React.createElement('div', { className: 'dsh-cron-recs-title' }, 'Рекомендуемые задачи'),
942
+ React.createElement('div', { className: 'dsh-cron-recs-title' }, T_KEY(t, 'recs.title')),
758
943
  React.createElement('div', { className: 'dsh-cron-recs-list' },
759
944
  recs.map((rec) =>
760
945
  React.createElement('div', {
761
946
  key: rec.id,
762
947
  className: 'dsh-cron-rec-card',
763
- onClick: () => openManualModal(rec)
948
+ role: 'button',
949
+ tabIndex: 0,
950
+ onClick: () => openManualModal(rec),
951
+ onKeyDown: (e) => {
952
+ if (e.key === 'Enter' || e.key === ' ') {
953
+ e.preventDefault();
954
+ openManualModal(rec);
955
+ }
956
+ }
764
957
  },
765
958
  React.createElement('div', { className: 'dsh-cron-rec-icon' },
766
- React.createElement('span', { dangerouslySetInnerHTML: {
767
- __html: rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY
768
- } })
959
+ React.createElement('span', iconHtml(rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY))
769
960
  ),
770
961
  React.createElement('div', { className: 'dsh-cron-rec-content' },
771
962
  React.createElement('div', { className: 'dsh-cron-rec-title' },
@@ -784,285 +975,288 @@ const React = window.React || require('react');
784
975
  React.createElement('div', {
785
976
  className: 'dsh-cron-modal',
786
977
  style: { maxWidth: '640px' },
978
+ role: 'dialog',
979
+ 'aria-modal': 'true',
787
980
  onClick: (e) => e.stopPropagation()
788
981
  },
789
982
  React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: formId ? '12px' : '18px' } },
790
- React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, formId ? 'Редактирование задачи' : 'Новая запланированная задача'),
983
+ React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, formId ? T_KEY(t, 'modal.editTitle') : T_KEY(t, 'modal.newTitle')),
791
984
  selectedTaskMeta && React.createElement('span', {
792
985
  style: {
793
986
  fontSize: '12px',
794
987
  padding: '3px 8px',
795
988
  borderRadius: '6px',
796
- background: selectedTaskMeta.status === 'active' ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)',
797
- color: selectedTaskMeta.status === 'active' ? '#10b981' : '#ef4444'
989
+ background: selectedTaskMeta.status === 'active' ? 'var(--dsh-cron-success-bg)' : 'var(--dsh-cron-danger-bg)',
990
+ color: selectedTaskMeta.status === 'active' ? 'var(--dsh-cron-success)' : 'var(--dsh-cron-danger)'
798
991
  }
799
- }, selectedTaskMeta.status === 'active' ? '● Активна' : '○ На паузе')
992
+ }, selectedTaskMeta.status === 'active' ? T_KEY(t, 'modal.statusActive') : T_KEY(t, 'modal.statusPaused'))
800
993
  ),
801
994
 
802
995
  formId && React.createElement('div', { className: 'dsh-cron-modal-nav' },
803
996
  React.createElement('button', {
804
997
  type: 'button',
805
998
  className: 'dsh-cron-modal-tab',
806
- 'data-active': modalTab === 'params',
999
+ 'data-active': modalTab === 'params' ? 'true' : undefined,
807
1000
  onClick: () => setModalTab('params')
808
- }, 'Параметры'),
1001
+ }, T_KEY(t, 'modal.params')),
809
1002
  React.createElement('button', {
810
1003
  type: 'button',
811
1004
  className: 'dsh-cron-modal-tab',
812
- 'data-active': modalTab === 'history',
1005
+ 'data-active': modalTab === 'history' ? 'true' : undefined,
813
1006
  onClick: () => { setModalTab('history'); if (formId) loadTaskHistory(formId); }
814
- }, 'История запусков (' + (taskHistory.length || 0) + ')')
1007
+ }, T_KEY(t, 'modal.history', { count: taskHistory.length || 0 }))
815
1008
  ),
816
1009
 
817
1010
  modalTab === 'history' && formId ? React.createElement('div', null,
818
- historyLoading ? React.createElement('div', { className: 'dsh-cron-history-empty' }, 'Загрузка истории...') :
819
- taskHistory.length === 0 ? React.createElement('div', { className: 'dsh-cron-history-empty' }, 'История запусков пока пуста') :
820
- React.createElement('div', { className: 'dsh-cron-history-list' },
821
- taskHistory.map((run) => React.createElement('div', { key: run.id, className: 'dsh-cron-history-item' },
822
- React.createElement('div', { className: 'dsh-cron-history-head' },
823
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
824
- React.createElement('span', {
825
- className: 'dsh-cron-history-status',
826
- 'data-status': run.status
827
- }, run.status === 'success' ? 'Успешно' : run.status === 'skipped' ? 'Пропущен' : 'Ошибка'),
828
- React.createElement('span', { className: 'dsh-cron-history-time' }, run.at ? new Date(run.at).toLocaleString() : '')
829
- ),
830
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
831
- run.costUsd > 0 && React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + run.costUsd.toFixed(5)),
832
- run.usage && (run.usage.inputTokens > 0 || run.usage.outputTokens > 0) && React.createElement('span', { className: 'dsh-cron-tokens-tag' }, (run.usage.inputTokens + run.usage.outputTokens) + ' tok'),
833
- React.createElement('span', { style: { color: '#888', fontSize: '11.5px' } }, (run.durationMs || 0) + ' ms')
834
- )
1011
+ historyLoading ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'history.loading')) :
1012
+ taskHistory.length === 0 ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'history.empty')) :
1013
+ React.createElement('div', { className: 'dsh-cron-history-list' },
1014
+ taskHistory.map((run) => React.createElement('div', { key: run.id, className: 'dsh-cron-history-item' },
1015
+ React.createElement('div', { className: 'dsh-cron-history-head' },
1016
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
1017
+ React.createElement('span', {
1018
+ className: 'dsh-cron-history-status',
1019
+ 'data-status': run.status
1020
+ }, statusLabel(t, run.status)),
1021
+ React.createElement('span', { className: 'dsh-cron-history-time' }, run.at ? new Date(run.at).toLocaleString() : '')
1022
+ ),
1023
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
1024
+ run.costUsd > 0 && React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + run.costUsd.toFixed(5)),
1025
+ run.usage && (run.usage.inputTokens > 0 || run.usage.outputTokens > 0) && React.createElement('span', { className: 'dsh-cron-tokens-tag' }, (run.usage.inputTokens + run.usage.outputTokens) + ' tok'),
1026
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary, #888)', fontSize: '11.5px' } }, (run.durationMs || 0) + ' ms')
1027
+ )
1028
+ ),
1029
+ run.output && React.createElement('div', { className: 'dsh-cron-history-output' }, run.output),
1030
+ run.error && React.createElement('div', { className: 'dsh-cron-history-output', style: { color: 'var(--dsh-cron-danger)', borderColor: 'var(--dsh-cron-danger-bg)' } }, run.error)
1031
+ ))
835
1032
  ),
836
- run.output && React.createElement('div', { className: 'dsh-cron-history-output' }, run.output),
837
- run.error && React.createElement('div', { className: 'dsh-cron-history-output', style: { color: '#ef4444', borderColor: '#552222' } }, run.error)
838
- ))
839
- ),
840
1033
  React.createElement('div', { className: 'dsh-cron-modal-foot', style: { marginTop: '18px' } },
841
1034
  React.createElement('button', {
842
1035
  type: 'button',
843
1036
  className: 'dsh-cron-btn-secondary',
844
1037
  onClick: () => { if (formId) loadTaskHistory(formId); }
845
- }, 'Обновить историю'),
1038
+ }, T_KEY(t, 'history.refresh')),
846
1039
  React.createElement('button', {
847
1040
  type: 'button',
848
1041
  className: 'dsh-cron-btn-primary',
849
1042
  onClick: () => setManualModalOpen(false)
850
- }, 'Закрыть')
1043
+ }, T_KEY(t, 'modal.close'))
851
1044
  )
852
1045
  ) :
853
1046
 
854
- React.createElement('form', { onSubmit: handleSaveManual },
855
- React.createElement('div', { className: 'dsh-cron-form-group' },
856
- React.createElement('label', null, 'Название задачи'),
857
- React.createElement('input', {
858
- required: true,
859
- value: formTitle,
860
- placeholder: 'Например: dsh-tags-watch',
861
- onChange: (e) => setFormTitle(e.target.value)
862
- })
863
- ),
864
- React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
1047
+ React.createElement('form', { onSubmit: handleSaveManual },
865
1048
  React.createElement('div', { className: 'dsh-cron-form-group' },
866
- React.createElement('label', null, 'Тип выполнения'),
867
- React.createElement('select', {
868
- value: formType,
869
- onChange: (e) => setFormType(e.target.value)
870
- },
871
- React.createElement('option', { value: 'llm' }, 'LLM (Агент с нейросетью)'),
872
- React.createElement('option', { value: 'script' }, 'NO-LLM (Shell-команда/Скрипт)')
873
- )
874
- ),
875
- React.createElement('div', { className: 'dsh-cron-form-group' },
876
- React.createElement('label', null, 'Расписание (cron или интервал)'),
1049
+ React.createElement('label', null, T_KEY(t, 'form.nameLabel')),
877
1050
  React.createElement('input', {
878
1051
  required: true,
879
- value: formSchedule,
880
- placeholder: '0 */6 * * * или every 2h или in 30m или at: 2026-09-05T15:00:00Z',
881
- onChange: (e) => setFormSchedule(e.target.value)
882
- })
883
- )
884
- ),
885
- React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
886
- React.createElement('div', { className: 'dsh-cron-form-group' },
887
- React.createElement('label', null, 'Таймаут выполнения (секунды)'),
888
- React.createElement('input', {
889
- type: 'number',
890
- min: 1,
891
- value: formTimeoutSeconds,
892
- placeholder: '1800',
893
- onChange: (e) => setFormTimeoutSeconds(e.target.value)
1052
+ autoFocus: true,
1053
+ value: formTitle,
1054
+ placeholder: T_KEY(t, 'form.namePlaceholder'),
1055
+ onChange: (e) => setFormTitle(e.target.value)
894
1056
  })
895
1057
  ),
896
- React.createElement('div', { className: 'dsh-cron-form-group' },
897
- React.createElement('label', null, 'Политика наложения (Overlap)'),
898
- React.createElement('select', {
899
- value: formOverlapPolicy,
900
- onChange: (e) => setFormOverlapPolicy(e.target.value)
901
- },
902
- React.createElement('option', { value: 'skip' }, 'Пропустить новый запуск (skip)'),
903
- React.createElement('option', { value: 'queue' }, 'Встать в очередь (queue)'),
904
- React.createElement('option', { value: 'replace' }, 'Отменить текущий и перезапустить (replace)')
1058
+ React.createElement('div', { className: 'dsh-cron-form-row' },
1059
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1060
+ React.createElement('label', null, T_KEY(t, 'form.typeLabel')),
1061
+ React.createElement('select', {
1062
+ value: formType,
1063
+ onChange: (e) => setFormType(e.target.value)
1064
+ },
1065
+ React.createElement('option', { value: 'llm' }, T_KEY(t, 'form.typeLlm')),
1066
+ React.createElement('option', { value: 'script' }, T_KEY(t, 'form.typeScript'))
1067
+ )
1068
+ ),
1069
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1070
+ React.createElement('label', null, T_KEY(t, 'form.scheduleLabel')),
1071
+ React.createElement('input', {
1072
+ required: true,
1073
+ value: formSchedule,
1074
+ placeholder: T_KEY(t, 'form.schedulePlaceholder'),
1075
+ onChange: (e) => setFormSchedule(e.target.value)
1076
+ })
905
1077
  )
906
- )
907
- ),
908
- React.createElement('div', { className: 'dsh-cron-form-group' },
909
- React.createElement('label', null, 'Создание карточки в Kanban (dsh-kanban)'),
910
- React.createElement('select', {
911
- value: formKanbanMode,
912
- onChange: (e) => setFormKanbanMode(e.target.value)
913
- },
914
- React.createElement('option', { value: 'none' }, 'Не создавать карточки (отключено)'),
915
- React.createElement('option', { value: 'on_failure' }, 'Создавать тикет при ошибке/сбое (в Backlog)'),
916
- React.createElement('option', { value: 'always' }, 'Создавать карточку при каждом завершении (Done / Backlog)')
917
- )
918
- ),
919
- formType !== 'script' && React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
920
- React.createElement('div', { className: 'dsh-cron-form-group' },
921
- React.createElement('label', null, 'Провайдер LLM'),
922
- React.createElement('select', {
923
- value: formProvider,
924
- onChange: handleProviderChange
925
- },
926
- React.createElement('option', { value: '' }, 'По умолчанию (DSH)'),
927
- providers.map((p) => React.createElement('option', { key: p.id, value: p.id }, p.name || p.id))
1078
+ ),
1079
+ React.createElement('div', { className: 'dsh-cron-form-row' },
1080
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1081
+ React.createElement('label', null, T_KEY(t, 'form.timeoutLabel')),
1082
+ React.createElement('input', {
1083
+ type: 'number',
1084
+ min: 1,
1085
+ value: formTimeoutSeconds,
1086
+ placeholder: '1800',
1087
+ onChange: (e) => setFormTimeoutSeconds(e.target.value)
1088
+ })
1089
+ ),
1090
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1091
+ React.createElement('label', null, T_KEY(t, 'form.overlapLabel')),
1092
+ React.createElement('select', {
1093
+ value: formOverlapPolicy,
1094
+ onChange: (e) => setFormOverlapPolicy(e.target.value)
1095
+ },
1096
+ React.createElement('option', { value: 'skip' }, T_KEY(t, 'form.overlapSkip')),
1097
+ React.createElement('option', { value: 'queue' }, T_KEY(t, 'form.overlapQueue')),
1098
+ React.createElement('option', { value: 'replace' }, T_KEY(t, 'form.overlapReplace'))
1099
+ )
928
1100
  )
929
1101
  ),
930
1102
  React.createElement('div', { className: 'dsh-cron-form-group' },
931
- React.createElement('label', null, 'Модель LLM'),
1103
+ React.createElement('label', null, T_KEY(t, 'form.kanbanLabel')),
932
1104
  React.createElement('select', {
933
- value: formModel,
934
- onChange: (e) => setFormModel(e.target.value)
1105
+ value: formKanbanMode,
1106
+ onChange: (e) => setFormKanbanMode(e.target.value)
935
1107
  },
936
- React.createElement('option', { value: '' }, 'По умолчанию'),
937
- models.map((m) => React.createElement('option', { key: m.id, value: m.id }, m.name || m.id))
1108
+ React.createElement('option', { value: 'none' }, T_KEY(t, 'form.kanbanNone')),
1109
+ React.createElement('option', { value: 'on_failure' }, T_KEY(t, 'form.kanbanOnFailure')),
1110
+ React.createElement('option', { value: 'always' }, T_KEY(t, 'form.kanbanAlways'))
938
1111
  )
939
- )
940
- ),
941
- React.createElement('div', { className: 'dsh-cron-form-group' },
942
- React.createElement('label', null, formType === 'script' ? 'Shell-команда или скрипт' : 'Инструкция / Промпт для агента'),
943
- React.createElement('textarea', {
944
- required: true,
945
- rows: 9,
946
- style: { fontFamily: 'monospace', fontSize: '12.5px', lineHeight: '1.45' },
947
- value: formPrompt,
948
- placeholder: formType === 'script' ? 'curl -fsSL https://... || exit 1' : 'Опишите, что именно агент должен сделать...',
949
- onChange: (e) => setFormPrompt(e.target.value)
950
- })
951
- ),
952
- React.createElement('div', {
953
- style: {
954
- background: 'rgba(255,255,255,0.02)',
955
- border: '1px solid #2e2e2e',
956
- borderRadius: '8px',
957
- padding: '12px 14px',
958
- marginBottom: '16px'
959
- }
960
- },
961
- React.createElement('div', { style: { fontWeight: 500, fontSize: '13px', color: '#eee', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px' } },
962
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
963
- 'Уведомления в Telegram'
964
1112
  ),
965
- React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: formNotifyTelegram ? '8px' : '0' } },
966
- React.createElement('input', {
967
- type: 'checkbox',
968
- checked: formNotifyTelegram,
969
- onChange: (e) => setFormNotifyTelegram(e.target.checked)
970
- }),
971
- React.createElement('span', null, 'Отправлять отчет о выполнении в Telegram')
1113
+ formType !== 'script' && React.createElement('div', { className: 'dsh-cron-form-row' },
1114
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1115
+ React.createElement('label', null, T_KEY(t, 'form.providerLabel')),
1116
+ React.createElement('select', {
1117
+ value: formProvider,
1118
+ onChange: handleProviderChange
1119
+ },
1120
+ React.createElement('option', { value: '' }, T_KEY(t, 'form.providerDefault')),
1121
+ providers.map((p) => React.createElement('option', { key: p.id, value: p.id }, p.name || p.id))
1122
+ )
1123
+ ),
1124
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1125
+ React.createElement('label', null, T_KEY(t, 'form.modelLabel')),
1126
+ React.createElement('select', {
1127
+ value: formModel,
1128
+ onChange: (e) => setFormModel(e.target.value)
1129
+ },
1130
+ React.createElement('option', { value: '' }, T_KEY(t, 'form.modelDefault')),
1131
+ models.map((m) => React.createElement('option', { key: m.id, value: m.id }, m.name || m.id))
1132
+ )
1133
+ )
972
1134
  ),
973
- formNotifyTelegram && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
974
- React.createElement('input', {
975
- type: 'checkbox',
976
- checked: formOnlyOnFailure,
977
- onChange: (e) => setFormOnlyOnFailure(e.target.checked)
978
- }),
979
- React.createElement('span', null, 'Только при ошибках (onlyOnFailure / silent mode при успехе)')
980
- )
981
- ),
982
- selectedTaskMeta && React.createElement('div', {
983
- style: {
984
- background: 'rgba(255,255,255,0.03)',
985
- border: '1px solid #2e2e2e',
986
- borderRadius: '8px',
987
- padding: '10px 14px',
988
- fontSize: '12.5px',
989
- color: '#999',
990
- marginBottom: '16px',
991
- display: 'flex',
992
- justifyContent: 'space-between',
993
- alignItems: 'center'
994
- }
995
- },
996
- React.createElement('div', null,
997
- React.createElement('span', null, 'Следующий запуск: '),
998
- React.createElement('strong', { style: { color: '#eee' } }, selectedTaskMeta.nextRunAt ? new Date(selectedTaskMeta.nextRunAt).toLocaleString() : 'не запланирован')
1135
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1136
+ React.createElement('label', null, formType === 'script' ? T_KEY(t, 'form.promptLabelScript') : T_KEY(t, 'form.promptLabelLlm')),
1137
+ React.createElement('textarea', {
1138
+ required: true,
1139
+ rows: 9,
1140
+ style: { fontFamily: 'monospace', fontSize: '12.5px', lineHeight: '1.45' },
1141
+ value: formPrompt,
1142
+ placeholder: formType === 'script' ? T_KEY(t, 'form.promptPlaceholderScript') : T_KEY(t, 'form.promptPlaceholderLlm'),
1143
+ onChange: (e) => setFormPrompt(e.target.value)
1144
+ })
999
1145
  ),
1000
- React.createElement('button', {
1001
- type: 'button',
1002
- className: 'dsh-cron-btn-secondary',
1003
- style: { padding: '4px 10px', fontSize: '12px' },
1004
- onClick: async () => {
1005
- await handleRunNow(selectedTaskMeta.id);
1006
- if (formId) loadTaskHistory(formId);
1146
+ React.createElement('div', {
1147
+ style: {
1148
+ background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))',
1149
+ border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
1150
+ borderRadius: '8px',
1151
+ padding: '12px 14px',
1152
+ marginBottom: '16px'
1007
1153
  }
1008
- }, 'Запустить сейчас')
1009
- ),
1010
- React.createElement('div', { className: 'dsh-cron-modal-foot' },
1011
- React.createElement('button', {
1012
- type: 'button',
1013
- className: 'dsh-cron-btn-secondary',
1014
- onClick: () => setManualModalOpen(false)
1015
- }, 'Отмена'),
1016
- React.createElement('button', {
1017
- type: 'submit',
1018
- className: 'dsh-cron-btn-primary'
1019
- }, formId ? 'Сохранить изменения' : 'Создать задачу')
1154
+ },
1155
+ React.createElement('div', { style: { fontWeight: 500, fontSize: '13px', color: 'var(--dsw-alias-label-primary, #eee)', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px' } },
1156
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
1157
+ T_KEY(t, 'form.notifyTitle')
1158
+ ),
1159
+ React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: formNotifyTelegram ? '8px' : '0' } },
1160
+ React.createElement('input', {
1161
+ type: 'checkbox',
1162
+ checked: formNotifyTelegram,
1163
+ onChange: (e) => setFormNotifyTelegram(e.target.checked)
1164
+ }),
1165
+ React.createElement('span', null, T_KEY(t, 'form.notifyTelegram'))
1166
+ ),
1167
+ formNotifyTelegram && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
1168
+ React.createElement('input', {
1169
+ type: 'checkbox',
1170
+ checked: formOnlyOnFailure,
1171
+ onChange: (e) => setFormOnlyOnFailure(e.target.checked)
1172
+ }),
1173
+ React.createElement('span', null, T_KEY(t, 'form.notifyOnlyFailure'))
1174
+ )
1175
+ ),
1176
+ selectedTaskMeta && React.createElement('div', {
1177
+ style: {
1178
+ background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.03))',
1179
+ border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
1180
+ borderRadius: '8px',
1181
+ padding: '10px 14px',
1182
+ fontSize: '12.5px',
1183
+ color: 'var(--dsw-alias-label-secondary, #999)',
1184
+ marginBottom: '16px',
1185
+ display: 'flex',
1186
+ justifyContent: 'space-between',
1187
+ alignItems: 'center'
1188
+ }
1189
+ },
1190
+ React.createElement('div', null,
1191
+ React.createElement('span', null, T_KEY(t, 'form.nextRunLabel')),
1192
+ React.createElement('strong', { style: { color: 'var(--dsw-alias-label-primary, #eee)' } }, selectedTaskMeta.nextRunAt ? new Date(selectedTaskMeta.nextRunAt).toLocaleString() : T_KEY(t, 'form.notScheduled'))
1193
+ ),
1194
+ React.createElement('button', {
1195
+ type: 'button',
1196
+ className: 'dsh-cron-btn-secondary',
1197
+ style: { padding: '4px 10px', fontSize: '12px' },
1198
+ onClick: async () => {
1199
+ await handleRunNow(selectedTaskMeta.id);
1200
+ if (formId) loadTaskHistory(formId);
1201
+ }
1202
+ }, T_KEY(t, 'actions.runNow'))
1203
+ ),
1204
+ React.createElement('div', { className: 'dsh-cron-modal-foot' },
1205
+ React.createElement('button', {
1206
+ type: 'button',
1207
+ className: 'dsh-cron-btn-secondary',
1208
+ onClick: () => setManualModalOpen(false)
1209
+ }, T_KEY(t, 'modal.cancel')),
1210
+ React.createElement('button', {
1211
+ type: 'submit',
1212
+ className: 'dsh-cron-btn-primary'
1213
+ }, formId ? T_KEY(t, 'modal.save') : T_KEY(t, 'modal.create'))
1214
+ )
1020
1215
  )
1021
- )
1022
1216
  )
1023
1217
  ),
1024
1218
 
1025
- // 1.5 Settings Modal
1219
+ // 2. Settings modal
1026
1220
  settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
1027
- React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
1221
+ React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
1028
1222
  React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
1029
1223
  React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0, display: 'flex', alignItems: 'center', gap: '8px' } },
1030
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
1031
- 'Настройки Telegram и уведомлений'
1224
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
1225
+ T_KEY(t, 'settings.modalTitle')
1032
1226
  ),
1033
1227
  React.createElement('button', {
1034
1228
  type: 'button',
1035
- style: { background: 'none', border: 'none', color: '#888', cursor: 'pointer', fontSize: '18px' },
1229
+ style: { background: 'none', border: 'none', color: 'var(--dsw-alias-label-tertiary, #888)', cursor: 'pointer', fontSize: '18px' },
1036
1230
  onClick: () => setSettingsModalOpen(false)
1037
1231
  }, '✕')
1038
1232
  ),
1039
1233
  React.createElement('form', { onSubmit: handleSaveSettings },
1040
1234
  React.createElement('div', { className: 'dsh-cron-form-group' },
1041
- React.createElement('label', null, 'Telegram Bot Token'),
1235
+ React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
1042
1236
  React.createElement('input', {
1043
1237
  type: 'password',
1044
1238
  value: settingsBotToken,
1045
- placeholder: settingsHasDefault ? 'Используется бот из настроек DSH (или введите свой)' : '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
1239
+ placeholder: settingsHasDefault ? T_KEY(t, 'settings.tokenPlaceholderDefault') : T_KEY(t, 'settings.tokenPlaceholder'),
1046
1240
  onChange: (e) => setSettingsBotToken(e.target.value)
1047
1241
  }),
1048
- settingsHasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, '✓ Обнаружен настроенный бот в профиле DSH')
1242
+ settingsHasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'settings.defaultHint'))
1049
1243
  ),
1050
1244
  React.createElement('div', { className: 'dsh-cron-form-group' },
1051
- React.createElement('label', null, 'Telegram Chat ID'),
1245
+ React.createElement('label', null, T_KEY(t, 'settings.chatIdLabel')),
1052
1246
  React.createElement('input', {
1053
1247
  value: settingsChatId,
1054
- placeholder: 'Например: 345678901',
1248
+ placeholder: T_KEY(t, 'settings.chatIdPlaceholder'),
1055
1249
  onChange: (e) => setSettingsChatId(e.target.value)
1056
1250
  })
1057
1251
  ),
1058
- React.createElement('div', { style: { background: 'rgba(255,255,255,0.02)', border: '1px solid #2e2e2e', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
1252
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
1059
1253
  React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: '8px' } },
1060
1254
  React.createElement('input', {
1061
1255
  type: 'checkbox',
1062
1256
  checked: settingsNotifyGlobal,
1063
1257
  onChange: (e) => setSettingsNotifyGlobal(e.target.checked)
1064
1258
  }),
1065
- React.createElement('span', null, 'Глобально отправлять отчеты всех задач в Telegram')
1259
+ React.createElement('span', null, T_KEY(t, 'settings.globalNotify'))
1066
1260
  ),
1067
1261
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
1068
1262
  React.createElement('input', {
@@ -1070,7 +1264,7 @@ const React = window.React || require('react');
1070
1264
  checked: settingsOnlyOnFailureGlobal,
1071
1265
  onChange: (e) => setSettingsOnlyOnFailureGlobal(e.target.checked)
1072
1266
  }),
1073
- React.createElement('span', null, 'Глобальный режим: Только при ошибках (silent при успехе)')
1267
+ React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
1074
1268
  )
1075
1269
  ),
1076
1270
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '14px' } },
@@ -1080,7 +1274,7 @@ const React = window.React || require('react');
1080
1274
  style: { display: 'inline-flex', alignItems: 'center', gap: '6px' },
1081
1275
  disabled: testStatus?.loading,
1082
1276
  onClick: handleTestTelegram
1083
- }, testStatus?.loading ? 'Отправка...' : '🔔 Проверить отправку в Telegram')
1277
+ }, testStatus?.loading ? T_KEY(t, 'settings.sending') : T_KEY(t, 'settings.testTelegram'))
1084
1278
  ),
1085
1279
  testStatus && testStatus.message && React.createElement('div', {
1086
1280
  className: 'dsh-cron-status-banner',
@@ -1091,28 +1285,29 @@ const React = window.React || require('react');
1091
1285
  type: 'button',
1092
1286
  className: 'dsh-cron-btn-secondary',
1093
1287
  onClick: () => setSettingsModalOpen(false)
1094
- }, 'Отмена'),
1288
+ }, T_KEY(t, 'modal.cancel')),
1095
1289
  React.createElement('button', {
1096
1290
  type: 'submit',
1097
1291
  className: 'dsh-cron-btn-primary'
1098
- }, 'Сохранить настройки')
1292
+ }, T_KEY(t, 'settings.save'))
1099
1293
  )
1100
1294
  )
1101
1295
  )
1102
1296
  ),
1103
- // 2. "Создать с DSH" modal
1297
+
1298
+ // 3. "Create with DSH" modal
1104
1299
  dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
1105
- React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
1106
- React.createElement('div', { className: 'dsh-cron-modal-title' }, 'Запланировать задачу с DSH Агентом'),
1300
+ React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
1301
+ React.createElement('div', { className: 'dsh-cron-modal-title' }, T_KEY(t, 'dsh.modalTitle')),
1107
1302
  React.createElement('div', { className: 'dsh-cron-form-group' },
1108
- React.createElement('label', null, 'Опишите задачу и желаемую периодичность:'),
1303
+ React.createElement('label', null, T_KEY(t, 'dsh.descLabel')),
1109
1304
  React.createElement('div', { className: 'dsh-cron-chat-box' },
1110
1305
  React.createElement('textarea', {
1111
1306
  className: 'dsh-cron-chat-input',
1112
1307
  rows: 4,
1113
1308
  autoFocus: true,
1114
1309
  disabled: dshLoading,
1115
- placeholder: 'Например: проверяй каждые 2 часа свободное место на сервере и наличие упавших systemd-сервисов, присылай алерт только если что-то не так...',
1310
+ placeholder: T_KEY(t, 'dsh.placeholder'),
1116
1311
  value: dshPrompt,
1117
1312
  onKeyDown: (e) => {
1118
1313
  if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
@@ -1123,18 +1318,15 @@ const React = window.React || require('react');
1123
1318
  onChange: (e) => setDshPrompt(e.target.value)
1124
1319
  }),
1125
1320
  React.createElement('div', { className: 'dsh-cron-chat-foot' },
1126
- React.createElement('span', { className: 'dsh-cron-chat-hint' }, 'Ctrl+Enter для отправки'),
1321
+ React.createElement('span', { className: 'dsh-cron-chat-hint' }, T_KEY(t, 'dsh.sendHint')),
1127
1322
  React.createElement('button', {
1128
1323
  type: 'button',
1129
1324
  className: 'dsh-cron-send-btn',
1130
1325
  disabled: dshLoading || !dshPrompt.trim(),
1131
- title: 'Отправить агенту',
1326
+ title: T_KEY(t, 'dsh.sendTitle'),
1132
1327
  onClick: handleStartWithDSH
1133
1328
  },
1134
- React.createElement('span', {
1135
- style: { display: 'flex', alignItems: 'center' },
1136
- dangerouslySetInnerHTML: { __html: ICON_SEND }
1137
- })
1329
+ React.createElement('span', svgProps(ICON_SEND))
1138
1330
  )
1139
1331
  )
1140
1332
  )
@@ -1145,7 +1337,7 @@ const React = window.React || require('react');
1145
1337
  className: 'dsh-cron-btn-secondary',
1146
1338
  disabled: dshLoading,
1147
1339
  onClick: () => setDshModalOpen(false)
1148
- }, 'Отмена')
1340
+ }, T_KEY(t, 'modal.cancel'))
1149
1341
  )
1150
1342
  )
1151
1343
  )
@@ -1154,6 +1346,7 @@ const React = window.React || require('react');
1154
1346
 
1155
1347
  function CronSettingsCard(props) {
1156
1348
  const { ctx, toggle } = props;
1349
+ const t = props.t || translate;
1157
1350
  const [token, setToken] = React.useState('');
1158
1351
  const [chatId, setChatId] = React.useState('');
1159
1352
  const [notify, setNotify] = React.useState(false);
@@ -1204,7 +1397,7 @@ const React = window.React || require('react');
1204
1397
  };
1205
1398
 
1206
1399
  const testTelegram = async () => {
1207
- setTestInfo({ loading: true, message: 'Отправка...' });
1400
+ setTestInfo({ loading: true, message: T_KEY(t, 'settings.sending') });
1208
1401
  try {
1209
1402
  const res = await fetch('/dsh-cron/telegram/test', {
1210
1403
  method: 'POST',
@@ -1213,17 +1406,17 @@ const React = window.React || require('react');
1213
1406
  });
1214
1407
  const data = await res.json();
1215
1408
  if (data && data.ok) {
1216
- setTestInfo({ success: true, message: '✅ Успешно доставлено в Telegram' });
1409
+ setTestInfo({ success: true, message: T_KEY(t, 'settings.testOk') });
1217
1410
  } else {
1218
- setTestInfo({ success: false, message: '❌ ' + (data?.error || 'Сбой отправки') });
1411
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.testFail', { error: (data && data.error) || '' }) });
1219
1412
  }
1220
1413
  } catch (err) {
1221
- setTestInfo({ success: false, message: '❌ ' + err.message });
1414
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.testFail', { error: err.message }) });
1222
1415
  }
1223
1416
  };
1224
1417
 
1225
1418
  const testKanban = async () => {
1226
- setTestInfo({ loading: true, message: 'Проверка Kanban...' });
1419
+ setTestInfo({ loading: true, message: T_KEY(t, 'settings.checkingKanban') });
1227
1420
  try {
1228
1421
  const res = await fetch('/dsh-cron/kanban/test', {
1229
1422
  method: 'POST',
@@ -1232,62 +1425,62 @@ const React = window.React || require('react');
1232
1425
  });
1233
1426
  const data = await res.json();
1234
1427
  if (data && data.ok) {
1235
- setTestInfo({ success: true, message: '✅ Карточка в Kanban создана' });
1428
+ setTestInfo({ success: true, message: T_KEY(t, 'settings.kanbanOk') });
1236
1429
  } else {
1237
- setTestInfo({ success: false, message: '❌ ' + (data?.error || 'Ошибка Kanban') });
1430
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.kanbanFail', { error: (data && data.error) || '' }) });
1238
1431
  }
1239
1432
  } catch (err) {
1240
- setTestInfo({ success: false, message: '❌ ' + err.message });
1433
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.kanbanFail', { error: err.message }) });
1241
1434
  }
1242
1435
  };
1243
1436
 
1244
1437
  return React.createElement('div', { className: 'dsh-cron-card' },
1245
1438
  React.createElement('div', { className: 'dsh-cron-card-head' },
1246
- React.createElement('span', { className: 'dsh-cron-card-title' }, '⏰ Планировщик задач (Cron)'),
1439
+ React.createElement('span', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
1247
1440
  React.createElement('button', {
1248
1441
  type: 'button',
1249
1442
  className: 'dsh-cron-btn-primary',
1250
1443
  onClick: () => { if (toggle) toggle.set(true); }
1251
- }, 'Открыть панель задач')
1444
+ }, T_KEY(t, 'settings.openPanel'))
1252
1445
  ),
1253
1446
  React.createElement('div', { className: 'dsh-cron-card-desc', style: { marginBottom: '16px' } },
1254
- 'Автоматическое выполнение задач по расписанию (cron/интервалы), фоновые сценарии агентов и интеграция с Telegram и Kanban.'
1447
+ T_KEY(t, 'settings.cardDesc')
1255
1448
  ),
1256
1449
  React.createElement('form', { onSubmit: saveSettings, style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
1257
1450
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1258
- React.createElement('label', null, 'Telegram Bot Token'),
1451
+ React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
1259
1452
  React.createElement('input', {
1260
1453
  type: 'password',
1261
1454
  value: token,
1262
- placeholder: hasDefault ? 'Используется бот из настроек DSH (или введите свой)' : '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
1455
+ placeholder: hasDefault ? T_KEY(t, 'settings.tokenPlaceholderDefault') : T_KEY(t, 'settings.tokenPlaceholder'),
1263
1456
  onChange: (e) => setToken(e.target.value)
1264
1457
  }),
1265
- hasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, '✓ Найден настроенный бот в профиле DSH')
1458
+ hasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'settings.defaultHint'))
1266
1459
  ),
1267
1460
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1268
- React.createElement('label', null, 'Telegram Chat ID'),
1461
+ React.createElement('label', null, T_KEY(t, 'settings.chatIdLabel')),
1269
1462
  React.createElement('input', {
1270
1463
  value: chatId,
1271
- placeholder: 'Например: 345678901',
1464
+ placeholder: T_KEY(t, 'settings.chatIdPlaceholder'),
1272
1465
  onChange: (e) => setChatId(e.target.value)
1273
1466
  })
1274
1467
  ),
1275
1468
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1276
- React.createElement('label', null, 'Kanban Base URL'),
1469
+ React.createElement('label', null, T_KEY(t, 'settings.kanbanUrlLabel')),
1277
1470
  React.createElement('input', {
1278
1471
  value: kanbanUrl,
1279
1472
  placeholder: 'http://127.0.0.1:3000',
1280
1473
  onChange: (e) => setKanbanUrl(e.target.value)
1281
1474
  })
1282
1475
  ),
1283
- React.createElement('div', { style: { background: 'rgba(255,255,255,0.02)', border: '1px solid #2e2e2e', borderRadius: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
1476
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
1284
1477
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
1285
1478
  React.createElement('input', {
1286
1479
  type: 'checkbox',
1287
1480
  checked: notify,
1288
1481
  onChange: (e) => setNotify(e.target.checked)
1289
1482
  }),
1290
- React.createElement('span', null, 'Глобально отправлять отчеты всех задач в Telegram')
1483
+ React.createElement('span', null, T_KEY(t, 'settings.globalNotify'))
1291
1484
  ),
1292
1485
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
1293
1486
  React.createElement('input', {
@@ -1295,7 +1488,7 @@ const React = window.React || require('react');
1295
1488
  checked: onlyFailure,
1296
1489
  onChange: (e) => setOnlyFailure(e.target.checked)
1297
1490
  }),
1298
- React.createElement('span', null, 'Уведомлять только при ошибках')
1491
+ React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
1299
1492
  )
1300
1493
  ),
1301
1494
  testInfo && testInfo.message && React.createElement('div', {
@@ -1308,19 +1501,19 @@ const React = window.React || require('react');
1308
1501
  type: 'submit',
1309
1502
  className: 'dsh-cron-btn-primary',
1310
1503
  style: { minWidth: '140px' }
1311
- }, saved ? '✓ Сохранено' : 'Сохранить настройки'),
1504
+ }, saved ? T_KEY(t, 'settings.saved') : T_KEY(t, 'settings.save')),
1312
1505
  React.createElement('button', {
1313
1506
  type: 'button',
1314
1507
  className: 'dsh-cron-btn-secondary',
1315
1508
  onClick: testTelegram,
1316
1509
  disabled: testInfo?.loading
1317
- }, '🔔 Тест Telegram'),
1510
+ }, T_KEY(t, 'settings.testTelegram')),
1318
1511
  React.createElement('button', {
1319
1512
  type: 'button',
1320
1513
  className: 'dsh-cron-btn-secondary',
1321
1514
  onClick: testKanban,
1322
1515
  disabled: testInfo?.loading
1323
- }, '📋 Тест Kanban')
1516
+ }, T_KEY(t, 'settings.testKanban'))
1324
1517
  )
1325
1518
  )
1326
1519
  );
@@ -1338,7 +1531,7 @@ const React = window.React || require('react');
1338
1531
  const entry = target.cloneNode(true);
1339
1532
  entry.className = String(target.className) + ' dsh-cron-entry-clone';
1340
1533
  entry.setAttribute(ENTRY_ATTR, '');
1341
- entry.setAttribute('data-dsh-plugin', 'cron');
1534
+ entry.setAttribute('data-dsh-plugin', 'dsh-cron');
1342
1535
  entry.setAttribute('data-dsh-part', 'sidebar-entry');
1343
1536
  entry.setAttribute('aria-label', label);
1344
1537
  entry.removeAttribute('id');
@@ -1416,13 +1609,13 @@ const React = window.React || require('react');
1416
1609
  if (column === null) return;
1417
1610
  container = document.createElement('div');
1418
1611
  container.setAttribute(VIEW_ATTR, '');
1419
- container.setAttribute('data-dsh-plugin', 'cron');
1612
+ container.setAttribute('data-dsh-plugin', 'dsh-cron');
1420
1613
  column.appendChild(container);
1421
1614
  if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
1422
1615
  root = ReactDOM.createRoot(container);
1423
- root.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false) }));
1616
+ root.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate }));
1424
1617
  } else if (ReactDOM && typeof ReactDOM.render === 'function') {
1425
- ReactDOM.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false) }), container);
1618
+ ReactDOM.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate }), container);
1426
1619
  }
1427
1620
  };
1428
1621
 
@@ -1465,43 +1658,88 @@ const React = window.React || require('react');
1465
1658
  };
1466
1659
  }
1467
1660
 
1468
- function registerFirst(ctx, candidates, component) {
1469
- if (!ctx || !ctx.slots) return undefined;
1470
- for (const entry of candidates) {
1471
- try {
1472
- let ok = false;
1473
- ctx.slots.inject(entry.name, () => {
1474
- ctx.slots.register(entry, component);
1475
- ok = true;
1476
- });
1477
- if (ok) return entry.name;
1478
- } catch (e) {}
1661
+ /**
1662
+ * Register a card into a DSH mount point. Mount points are populated via
1663
+ * ctx.slots.inject(mount, cb) — a direct slots.register does not throw but
1664
+ * also never appears in the target surface (settings plugins tab, header),
1665
+ * so the inject contract is mandatory (#85).
1666
+ */
1667
+ function registerIntoMount(ctx, mountName, entry, component) {
1668
+ if (!ctx || !ctx.slots || typeof ctx.slots.inject !== 'function') return false;
1669
+ try {
1670
+ return !!ctx.slots.inject(mountName, () => {
1671
+ ctx.slots.register(entry, component);
1672
+ });
1673
+ } catch (err) {
1674
+ console.warn('[dsh-cron] slot injection skipped:', mountName, '-', err && err.message);
1675
+ return false;
1479
1676
  }
1480
- return undefined;
1481
1677
  }
1482
1678
 
1483
1679
  function apply(ctx) {
1484
1680
  ensureStyles();
1681
+
1682
+ // Register the English canonical strings; rebinding the translator lets
1683
+ // the translation plugin drive the active language at runtime (#87).
1684
+ try {
1685
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
1686
+ ctx.locale.register(NS, STRINGS);
1687
+ }
1688
+ } catch (e) {
1689
+ // Re-registration in the same session throws — the dictionary is live already.
1690
+ }
1691
+ try {
1692
+ if (ctx.locale && typeof ctx.locale.bind === 'function') {
1693
+ translate = ctx.locale.bind(NS);
1694
+ }
1695
+ } catch (e) {}
1696
+
1485
1697
  const toggle = createToggle();
1486
1698
 
1487
- const cardSlot = registerFirst(ctx, [
1488
- { name: 'settings.plugin.item', key: NS, inject: () => ({ ctx, toggle }) },
1489
- { name: 'settings.section', id: '@goodandready/dsh-cron', order: 34, label: () => 'Cron Задачи', inject: () => ({ ctx, toggle }) }
1490
- ], CronSettingsCard);
1491
-
1492
- const chipSlot = registerFirst(ctx, [
1493
- { name: 'conversation.session.header.utilities', id: '@goodandready/dsh-cron.chip', order: 27, inject: () => ({ ctx, toggle }) }
1494
- ], () => React.createElement('button', {
1495
- type: 'button',
1496
- style: { background: 'none', border: 'none', color: 'var(--dsw-alias-label-secondary, #999)', cursor: 'pointer', padding: '4px', display: 'inline-flex', alignItems: 'center' },
1497
- title: 'Запланированные задачи (Cron)',
1498
- onClick: () => toggle.toggle(),
1499
- dangerouslySetInnerHTML: { __html: ICON_TIMER }
1500
- }));
1699
+ const cardOk = registerIntoMount(ctx, 'settings.plugin.item', {
1700
+ name: 'settings.plugin.item',
1701
+ key: NS,
1702
+ locale: NS,
1703
+ inject: () => ({ ctx, toggle }),
1704
+ }, CronSettingsCard);
1705
+ if (!cardOk) {
1706
+ // Fallback for builds without the settings.plugin.item mount: a
1707
+ // dedicated section so the settings are never lost (#85).
1708
+ registerIntoMount(ctx, 'settings.section', {
1709
+ name: 'settings.section',
1710
+ id: PLUGIN_ID,
1711
+ order: 34,
1712
+ locale: NS,
1713
+ label: () => translate('settings.sectionLabel'),
1714
+ inject: () => ({ ctx, toggle }),
1715
+ }, CronSettingsCard);
1716
+ }
1717
+
1718
+ const chipOk = registerIntoMount(ctx, 'conversation.session.header.utilities', {
1719
+ name: 'conversation.session.header.utilities',
1720
+ id: PLUGIN_ID + '.chip',
1721
+ order: 27,
1722
+ locale: NS,
1723
+ inject: () => ({ ctx, toggle }),
1724
+ }, CronChip);
1725
+ const cardSlot = cardOk ? 'settings.plugin.item' : 'settings.section';
1726
+ const chipSlot = chipOk ? 'conversation.session.header.utilities' : undefined;
1727
+
1728
+ function CronChip(props) {
1729
+ const chipT = props.t || translate;
1730
+ return React.createElement('button', {
1731
+ type: 'button',
1732
+ style: { background: 'none', border: 'none', color: 'var(--dsw-alias-label-secondary, #999)', cursor: 'pointer', padding: '4px', display: 'inline-flex', alignItems: 'center' },
1733
+ title: T_KEY(chipT, 'chip.title'),
1734
+ 'aria-label': T_KEY(chipT, 'chip.title'),
1735
+ onClick: () => toggle.toggle(),
1736
+ dangerouslySetInnerHTML: { __html: ICON_TIMER }
1737
+ });
1738
+ }
1501
1739
 
1502
1740
  ctx.effect(() => {
1503
1741
  const off = [
1504
- mountSidebarEntry(toggle, 'Запланированные задачи'),
1742
+ mountSidebarEntry(toggle, translate('sidebar.label')),
1505
1743
  mountCronScreen(ctx, toggle)
1506
1744
  ];
1507
1745
  return () => { for (const dispose of off) dispose(); };