@goodandready/dsh-cron 0.1.23 → 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
  }
@@ -342,7 +506,23 @@ const React = window.React || require('react');
342
506
  }
343
507
  };
344
508
 
345
- React.useEffect(() => { fetchTasks(); }, [tab, query]);
509
+ React.useEffect(() => {
510
+ fetchTasks();
511
+ const pollInterval = setInterval(() => {
512
+ // Background polling without resetting loading spinner
513
+ fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query))
514
+ .then(res => res.json())
515
+ .then(data => {
516
+ if (data && data.ok) {
517
+ setTasks(data.tasks || []);
518
+ setRecs(data.recommendations || []);
519
+ setStats(data.stats || null);
520
+ }
521
+ })
522
+ .catch(() => {});
523
+ }, 8000);
524
+ return () => clearInterval(pollInterval);
525
+ }, [tab, query]);
346
526
  React.useEffect(() => { loadModels(); }, []);
347
527
 
348
528
  const handleToggleTask = async (id) => {
@@ -354,14 +534,16 @@ const React = window.React || require('react');
354
534
 
355
535
  const handleRunNow = async (id) => {
356
536
  try {
537
+ setTasks(prev => prev.map(t2 => t2.id === id ? { ...t2, lastStatus: 'running' } : t2));
357
538
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id) + '/run', { method: 'POST' });
358
- alert('Задача запущена');
359
- fetchTasks();
360
- } catch (err) {}
539
+ setTimeout(() => fetchTasks(), 1500);
540
+ } catch (err) {
541
+ console.error('[dsh-cron] run now error:', err);
542
+ }
361
543
  };
362
544
 
363
545
  const handleDelete = async (id) => {
364
- if (!confirm('Удалить эту задачу?')) return;
546
+ if (!confirm(T_KEY(t, 'confirm.delete'))) return;
365
547
  try {
366
548
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
367
549
  fetchTasks();
@@ -404,15 +586,15 @@ const React = window.React || require('react');
404
586
  if (data && data.ok) {
405
587
  setSettingsModalOpen(false);
406
588
  } else {
407
- alert('Ошибка сохранения: ' + (data?.error || 'неизвестная ошибка'));
589
+ alert(T_KEY(t, 'settings.saveError', { error: (data && data.error) || T_KEY(t, 'settings.saveErrorUnknown') }));
408
590
  }
409
591
  } catch (err) {
410
- alert('Ошибка сохранения: ' + err.message);
592
+ alert(T_KEY(t, 'settings.saveError', { error: err.message }));
411
593
  }
412
594
  };
413
595
 
414
596
  const handleTestTelegram = async () => {
415
- setTestStatus({ loading: true, message: 'Отправка тестового сообщения...' });
597
+ setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
416
598
  try {
417
599
  const res = await fetch('/dsh-cron/telegram/test', {
418
600
  method: 'POST',
@@ -424,12 +606,12 @@ const React = window.React || require('react');
424
606
  });
425
607
  const data = await res.json();
426
608
  if (data && data.ok) {
427
- setTestStatus({ loading: false, success: true, message: '✅ Тестовое сообщение успешно отправлено в Telegram!' });
609
+ setTestStatus({ loading: false, success: true, message: T_KEY(t, 'settings.testOk') });
428
610
  } else {
429
- setTestStatus({ loading: false, success: false, message: '❌ ' + (data?.error || 'Не удалось отправить сообщение') });
611
+ setTestStatus({ loading: false, success: false, message: T_KEY(t, 'settings.testFail', { error: (data && data.error) || '' }) });
430
612
  }
431
613
  } catch (err) {
432
- setTestStatus({ loading: false, success: false, message: '❌ ' + err.message });
614
+ setTestStatus({ loading: false, success: false, message: T_KEY(t, 'settings.testFail', { error: err.message }) });
433
615
  }
434
616
  };
435
617
 
@@ -526,13 +708,17 @@ const React = window.React || require('react');
526
708
  overlapPolicy: formOverlapPolicy,
527
709
  kanbanMode: formKanbanMode,
528
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';
529
715
  const res = await fetch('/dsh-cron/tasks', {
530
716
  method: 'POST',
531
- headers: { 'Content-Type': 'application/json' },
717
+ headers,
532
718
  body: JSON.stringify(payload)
533
719
  });
534
720
  const data = await res.json();
535
- if (!data.ok) { alert(data.error || 'Ошибка сохранения'); return; }
721
+ if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
536
722
  setManualModalOpen(false);
537
723
  fetchTasks();
538
724
  } catch (err) { alert(err.message); }
@@ -557,7 +743,7 @@ const React = window.React || require('react');
557
743
  });
558
744
  const data = await res.json();
559
745
  if (!data.ok || !data.sessionId) {
560
- alert(data.error || 'Не удалось запустить диалог с агентом');
746
+ alert(T_KEY(t, 'dsh.startFailed', { error: data.error || '' }));
561
747
  setDshLoading(false);
562
748
  return;
563
749
  }
@@ -568,61 +754,63 @@ const React = window.React || require('react');
568
754
 
569
755
  await openSession(ctx, data.sessionId);
570
756
  } catch (err) {
571
- alert('Ошибка запуска: ' + err.message);
757
+ alert(T_KEY(t, 'dsh.launchError', { error: err.message }));
572
758
  } finally {
573
759
  setDshLoading(false);
574
760
  }
575
761
  };
576
762
 
