@goodandready/dsh-cron 0.1.24 → 0.2.1

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,183 @@ 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>';
32
-
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.loading': 'Loading settings...',
123
+ 'settings.unavailable': 'Settings are unavailable right now.',
124
+ 'settings.retry': 'Retry',
125
+ 'settings.cardTitle': '⏰ Task scheduler (Cron)',
126
+ 'settings.cardDesc': 'Scheduled task execution (cron/intervals), background agent scenarios and Telegram/Kanban integration.',
127
+ 'settings.openPanel': 'Open task panel',
128
+ 'settings.modalTitle': 'Telegram & notification settings',
129
+ 'settings.tokenLabel': 'Telegram Bot Token',
130
+ 'settings.tokenPlaceholder': '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
131
+ 'settings.tokenPlaceholderDefault': 'Using the bot configured in DSH settings (or enter your own)',
132
+ 'settings.defaultHint': '✓ A configured bot was found in the DSH profile',
133
+ 'settings.chatIdLabel': 'Telegram Chat ID',
134
+ 'settings.chatIdPlaceholder': 'For example: 345678901',
135
+ 'settings.kanbanUrlLabel': 'Kanban Base URL',
136
+ 'settings.globalNotify': 'Deliver reports of all tasks to Telegram globally',
137
+ 'settings.globalOnlyFailure': 'Global mode: only on failures (silent on success)',
138
+ 'settings.testTelegram': '🔔 Test Telegram delivery',
139
+ 'settings.testKanban': '📋 Test Kanban',
140
+ 'settings.sending': 'Sending...',
141
+ 'settings.checkingKanban': 'Checking Kanban...',
142
+ 'settings.testOk': '✅ Test message delivered to Telegram!',
143
+ 'settings.testFail': '❌ {error}',
144
+ 'settings.kanbanOk': '✅ Kanban card created',
145
+ 'settings.kanbanFail': '❌ {error}',
146
+ 'settings.save': 'Save settings',
147
+ 'settings.saved': '✓ Saved',
148
+ 'settings.saveError': 'Save failed: {error}',
149
+ 'settings.saveErrorUnknown': 'unknown error',
150
+ 'dsh.modalTitle': 'Schedule a task with the DSH agent',
151
+ 'dsh.descLabel': 'Describe the task and the desired frequency:',
152
+ 'dsh.placeholder': 'For example: check free disk space and failed systemd services every 2 hours, and alert only if something is wrong...',
153
+ 'dsh.sendHint': 'Ctrl+Enter to send',
154
+ 'dsh.sendTitle': 'Send to agent',
155
+ 'dsh.startFailed': 'Failed to start the agent dialog: {error}',
156
+ 'dsh.launchError': 'Launch failed: {error}',
157
+ 'confirm.delete': 'Delete this task?',
158
+ 'list.loadErrorPrefix': 'Failed to load tasks'
159
+ }
160
+ };
161
+
162
+ // Module-level translator. apply() rebinds it to the DSH locale service so
163
+ // the active language and live language switches are honored.
164
+ let translate = (key, vars) => {
165
+ let s = (STRINGS.en && STRINGS.en[key]) || key;
166
+ if (vars) {
167
+ for (const k of Object.keys(vars)) {
168
+ s = s.split('{' + k + '}').join(String(vars[k]));
169
+ }
170
+ }
171
+ return s;
172
+ };
173
+ const T = (key, vars) => translate(key, vars);
174
+
175
+ // Core chevron primitive when the build ships it; own SVG fallback otherwise.
176
+ let CoreChevron = null;
177
+ try {
178
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
179
+ CoreChevron = primitives && primitives.IconChevronDownOutline14;
180
+ } catch (e) {}
181
+ const chevronNode = () => (CoreChevron ? React.createElement(CoreChevron) : React.createElement('span', { style: { display: 'flex', alignItems: 'center' }, dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } }));
182
+
183
+ // Status colors: core semantic tokens first, plugin fallbacks as a single
184
+ // point of change (Issue #91 — no scattered hardcoded hex).
33
185
  const STYLES = `
186
+ :root {
187
+ --dsh-cron-success: var(--dsw-alias-label-success, #10b981);
188
+ --dsh-cron-success-bg: var(--dsw-alias-bg-success, rgba(16, 185, 129, 0.15));
189
+ --dsh-cron-danger: var(--dsw-alias-label-danger, #ef4444);
190
+ --dsh-cron-danger-bg: var(--dsw-alias-bg-danger, rgba(239, 68, 68, 0.15));
191
+ --dsh-cron-info: var(--dsw-alias-label-info, #60a5fa);
192
+ --dsh-cron-info-bg: var(--dsw-alias-bg-info, rgba(59, 130, 246, 0.15));
193
+ --dsh-cron-accent: var(--dsw-alias-label-accent, #c084fc);
194
+ --dsh-cron-accent-bg: var(--dsw-alias-bg-accent, rgba(168, 85, 247, 0.15));
195
+ --dsh-cron-warning: var(--dsw-alias-label-warning, #eab308);
196
+ --dsh-cron-warning-bg: var(--dsw-alias-bg-warning, rgba(234, 179, 8, 0.15));
197
+ }
34
198
  [data-pane=conversation], [class*=centerCol] {
35
199
  position: relative !important;
36
200
  }
@@ -56,7 +220,7 @@ const React = window.React || require('react');
56
220
 
57
221
  .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
222
  .dsh-cron-container { max-width: 860px; width: 100%; margin: 0 auto; }
59
-
223
+
60
224
  .dsh-cron-top-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
61
225
  .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
226
  .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,97 +228,98 @@ const React = window.React || require('react');
64
228
  .dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }
65
229
  .dsh-cron-title { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 6px; }
66
230
  .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); }
231
+ .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; }
232
+ .dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, var(--dsw-alias-interactive-bg-hover, #333)); }
69
233
  .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); }
234
+ .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; }
235
+ .dsh-cron-dropdown-item:hover { background: var(--dsw-alias-interactive-bg-hover, #2a2a2a); }
72
236
  .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; }
237
+ .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; }
238
+ .dsh-cron-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--dsw-alias-label-tertiary, #666); }
75
239
  .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; }
240
+ .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; }
241
+ .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
242
  .dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 40px; }
79
243
  .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; }
244
+ .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; }
245
+ .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
246
  .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; }
247
+ .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
248
  .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; }
249
+ .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; }
250
+ .dsh-cron-icon-btn:hover { background: var(--dsw-alias-interactive-bg-hover, #2a2a2a); color: var(--dsw-alias-label-primary, #fff); }
251
+ .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; }
252
+ .dsh-cron-task-status-btn[data-active="true"] { color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
93
253
  .dsh-cron-recs-title { font-size: 17px; font-weight: 600; margin-bottom: 14px; }
94
254
  .dsh-cron-recs-list { display: flex; flex-direction: column; gap: 10px; }
95
255
  .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; }
256
+ .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; }
257
+ .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
258
  .dsh-cron-rec-content { flex: 1; min-width: 0; }
99
259
  .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
-
260
+ .dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary, #9ca3af); font-weight: normal; margin-left: 8px; }
261
+ .dsh-cron-rec-desc { font-size: 13px; color: var(--dsw-alias-label-tertiary, #71717a); }
262
+
103
263
  .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
264
  .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
265
  .dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
106
266
  .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; }
267
+ .dsh-cron-form-group label { display: block; font-size: 13px; color: var(--dsw-alias-label-secondary, #aaa); margin-bottom: 6px; }
268
+ .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
269
  .dsh-cron-form-group select { cursor: pointer; }
110
270
  .dsh-cron-form-group textarea { min-height: 80px; resize: vertical; }
111
271
  .dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
112
272
  .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; }
273
+ .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
274
  .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); }
275
+ .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; }
276
+ .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; }
277
+ .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; }
278
+ .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); }
279
+ .dsh-cron-chat-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary, #666); }
280
+ .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; }
281
+ .dsh-cron-send-btn:hover:not(:disabled) { transform: translateY(-1px); }
122
282
  .dsh-cron-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
123
283
 
124
- .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; }
125
- .dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
284
+ .dsh-cron-card { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l1, #333); border-radius: 12px; padding: 14px 20px; margin-bottom: 16px; }
285
+ .dsh-cron-card-head-btn { appearance: none; width: 100%; font: inherit; color: inherit; text-align: left; cursor: pointer; background: 0 0; border: 0; border-radius: 12px; display: flex; align-items: center; gap: 12px; padding: 4px 0; }
286
+ .dsh-cron-card-head-btn .dsh-cron-card-title { flex: 1; }
126
287
  .dsh-cron-card-title { font-size: 15px; font-weight: 600; color: var(--dsw-alias-label-primary, #fff); }
288
+ .dsh-cron-chev { margin-left: auto; flex: none; color: var(--dsw-alias-label-tertiary, #888); transition: transform 0.16s; display: flex; align-items: center; }
289
+ .dsh-cron-chev-open { transform: rotate(180deg); }
290
+ .dsh-cron-card-body { border-top: 1px solid var(--dsw-alias-border-l1, #333); margin-top: 12px; padding-top: 14px; }
291
+ .dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
127
292
  .dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #888); margin-bottom: 16px; line-height: 1.4; }
128
293
 
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; }
294
+ .dsh-cron-modal-nav { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l2, #2e2e2e); margin-bottom: 18px; padding-bottom: 2px; }
295
+ .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; }
296
+ .dsh-cron-modal-tab:hover { color: var(--dsw-alias-label-secondary, #ccc); }
297
+ .dsh-cron-modal-tab[data-active="true"] { color: var(--dsw-alias-label-primary, #fff); border-bottom-color: var(--dsw-alias-label-primary, #ededed); }
133
298
  .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; }
299
+ .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
300
  .dsh-cron-history-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
136
301
  .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; }
302
+ .dsh-cron-history-status[data-status="success"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); }
303
+ .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); }
304
+ .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); }
305
+ .dsh-cron-history-time { color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; }
306
+ .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; }
307
+ .dsh-cron-history-empty { text-align: center; padding: 36px 0; color: var(--dsw-alias-label-tertiary, #666); font-size: 13.5px; }
143
308
 
144
309
  .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; }
310
+ .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; }
311
+ .dsh-cron-stat-val { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, #fff); }
312
+ .dsh-cron-stat-lbl { font-size: 11.5px; color: var(--dsw-alias-label-tertiary, #777); margin-top: 2px; }
313
+ .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; }
314
+ .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; }
315
+ .dsh-cron-tokens-tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); }
316
+ .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; }
317
+ .dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: var(--dsw-alias-label-primary, #ededed); }
318
+ .dsh-cron-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); margin-top: 4px; }
154
319
  .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; }
320
+ .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); }
321
+ .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); }
322
+ .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
323
  `;