763
+ const iconHtml = (html) => ({ dangerouslySetInnerHTML: { __html: html } });
764
+ const svgProps = (html) => ({ style: { display: 'flex', alignItems: 'center' }, ...iconHtml(html) });
765
+
577
766
  return React.createElement('div', { className: 'dsh-cron-overlay' },
578
767
  React.createElement('div', { className: 'dsh-cron-container' },
579
768
  React.createElement('div', { className: 'dsh-cron-top-bar' },
580
769
  React.createElement('button', {
581
770
  type: 'button',
582
771
  className: 'dsh-cron-back-btn',
583
- title: 'Назад к чату',
772
+ title: T_KEY(t, 'panel.back'),
584
773
  onClick: () => { if (onClose) onClose(); else toggle.set(false); }
585
774
  },
586
- React.createElement('span', {
587
- style: { display: 'flex', alignItems: 'center' },
588
- dangerouslySetInnerHTML: { __html: BACK_ICON }
589
- }),
590
- React.createElement('span', null, 'Назад к чату')
775
+ React.createElement('span', svgProps(BACK_ICON)),
776
+ React.createElement('span', null, T_KEY(t, 'panel.back'))
591
777
  )
592
778
  ),
593
779
  React.createElement('div', { className: 'dsh-cron-header' },
594
780
  React.createElement('div', null,
595
- React.createElement('div', { className: 'dsh-cron-title' }, 'Запланированные задачи'),
596
- 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'))
597
783
  ),
598
784
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
599
785
  React.createElement('button', {
600
786
  type: 'button',
601
787
  className: 'dsh-cron-btn-secondary',
602
788
  style: { display: 'flex', alignItems: 'center', gap: '6px', height: '36px', padding: '0 14px' },
603
- title: 'Настройки Telegram и уведомлений',
789
+ title: T_KEY(t, 'actions.settings'),
604
790
  onClick: openSettingsModal
605
791
  },
606
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
607
- 'Telegram & Оповещения'
792
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
793
+ T_KEY(t, 'actions.settings')
608
794
  ),
609
795
  React.createElement('div', { style: { position: 'relative' } },
610
796
  React.createElement('button', { className: 'dsh-cron-create-btn', onClick: () => setDropdownOpen(!dropdownOpen) },
611
- 'Создать',
612
- React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } })
797
+ T_KEY(t, 'actions.create'),
798
+ React.createElement('span', iconHtml(ICON_CHEVRON_DOWN))
613
799
  ),
614
800
  dropdownOpen && React.createElement('div', { className: 'dsh-cron-dropdown' },
615
- React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: openDshModal }, '💬 Создать с DSH'),
616
- 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'))
617
803
  )
618
804
  )
619
805
  )
620
806
  ),
621
807
  React.createElement('div', { className: 'dsh-cron-search-bar' },
622
- React.createElement('span', { className: 'dsh-cron-search-icon', dangerouslySetInnerHTML: { __html: ICON_SEARCH } }),
808
+ React.createElement('span', { className: 'dsh-cron-search-icon', ...iconHtml(ICON_SEARCH) }),
623
809
  React.createElement('input', {
624
810
  className: 'dsh-cron-search-input',
625
- placeholder: 'Поиск запланированных задач',
811
+ type: 'search',
812
+ 'aria-label': T_KEY(t, 'search.placeholder'),
813
+ placeholder: T_KEY(t, 'search.placeholder'),
626
814
  value: query,
627
815
  onChange: (e) => setQuery(e.target.value)
628
816
  })
@@ -630,29 +818,33 @@ const React = window.React || require('react');
630
818
  stats && React.createElement('div', { className: 'dsh-cron-stats-bar' },
631
819
  React.createElement('div', { className: 'dsh-cron-stat-card' },
632
820
  React.createElement('div', { className: 'dsh-cron-stat-val' }, stats.activeTasks || 0),
633
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Активных задач')
821
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.activeTasks'))
634
822
  ),
635
823
  React.createElement('div', { className: 'dsh-cron-stat-card' },
636
824
  React.createElement('div', { className: 'dsh-cron-stat-val' }, stats.totalRuns || 0),
637
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Всего запусков')
825
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalRuns'))
638
826
  ),
639
827
  React.createElement('div', { className: 'dsh-cron-stat-card' },
640
828
  React.createElement('div', { className: 'dsh-cron-stat-val' }, (stats.totalTokens || 0).toLocaleString()),
641
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Токенов израсходовано')
829
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalTokens'))
642
830
  ),
643
831
  React.createElement('div', { className: 'dsh-cron-stat-card' },
644
- React.createElement('div', { className: 'dsh-cron-stat-val', style: { color: '#10b981' } }, '$' + (stats.totalCostUsd || 0).toFixed(4)),
645
- 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'))
646
834
  )
647
835
  ),
648
836
  React.createElement('div', { className: 'dsh-cron-tabs' },
649
- ['all', 'active', 'paused', 'completed'].map((t) =>
837
+ ['all', 'active', 'paused', 'completed'].map((tabName) =>
650
838
  React.createElement('button', {
651
- key: t,
839
+ key: tabName,
652
840
  className: 'dsh-cron-tab',
653
- 'data-active': tab === t ? 'true' : undefined,
654
- onClick: () => setTab(t)
655
- }, 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'))
656
848
  )
657
849
  ),
658
850
  fetchError ? React.createElement('div', {
@@ -660,50 +852,59 @@ const React = window.React || require('react');
660
852
  'data-type': 'error',
661
853
  style: { margin: '8px 0', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }
662
854
  },
663
- React.createElement('span', null, 'Ошибка загрузки задач: ' + fetchError),
855
+ React.createElement('span', null, T_KEY(t, 'banner.loadError', { error: fetchError })),
664
856
  React.createElement('button', {
665
857
  type: 'button',
666
- className: 'dsh-cron-btn',
858
+ className: 'dsh-cron-btn-secondary',
667
859
  style: { marginLeft: '8px', padding: '2px 8px', fontSize: '12px' },
668
860
  onClick: () => fetchTasks()
669
- }, 'Повторить')
861
+ }, T_KEY(t, 'banner.retry'))
670
862
  ) : null,
671
863
  React.createElement('div', { className: 'dsh-cron-task-list' },
672
864
  loading && tasks.length === 0
673
- ? 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'))
674
866
  : tasks.length === 0
675
- ? 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'))
676
868
  : tasks.map((task) =>
677
869
  React.createElement('div', {
678
870
  key: task.id,
679
871
  className: 'dsh-cron-task-item',
680
- 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
+ }
681
882
  },
682
883
  React.createElement('button', {
683
884
  type: 'button',
684
885
  className: 'dsh-cron-task-status-btn',
685
886
  'data-active': task.status === 'active' ? 'true' : undefined,
686
- 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'),
687
889
  onClick: (e) => { e.stopPropagation(); handleToggleTask(task.id); }
688
- }, React.createElement('span', { dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PAUSE : ICON_PLAY } })),
890
+ }, React.createElement('span', svgProps(task.status === 'active' ? ICON_PAUSE : ICON_PLAY))),
689
891
  React.createElement('div', { className: 'dsh-cron-task-info' },
690
892
  React.createElement('div', { className: 'dsh-cron-task-title' },
691
893
  task.title,
692
894
  React.createElement('span', {
693
895
  className: 'dsh-cron-type-tag',
694
- style: {
695
- background: task.type === 'script' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(16, 185, 129, 0.15)',
696
- color: task.type === 'script' ? '#60a5fa' : '#34d399'
697
- }
698
- }, 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')),
699
900
  task.model ? React.createElement('span', { className: 'dsh-cron-task-model-tag' }, task.model.split('/').pop()) : null,
700
- 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,
701
902
  task.totalCostUsd > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + task.totalCostUsd.toFixed(4)) : null,
702
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
703
904
  ),
704
905
  React.createElement('div', { className: 'dsh-cron-task-sched' },
705
906
  task.scheduleText || task.schedule,
706
- 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' }) }) : ''
707
908
  ),
708
909
  task.prompt ? React.createElement('div', {
709
910
  className: 'dsh-cron-task-prompt-preview',
@@ -717,37 +918,45 @@ const React = window.React || require('react');
717
918
  React.createElement('button', {
718
919
  type: 'button',
719
920
  className: 'dsh-cron-icon-btn',
720
- title: 'Запустить немедленно',
921
+ title: T_KEY(t, 'actions.runNow'),
721
922
  onClick: () => handleRunNow(task.id)
722
- }, 'Запустить'),
923
+ }, T_KEY(t, 'actions.runNowShort')),
723
924
  React.createElement('button', {
724
925
  type: 'button',
725
926
  className: 'dsh-cron-icon-btn',
726
- title: 'Редактировать / Подробнее',
927
+ title: T_KEY(t, 'actions.edit'),
928
+ 'aria-label': T_KEY(t, 'actions.edit'),
727
929
  onClick: () => openEditModal(task)
728
- }, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_EDIT } })),
930
+ }, React.createElement('span', svgProps(ICON_EDIT))),
729
931
  React.createElement('button', {
730
932
  type: 'button',
731
933
  className: 'dsh-cron-icon-btn',
732
- title: 'Удалить задачу',
934
+ title: T_KEY(t, 'actions.delete'),
935
+ 'aria-label': T_KEY(t, 'actions.delete'),
733
936
  onClick: () => handleDelete(task.id)
734
- }, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_TRASH } }))
937
+ }, React.createElement('span', svgProps(ICON_TRASH)))
735
938
  )