159
324
 
160
325
  function ensureStyles() {
@@ -163,6 +328,8 @@ const React = window.React || require('react');
163
328
  if (!el) {
164
329
  el = document.createElement('style');
165
330
  el.id = 'dsh-cron-styles';
331
+ // Mark ownership before insertion so neighbor cleanups leave it alone (#91)
332
+ el.dataset.dshPlugin = 'dsh-cron';
166
333
  document.head.appendChild(el);
167
334
  }
168
335
  el.textContent = STYLES;
@@ -194,7 +361,7 @@ const React = window.React || require('react');
194
361
  const getSessions = () => (ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : (ctx && ctx.sessions));
195
362
  let sessions = getSessions();
196
363
 
197
- // 1. Попытка вызвать sessions.open(sessionId) немедленно
364
+ // 1. Try sessions.open(sessionId) immediately
198
365
  if (sessions && typeof sessions.open === 'function') {
199
366
  try {
200
367
  sessions.open(sessionId);
@@ -202,7 +369,7 @@ const React = window.React || require('react');
202
369
  } catch (e) {}
203
370
  }
204
371
 
205
- // 2. Если сессия еще не попала в manager.summaries, обновляем список с бэкенда
372
+ // 2. If the session is not in manager.summaries yet, refresh from the backend
206
373
  if (sessions && typeof sessions.refresh === 'function') {
207
374
  try {
208
375
  await sessions.refresh();
@@ -211,7 +378,7 @@ const React = window.React || require('react');
211
378
  } catch (e) {}
212
379
  }
213
380
 
214
- // 3. Асинхронные ретраи с интервалом (пока WebSocket/SSE событие добавляет сессию в список)
381
+ // 3. Async retries until the WebSocket/SSE event adds the session to the list
215
382
  for (let i = 0; i < 15; i++) {
216
383
  await new Promise(resolve => setTimeout(resolve, 120));
217
384
  sessions = getSessions();
@@ -221,7 +388,7 @@ const React = window.React || require('react');
221
388
  return true;
222
389
  } catch (e) {}
223
390
  }
224
- // DOM fallback: клик по сессии в боковой панели, если она уже отрендерилась
391
+ // DOM fallback: click the session in the sidebar if it has rendered
225
392
  if (typeof document !== 'undefined') {
226
393
  const domItem = document.querySelector('[data-session-id="' + sessionId + '"], [role="treeitem"][data-id="' + sessionId + '"], [data-id="' + sessionId + '"]');
227
394
  if (domItem) {
@@ -233,8 +400,20 @@ const React = window.React || require('react');
233
400
  return false;
234
401
  }
235
402
 
403
+ function statusLabel(t, status) {
404
+ if (status === 'success') return T_KEY(t, 'history.statusSuccess');
405
+ if (status === 'skipped') return T_KEY(t, 'history.statusSkipped');
406
+ if (status === 'missed') return T_KEY(t, 'history.statusMissed');
407
+ if (status === 'timeout') return T_KEY(t, 'history.statusTimeout');
408
+ return T_KEY(t, 'history.statusFailed');
409
+ }
410
+ function T_KEY(t, key, vars) {
411
+ return (typeof t === 'function' ? t : translate)(key, vars);
412
+ }
413
+
236
414
  function CronScreen(props) {
237
415
  const { ctx, toggle, onClose } = props;
416
+ const t = props.t || translate;
238
417
  const [tab, setTab] = React.useState('all');
239
418
  const [query, setQuery] = React.useState('');
240
419
  const [tasks, setTasks] = React.useState([]);
@@ -311,11 +490,11 @@ const React = window.React || require('react');
311
490
  setRecs(data.recommendations || []);
312
491
  setStats(data.stats || null);
313
492
  } else {
314
- setFetchError(data && data.error ? data.error : 'Не удалось загрузить задачи');
493
+ setFetchError(data && data.error ? data.error : T_KEY(t, 'list.loadErrorPrefix'));
315
494
  }
316
495
  } catch (err) {
317
496
  console.error('[dsh-cron] fetch tasks error:', err);
318
- setFetchError(err.message || 'Ошибка сети при загрузке задач');
497
+ setFetchError(err.message || T_KEY(t, 'list.loadErrorPrefix'));
319
498
  } finally {
320
499
  setLoading(false);
321
500
  }
@@ -329,7 +508,7 @@ const React = window.React || require('react');
329
508
  if (data && data.ok) {
330
509
  if (!providerName) {
331
510
  setProviders(data.providers || []);
332
- setDefaultModelInfo(data.defaultModel || null);
511
+ setDefaultModelInfo(data.defaultModel || data.current || null);
333
512
  if (data.defaultModel?.models) {
334
513
  setModels(data.defaultModel.models);
335
514
  }
@@ -370,7 +549,7 @@ const React = window.React || require('react');
370
549
 
371
550
  const handleRunNow = async (id) => {
372
551
  try {
373
- setTasks(prev => prev.map(t => t.id === id ? { ...t, lastStatus: 'running' } : t));
552
+ setTasks(prev => prev.map(t2 => t2.id === id ? { ...t2, lastStatus: 'running' } : t2));
374
553
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id) + '/run', { method: 'POST' });
375
554
  setTimeout(() => fetchTasks(), 1500);
376
555
  } catch (err) {
@@ -379,7 +558,7 @@ const React = window.React || require('react');
379
558
  };
380
559
 
381
560
  const handleDelete = async (id) => {
382
- if (!confirm('Удалить эту задачу?')) return;
561
+ if (!confirm(T_KEY(t, 'confirm.delete'))) return;
383
562
  try {
384
563
  await fetch('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
385
564
  fetchTasks();
@@ -422,15 +601,15 @@ const React = window.React || require('react');
422
601
  if (data && data.ok) {
423
602
  setSettingsModalOpen(false);
424
603
  } else {
425
- alert('Ошибка сохранения: ' + (data?.error || 'неизвестная ошибка'));
604
+ alert(T_KEY(t, 'settings.saveError', { error: (data && data.error) || T_KEY(t, 'settings.saveErrorUnknown') }));
426
605
  }
427
606
  } catch (err) {
428
- alert('Ошибка сохранения: ' + err.message);
607
+ alert(T_KEY(t, 'settings.saveError', { error: err.message }));
429
608
  }
430
609
  };
431
610
 
432
611
  const handleTestTelegram = async () => {
433
- setTestStatus({ loading: true, message: 'Отправка тестового сообщения...' });
612
+ setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
434
613
  try {
435
614
  const res = await fetch('/dsh-cron/telegram/test', {
436
615
  method: 'POST',
@@ -442,12 +621,12 @@ const React = window.React || require('react');
442
621
  });
443
622
  const data = await res.json();
444
623
  if (data && data.ok) {
445
- setTestStatus({ loading: false, success: true, message: '✅ Тестовое сообщение успешно отправлено в Telegram!' });
624
+ setTestStatus({ loading: false, success: true, message: T_KEY(t, 'settings.testOk') });
446
625
  } else {
447
- setTestStatus({ loading: false, success: false, message: '❌ ' + (data?.error || 'Не удалось отправить сообщение') });
626
+ setTestStatus({ loading: false, success: false, message: T_KEY(t, 'settings.testFail', { error: (data && data.error) || '' }) });
448
627
  }
449
628
  } catch (err) {
450
- setTestStatus({ loading: false, success: false, message: '❌ ' + err.message });
629
+ setTestStatus({ loading: false, success: false, message: T_KEY(t, 'settings.testFail', { error: err.message }) });
451
630
  }
452
631
  };
453
632
 
@@ -544,13 +723,17 @@ const React = window.React || require('react');
544
723
  overlapPolicy: formOverlapPolicy,
545
724
  kanbanMode: formKanbanMode,
546
725
  };
726
+ // Cross-origin hardening: script tasks need the explicit confirm
727
+ // header, which browsers cannot attach to forged cross-site posts (#86)
728
+ const headers = { 'Content-Type': 'application/json' };
729
+ if (formType === 'script') headers[SCRIPT_CONFIRM_HEADER] = 'script';
547
730
  const res = await fetch('/dsh-cron/tasks', {
548
731
  method: 'POST',
549
- headers: { 'Content-Type': 'application/json' },
732
+ headers,
550
733
  body: JSON.stringify(payload)
551
734
  });
552
735
  const data = await res.json();
553
- if (!data.ok) { alert(data.error || 'Ошибка сохранения'); return; }
736
+ if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
554
737
  setManualModalOpen(false);
555
738
  fetchTasks();
556
739
  } catch (err) { alert(err.message); }
@@ -575,7 +758,7 @@ const React = window.React || require('react');
575
758
  });
576
759
  const data = await res.json();
577
760
  if (!data.ok || !data.sessionId) {
578
- alert(data.error || 'Не удалось запустить диалог с агентом');
761
+ alert(T_KEY(t, 'dsh.startFailed', { error: data.error || '' }));
579
762
  setDshLoading(false);
580
763
  return;
581
764
  }
@@ -586,61 +769,63 @@ const React = window.React || require('react');
586
769
 
587
770
  await openSession(ctx, data.sessionId);
588
771
  } catch (err) {
589
- alert('Ошибка запуска: ' + err.message);
772
+ alert(T_KEY(t, 'dsh.launchError', { error: err.message }));
590
773
  } finally {
591
774
  setDshLoading(false);
592
775
  }
593
776
  };
594
777
 
778
+ const iconHtml = (html) => ({ dangerouslySetInnerHTML: { __html: html } });
779
+ const svgProps = (html) => ({ style: { display: 'flex', alignItems: 'center' }, ...iconHtml(html) });
780
+
595
781
  return React.createElement('div', { className: 'dsh-cron-overlay' },
596
782
  React.createElement('div', { className: 'dsh-cron-container' },
597
783
  React.createElement('div', { className: 'dsh-cron-top-bar' },
598
784
  React.createElement('button', {
599
785
  type: 'button',
600
786
  className: 'dsh-cron-back-btn',
601
- title: 'Назад к чату',
787
+ title: T_KEY(t, 'panel.back'),
602
788
  onClick: () => { if (onClose) onClose(); else toggle.set(false); }
603
789
  },
604
- React.createElement('span', {
605
- style: { display: 'flex', alignItems: 'center' },
606
- dangerouslySetInnerHTML: { __html: BACK_ICON }
607
- }),
608
- React.createElement('span', null, 'Назад к чату')
790
+ React.createElement('span', svgProps(BACK_ICON)),
791
+ React.createElement('span', null, T_KEY(t, 'panel.back'))
609
792
  )
610
793
  ),
611
794
  React.createElement('div', { className: 'dsh-cron-header' },
612
795
  React.createElement('div', null,
613
- React.createElement('div', { className: 'dsh-cron-title' }, 'Запланированные задачи'),
614
- React.createElement('div', { className: 'dsh-cron-subtitle' }, 'Попросите DSH планировать задачи, ставить напоминания или отслеживать обновления')
796
+ React.createElement('div', { className: 'dsh-cron-title' }, T_KEY(t, 'panel.title')),
797
+ React.createElement('div', { className: 'dsh-cron-subtitle' }, T_KEY(t, 'panel.subtitle'))
615
798
  ),
616
799
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
617
800
  React.createElement('button', {
618
801
  type: 'button',
619
802
  className: 'dsh-cron-btn-secondary',
620
803
  style: { display: 'flex', alignItems: 'center', gap: '6px', height: '36px', padding: '0 14px' },
621
- title: 'Настройки Telegram и уведомлений',
804
+ title: T_KEY(t, 'actions.settings'),
622
805
  onClick: openSettingsModal
623
806
  },
624
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
625
- 'Telegram & Оповещения'
807
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
808
+ T_KEY(t, 'actions.settings')
626
809
  ),
627
810
  React.createElement('div', { style: { position: 'relative' } },
628
811
  React.createElement('button', { className: 'dsh-cron-create-btn', onClick: () => setDropdownOpen(!dropdownOpen) },
629
- 'Создать',
630
- React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } })
812
+ T_KEY(t, 'actions.create'),
813
+ React.createElement('span', iconHtml(ICON_CHEVRON_DOWN))
631
814
  ),
632
815
  dropdownOpen && React.createElement('div', { className: 'dsh-cron-dropdown' },
633
- React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: openDshModal }, '💬 Создать с DSH'),
634
- React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, '✏️ Настроить вручную')
816
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: openDshModal }, T_KEY(t, 'actions.createWithDsh')),
817
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, T_KEY(t, 'actions.setupManually'))
635
818
  )
636
819
  )
637
820
  )
638
821
  ),
639
822
  React.createElement('div', { className: 'dsh-cron-search-bar' },
640
- React.createElement('span', { className: 'dsh-cron-search-icon', dangerouslySetInnerHTML: { __html: ICON_SEARCH } }),
823
+ React.createElement('span', { className: 'dsh-cron-search-icon', ...iconHtml(ICON_SEARCH) }),
641
824
  React.createElement('input', {
642
825
  className: 'dsh-cron-search-input',
643
- placeholder: 'Поиск запланированных задач',
826
+ type: 'search',
827
+ 'aria-label': T_KEY(t, 'search.placeholder'),
828
+ placeholder: T_KEY(t, 'search.placeholder'),
644
829
  value: query,
645
830
  onChange: (e) => setQuery(e.target.value)
646
831
  })
@@ -648,29 +833,33 @@ const React = window.React || require('react');
648
833
  stats && React.createElement('div', { className: 'dsh-cron-stats-bar' },
649
834
  React.createElement('div', { className: 'dsh-cron-stat-card' },
650
835
  React.createElement('div', { className: 'dsh-cron-stat-val' }, stats.activeTasks || 0),
651
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Активных задач')
836
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.activeTasks'))
652
837
  ),
653
838
  React.createElement('div', { className: 'dsh-cron-stat-card' },
654
839
  React.createElement('div', { className: 'dsh-cron-stat-val' }, stats.totalRuns || 0),
655
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Всего запусков')
840
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalRuns'))
656
841
  ),
657
842
  React.createElement('div', { className: 'dsh-cron-stat-card' },
658
843
  React.createElement('div', { className: 'dsh-cron-stat-val' }, (stats.totalTokens || 0).toLocaleString()),
659
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Токенов израсходовано')
844
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalTokens'))
660
845
  ),
661
846
  React.createElement('div', { className: 'dsh-cron-stat-card' },
662
- React.createElement('div', { className: 'dsh-cron-stat-val', style: { color: '#10b981' } }, '$' + (stats.totalCostUsd || 0).toFixed(4)),
663
- React.createElement('div', { className: 'dsh-cron-stat-lbl' }, 'Оценочная стоимость')
847
+ React.createElement('div', { className: 'dsh-cron-stat-val', style: { color: 'var(--dsh-cron-success)' } }, '$' + (stats.totalCostUsd || 0).toFixed(4)),
848
+ React.createElement('div', { className: 'dsh-cron-stat-lbl' }, T_KEY(t, 'stats.totalCost'))
664
849
  )
665
850
  ),
666
851
  React.createElement('div', { className: 'dsh-cron-tabs' },
667
- ['all', 'active', 'paused', 'completed'].map((t) =>
852
+ ['all', 'active', 'paused', 'completed'].map((tabName) =>
668
853
  React.createElement('button', {
669
- key: t,
854
+ key: tabName,
670
855
  className: 'dsh-cron-tab',
671
- 'data-active': tab === t ? 'true' : undefined,
672
- onClick: () => setTab(t)
673
- }, t === 'all' ? 'Все' : t === 'active' ? 'Активные' : t === 'paused' ? 'На паузе' : 'Завершенные')
856
+ 'data-active': tab === tabName ? 'true' : undefined,
857
+ onClick: () => setTab(tabName)
858
+ },
859
+ tabName === 'all' ? T_KEY(t, 'tabs.all')
860
+ : tabName === 'active' ? T_KEY(t, 'tabs.active')
861
+ : tabName === 'paused' ? T_KEY(t, 'tabs.paused')
862
+ : T_KEY(t, 'tabs.completed'))
674
863
  )
675
864
  ),
676
865
  fetchError ? React.createElement('div', {
@@ -678,50 +867,59 @@ const React = window.React || require('react');
678
867
  'data-type': 'error',
679
868
  style: { margin: '8px 0', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }
680
869
  },
681
- React.createElement('span', null, 'Ошибка загрузки задач: ' + fetchError),
870
+ React.createElement('span', null, T_KEY(t, 'banner.loadError', { error: fetchError })),
682
871
  React.createElement('button', {
683
872
  type: 'button',
684
- className: 'dsh-cron-btn',
873
+ className: 'dsh-cron-btn-secondary',
685
874
  style: { marginLeft: '8px', padding: '2px 8px', fontSize: '12px' },
686
875
  onClick: () => fetchTasks()
687
- }, 'Повторить')
876
+ }, T_KEY(t, 'banner.retry'))
688
877
  ) : null,
689
878
  React.createElement('div', { className: 'dsh-cron-task-list' },
690
879
  loading && tasks.length === 0
691
- ? React.createElement('div', { style: { color: '#888', padding: '16px 0', fontSize: '14px' } }, 'Загрузка запланированных задач...')
880
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #888)', padding: '16px 0', fontSize: '14px' } }, T_KEY(t, 'list.loading'))
692
881
  : tasks.length === 0
693
- ? React.createElement('div', { style: { color: '#777', padding: '16px 0', fontSize: '14px' } }, 'Нет запланированных задач')
882
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #777)', padding: '16px 0', fontSize: '14px' } }, T_KEY(t, 'list.empty'))
694
883
  : tasks.map((task) =>
695
884
  React.createElement('div', {
696
885
  key: task.id,
697
886
  className: 'dsh-cron-task-item',
698
- onClick: () => openEditModal(task)
887
+ role: 'button',
888
+ tabIndex: 0,
889
+ 'aria-label': task.title,
890
+ onClick: () => openEditModal(task),
891
+ onKeyDown: (e) => {
892
+ if (e.key === 'Enter' || e.key === ' ') {
893
+ e.preventDefault();
894
+ openEditModal(task);
895
+ }
896
+ }
699
897
  },
700
898
  React.createElement('button', {
701
899
  type: 'button',
702
900
  className: 'dsh-cron-task-status-btn',
703
901
  'data-active': task.status === 'active' ? 'true' : undefined,
704
- title: task.status === 'active' ? 'Приостановить' : 'Возобновить',
902
+ title: task.status === 'active' ? T_KEY(t, 'actions.pause') : T_KEY(t, 'actions.resume'),
903
+ 'aria-label': task.status === 'active' ? T_KEY(t, 'actions.pause') : T_KEY(t, 'actions.resume'),
705
904
  onClick: (e) => { e.stopPropagation(); handleToggleTask(task.id); }
706
- }, React.createElement('span', { dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PAUSE : ICON_PLAY } })),
905
+ }, React.createElement('span', svgProps(task.status === 'active' ? ICON_PAUSE : ICON_PLAY))),
707
906
  React.createElement('div', { className: 'dsh-cron-task-info' },
708
907
  React.createElement('div', { className: 'dsh-cron-task-title' },
709
908
  task.title,
710
909
  React.createElement('span', {
711
910
  className: 'dsh-cron-type-tag',
712
- style: {
713
- background: task.type === 'script' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(16, 185, 129, 0.15)',
714
- color: task.type === 'script' ? '#60a5fa' : '#34d399'
715
- }
716
- }, task.type === 'script' ? 'Shell' : 'LLM'),
911
+ style: task.type === 'script'
912
+ ? { background: 'var(--dsh-cron-info-bg)', color: 'var(--dsh-cron-info)' }
913
+ : { background: 'var(--dsh-cron-success-bg)', color: 'var(--dsh-cron-success)' }
914
+ }, task.type === 'script' ? T_KEY(t, 'list.typeShell') : T_KEY(t, 'list.typeLlm')),
717
915
  task.model ? React.createElement('span', { className: 'dsh-cron-task-model-tag' }, task.model.split('/').pop()) : null,
718
- task.oneShot ? React.createElement('span', { className: 'dsh-cron-oneshot-tag' }, 'Разовая') : null,
916
+ task.oneShot ? React.createElement('span', { className: 'dsh-cron-oneshot-tag' }, T_KEY(t, 'list.oneShot')) : null,
719
917
  task.totalCostUsd > 0 ? React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + task.totalCostUsd.toFixed(4)) : null,
720
918
  task.totalTokens > 0 ? React.createElement('span', { className: 'dsh-cron-tokens-tag' }, (task.totalTokens > 1000 ? Math.round(task.totalTokens / 1000) + 'k' : task.totalTokens) + ' tok') : null
721
919
  ),