736
939
  )
737
940
  )
738
941
  ),
739
- React.createElement('div', { className: 'dsh-cron-recs-title' }, 'Рекомендуемые задачи'),
942
+ React.createElement('div', { className: 'dsh-cron-recs-title' }, T_KEY(t, 'recs.title')),
740
943
  React.createElement('div', { className: 'dsh-cron-recs-list' },
741
944
  recs.map((rec) =>
742
945
  React.createElement('div', {
743
946
  key: rec.id,
744
947
  className: 'dsh-cron-rec-card',
745
- 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
+ }
746
957
  },
747
958
  React.createElement('div', { className: 'dsh-cron-rec-icon' },
748
- React.createElement('span', { dangerouslySetInnerHTML: {
749
- __html: rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY
750
- } })
959
+ React.createElement('span', iconHtml(rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY))
751
960
  ),
752
961
  React.createElement('div', { className: 'dsh-cron-rec-content' },
753
962
  React.createElement('div', { className: 'dsh-cron-rec-title' },
@@ -766,285 +975,288 @@ const React = window.React || require('react');
766
975
  React.createElement('div', {
767
976
  className: 'dsh-cron-modal',
768
977
  style: { maxWidth: '640px' },
978
+ role: 'dialog',
979
+ 'aria-modal': 'true',
769
980
  onClick: (e) => e.stopPropagation()
770
981
  },
771
982
  React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: formId ? '12px' : '18px' } },
772
- 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')),
773
984
  selectedTaskMeta && React.createElement('span', {
774
985
  style: {
775
986
  fontSize: '12px',
776
987
  padding: '3px 8px',
777
988
  borderRadius: '6px',
778
- background: selectedTaskMeta.status === 'active' ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)',
779
- 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)'
780
991
  }
781
- }, selectedTaskMeta.status === 'active' ? '● Активна' : '○ На паузе')
992
+ }, selectedTaskMeta.status === 'active' ? T_KEY(t, 'modal.statusActive') : T_KEY(t, 'modal.statusPaused'))
782
993
  ),
783
994
 
784
995
  formId && React.createElement('div', { className: 'dsh-cron-modal-nav' },
785
996
  React.createElement('button', {
786
997
  type: 'button',
787
998
  className: 'dsh-cron-modal-tab',
788
- 'data-active': modalTab === 'params',
999
+ 'data-active': modalTab === 'params' ? 'true' : undefined,
789
1000
  onClick: () => setModalTab('params')
790
- }, 'Параметры'),
1001
+ }, T_KEY(t, 'modal.params')),
791
1002
  React.createElement('button', {
792
1003
  type: 'button',
793
1004
  className: 'dsh-cron-modal-tab',
794
- 'data-active': modalTab === 'history',
1005
+ 'data-active': modalTab === 'history' ? 'true' : undefined,
795
1006
  onClick: () => { setModalTab('history'); if (formId) loadTaskHistory(formId); }
796
- }, 'История запусков (' + (taskHistory.length || 0) + ')')
1007
+ }, T_KEY(t, 'modal.history', { count: taskHistory.length || 0 }))
797
1008
  ),
798
1009
 
799
1010
  modalTab === 'history' && formId ? React.createElement('div', null,
800
- historyLoading ? React.createElement('div', { className: 'dsh-cron-history-empty' }, 'Загрузка истории...') :
801
- taskHistory.length === 0 ? React.createElement('div', { className: 'dsh-cron-history-empty' }, 'История запусков пока пуста') :
802
- React.createElement('div', { className: 'dsh-cron-history-list' },
803
- taskHistory.map((run) => React.createElement('div', { key: run.id, className: 'dsh-cron-history-item' },
804
- React.createElement('div', { className: 'dsh-cron-history-head' },
805
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
806
- React.createElement('span', {
807
- className: 'dsh-cron-history-status',
808
- 'data-status': run.status
809
- }, run.status === 'success' ? 'Успешно' : run.status === 'skipped' ? 'Пропущен' : 'Ошибка'),
810
- React.createElement('span', { className: 'dsh-cron-history-time' }, run.at ? new Date(run.at).toLocaleString() : '')
811
- ),
812
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
813
- run.costUsd > 0 && React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + run.costUsd.toFixed(5)),
814
- 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'),
815
- React.createElement('span', { style: { color: '#888', fontSize: '11.5px' } }, (run.durationMs || 0) + ' ms')
816
- )
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
+ ))
817
1032
  ),
818
- run.output && React.createElement('div', { className: 'dsh-cron-history-output' }, run.output),
819
- run.error && React.createElement('div', { className: 'dsh-cron-history-output', style: { color: '#ef4444', borderColor: '#552222' } }, run.error)
820
- ))
821
- ),
822
1033
  React.createElement('div', { className: 'dsh-cron-modal-foot', style: { marginTop: '18px' } },
823
1034
  React.createElement('button', {
824
1035
  type: 'button',
825
1036
  className: 'dsh-cron-btn-secondary',
826
1037
  onClick: () => { if (formId) loadTaskHistory(formId); }
827
- }, 'Обновить историю'),
1038
+ }, T_KEY(t, 'history.refresh')),
828
1039
  React.createElement('button', {
829
1040
  type: 'button',
830
1041
  className: 'dsh-cron-btn-primary',
831
1042
  onClick: () => setManualModalOpen(false)
832
- }, 'Закрыть')
1043
+ }, T_KEY(t, 'modal.close'))
833
1044
  )
834
1045
  ) :
835
1046
 
836
- React.createElement('form', { onSubmit: handleSaveManual },
837
- React.createElement('div', { className: 'dsh-cron-form-group' },
838
- React.createElement('label', null, 'Название задачи'),
839
- React.createElement('input', {
840
- required: true,
841
- value: formTitle,
842
- placeholder: 'Например: dsh-tags-watch',
843
- onChange: (e) => setFormTitle(e.target.value)
844
- })
845
- ),
846
- React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
847
- React.createElement('div', { className: 'dsh-cron-form-group' },
848
- React.createElement('label', null, 'Тип выполнения'),
849
- React.createElement('select', {
850
- value: formType,
851
- onChange: (e) => setFormType(e.target.value)
852
- },
853
- React.createElement('option', { value: 'llm' }, 'LLM (Агент с нейросетью)'),
854
- React.createElement('option', { value: 'script' }, 'NO-LLM (Shell-команда/Скрипт)')
855
- )
856
- ),
1047
+ React.createElement('form', { onSubmit: handleSaveManual },
857
1048
  React.createElement('div', { className: 'dsh-cron-form-group' },
858
- React.createElement('label', null, 'Расписание (cron или интервал)'),
1049
+ React.createElement('label', null, T_KEY(t, 'form.nameLabel')),
859
1050
  React.createElement('input', {
860
1051
  required: true,
861
- value: formSchedule,
862
- placeholder: '0 */6 * * * или every 2h или in 30m или at: 2026-09-05T15:00:00Z',
863
- onChange: (e) => setFormSchedule(e.target.value)
864
- })
865
- )
866
- ),
867
- React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
868
- React.createElement('div', { className: 'dsh-cron-form-group' },
869
- React.createElement('label', null, 'Таймаут выполнения (секунды)'),
870
- React.createElement('input', {
871
- type: 'number',
872
- min: 1,
873
- value: formTimeoutSeconds,
874
- placeholder: '1800',
875
- 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)
876
1056
  })
877
1057
  ),
878
- React.createElement('div', { className: 'dsh-cron-form-group' },
879
- React.createElement('label', null, 'Политика наложения (Overlap)'),
880
- React.createElement('select', {
881
- value: formOverlapPolicy,
882
- onChange: (e) => setFormOverlapPolicy(e.target.value)
883
- },
884
- React.createElement('option', { value: 'skip' }, 'Пропустить новый запуск (skip)'),
885
- React.createElement('option', { value: 'queue' }, 'Встать в очередь (queue)'),
886
- 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
+ })
887
1077
  )
888
- )
889
- ),
890
- React.createElement('div', { className: 'dsh-cron-form-group' },
891
- React.createElement('label', null, 'Создание карточки в Kanban (dsh-kanban)'),
892
- React.createElement('select', {
893
- value: formKanbanMode,
894
- onChange: (e) => setFormKanbanMode(e.target.value)
895
- },
896
- React.createElement('option', { value: 'none' }, 'Не создавать карточки (отключено)'),
897
- React.createElement('option', { value: 'on_failure' }, 'Создавать тикет при ошибке/сбое (в Backlog)'),
898
- React.createElement('option', { value: 'always' }, 'Создавать карточку при каждом завершении (Done / Backlog)')
899
- )
900
- ),
901
- formType !== 'script' && React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
902
- React.createElement('div', { className: 'dsh-cron-form-group' },
903
- React.createElement('label', null, 'Провайдер LLM'),
904
- React.createElement('select', {
905
- value: formProvider,
906
- onChange: handleProviderChange
907
- },
908
- React.createElement('option', { value: '' }, 'По умолчанию (DSH)'),
909
- 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
+ )
910
1100
  )