722
920
  React.createElement('div', { className: 'dsh-cron-task-sched' },
723
921
  task.scheduleText || task.schedule,
724
- task.nextRunAt ? ` · след: ${new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''
922
+ task.nextRunAt ? T_KEY(t, 'list.next', { time: new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }) : ''
725
923
  ),
726
924
  task.prompt ? React.createElement('div', {
727
925
  className: 'dsh-cron-task-prompt-preview',
@@ -735,37 +933,45 @@ const React = window.React || require('react');
735
933
  React.createElement('button', {
736
934
  type: 'button',
737
935
  className: 'dsh-cron-icon-btn',
738
- title: 'Запустить немедленно',
936
+ title: T_KEY(t, 'actions.runNow'),
739
937
  onClick: () => handleRunNow(task.id)
740
- }, 'Запустить'),
938
+ }, T_KEY(t, 'actions.runNowShort')),
741
939
  React.createElement('button', {
742
940
  type: 'button',
743
941
  className: 'dsh-cron-icon-btn',
744
- title: 'Редактировать / Подробнее',
942
+ title: T_KEY(t, 'actions.edit'),
943
+ 'aria-label': T_KEY(t, 'actions.edit'),
745
944
  onClick: () => openEditModal(task)
746
- }, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_EDIT } })),
945
+ }, React.createElement('span', svgProps(ICON_EDIT))),
747
946
  React.createElement('button', {
748
947
  type: 'button',
749
948
  className: 'dsh-cron-icon-btn',
750
- title: 'Удалить задачу',
949
+ title: T_KEY(t, 'actions.delete'),
950
+ 'aria-label': T_KEY(t, 'actions.delete'),
751
951
  onClick: () => handleDelete(task.id)
752
- }, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_TRASH } }))
952
+ }, React.createElement('span', svgProps(ICON_TRASH)))
753
953
  )
754
954
  )
755
955
  )
756
956
  ),
757
- React.createElement('div', { className: 'dsh-cron-recs-title' }, 'Рекомендуемые задачи'),
957
+ React.createElement('div', { className: 'dsh-cron-recs-title' }, T_KEY(t, 'recs.title')),
758
958
  React.createElement('div', { className: 'dsh-cron-recs-list' },
759
959
  recs.map((rec) =>
760
960
  React.createElement('div', {
761
961
  key: rec.id,
762
962
  className: 'dsh-cron-rec-card',
763
- onClick: () => openManualModal(rec)
963
+ role: 'button',
964
+ tabIndex: 0,
965
+ onClick: () => openManualModal(rec),
966
+ onKeyDown: (e) => {
967
+ if (e.key === 'Enter' || e.key === ' ') {
968
+ e.preventDefault();
969
+ openManualModal(rec);
970
+ }
971
+ }
764
972
  },
765
973
  React.createElement('div', { className: 'dsh-cron-rec-icon' },
766
- React.createElement('span', { dangerouslySetInnerHTML: {
767
- __html: rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY
768
- } })
974
+ React.createElement('span', iconHtml(rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY))
769
975
  ),
770
976
  React.createElement('div', { className: 'dsh-cron-rec-content' },
771
977
  React.createElement('div', { className: 'dsh-cron-rec-title' },
@@ -784,285 +990,288 @@ const React = window.React || require('react');
784
990
  React.createElement('div', {
785
991
  className: 'dsh-cron-modal',
786
992
  style: { maxWidth: '640px' },
993
+ role: 'dialog',
994
+ 'aria-modal': 'true',
787
995
  onClick: (e) => e.stopPropagation()
788
996
  },
789
997
  React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: formId ? '12px' : '18px' } },
790
- React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, formId ? 'Редактирование задачи' : 'Новая запланированная задача'),
998
+ React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, formId ? T_KEY(t, 'modal.editTitle') : T_KEY(t, 'modal.newTitle')),
791
999
  selectedTaskMeta && React.createElement('span', {
792
1000
  style: {
793
1001
  fontSize: '12px',
794
1002
  padding: '3px 8px',
795
1003
  borderRadius: '6px',
796
- background: selectedTaskMeta.status === 'active' ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)',
797
- color: selectedTaskMeta.status === 'active' ? '#10b981' : '#ef4444'
1004
+ background: selectedTaskMeta.status === 'active' ? 'var(--dsh-cron-success-bg)' : 'var(--dsh-cron-danger-bg)',
1005
+ color: selectedTaskMeta.status === 'active' ? 'var(--dsh-cron-success)' : 'var(--dsh-cron-danger)'
798
1006
  }
799
- }, selectedTaskMeta.status === 'active' ? '● Активна' : '○ На паузе')
1007
+ }, selectedTaskMeta.status === 'active' ? T_KEY(t, 'modal.statusActive') : T_KEY(t, 'modal.statusPaused'))
800
1008
  ),
801
1009
 
802
1010
  formId && React.createElement('div', { className: 'dsh-cron-modal-nav' },
803
1011
  React.createElement('button', {
804
1012
  type: 'button',
805
1013
  className: 'dsh-cron-modal-tab',
806
- 'data-active': modalTab === 'params',
1014
+ 'data-active': modalTab === 'params' ? 'true' : undefined,
807
1015
  onClick: () => setModalTab('params')
808
- }, 'Параметры'),
1016
+ }, T_KEY(t, 'modal.params')),
809
1017
  React.createElement('button', {
810
1018
  type: 'button',
811
1019
  className: 'dsh-cron-modal-tab',
812
- 'data-active': modalTab === 'history',
1020
+ 'data-active': modalTab === 'history' ? 'true' : undefined,
813
1021
  onClick: () => { setModalTab('history'); if (formId) loadTaskHistory(formId); }
814
- }, 'История запусков (' + (taskHistory.length || 0) + ')')
1022
+ }, T_KEY(t, 'modal.history', { count: taskHistory.length || 0 }))
815
1023
  ),
816
1024
 
817
1025
  modalTab === 'history' && formId ? React.createElement('div', null,
818
- historyLoading ? React.createElement('div', { className: 'dsh-cron-history-empty' }, 'Загрузка истории...') :
819
- taskHistory.length === 0 ? React.createElement('div', { className: 'dsh-cron-history-empty' }, 'История запусков пока пуста') :
820
- React.createElement('div', { className: 'dsh-cron-history-list' },
821
- taskHistory.map((run) => React.createElement('div', { key: run.id, className: 'dsh-cron-history-item' },
822
- React.createElement('div', { className: 'dsh-cron-history-head' },
823
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
824
- React.createElement('span', {
825
- className: 'dsh-cron-history-status',
826
- 'data-status': run.status
827
- }, run.status === 'success' ? 'Успешно' : run.status === 'skipped' ? 'Пропущен' : 'Ошибка'),
828
- React.createElement('span', { className: 'dsh-cron-history-time' }, run.at ? new Date(run.at).toLocaleString() : '')
829
- ),
830
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
831
- run.costUsd > 0 && React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + run.costUsd.toFixed(5)),
832
- run.usage && (run.usage.inputTokens > 0 || run.usage.outputTokens > 0) && React.createElement('span', { className: 'dsh-cron-tokens-tag' }, (run.usage.inputTokens + run.usage.outputTokens) + ' tok'),
833
- React.createElement('span', { style: { color: '#888', fontSize: '11.5px' } }, (run.durationMs || 0) + ' ms')
834
- )
1026
+ historyLoading ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'history.loading')) :
1027
+ taskHistory.length === 0 ? React.createElement('div', { className: 'dsh-cron-history-empty' }, T_KEY(t, 'history.empty')) :
1028
+ React.createElement('div', { className: 'dsh-cron-history-list' },
1029
+ taskHistory.map((run) => React.createElement('div', { key: run.id, className: 'dsh-cron-history-item' },
1030
+ React.createElement('div', { className: 'dsh-cron-history-head' },
1031
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
1032
+ React.createElement('span', {
1033
+ className: 'dsh-cron-history-status',
1034
+ 'data-status': run.status
1035
+ }, statusLabel(t, run.status)),
1036
+ React.createElement('span', { className: 'dsh-cron-history-time' }, run.at ? new Date(run.at).toLocaleString() : '')
1037
+ ),
1038
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
1039
+ run.costUsd > 0 && React.createElement('span', { className: 'dsh-cron-cost-tag' }, '$' + run.costUsd.toFixed(5)),
1040
+ 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'),
1041
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary, #888)', fontSize: '11.5px' } }, (run.durationMs || 0) + ' ms')
1042
+ )
1043
+ ),
1044
+ run.output && React.createElement('div', { className: 'dsh-cron-history-output' }, run.output),
1045
+ run.error && React.createElement('div', { className: 'dsh-cron-history-output', style: { color: 'var(--dsh-cron-danger)', borderColor: 'var(--dsh-cron-danger-bg)' } }, run.error)
1046
+ ))
835
1047
  ),
836
- run.output && React.createElement('div', { className: 'dsh-cron-history-output' }, run.output),
837
- run.error && React.createElement('div', { className: 'dsh-cron-history-output', style: { color: '#ef4444', borderColor: '#552222' } }, run.error)
838
- ))
839
- ),
840
1048
  React.createElement('div', { className: 'dsh-cron-modal-foot', style: { marginTop: '18px' } },
841
1049
  React.createElement('button', {
842
1050
  type: 'button',
843
1051
  className: 'dsh-cron-btn-secondary',
844
1052
  onClick: () => { if (formId) loadTaskHistory(formId); }
845
- }, 'Обновить историю'),
1053
+ }, T_KEY(t, 'history.refresh')),
846
1054
  React.createElement('button', {
847
1055
  type: 'button',
848
1056
  className: 'dsh-cron-btn-primary',
849
1057
  onClick: () => setManualModalOpen(false)
850
- }, 'Закрыть')
1058
+ }, T_KEY(t, 'modal.close'))
851
1059
  )
852
1060
  ) :
853
1061
 
854
- React.createElement('form', { onSubmit: handleSaveManual },
855
- React.createElement('div', { className: 'dsh-cron-form-group' },
856
- React.createElement('label', null, 'Название задачи'),
857
- React.createElement('input', {
858
- required: true,
859
- value: formTitle,
860
- placeholder: 'Например: dsh-tags-watch',
861
- onChange: (e) => setFormTitle(e.target.value)
862
- })
863
- ),
864
- React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
1062
+ React.createElement('form', { onSubmit: handleSaveManual },
865
1063
  React.createElement('div', { className: 'dsh-cron-form-group' },
866
- React.createElement('label', null, 'Тип выполнения'),
867
- React.createElement('select', {
868
- value: formType,
869
- onChange: (e) => setFormType(e.target.value)
870
- },
871
- React.createElement('option', { value: 'llm' }, 'LLM (Агент с нейросетью)'),
872
- React.createElement('option', { value: 'script' }, 'NO-LLM (Shell-команда/Скрипт)')
873
- )
874
- ),
875
- React.createElement('div', { className: 'dsh-cron-form-group' },
876
- React.createElement('label', null, 'Расписание (cron или интервал)'),
1064
+ React.createElement('label', null, T_KEY(t, 'form.nameLabel')),
877
1065
  React.createElement('input', {
878
1066
  required: true,
879
- value: formSchedule,
880
- placeholder: '0 */6 * * * или every 2h или in 30m или at: 2026-09-05T15:00:00Z',
881
- onChange: (e) => setFormSchedule(e.target.value)
882
- })
883
- )
884
- ),
885
- React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
886
- React.createElement('div', { className: 'dsh-cron-form-group' },
887
- React.createElement('label', null, 'Таймаут выполнения (секунды)'),
888
- React.createElement('input', {
889
- type: 'number',
890
- min: 1,
891
- value: formTimeoutSeconds,
892
- placeholder: '1800',
893
- onChange: (e) => setFormTimeoutSeconds(e.target.value)
1067
+ autoFocus: true,
1068
+ value: formTitle,
1069
+ placeholder: T_KEY(t, 'form.namePlaceholder'),
1070
+ onChange: (e) => setFormTitle(e.target.value)
894
1071
  })
895
1072
  ),
896
- React.createElement('div', { className: 'dsh-cron-form-group' },
897
- React.createElement('label', null, 'Политика наложения (Overlap)'),
898
- React.createElement('select', {
899
- value: formOverlapPolicy,
900
- onChange: (e) => setFormOverlapPolicy(e.target.value)
901
- },
902
- React.createElement('option', { value: 'skip' }, 'Пропустить новый запуск (skip)'),
903
- React.createElement('option', { value: 'queue' }, 'Встать в очередь (queue)'),
904
- React.createElement('option', { value: 'replace' }, 'Отменить текущий и перезапустить (replace)')
1073
+ React.createElement('div', { className: 'dsh-cron-form-row' },
1074
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1075
+ React.createElement('label', null, T_KEY(t, 'form.typeLabel')),
1076
+ React.createElement('select', {
1077
+ value: formType,
1078
+ onChange: (e) => setFormType(e.target.value)
1079
+ },
1080
+ React.createElement('option', { value: 'llm' }, T_KEY(t, 'form.typeLlm')),
1081
+ React.createElement('option', { value: 'script' }, T_KEY(t, 'form.typeScript'))
1082
+ )
1083
+ ),
1084
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1085
+ React.createElement('label', null, T_KEY(t, 'form.scheduleLabel')),
1086
+ React.createElement('input', {
1087
+ required: true,
1088
+ value: formSchedule,
1089
+ placeholder: T_KEY(t, 'form.schedulePlaceholder'),
1090
+ onChange: (e) => setFormSchedule(e.target.value)
1091
+ })
905
1092
  )
906
- )
907
- ),
908
- React.createElement('div', { className: 'dsh-cron-form-group' },
909
- React.createElement('label', null, 'Создание карточки в Kanban (dsh-kanban)'),
910
- React.createElement('select', {
911
- value: formKanbanMode,
912
- onChange: (e) => setFormKanbanMode(e.target.value)
913
- },
914
- React.createElement('option', { value: 'none' }, 'Не создавать карточки (отключено)'),
915
- React.createElement('option', { value: 'on_failure' }, 'Создавать тикет при ошибке/сбое (в Backlog)'),
916
- React.createElement('option', { value: 'always' }, 'Создавать карточку при каждом завершении (Done / Backlog)')
917
- )
918
- ),
919
- formType !== 'script' && React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
920
- React.createElement('div', { className: 'dsh-cron-form-group' },
921
- React.createElement('label', null, 'Провайдер LLM'),
922
- React.createElement('select', {
923
- value: formProvider,
924
- onChange: handleProviderChange
925
- },
926
- React.createElement('option', { value: '' }, 'По умолчанию (DSH)'),
927
- providers.map((p) => React.createElement('option', { key: p.id, value: p.id }, p.name || p.id))
1093
+ ),
1094
+ React.createElement('div', { className: 'dsh-cron-form-row' },
1095
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1096
+ React.createElement('label', null, T_KEY(t, 'form.timeoutLabel')),
1097
+ React.createElement('input', {
1098
+ type: 'number',
1099
+ min: 1,
1100
+ value: formTimeoutSeconds,
1101
+ placeholder: '1800',
1102
+ onChange: (e) => setFormTimeoutSeconds(e.target.value)
1103
+ })
1104
+ ),
1105
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1106
+ React.createElement('label', null, T_KEY(t, 'form.overlapLabel')),
1107
+ React.createElement('select', {
1108
+ value: formOverlapPolicy,
1109
+ onChange: (e) => setFormOverlapPolicy(e.target.value)
1110
+ },
1111
+ React.createElement('option', { value: 'skip' }, T_KEY(t, 'form.overlapSkip')),
1112
+ React.createElement('option', { value: 'queue' }, T_KEY(t, 'form.overlapQueue')),
1113
+ React.createElement('option', { value: 'replace' }, T_KEY(t, 'form.overlapReplace'))
1114
+ )
928
1115
  )
929
1116
  ),
930
1117
  React.createElement('div', { className: 'dsh-cron-form-group' },
931
- React.createElement('label', null, 'Модель LLM'),
1118
+ React.createElement('label', null, T_KEY(t, 'form.kanbanLabel')),
932
1119
  React.createElement('select', {
933
- value: formModel,
934
- onChange: (e) => setFormModel(e.target.value)
1120
+ value: formKanbanMode,
1121
+ onChange: (e) => setFormKanbanMode(e.target.value)
935
1122
  },
936
- React.createElement('option', { value: '' }, 'По умолчанию'),
937
- models.map((m) => React.createElement('option', { key: m.id, value: m.id }, m.name || m.id))
1123
+ React.createElement('option', { value: 'none' }, T_KEY(t, 'form.kanbanNone')),
1124
+ React.createElement('option', { value: 'on_failure' }, T_KEY(t, 'form.kanbanOnFailure')),
1125
+ React.createElement('option', { value: 'always' }, T_KEY(t, 'form.kanbanAlways'))
938
1126
  )
939
- )
940
- ),
941
- React.createElement('div', { className: 'dsh-cron-form-group' },
942
- React.createElement('label', null, formType === 'script' ? 'Shell-команда или скрипт' : 'Инструкция / Промпт для агента'),
943
- React.createElement('textarea', {
944
- required: true,
945
- rows: 9,
946
- style: { fontFamily: 'monospace', fontSize: '12.5px', lineHeight: '1.45' },
947
- value: formPrompt,
948
- placeholder: formType === 'script' ? 'curl -fsSL https://... || exit 1' : 'Опишите, что именно агент должен сделать...',
949
- onChange: (e) => setFormPrompt(e.target.value)
950
- })
951
- ),
952
- React.createElement('div', {
953
- style: {
954
- background: 'rgba(255,255,255,0.02)',
955
- border: '1px solid #2e2e2e',
956
- borderRadius: '8px',
957
- padding: '12px 14px',
958
- marginBottom: '16px'
959
- }
960
- },
961
- React.createElement('div', { style: { fontWeight: 500, fontSize: '13px', color: '#eee', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px' } },
962
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
963
- 'Уведомления в Telegram'
964
1127
  ),
965
- React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: formNotifyTelegram ? '8px' : '0' } },
966
- React.createElement('input', {
967
- type: 'checkbox',
968
- checked: formNotifyTelegram,
969
- onChange: (e) => setFormNotifyTelegram(e.target.checked)
970
- }),
971
- React.createElement('span', null, 'Отправлять отчет о выполнении в Telegram')
1128
+ formType !== 'script' && React.createElement('div', { className: 'dsh-cron-form-row' },
1129
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1130
+ React.createElement('label', null, T_KEY(t, 'form.providerLabel')),
1131
+ React.createElement('select', {
1132
+ value: formProvider,
1133
+ onChange: handleProviderChange
1134
+ },
1135
+ React.createElement('option', { value: '' }, T_KEY(t, 'form.providerDefault')),
1136
+ providers.map((p) => React.createElement('option', { key: p.id, value: p.id }, p.name || p.id))
1137
+ )
1138
+ ),
1139
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1140
+ React.createElement('label', null, T_KEY(t, 'form.modelLabel')),
1141
+ React.createElement('select', {
1142
+ value: formModel,
1143
+ onChange: (e) => setFormModel(e.target.value)
1144
+ },
1145
+ React.createElement('option', { value: '' }, T_KEY(t, 'form.modelDefault')),
1146
+ models.map((m) => React.createElement('option', { key: m.id, value: m.id }, m.name || m.id))
1147
+ )
1148
+ )
972
1149
  ),
973
- formNotifyTelegram && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
974
- React.createElement('input', {
975
- type: 'checkbox',
976
- checked: formOnlyOnFailure,
977
- onChange: (e) => setFormOnlyOnFailure(e.target.checked)
978
- }),
979
- React.createElement('span', null, 'Только при ошибках (onlyOnFailure / silent mode при успехе)')
980
- )
981
- ),
982
- selectedTaskMeta && React.createElement('div', {
983
- style: {
984
- background: 'rgba(255,255,255,0.03)',
985
- border: '1px solid #2e2e2e',
986
- borderRadius: '8px',
987
- padding: '10px 14px',
988
- fontSize: '12.5px',
989
- color: '#999',
990
- marginBottom: '16px',
991
- display: 'flex',
992
- justifyContent: 'space-between',
993
- alignItems: 'center'
994
- }
995
- },
996
- React.createElement('div', null,
997
- React.createElement('span', null, 'Следующий запуск: '),
998
- React.createElement('strong', { style: { color: '#eee' } }, selectedTaskMeta.nextRunAt ? new Date(selectedTaskMeta.nextRunAt).toLocaleString() : 'не запланирован')
1150
+ React.createElement('div', { className: 'dsh-cron-form-group' },
1151
+ React.createElement('label', null, formType === 'script' ? T_KEY(t, 'form.promptLabelScript') : T_KEY(t, 'form.promptLabelLlm')),
1152
+ React.createElement('textarea', {
1153
+ required: true,
1154
+ rows: 9,
1155
+ style: { fontFamily: 'monospace', fontSize: '12.5px', lineHeight: '1.45' },
1156
+ value: formPrompt,
1157
+ placeholder: formType === 'script' ? T_KEY(t, 'form.promptPlaceholderScript') : T_KEY(t, 'form.promptPlaceholderLlm'),
1158
+ onChange: (e) => setFormPrompt(e.target.value)
1159
+ })
999
1160
  ),
1000
- React.createElement('button', {
1001
- type: 'button',
1002
- className: 'dsh-cron-btn-secondary',
1003
- style: { padding: '4px 10px', fontSize: '12px' },
1004
- onClick: async () => {
1005
- await handleRunNow(selectedTaskMeta.id);
1006
- if (formId) loadTaskHistory(formId);
1161
+ React.createElement('div', {
1162
+ style: {
1163
+ background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))',
1164
+ border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
1165
+ borderRadius: '8px',
1166
+ padding: '12px 14px',
1167
+ marginBottom: '16px'
1007
1168
  }
1008
- }, 'Запустить сейчас')
1009
- ),
1010
- React.createElement('div', { className: 'dsh-cron-modal-foot' },
1011
- React.createElement('button', {
1012
- type: 'button',
1013
- className: 'dsh-cron-btn-secondary',
1014
- onClick: () => setManualModalOpen(false)
1015
- }, 'Отмена'),
1016
- React.createElement('button', {
1017
- type: 'submit',
1018
- className: 'dsh-cron-btn-primary'
1019
- }, formId ? 'Сохранить изменения' : 'Создать задачу')
1169
+ },
1170
+ React.createElement('div', { style: { fontWeight: 500, fontSize: '13px', color: 'var(--dsw-alias-label-primary, #eee)', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px' } },
1171
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
1172
+ T_KEY(t, 'form.notifyTitle')
1173
+ ),
1174
+ React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: formNotifyTelegram ? '8px' : '0' } },
1175
+ React.createElement('input', {
1176
+ type: 'checkbox',
1177
+ checked: formNotifyTelegram,
1178
+ onChange: (e) => setFormNotifyTelegram(e.target.checked)
1179
+ }),
1180
+ React.createElement('span', null, T_KEY(t, 'form.notifyTelegram'))
1181
+ ),
1182
+ formNotifyTelegram && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
1183
+ React.createElement('input', {
1184
+ type: 'checkbox',
1185
+ checked: formOnlyOnFailure,
1186
+ onChange: (e) => setFormOnlyOnFailure(e.target.checked)
1187
+ }),
1188
+ React.createElement('span', null, T_KEY(t, 'form.notifyOnlyFailure'))
1189
+ )
1190
+ ),
1191
+ selectedTaskMeta && React.createElement('div', {
1192
+ style: {
1193
+ background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.03))',
1194
+ border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)',
1195
+ borderRadius: '8px',
1196
+ padding: '10px 14px',
1197
+ fontSize: '12.5px',
1198
+ color: 'var(--dsw-alias-label-secondary, #999)',
1199
+ marginBottom: '16px',
1200
+ display: 'flex',
1201
+ justifyContent: 'space-between',
1202
+ alignItems: 'center'
1203
+ }
1204
+ },
1205
+ React.createElement('div', null,
1206
+ React.createElement('span', null, T_KEY(t, 'form.nextRunLabel')),
1207
+ React.createElement('strong', { style: { color: 'var(--dsw-alias-label-primary, #eee)' } }, selectedTaskMeta.nextRunAt ? new Date(selectedTaskMeta.nextRunAt).toLocaleString() : T_KEY(t, 'form.notScheduled'))
1208
+ ),
1209
+ React.createElement('button', {
1210
+ type: 'button',
1211
+ className: 'dsh-cron-btn-secondary',
1212
+ style: { padding: '4px 10px', fontSize: '12px' },
1213
+ onClick: async () => {
1214
+ await handleRunNow(selectedTaskMeta.id);
1215
+ if (formId) loadTaskHistory(formId);
1216
+ }
1217
+ }, T_KEY(t, 'actions.runNow'))
1218
+ ),
1219
+ React.createElement('div', { className: 'dsh-cron-modal-foot' },
1220
+ React.createElement('button', {
1221
+ type: 'button',
1222
+ className: 'dsh-cron-btn-secondary',
1223
+ onClick: () => setManualModalOpen(false)
1224
+ }, T_KEY(t, 'modal.cancel')),
1225
+ React.createElement('button', {
1226
+ type: 'submit',
1227
+ className: 'dsh-cron-btn-primary'
1228
+ }, formId ? T_KEY(t, 'modal.save') : T_KEY(t, 'modal.create'))
1229
+ )
1020
1230
  )
1021
- )
1022
1231
  )
1023
1232
  ),
1024
1233
 
1025
- // 1.5 Settings Modal
1234
+ // 2. Settings modal
1026
1235
  settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
1027
- React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
1236
+ React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
1028
1237
  React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
1029
1238
  React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0, display: 'flex', alignItems: 'center', gap: '8px' } },
1030
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
1031
- 'Настройки Telegram и уведомлений'
1239
+ React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
1240
+ T_KEY(t, 'settings.modalTitle')
1032
1241
  ),
1033
1242
  React.createElement('button', {
1034
1243
  type: 'button',
1035
- style: { background: 'none', border: 'none', color: '#888', cursor: 'pointer', fontSize: '18px' },
1244
+ style: { background: 'none', border: 'none', color: 'var(--dsw-alias-label-tertiary, #888)', cursor: 'pointer', fontSize: '18px' },
1036
1245
  onClick: () => setSettingsModalOpen(false)
1037
1246
  }, '✕')
1038
1247
  ),
1039
1248
  React.createElement('form', { onSubmit: handleSaveSettings },
1040
1249
  React.createElement('div', { className: 'dsh-cron-form-group' },
1041
- React.createElement('label', null, 'Telegram Bot Token'),
1250
+ React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
1042
1251
  React.createElement('input', {
1043
1252
  type: 'password',
1044
1253
  value: settingsBotToken,
1045
- placeholder: settingsHasDefault ? 'Используется бот из настроек DSH (или введите свой)' : '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
1254
+ placeholder: settingsHasDefault ? T_KEY(t, 'settings.tokenPlaceholderDefault') : T_KEY(t, 'settings.tokenPlaceholder'),
1046
1255
  onChange: (e) => setSettingsBotToken(e.target.value)
1047
1256
  }),
1048
- settingsHasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, '✓ Обнаружен настроенный бот в профиле DSH')
1257
+ settingsHasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'settings.defaultHint'))
1049
1258
  ),
1050
1259
  React.createElement('div', { className: 'dsh-cron-form-group' },
1051
- React.createElement('label', null, 'Telegram Chat ID'),
1260
+ React.createElement('label', null, T_KEY(t, 'settings.chatIdLabel')),
1052
1261
  React.createElement('input', {
1053
1262
  value: settingsChatId,
1054
- placeholder: 'Например: 345678901',
1263
+ placeholder: T_KEY(t, 'settings.chatIdPlaceholder'),
1055
1264
  onChange: (e) => setSettingsChatId(e.target.value)
1056
1265
  })
1057
1266
  ),
1058
- React.createElement('div', { style: { background: 'rgba(255,255,255,0.02)', border: '1px solid #2e2e2e', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
1267
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
1059
1268
  React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: '8px' } },
1060
1269
  React.createElement('input', {
1061
1270
  type: 'checkbox',
1062
1271
  checked: settingsNotifyGlobal,
1063
1272
  onChange: (e) => setSettingsNotifyGlobal(e.target.checked)
1064
1273
  }),
1065
- React.createElement('span', null, 'Глобально отправлять отчеты всех задач в Telegram')
1274
+ React.createElement('span', null, T_KEY(t, 'settings.globalNotify'))
1066
1275
  ),
1067
1276
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
1068
1277
  React.createElement('input', {
@@ -1070,7 +1279,7 @@ const React = window.React || require('react');
1070
1279
  checked: settingsOnlyOnFailureGlobal,
1071
1280
  onChange: (e) => setSettingsOnlyOnFailureGlobal(e.target.checked)
1072
1281
  }),
1073
- React.createElement('span', null, 'Глобальный режим: Только при ошибках (silent при успехе)')
1282
+ React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
1074
1283
  )
1075
1284
  ),
1076
1285
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '14px' } },
@@ -1080,7 +1289,7 @@ const React = window.React || require('react');
1080
1289
  style: { display: 'inline-flex', alignItems: 'center', gap: '6px' },
1081
1290
  disabled: testStatus?.loading,
1082
1291
  onClick: handleTestTelegram
1083
- }, testStatus?.loading ? 'Отправка...' : '🔔 Проверить отправку в Telegram')
1292
+ }, testStatus?.loading ? T_KEY(t, 'settings.sending') : T_KEY(t, 'settings.testTelegram'))
1084
1293
  ),
1085
1294
  testStatus && testStatus.message && React.createElement('div', {
1086
1295
  className: 'dsh-cron-status-banner',
@@ -1091,28 +1300,29 @@ const React = window.React || require('react');
1091
1300
  type: 'button',
1092
1301
  className: 'dsh-cron-btn-secondary',
1093
1302
  onClick: () => setSettingsModalOpen(false)
1094
- }, 'Отмена'),
1303
+ }, T_KEY(t, 'modal.cancel')),
1095
1304
  React.createElement('button', {
1096
1305
  type: 'submit',
1097
1306
  className: 'dsh-cron-btn-primary'
1098
- }, 'Сохранить настройки')
1307
+ }, T_KEY(t, 'settings.save'))
1099
1308
  )
1100
1309
  )
1101
1310
  )
1102
1311
  ),
1103
- // 2. "Создать с DSH" modal
1312
+
1313
+ // 3. "Create with DSH" modal
1104
1314
  dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
1105
- React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
1106
- React.createElement('div', { className: 'dsh-cron-modal-title' }, 'Запланировать задачу с DSH Агентом'),
1315
+ React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
1316
+ React.createElement('div', { className: 'dsh-cron-modal-title' }, T_KEY(t, 'dsh.modalTitle')),
1107
1317
  React.createElement('div', { className: 'dsh-cron-form-group' },
1108
- React.createElement('label', null, 'Опишите задачу и желаемую периодичность:'),
1318
+ React.createElement('label', null, T_KEY(t, 'dsh.descLabel')),
1109
1319
  React.createElement('div', { className: 'dsh-cron-chat-box' },
1110
1320
  React.createElement('textarea', {
1111
1321
  className: 'dsh-cron-chat-input',
1112
1322
  rows: 4,
1113
1323
  autoFocus: true,
1114
1324
  disabled: dshLoading,
1115
- placeholder: 'Например: проверяй каждые 2 часа свободное место на сервере и наличие упавших systemd-сервисов, присылай алерт только если что-то не так...',
1325
+ placeholder: T_KEY(t, 'dsh.placeholder'),
1116
1326
  value: dshPrompt,
1117
1327
  onKeyDown: (e) => {
1118
1328
  if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
@@ -1123,18 +1333,15 @@ const React = window.React || require('react');
1123
1333
  onChange: (e) => setDshPrompt(e.target.value)
1124
1334
  }),
1125
1335
  React.createElement('div', { className: 'dsh-cron-chat-foot' },
1126
- React.createElement('span', { className: 'dsh-cron-chat-hint' }, 'Ctrl+Enter для отправки'),
1336
+ React.createElement('span', { className: 'dsh-cron-chat-hint' }, T_KEY(t, 'dsh.sendHint')),
1127
1337
  React.createElement('button', {
1128
1338
  type: 'button',
1129
1339
  className: 'dsh-cron-send-btn',
1130
1340
  disabled: dshLoading || !dshPrompt.trim(),
1131
- title: 'Отправить агенту',
1341
+ title: T_KEY(t, 'dsh.sendTitle'),
1132
1342
  onClick: handleStartWithDSH
1133
1343
  },
1134
- React.createElement('span', {
1135
- style: { display: 'flex', alignItems: 'center' },
1136
- dangerouslySetInnerHTML: { __html: ICON_SEND }
1137
- })
1344
+ React.createElement('span', svgProps(ICON_SEND))
1138
1345
  )
1139
1346
  )
1140
1347
  )
@@ -1145,7 +1352,7 @@ const React = window.React || require('react');
1145
1352
  className: 'dsh-cron-btn-secondary',
1146
1353
  disabled: dshLoading,
1147
1354
  onClick: () => setDshModalOpen(false)
1148
- }, 'Отмена')
1355
+ }, T_KEY(t, 'modal.cancel'))
1149
1356
  )
1150
1357
  )
1151
1358
  )
@@ -1154,6 +1361,7 @@ const React = window.React || require('react');
1154
1361
 
1155
1362
  function CronSettingsCard(props) {
1156
1363
  const { ctx, toggle } = props;
1364
+ const t = props.t || translate;
1157
1365
  const [token, setToken] = React.useState('');
1158
1366
  const [chatId, setChatId] = React.useState('');
1159
1367
  const [notify, setNotify] = React.useState(false);
@@ -1162,24 +1370,36 @@ const React = window.React || require('react');
1162
1370
  const [hasDefault, setHasDefault] = React.useState(false);
1163
1371
  const [saved, setSaved] = React.useState(false);
1164
1372
  const [testInfo, setTestInfo] = React.useState(null);
1373
+ // Collapsed by default, head is the toggle (canonical card contract, #100)
1374
+ const [cardOpen, setCardOpen] = React.useState(false);
1375
+
1376
+ // Settings snapshot status (#102): never render phantom inputs before
1377
+ // the snapshot arrives; offer a retry when it is unavailable.
1378
+ const [loadState, setLoadState] = React.useState('loading');
1379
+ const [reloadTick, setReloadTick] = React.useState(0);
1165
1380
 
1166
1381
  React.useEffect(() => {
1167
1382
  let active = true;
1383
+ setLoadState('loading');
1168
1384
  fetch('/dsh-cron/settings')
1169
1385
  .then(r => r.json())
1170
1386
  .then(data => {
1171
- if (active && data && data.ok && data.settings) {
1387
+ if (!active) return;
1388
+ if (data && data.ok && data.settings) {
1172
1389
  setToken(data.settings.botToken || '');
1173
1390
  setChatId(data.settings.chatId || '');
1174
1391
  setNotify(Boolean(data.settings.notifyTelegram));
1175
1392
  setOnlyFailure(Boolean(data.settings.onlyOnFailure));
1176
1393
  setKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
1177
1394
  setHasDefault(Boolean(data.settings.hasDefaultCredentials));
1395
+ setLoadState('ready');
1396
+ } else {
1397
+ setLoadState('error');
1178
1398
  }
1179
1399
  })
1180
- .catch(() => {});
1400
+ .catch(() => { if (active) setLoadState('error'); });
1181
1401
  return () => { active = false; };
1182
- }, []);
1402
+ }, [reloadTick]);
1183
1403
 
1184
1404
  const saveSettings = async (e) => {
1185
1405
  if (e) e.preventDefault();
@@ -1204,7 +1424,7 @@ const React = window.React || require('react');
1204
1424
  };
1205
1425
 
1206
1426
  const testTelegram = async () => {
1207
- setTestInfo({ loading: true, message: 'Отправка...' });
1427
+ setTestInfo({ loading: true, message: T_KEY(t, 'settings.sending') });
1208
1428
  try {
1209
1429
  const res = await fetch('/dsh-cron/telegram/test', {
1210
1430
  method: 'POST',
@@ -1213,17 +1433,17 @@ const React = window.React || require('react');
1213
1433
  });
1214
1434
  const data = await res.json();
1215
1435
  if (data && data.ok) {
1216
- setTestInfo({ success: true, message: '✅ Успешно доставлено в Telegram' });
1436
+ setTestInfo({ success: true, message: T_KEY(t, 'settings.testOk') });
1217
1437
  } else {
1218
- setTestInfo({ success: false, message: '❌ ' + (data?.error || 'Сбой отправки') });
1438
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.testFail', { error: (data && data.error) || '' }) });
1219
1439
  }
1220
1440
  } catch (err) {
1221
- setTestInfo({ success: false, message: '❌ ' + err.message });
1441
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.testFail', { error: err.message }) });
1222
1442
  }
1223
1443
  };
1224
1444
 
1225
1445
  const testKanban = async () => {
1226
- setTestInfo({ loading: true, message: 'Проверка Kanban...' });
1446
+ setTestInfo({ loading: true, message: T_KEY(t, 'settings.checkingKanban') });
1227
1447
  try {
1228
1448
  const res = await fetch('/dsh-cron/kanban/test', {
1229
1449
  method: 'POST',
@@ -1232,62 +1452,87 @@ const React = window.React || require('react');
1232
1452
  });
1233
1453
  const data = await res.json();
1234
1454
  if (data && data.ok) {
1235
- setTestInfo({ success: true, message: '✅ Карточка в Kanban создана' });
1455
+ setTestInfo({ success: true, message: T_KEY(t, 'settings.kanbanOk') });
1236
1456
  } else {
1237
- setTestInfo({ success: false, message: '❌ ' + (data?.error || 'Ошибка Kanban') });
1457
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.kanbanFail', { error: (data && data.error) || '' }) });
1238
1458
  }
1239
1459
  } catch (err) {
1240
- setTestInfo({ success: false, message: '❌ ' + err.message });
1460
+ setTestInfo({ success: false, message: T_KEY(t, 'settings.kanbanFail', { error: err.message }) });
1241
1461
  }
1242
1462
  };
1243
1463
 
1464
+ if (loadState !== 'ready') {
1465
+ return React.createElement('div', { className: 'dsh-cron-card' },
1466
+ React.createElement('div', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
1467
+ React.createElement('div', {
1468
+ className: 'dsh-cron-status-banner',
1469
+ 'data-type': loadState === 'error' ? 'error' : 'info',
1470
+ style: { marginTop: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '10px' }
1471
+ },
1472
+ React.createElement('span', null, loadState === 'error' ? T_KEY(t, 'settings.unavailable') : T_KEY(t, 'settings.loading')),
1473
+ loadState === 'error' && React.createElement('button', {
1474
+ type: 'button',
1475
+ className: 'dsh-cron-btn-secondary',
1476
+ onClick: () => setReloadTick(x => x + 1)
1477
+ }, T_KEY(t, 'settings.retry'))
1478
+ )
1479
+ );
1480
+ }
1481
+
1244
1482
  return React.createElement('div', { className: 'dsh-cron-card' },
1245
- React.createElement('div', { className: 'dsh-cron-card-head' },
1246
- React.createElement('span', { className: 'dsh-cron-card-title' }, '⏰ Планировщик задач (Cron)'),
1247
- React.createElement('button', {
1248
- type: 'button',
1249
- className: 'dsh-cron-btn-primary',
1250
- onClick: () => { if (toggle) toggle.set(true); }
1251
- }, 'Открыть панель задач')
1252
- ),
1253
- React.createElement('div', { className: 'dsh-cron-card-desc', style: { marginBottom: '16px' } },
1254
- 'Автоматическое выполнение задач по расписанию (cron/интервалы), фоновые сценарии агентов и интеграция с Telegram и Kanban.'
1483
+ React.createElement('button', {
1484
+ type: 'button',
1485
+ className: 'dsh-cron-card-head-btn',
1486
+ 'aria-expanded': cardOpen ? 'true' : 'false',
1487
+ onClick: () => setCardOpen(!cardOpen)
1488
+ },
1489
+ React.createElement('span', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
1490
+ React.createElement('span', { className: 'dsh-cron-chev' + (cardOpen ? ' dsh-cron-chev-open' : '') }, chevronNode())
1255
1491
  ),
1256
- React.createElement('form', { onSubmit: saveSettings, style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
1492
+ cardOpen && React.createElement('div', { className: 'dsh-cron-card-body' },
1493
+ React.createElement('div', { className: 'dsh-cron-card-desc' }, T_KEY(t, 'settings.cardDesc')),
1494
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: '12px' } },
1495
+ React.createElement('button', {
1496
+ type: 'button',
1497
+ className: 'dsh-cron-btn-primary',
1498
+ onClick: () => { if (toggle) toggle.set(true); }
1499
+ }, T_KEY(t, 'settings.openPanel'))
1500
+ ),
1501
+ React.createElement('form', { onSubmit: saveSettings, style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
1257
1502
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1258
- React.createElement('label', null, 'Telegram Bot Token'),
1503
+ React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
1259
1504
  React.createElement('input', {
1260
1505
  type: 'password',
1261
1506
  value: token,
1262
- placeholder: hasDefault ? 'Используется бот из настроек DSH (или введите свой)' : '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
1507
+ placeholder: hasDefault ? T_KEY(t, 'settings.tokenPlaceholderDefault') : T_KEY(t, 'settings.tokenPlaceholder'),
1263
1508
  onChange: (e) => setToken(e.target.value)
1264
1509
  }),
1265
- hasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, '✓ Найден настроенный бот в профиле DSH')
1510
+ hasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'settings.defaultHint'))
1266
1511
  ),
1267
1512
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1268
- React.createElement('label', null, 'Telegram Chat ID'),
1513
+ React.createElement('label', null, T_KEY(t, 'settings.chatIdLabel')),
1269
1514
  React.createElement('input', {
1270
1515
  value: chatId,
1271
- placeholder: 'Например: 345678901',
1516
+ placeholder: T_KEY(t, 'settings.chatIdPlaceholder'),
1272
1517
  onChange: (e) => setChatId(e.target.value)
1273
1518
  })
1274
1519
  ),
1275
1520
  React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
1276
- React.createElement('label', null, 'Kanban Base URL'),
1521
+ React.createElement('label', null, T_KEY(t, 'settings.kanbanUrlLabel')),
1277
1522
  React.createElement('input', {
1278
1523
  value: kanbanUrl,
1279
1524
  placeholder: 'http://127.0.0.1:3000',
1280
1525
  onChange: (e) => setKanbanUrl(e.target.value)
1281
1526
  })
1282
1527
  ),
1283
- React.createElement('div', { style: { background: 'rgba(255,255,255,0.02)', border: '1px solid #2e2e2e', borderRadius: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
1528
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.02))', border: '1px solid var(--dsw-alias-border-l2, #2e2e2e)', borderRadius: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
1284
1529
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
1285
1530
  React.createElement('input', {
1286
1531
  type: 'checkbox',
1287
1532
  checked: notify,
1288
1533
  onChange: (e) => setNotify(e.target.checked)
1289
1534
  }),
1290
- React.createElement('span', null, 'Глобально отправлять отчеты всех задач в Telegram')
1535
+ React.createElement('span', null, T_KEY(t, 'settings.globalNotify'))
1291
1536
  ),
1292
1537
  React.createElement('label', { className: 'dsh-cron-checkbox-row' },
1293
1538
  React.createElement('input', {
@@ -1295,7 +1540,7 @@ const React = window.React || require('react');
1295
1540
  checked: onlyFailure,
1296
1541
  onChange: (e) => setOnlyFailure(e.target.checked)
1297
1542
  }),
1298
- React.createElement('span', null, 'Уведомлять только при ошибках')
1543
+ React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
1299
1544
  )
1300
1545
  ),
1301
1546
  testInfo && testInfo.message && React.createElement('div', {
@@ -1308,22 +1553,23 @@ const React = window.React || require('react');
1308
1553
  type: 'submit',
1309
1554
  className: 'dsh-cron-btn-primary',
1310
1555
  style: { minWidth: '140px' }
1311
- }, saved ? '✓ Сохранено' : 'Сохранить настройки'),
1556
+ }, saved ? T_KEY(t, 'settings.saved') : T_KEY(t, 'settings.save')),
1312
1557
  React.createElement('button', {
1313
1558
  type: 'button',
1314
1559
  className: 'dsh-cron-btn-secondary',
1315
1560
  onClick: testTelegram,
1316
1561
  disabled: testInfo?.loading
1317
- }, '🔔 Тест Telegram'),
1562
+ }, T_KEY(t, 'settings.testTelegram')),
1318
1563
  React.createElement('button', {
1319
1564
  type: 'button',
1320
1565
  className: 'dsh-cron-btn-secondary',
1321
1566
  onClick: testKanban,
1322
1567
  disabled: testInfo?.loading
1323
- }, '📋 Тест Kanban')
1568
+ }, T_KEY(t, 'settings.testKanban'))
1324
1569
  )
1325
1570
  )
1326
- );
1571
+ )
1572
+ );
1327
1573
  }
1328
1574
 
1329
1575
  function nativeButton() {
@@ -1338,7 +1584,7 @@ const React = window.React || require('react');
1338
1584
  const entry = target.cloneNode(true);
1339
1585
  entry.className = String(target.className) + ' dsh-cron-entry-clone';
1340
1586
  entry.setAttribute(ENTRY_ATTR, '');
1341
- entry.setAttribute('data-dsh-plugin', 'cron');
1587
+ entry.setAttribute('data-dsh-plugin', 'dsh-cron');
1342
1588
  entry.setAttribute('data-dsh-part', 'sidebar-entry');
1343
1589
  entry.setAttribute('aria-label', label);
1344
1590
  entry.removeAttribute('id');
@@ -1416,13 +1662,13 @@ const React = window.React || require('react');
1416
1662
  if (column === null) return;
1417
1663
  container = document.createElement('div');
1418
1664
  container.setAttribute(VIEW_ATTR, '');
1419
- container.setAttribute('data-dsh-plugin', 'cron');
1665
+ container.setAttribute('data-dsh-plugin', 'dsh-cron');
1420
1666
  column.appendChild(container);
1421
1667
  if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
1422
1668
  root = ReactDOM.createRoot(container);
1423
- root.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false) }));
1669
+ root.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate }));
1424
1670
  } else if (ReactDOM && typeof ReactDOM.render === 'function') {
1425
- ReactDOM.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false) }), container);
1671
+ ReactDOM.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate }), container);
1426
1672
  }
1427
1673
  };
1428
1674
 
@@ -1465,43 +1711,82 @@ const React = window.React || require('react');
1465
1711
  };
1466
1712
  }
1467
1713
 
1468
- function registerFirst(ctx, candidates, component) {
1469
- if (!ctx || !ctx.slots) return undefined;
1470
- for (const entry of candidates) {
1471
- try {
1472
- let ok = false;
1473
- ctx.slots.inject(entry.name, () => {
1474
- ctx.slots.register(entry, component);
1475
- ok = true;
1476
- });
1477
- if (ok) return entry.name;
1478
- } catch (e) {}
1714
+ /**
1715
+ * Register a card into a DSH mount point. Mount points are populated via
1716
+ * ctx.slots.inject(mount, cb) — a direct slots.register does not throw but
1717
+ * also never appears in the target surface (settings plugins tab, header),
1718
+ * so the inject contract is mandatory (#85).
1719
+ */
1720
+ function registerIntoMount(ctx, mountName, entry, component) {
1721
+ if (!ctx || !ctx.slots || typeof ctx.slots.inject !== 'function') return false;
1722
+ try {
1723
+ return !!ctx.slots.inject(mountName, () => {
1724
+ ctx.slots.register(entry, component);
1725
+ });
1726
+ } catch (err) {
1727
+ console.warn('[dsh-cron] slot injection skipped:', mountName, '-', err && err.message);
1728
+ return false;
1479
1729
  }
1480
- return undefined;
1481
1730
  }
1482
1731
 
1483
1732
  function apply(ctx) {
1484
1733
  ensureStyles();
1734
+
1735
+ // Register the English canonical strings; rebinding the translator lets
1736
+ // the translation plugin drive the active language at runtime (#87).
1737
+ try {
1738
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
1739
+ ctx.locale.register(NS, STRINGS);
1740
+ }
1741
+ } catch (e) {
1742
+ // Re-registration in the same session throws — the dictionary is live already.
1743
+ }
1744
+ try {
1745
+ if (ctx.locale && typeof ctx.locale.bind === 'function') {
1746
+ translate = ctx.locale.bind(NS);
1747
+ }
1748
+ } catch (e) {}
1749
+
1485
1750
  const toggle = createToggle();
1486
1751
 
1487
- const cardSlot = registerFirst(ctx, [
1488
- { name: 'settings.plugin.item', key: NS, inject: () => ({ ctx, toggle }) },
1489
- { name: 'settings.section', id: '@goodandready/dsh-cron', order: 34, label: () => 'Cron Задачи', inject: () => ({ ctx, toggle }) }
1490
- ], CronSettingsCard);
1491
-
1492
- const chipSlot = registerFirst(ctx, [
1493
- { name: 'conversation.session.header.utilities', id: '@goodandready/dsh-cron.chip', order: 27, inject: () => ({ ctx, toggle }) }
1494
- ], () => React.createElement('button', {
1495
- type: 'button',
1496
- style: { background: 'none', border: 'none', color: 'var(--dsw-alias-label-secondary, #999)', cursor: 'pointer', padding: '4px', display: 'inline-flex', alignItems: 'center' },
1497
- title: 'Запланированные задачи (Cron)',
1498
- onClick: () => toggle.toggle(),
1499
- dangerouslySetInnerHTML: { __html: ICON_TIMER }
1500
- }));
1752
+ // The settings card lives ONLY in the plugins settings tab (#102): no
1753
+ // top-level section fallback — the flat sidebar list is a shared core
1754
+ // resource, and the card slot is proven on current builds (#85).
1755
+ const cardOk = registerIntoMount(ctx, 'settings.plugin.item', {
1756
+ name: 'settings.plugin.item',
1757
+ key: NS,
1758
+ locale: NS,
1759
+ inject: () => ({ ctx, toggle }),
1760
+ }, CronSettingsCard);
1761
+ if (!cardOk) {
1762
+ console.warn('[dsh-cron] settings.plugin.item slot unavailable — settings card not registered');
1763
+ }
1764
+
1765
+ const chipOk = registerIntoMount(ctx, 'conversation.session.header.utilities', {
1766
+ name: 'conversation.session.header.utilities',
1767
+ id: PLUGIN_ID + '.chip',
1768
+ order: 27,
1769
+ locale: NS,
1770
+ inject: () => ({ ctx, toggle }),
1771
+ }, CronChip);
1772
+ const cardSlot = cardOk ? 'settings.plugin.item' : undefined;
1773
+ const chipSlot = chipOk ? 'conversation.session.header.utilities' : undefined;
1774
+
1775
+ function CronChip(props) {
1776
+ const chipT = props.t || translate;
1777
+ return React.createElement('button', {
1778
+ type: 'button',
1779
+ style: { background: 'none', border: 'none', color: 'var(--dsw-alias-label-secondary, #999)', cursor: 'pointer', padding: '4px', display: 'inline-flex', alignItems: 'center' },
1780
+ title: T_KEY(chipT, 'chip.title'),
1781
+ 'aria-label': T_KEY(chipT, 'chip.title'),
1782
+ onClick: () => toggle.toggle(),
1783
+ dangerouslySetInnerHTML: { __html: ICON_TIMER }
1784
+ });
1785
+ }
1501
1786
 
1502
1787
  ctx.effect(() => {
1503
1788
  const off = [
1504
- mountSidebarEntry(toggle, 'Запланированные задачи'),
1789
+ mountSidebarEntry(toggle, translate('sidebar.label')),
1505
1790
  mountCronScreen(ctx, toggle)
1506
1791
  ];
1507
1792
  return () => { for (const dispose of off) dispose(); };