911
1101
  ),
912
1102
  React.createElement('div', { className: 'dsh-cron-form-group' },
913
- React.createElement('label', null, 'Модель LLM'),
1103
+ React.createElement('label', null, T_KEY(t, 'form.kanbanLabel')),
914
1104
  React.createElement('select', {
915
- value: formModel,
916
- onChange: (e) => setFormModel(e.target.value)
1105
+ value: formKanbanMode,
1106
+ onChange: (e) => setFormKanbanMode(e.target.value)
917
1107
  },
918
- React.createElement('option', { value: '' }, 'По умолчанию'),
919
- 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'))
920
1111
  )
921
- )
922
- ),
923
- React.createElement('div', { className: 'dsh-cron-form-group' },
924
- React.createElement('label', null, formType === 'script' ? 'Shell-команда или скрипт' : 'Инструкция / Промпт для агента'),
925
- React.createElement('textarea', {
926
- required: true,
927
- rows: 9,
928
- style: { fontFamily: 'monospace', fontSize: '12.5px', lineHeight: '1.45' },
929
- value: formPrompt,
930
- placeholder: formType === 'script' ? 'curl -fsSL https://... || exit 1' : 'Опишите, что именно агент должен сделать...',
931
- onChange: (e) => setFormPrompt(e.target.value)
932
- })
933
- ),
934
- React.createElement('div', {
935
- style: {
936
- background: 'rgba(255,255,255,0.02)',
937
- border: '1px solid #2e2e2e',
938
- borderRadius: '8px',
939
- padding: '12px 14px',
940
- marginBottom: '16px'
941
- }
942
- },
943
- React.createElement('div', { style: { fontWeight: 500, fontSize: '13px', color: '#eee', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px' } },
944
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
945
- 'Уведомления в Telegram'
946
1112
  ),
947
- React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: formNotifyTelegram ? '8px' : '0' } },
948
- React.createElement('input', {
949
- type: 'checkbox',
950
- checked: formNotifyTelegram,
951
- onChange: (e) => setFormNotifyTelegram(e.target.checked)
952
- }),
953
- 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
+ )
954
1134
  ),
955
- formNotifyTelegram && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
956
- React.createElement('input', {
957
- type: 'checkbox',
958
- checked: formOnlyOnFailure,
959
- onChange: (e) => setFormOnlyOnFailure(e.target.checked)
960
- }),
961
- React.createElement('span', null, 'Только при ошибках (onlyOnFailure / silent mode при успехе)')
962
- )
963
- ),
964
- selectedTaskMeta && React.createElement('div', {
965
- style: {
966
- background: 'rgba(255,255,255,0.03)',
967
- border: '1px solid #2e2e2e',
968
- borderRadius: '8px',
969
- padding: '10px 14px',
970
- fontSize: '12.5px',
971
- color: '#999',
972
- marginBottom: '16px',
973
- display: 'flex',
974
- justifyContent: 'space-between',
975
- alignItems: 'center'
976
- }
977
- },
978
- React.createElement('div', null,
979
- React.createElement('span', null, 'Следующий запуск: '),
980
- 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
+ })
981
1145
  ),
982
- React.createElement('button', {
983
- type: 'button',
984
- className: 'dsh-cron-btn-secondary',
985
- style: { padding: '4px 10px', fontSize: '12px' },
986
- onClick: async () => {
987
- await handleRunNow(selectedTaskMeta.id);
988
- 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'
989
1153
  }
990
- }, 'Запустить сейчас')
991
- ),
992
- React.createElement('div', { className: 'dsh-cron-modal-foot' },
993
- React.createElement('button', {
994
- type: 'button',
995
- className: 'dsh-cron-btn-secondary',
996
- onClick: () => setManualModalOpen(false)
997
- }, 'Отмена'),
998
- React.createElement('button', {
999
- type: 'submit',
1000
- className: 'dsh-cron-btn-primary'
1001
- }, 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
+ )
1002
1215
  )
1003
- )
1004
1216
  )
1005
1217
  ),
1006
1218
 
1007
- // 1.5 Settings Modal
1219
+ // 2. Settings modal
1008
1220
  settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
1009
- 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() },
1010
1222
  React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
1011
1223
  React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0, display: 'flex', alignItems: 'center', gap: '8px' } },
1012
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
1013
- 'Настройки Telegram и уведомлений'
1224
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
1225
+ T_KEY(t, 'settings.modalTitle')
1014
1226
  ),
1015
1227
  React.createElement('button', {
1016
1228
  type: 'button',
1017
- 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' },
1018
1230
  onClick: () => setSettingsModalOpen(false)
1019
1231
  }, '✕')
1020
1232
  ),
1021
1233
  React.createElement('form', { onSubmit: handleSaveSettings },
1022
1234
  React.createElement('div', { className: 'dsh-cron-form-group' },
1023
- React.createElement('label', null, 'Telegram Bot Token'),
1235
+ React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
1024
1236
  React.createElement('input', {
1025
1237
  type: 'password',
1026
1238
  value: settingsBotToken,
1027
- placeholder: settingsHasDefault ? 'Используется бот из настроек DSH (или введите свой)' : '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
1239
+ placeholder: settingsHasDefault ? T_KEY(t, 'settings.tokenPlaceholderDefault') : T_KEY(t, 'settings.tokenPlaceholder'),
1028
1240
  onChange: (e) => setSettingsBotToken(e.target.value)
1029
1241
  }),
1030
- settingsHasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, '✓ Обнаружен настроенный бот в профиле DSH')
1242
+ settingsHasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'settings.defaultHint'))
1031
1243
  ),
1032
1244
  React.createElement('div', { className: 'dsh-cron-form-group' },
1033
- React.createElement('label', null, 'Telegram Chat ID'),
1245
+ React.createElement('label', null, T_KEY(t, 'settings.chatIdLabel')),
1034
1246
  React.createElement('input', {
1035
1247
  value: settingsChatId,
1036
- placeholder: 'Например: 345678901',
1248
+ placeholder: T_KEY(t, 'settings.chatIdPlaceholder'),
1037
1249
  onChange: (e) => setSettingsChatId(e.target.value)
1038
1250
  })
1039
1251
  ),
1040
- 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' } },
1041
1253
  React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: '8px' } },
1042
1254
  React.createElement('input', {
1043
1255
  type: 'checkbox',
1044
1256
  checked: settingsNotifyGlobal,
1045
1257
  onChange: (e) => setSettingsNotifyGlobal(e.target.checked)
1046
1258
  }),
1047
- React.createElement('span', null, 'Глобально отправлять отчеты всех задач в Telegram')
1259
+ React.createElement('span', null, T_KEY(t, 'settings.globalNotify'))
1048
1260
  ),
1049
1261
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
1050
1262
  React.createElement('input', {
@@ -1052,7 +1264,7 @@ const React = window.React || require('react');
1052
1264
  checked: settingsOnlyOnFailureGlobal,
1053
1265
  onChange: (e) => setSettingsOnlyOnFailureGlobal(e.target.checked)
1054
1266
  }),
1055
- React.createElement('span', null, 'Глобальный режим: Только при ошибках (silent при успехе)')
1267
+ React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
1056
1268
  )
1057
1269
  ),
1058
1270
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '14px' } },
@@ -1062,7 +1274,7 @@ const React = window.React || require('react');
1062
1274
  style: { display: 'inline-flex', alignItems: 'center', gap: '6px' },
1063
1275
  disabled: testStatus?.loading,
1064
1276
  onClick: handleTestTelegram
1065
- }, testStatus?.loading ? 'Отправка...' : '🔔 Проверить отправку в Telegram')
1277
+ }, testStatus?.loading ? T_KEY(t, 'settings.sending') : T_KEY(t, 'settings.testTelegram'))
1066
1278
  ),
1067
1279
  testStatus && testStatus.message && React.createElement('div', {
1068
1280
  className: 'dsh-cron-status-banner',
@@ -1073,28 +1285,29 @@ const React = window.React || require('react');
1073
1285
  type: 'button',
1074
1286
  className: 'dsh-cron-btn-secondary',
1075
1287
  onClick: () => setSettingsModalOpen(false)
1076
- }, 'Отмена'),
1288
+ }, T_KEY(t, 'modal.cancel')),
1077
1289
  React.createElement('button', {
1078
1290
  type: 'submit',
1079
1291
  className: 'dsh-cron-btn-primary'
1080
- }, 'Сохранить настройки')
1292
+ }, T_KEY(t, 'settings.save'))
1081
1293
  )
1082
1294
  )
1083
1295
  )
1084
1296
  ),
1085
- // 2. "Создать с DSH" modal
1297
+
1298
+ // 3. "Create with DSH" modal
1086
1299
  dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
1087
- React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
1088
- 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')),
1089
1302
  React.createElement('div', { className: 'dsh-cron-form-group' },
1090
- React.createElement('label', null, 'Опишите задачу и желаемую периодичность:'),
1303
+ React.createElement('label', null, T_KEY(t, 'dsh.descLabel')),
1091
1304
  React.createElement('div', { className: 'dsh-cron-chat-box' },
1092
1305
  React.createElement('textarea', {
1093
1306
  className: 'dsh-cron-chat-input',
1094
1307
  rows: 4,
1095
1308
  autoFocus: true,
1096
1309
  disabled: dshLoading,
1097
- placeholder: 'Например: проверяй каждые 2 часа свободное место на сервере и наличие упавших systemd-сервисов, присылай алерт только если что-то не так...',
1310
+ placeholder: T_KEY(t, 'dsh.placeholder'),
1098
1311
  value: dshPrompt,
1099
1312
  onKeyDown: (e) => {
1100
1313
  if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
@@ -1105,18 +1318,15 @@ const React = window.React || require('react');
1105
1318
  onChange: (e) => setDshPrompt(e.target.value)
1106
1319
  }),
1107
1320
  React.createElement('div', { className: 'dsh-cron-chat-foot' },
1108
- React.createElement('span', { className: 'dsh-cron-chat-hint' }, 'Ctrl+Enter для отправки'),
1321
+ React.createElement('span', { className: 'dsh-cron-chat-hint' }, T_KEY(t, 'dsh.sendHint')),
1109
1322
  React.createElement('button', {
1110
1323
  type: 'button',
1111
1324
  className: 'dsh-cron-send-btn',
1112
1325
  disabled: dshLoading || !dshPrompt.trim(),
1113
- title: 'Отправить агенту',
1326
+ title: T_KEY(t, 'dsh.sendTitle'),
1114
1327
  onClick: handleStartWithDSH
1115
1328
  },
1116
- React.createElement('span', {
1117
- style: { display: 'flex', alignItems: 'center' },
1118
- dangerouslySetInnerHTML: { __html: ICON_SEND }
1119
- })
1329
+ React.createElement('span', svgProps(ICON_SEND))
1120
1330
  )
1121
1331
  )
1122
1332
  )
@@ -1127,7 +1337,7 @@ const React = window.React || require('react');
1127
1337
  className: 'dsh-cron-btn-secondary',
1128
1338
  disabled: dshLoading,
1129
1339
  onClick: () => setDshModalOpen(false)
1130
- }, 'Отмена')
1340
+ }, T_KEY(t, 'modal.cancel'))
1131
1341
  )
1132
1342
  )
1133
1343
  )
@@ -1136,6 +1346,7 @@ const React = window.React || require('react');
1136
1346
 
1137
1347
  function CronSettingsCard(props) {
1138
1348
  const { ctx, toggle } = props;
1349
+ const t = props.t || translate;
1139
1350
  const [token, setToken] = React.useState('');
1140
1351
  const [chatId, setChatId] = React.useState('');
1141
1352
  const [notify, setNotify] = React.useState(false);
@@ -1186,7 +1397,7 @@ const React = window.React || require('react');
1186
1397
  };
1187
1398
 
1188
1399
  const testTelegram = async () => {
1189
- setTestInfo({ loading: true, message: 'Отправка...' });
1400
+ setTestInfo({ loading: true, message: T_KEY(t, 'settings.sending') });
1190
1401
  try {
1191
1402
  const res = await fetch('/dsh-cron/telegram/test', {
1192
1403
  method: 'POST',
@@ -1195,17 +1406,17 @@ const React = window.React || require('react');
1195
1406
  });
1196
1407
  const data = await res.json();
1197
1408
  if (data && data.ok) {
1198
- setTestInfo({ success: true, message: '✅ Успешно доставлено в Telegram' });
1409
+ setTestInfo({ success: true, message: T_KEY(t, 'settings.testOk') });
1199
1410
  } else {
1200
- setTestInfo({ success: false, message: '❌ ' + (data?.error || 'Сбой отправки') });
1411
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.testFail', { error: (data && data.error) || '' }) });
1201
1412
  }
1202
1413
  } catch (err) {
1203
- setTestInfo({ success: false, message: '❌ ' + err.message });
1414
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.testFail', { error: err.message }) });
1204
1415
  }
1205
1416
  };
1206
1417
 
1207
1418
  const testKanban = async () => {
1208
- setTestInfo({ loading: true, message: 'Проверка Kanban...' });
1419
+ setTestInfo({ loading: true, message: T_KEY(t, 'settings.checkingKanban') });
1209
1420
  try {
1210
1421
  const res = await fetch('/dsh-cron/kanban/test', {
1211
1422
  method: 'POST',
@@ -1214,62 +1425,62 @@ const React = window.React || require('react');
1214
1425
  });
1215
1426
  const data = await res.json();
1216
1427
  if (data && data.ok) {
1217
- setTestInfo({ success: true, message: '✅ Карточка в Kanban создана' });
1428
+ setTestInfo({ success: true, message: T_KEY(t, 'settings.kanbanOk') });
1218
1429
  } else {
1219
- setTestInfo({ success: false, message: '❌ ' + (data?.error || 'Ошибка Kanban') });
1430
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.kanbanFail', { error: (data && data.error) || '' }) });
1220
1431
  }
1221
1432
  } catch (err) {
1222
- setTestInfo({ success: false, message: '❌ ' + err.message });
1433
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.kanbanFail', { error: err.message }) });
1223
1434
  }
1224
1435
  };
1225
1436
 
1226
1437
  return React.createElement('div', { className: 'dsh-cron-card' },
1227
1438
  React.createElement('div', { className: 'dsh-cron-card-head' },
1228
- React.createElement('span', { className: 'dsh-cron-card-title' }, '⏰ Планировщик задач (Cron)'),
1439
+ React.createElement('span', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
1229
1440
  React.createElement('button', {
1230
1441
  type: 'button',
1231
1442
  className: 'dsh-cron-btn-primary',
1232
1443
  onClick: () => { if (toggle) toggle.set(true); }
1233
- }, 'Открыть панель задач')
1444
+ }, T_KEY(t, 'settings.openPanel'))
1234
1445
  ),
1235
1446
  React.createElement('div', { className: 'dsh-cron-card-desc', style: { marginBottom: '16px' } },
1236
- 'Автоматическое выполнение задач по расписанию (cron/интервалы), фоновые сценарии агентов и интеграция с Telegram и Kanban.'
1447
+ T_KEY(t, 'settings.cardDesc')
1237
1448
  ),
1238
1449
  React.createElement('form', { onSubmit: saveSettings, style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
1239
1450
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1240
- React.createElement('label', null, 'Telegram Bot Token'),
1451
+ React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
1241
1452
  React.createElement('input', {
1242
1453
  type: 'password',
1243
1454
  value: token,
1244
- placeholder: hasDefault ? 'Используется бот из настроек DSH (или введите свой)' : '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
1455
+ placeholder: hasDefault ? T_KEY(t, 'settings.tokenPlaceholderDefault') : T_KEY(t, 'settings.tokenPlaceholder'),
1245
1456
  onChange: (e) => setToken(e.target.value)
1246
1457
  }),
1247
- hasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, '✓ Найден настроенный бот в профиле DSH')
1458
+ hasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'settings.defaultHint'))
1248
1459
  ),
1249
1460
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1250
- React.createElement('label', null, 'Telegram Chat ID'),
1461
+ React.createElement('label', null, T_KEY(t, 'settings.chatIdLabel')),
1251
1462
  React.createElement('input', {
1252
1463
  value: chatId,
1253
- placeholder: 'Например: 345678901',
1464
+ placeholder: T_KEY(t, 'settings.chatIdPlaceholder'),
1254
1465
  onChange: (e) => setChatId(e.target.value)
1255
1466
  })
1256
1467
  ),
1257
1468
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1258
- React.createElement('label', null, 'Kanban Base URL'),
1469
+ React.createElement('label', null, T_KEY(t, 'settings.kanbanUrlLabel')),
1259
1470
  React.createElement('input', {
1260
1471
  value: kanbanUrl,
1261
1472
  placeholder: 'http://127.0.0.1:3000',
1262
1473
  onChange: (e) => setKanbanUrl(e.target.value)
1263
1474
  })
1264
1475
  ),
1265
- 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' } },
1266
1477
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
1267
1478
  React.createElement('input', {
1268
1479
  type: 'checkbox',
1269
1480
  checked: notify,
1270
1481
  onChange: (e) => setNotify(e.target.checked)
1271
1482
  }),
1272
- React.createElement('span', null, 'Глобально отправлять отчеты всех задач в Telegram')
1483
+ React.createElement('span', null, T_KEY(t, 'settings.globalNotify'))
1273
1484
  ),
1274
1485
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
1275
1486
  React.createElement('input', {
@@ -1277,7 +1488,7 @@ const React = window.React || require('react');
1277
1488
  checked: onlyFailure,
1278
1489
  onChange: (e) => setOnlyFailure(e.target.checked)
1279
1490
  }),
1280
- React.createElement('span', null, 'Уведомлять только при ошибках')
1491
+ React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
1281
1492
  )
1282
1493
  ),
1283
1494
  testInfo && testInfo.message && React.createElement('div', {
@@ -1290,19 +1501,19 @@ const React = window.React || require('react');
1290
1501
  type: 'submit',
1291
1502
  className: 'dsh-cron-btn-primary',
1292
1503
  style: { minWidth: '140px' }
1293
- }, saved ? '✓ Сохранено' : 'Сохранить настройки'),
1504
+ }, saved ? T_KEY(t, 'settings.saved') : T_KEY(t, 'settings.save')),
1294
1505
  React.createElement('button', {
1295
1506
  type: 'button',
1296
1507
  className: 'dsh-cron-btn-secondary',
1297
1508
  onClick: testTelegram,
1298
1509
  disabled: testInfo?.loading
1299
- }, '🔔 Тест Telegram'),
1510
+ }, T_KEY(t, 'settings.testTelegram')),
1300
1511
  React.createElement('button', {
1301
1512
  type: 'button',
1302
1513
  className: 'dsh-cron-btn-secondary',
1303
1514
  onClick: testKanban,
1304
1515
  disabled: testInfo?.loading
1305
- }, '📋 Тест Kanban')
1516
+ }, T_KEY(t, 'settings.testKanban'))
1306
1517
  )
1307
1518
  )
1308
1519
  );
@@ -1320,7 +1531,7 @@ const React = window.React || require('react');
1320
1531
  const entry = target.cloneNode(true);
1321
1532
  entry.className = String(target.className) + ' dsh-cron-entry-clone';
1322
1533
  entry.setAttribute(ENTRY_ATTR, '');
1323
- entry.setAttribute('data-dsh-plugin', 'cron');
1534
+ entry.setAttribute('data-dsh-plugin', 'dsh-cron');
1324
1535
  entry.setAttribute('data-dsh-part', 'sidebar-entry');
1325
1536
  entry.setAttribute('aria-label', label);
1326
1537
  entry.removeAttribute('id');
@@ -1398,13 +1609,13 @@ const React = window.React || require('react');
1398
1609
  if (column === null) return;
1399
1610
  container = document.createElement('div');
1400
1611
  container.setAttribute(VIEW_ATTR, '');
1401
- container.setAttribute('data-dsh-plugin', 'cron');
1612
+ container.setAttribute('data-dsh-plugin', 'dsh-cron');
1402
1613
  column.appendChild(container);
1403
1614
  if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
1404
1615
  root = ReactDOM.createRoot(container);
1405
- 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 }));
1406
1617
  } else if (ReactDOM && typeof ReactDOM.render === 'function') {
1407
- 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);
1408
1619
  }
1409
1620
  };
1410
1621
 
@@ -1447,43 +1658,88 @@ const React = window.React || require('react');
1447
1658
  };
1448
1659
  }
1449
1660
 
1450
- function registerFirst(ctx, candidates, component) {
1451
- if (!ctx || !ctx.slots) return undefined;
1452
- for (const entry of candidates) {
1453
- try {
1454
- let ok = false;
1455
- ctx.slots.inject(entry.name, () => {
1456
- ctx.slots.register(entry, component);
1457
- ok = true;
1458
- });
1459
- if (ok) return entry.name;
1460
- } 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;
1461
1676
  }
1462
- return undefined;
1463
1677
  }
1464
1678
 
1465
1679
  function apply(ctx) {
1466
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
+
1467
1697
  const toggle = createToggle();
1468
1698
 
1469
- const cardSlot = registerFirst(ctx, [
1470
- { name: 'settings.plugin.item', key: NS, inject: () => ({ ctx, toggle }) },
1471
- { name: 'settings.section', id: '@goodandready/dsh-cron', order: 34, label: () => 'Cron Задачи', inject: () => ({ ctx, toggle }) }
1472
- ], CronSettingsCard);
1473
-
1474
- const chipSlot = registerFirst(ctx, [
1475
- { name: 'conversation.session.header.utilities', id: '@goodandready/dsh-cron.chip', order: 27, inject: () => ({ ctx, toggle }) }
1476
- ], () => React.createElement('button', {
1477
- type: 'button',
1478
- style: { background: 'none', border: 'none', color: 'var(--dsw-alias-label-secondary, #999)', cursor: 'pointer', padding: '4px', display: 'inline-flex', alignItems: 'center' },
1479
- title: 'Запланированные задачи (Cron)',
1480
- onClick: () => toggle.toggle(),
1481
- dangerouslySetInnerHTML: { __html: ICON_TIMER }
1482
- }));
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
+ }
1483
1739
 
1484
1740
  ctx.effect(() => {
1485
1741
  const off = [
1486
- mountSidebarEntry(toggle, 'Запланированные задачи'),
1742
+ mountSidebarEntry(toggle, translate('sidebar.label')),
1487
1743
  mountCronScreen(ctx, toggle)
1488
1744
  ];
1489
1745
  return () => { for (const dispose of off) dispose(); };