@goodandready/dsh-cron 0.2.1 → 0.2.2

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
@@ -180,20 +180,57 @@ window.__ModuleLoader__.load({
180
180
  } catch (e) {}
181
181
  const chevronNode = () => (CoreChevron ? React.createElement(CoreChevron) : React.createElement('span', { style: { display: 'flex', alignItems: 'center' }, dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } }));
182
182
 
183
- // Status colors: core semantic tokens first, plugin fallbacks as a single
184
- // point of change (Issue #91 — no scattered hardcoded hex).
183
+ // Own React error boundary (#106): the core's SlotErrorBoundary renders an
184
+ // empty div on crash, so a slot failure would silently vanish. Ours keeps
185
+ // the failure visible with a retry.
186
+ function createErrorBoundary() {
187
+ if (!React || typeof React.Component !== 'function') {
188
+ return function NoopBoundary(props) { return (props && props.children) || null; };
189
+ }
190
+ return class ErrorBoundary extends React.Component {
191
+ constructor(props) {
192
+ super(props);
193
+ this.state = { hasError: false, error: null };
194
+ }
195
+ static getDerivedStateFromError(error) {
196
+ return { hasError: true, error };
197
+ }
198
+ componentDidCatch(error, errorInfo) {
199
+ console.error('[dsh-cron] React error:', error, errorInfo);
200
+ }
201
+ render() {
202
+ if (this.state.hasError) {
203
+ return React.createElement('div', { className: 'dsh-cron-status-banner', 'data-type': 'error', style: { margin: '12px 0' } },
204
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ Cron UI error:'),
205
+ React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String((this.state.error && this.state.error.message) || this.state.error)),
206
+ React.createElement('button', {
207
+ type: 'button',
208
+ className: 'dsh-cron-btn-secondary',
209
+ style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
210
+ onClick: () => this.setState({ hasError: false, error: null })
211
+ }, 'Retry')
212
+ );
213
+ }
214
+ return (this.props && this.props.children) || null;
215
+ }
216
+ };
217
+ }
218
+ const ErrorBoundary = createErrorBoundary();
219
+
220
+ // Status colors: core semantic state tokens first (same set dsh-clinebot
221
+ // uses), plugin fallbacks as a single point of change (#105).
185
222
  const STYLES = `
186
223
  :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));
224
+ --dsh-cron-success: var(--dsw-alias-state-success-primary, #10b981);
225
+ --dsh-cron-success-bg: rgba(16, 185, 129, 0.1);
226
+ --dsh-cron-danger: var(--dsw-alias-state-error-primary, #ef4444);
227
+ --dsh-cron-danger-bg: rgba(239, 68, 68, 0.1);
228
+ --dsh-cron-info: var(--dsw-alias-state-brand-primary, #60a5fa);
229
+ --dsh-cron-info-bg: rgba(96, 165, 250, 0.12);
230
+ --dsh-cron-accent: var(--dsw-alias-state-info-primary, #c084fc);
231
+ --dsh-cron-accent-bg: rgba(168, 85, 247, 0.12);
232
+ --dsh-cron-warning: var(--dsw-alias-state-warning-primary, #eab308);
233
+ --dsh-cron-warning-bg: rgba(234, 179, 8, 0.12);
197
234
  }
198
235
  [data-pane=conversation], [class*=centerCol] {
199
236
  position: relative !important;
@@ -218,43 +255,46 @@ window.__ModuleLoader__.load({
218
255
  .dsh-cron-entry-label { text-overflow: ellipsis; overflow: hidden; }
219
256
  [data-dsh-frame][data-sidebar-collapsed] .dsh-cron-entry-label { display: none; }
220
257
 
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; }
222
- .dsh-cron-container { max-width: 860px; width: 100%; margin: 0 auto; }
258
+ .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: 24px 48px 32px; z-index: 100; box-sizing: border-box; }
259
+ .dsh-cron-container { max-width: 960px; width: 100%; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; padding: 8px 0 32px; box-sizing: border-box; }
223
260
 
224
- .dsh-cron-top-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
261
+ .dsh-cron-top-bar { display: flex; align-items: center; gap: 12px; }
225
262
  .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; }
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)); }
263
+ .dsh-cron-back-btn:hover { color: var(--dsw-alias-label-primary, #ededed); background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06))); }
227
264
 
228
- .dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }
229
- .dsh-cron-title { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 6px; }
230
- .dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary, #9ca3af); }
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; }
265
+ .dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding-bottom: 16px; border-bottom: 1px solid var(--dsw-alias-border-l2, #383838); }
266
+ .dsh-cron-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 6px; }
267
+ .dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary, #9ca3af); line-height: 1.5; }
268
+ .dsh-cron-create-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-2, #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
269
  .dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, var(--dsw-alias-interactive-bg-hover, #333)); }
233
270
  .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; }
234
271
  .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
272
  .dsh-cron-dropdown-item:hover { background: var(--dsw-alias-interactive-bg-hover, #2a2a2a); }
236
- .dsh-cron-search-bar { position: relative; margin-bottom: 20px; }
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; }
273
+ .dsh-cron-search-bar { position: relative; margin-bottom: 0; }
274
+ .dsh-cron-search-input { width: 100%; height: 36px; background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 0 12px 0 38px; color: var(--dsw-alias-label-primary, inherit); font-size: 13px; outline: none; box-sizing: border-box; }
275
+ .dsh-cron-search-input:focus { outline: none; border-color: var(--dsh-cron-info); }
238
276
  .dsh-cron-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--dsw-alias-label-tertiary, #666); }
239
- .dsh-cron-tabs { display: flex; gap: 8px; margin-bottom: 24px; }
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; }
242
- .dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 40px; }
277
+ .dsh-cron-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
278
+ .dsh-cron-tab { appearance: none; background: transparent; border: 1px solid var(--dsw-alias-border-l2, #383838); padding: 4px 12px; border-radius: 999px; color: var(--dsw-alias-label-secondary, #9ca3af); font-size: 12px; font-weight: 500; cursor: pointer; }
279
+ .dsh-cron-tab[data-active="true"] { background: var(--dsw-alias-bg-layer-4, #2a2a2a); color: var(--dsw-alias-label-primary, #fff); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #383838)); }
280
+ .dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 0; }
243
281
  .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; }
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; }
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; }
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; }
282
+ .dsh-cron-task-item:hover, .dsh-cron-task-item:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l1, #444)); outline: none; }
283
+ .dsh-cron-task-prompt-preview { font-size: 12.5px; color: var(--dsw-alias-label-secondary, #888); margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 520px; line-height: 1.3; }
284
+ .dsh-cron-type-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); text-transform: uppercase; font-weight: 500; margin-left: 8px; vertical-align: middle; }
285
+ .dsh-cron-task-model-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); color: var(--dsw-alias-label-secondary, #aaa); margin-left: 8px; vertical-align: middle; }
248
286
  .dsh-cron-task-actions { display: flex; gap: 8px; }
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; }
287
+ .dsh-cron-icon-btn { appearance: none; font: inherit; background: var(--dsw-alias-bg-layer-2, transparent); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 6px 12px; font-size: 12.5px; font-weight: 500; color: var(--dsw-alias-label-primary, #aaa); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s ease; }
288
+ .dsh-cron-icon-btn:hover { background: var(--dsw-alias-bg-layer-4, #2a2a2a); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #444)); }
289
+ .dsh-cron-icon-btn.dsh-cron-danger { color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger-bg); }
290
+ .dsh-cron-icon-btn.dsh-cron-danger:hover { background: var(--dsh-cron-danger-bg); border-color: var(--dsh-cron-danger); }
291
+ .dsh-cron-task-status-btn { background: none; border: 1px solid var(--dsw-alias-border-l2, #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
292
  .dsh-cron-task-status-btn[data-active="true"] { color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
253
- .dsh-cron-recs-title { font-size: 17px; font-weight: 600; margin-bottom: 14px; }
293
+ .dsh-cron-recs-title { font-size: 16px; font-weight: 600; margin-bottom: 14px; }
254
294
  .dsh-cron-recs-list { display: flex; flex-direction: column; gap: 10px; }
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; }
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; }
295
+ .dsh-cron-rec-card { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1c1c1c); border: 1px solid var(--dsw-alias-border-l2, #282828); border-radius: 12px; cursor: pointer; transition: background 0.15s; }
296
+ .dsh-cron-rec-card:hover, .dsh-cron-rec-card:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #383838)); outline: none; }
297
+ .dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.03)); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
258
298
  .dsh-cron-rec-content { flex: 1; min-width: 0; }
259
299
  .dsh-cron-rec-title { font-size: 14.5px; font-weight: 500; margin-bottom: 4px; }
260
300
  .dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary, #9ca3af); font-weight: normal; margin-left: 8px; }
@@ -265,14 +305,17 @@ window.__ModuleLoader__.load({
265
305
  .dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
266
306
  .dsh-cron-form-group { margin-bottom: 16px; }
267
307
  .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; }
308
+ .dsh-cron-form-group input, .dsh-cron-form-group textarea, .dsh-cron-form-group select { width: 100%; background: var(--dsw-alias-bg-layer-2, #141414); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 8px 12px; color: var(--dsw-alias-label-primary, #fff); font-size: 13px; outline: none; box-sizing: border-box; }
309
+ .dsh-cron-form-group input:focus, .dsh-cron-form-group textarea:focus, .dsh-cron-form-group select:focus { outline: none; border-color: var(--dsh-cron-info); }
269
310
  .dsh-cron-form-group select { cursor: pointer; }
270
311
  .dsh-cron-form-group textarea { min-height: 80px; resize: vertical; }
271
312
  .dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
272
313
  .dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; }
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; }
314
+ .dsh-cron-btn-primary { background: var(--dsw-alias-label-primary, #ededed); color: var(--dsw-alias-bg-layer-3, #111); border: 1px solid transparent; border-radius: 8px; padding: 7px 14px; font-size: 13px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
315
+ .dsh-cron-btn-primary:hover:not(:disabled) { opacity: 0.88; }
274
316
  .dsh-cron-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
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; }
317
+ .dsh-cron-btn-secondary { appearance: none; font: inherit; background: var(--dsw-alias-bg-layer-2, transparent); color: var(--dsw-alias-label-primary, #999); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 8px; padding: 7px 14px; font-size: 13px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s ease; }
318
+ .dsh-cron-btn-secondary:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-4, #2a2a2a); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #444)); }
276
319
  .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
320
  .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
321
  .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); }
@@ -281,15 +324,15 @@ window.__ModuleLoader__.load({
281
324
  .dsh-cron-send-btn:hover:not(:disabled) { transform: translateY(-1px); }
282
325
  .dsh-cron-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
283
326
 
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; }
327
+ .dsh-cron-card { background: var(--dsw-alias-bg-layer-3, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 12px; padding: 18px 20px; margin-bottom: 0; display: flex; flex-direction: column; gap: 14px; }
328
+ .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: 0; }
286
329
  .dsh-cron-card-head-btn .dsh-cron-card-title { flex: 1; }
287
- .dsh-cron-card-title { font-size: 15px; font-weight: 600; color: var(--dsw-alias-label-primary, #fff); }
330
+ .dsh-cron-card-title { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, #fff); }
288
331
  .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
332
  .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; }
333
+ .dsh-cron-card-body { border-top: 1px solid var(--dsw-alias-border-l2, #333); margin-top: 2px; padding-top: 14px; }
291
334
  .dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
292
- .dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #888); margin-bottom: 16px; line-height: 1.4; }
335
+ .dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #888); line-height: 1.4; }
293
336
 
294
337
  .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
338
  .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; }
@@ -298,21 +341,21 @@ window.__ModuleLoader__.load({
298
341
  .dsh-cron-history-list { display: flex; flex-direction: column; gap: 10px; max-height: 420px; overflow-y: auto; padding-right: 4px; }
299
342
  .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; }
300
343
  .dsh-cron-history-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
301
- .dsh-cron-history-status { font-weight: 600; font-size: 11.5px; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; }
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); }
344
+ .dsh-cron-history-status { font-weight: 500; font-size: 11px; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); }
345
+ .dsh-cron-history-status[data-status="success"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
346
+ .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); border-color: var(--dsh-cron-danger); }
347
+ .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); border-color: var(--dsh-cron-warning); }
305
348
  .dsh-cron-history-time { color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; }
306
349
  .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
350
  .dsh-cron-history-empty { text-align: center; padding: 36px 0; color: var(--dsw-alias-label-tertiary, #666); font-size: 13.5px; }
308
351
 
309
- .dsh-cron-stats-bar { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 18px; }
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); }
352
+ .dsh-cron-stats-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 0; }
353
+ .dsh-cron-stat-card { background: var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.02)); border: 1px solid var(--dsw-alias-border-l2, #222); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; }
354
+ .dsh-cron-stat-val { font-size: 18px; font-weight: 700; color: var(--dsw-alias-label-primary, #fff); }
355
+ .dsh-cron-stat-lbl { font-size: 12px; color: var(--dsw-alias-label-secondary, #777); margin-top: 0; }
356
+ .dsh-cron-oneshot-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-accent); background: var(--dsh-cron-accent-bg); color: var(--dsh-cron-accent); font-weight: 500; }
357
+ .dsh-cron-cost-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-success); background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); font-weight: 500; }
358
+ .dsh-cron-tokens-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-info); background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); }
316
359
  .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
360
  .dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: var(--dsw-alias-label-primary, #ededed); }
318
361
  .dsh-cron-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); margin-top: 4px; }
@@ -945,7 +988,7 @@ window.__ModuleLoader__.load({
945
988
  }, React.createElement('span', svgProps(ICON_EDIT))),
946
989
  React.createElement('button', {
947
990
  type: 'button',
948
- className: 'dsh-cron-icon-btn',
991
+ className: 'dsh-cron-icon-btn dsh-cron-danger',
949
992
  title: T_KEY(t, 'actions.delete'),
950
993
  'aria-label': T_KEY(t, 'actions.delete'),
951
994
  onClick: () => handleDelete(task.id)
@@ -1666,7 +1709,7 @@ window.__ModuleLoader__.load({
1666
1709
  column.appendChild(container);
1667
1710
  if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
1668
1711
  root = ReactDOM.createRoot(container);
1669
- root.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate }));
1712
+ root.render(React.createElement(ErrorBoundary, null, React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate })));
1670
1713
  } else if (ReactDOM && typeof ReactDOM.render === 'function') {
1671
1714
  ReactDOM.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate }), container);
1672
1715
  }
@@ -1752,12 +1795,15 @@ window.__ModuleLoader__.load({
1752
1795
  // The settings card lives ONLY in the plugins settings tab (#102): no
1753
1796
  // top-level section fallback — the flat sidebar list is a shared core
1754
1797
  // resource, and the card slot is proven on current builds (#85).
1798
+ function SettingsCardWithBoundary(props) {
1799
+ return React.createElement(ErrorBoundary, null, CronSettingsCard(props));
1800
+ }
1755
1801
  const cardOk = registerIntoMount(ctx, 'settings.plugin.item', {
1756
1802
  name: 'settings.plugin.item',
1757
1803
  key: NS,
1758
1804
  locale: NS,
1759
1805
  inject: () => ({ ctx, toggle }),
1760
- }, CronSettingsCard);
1806
+ }, SettingsCardWithBoundary);
1761
1807
  if (!cardOk) {
1762
1808
  console.warn('[dsh-cron] settings.plugin.item slot unavailable — settings card not registered');
1763
1809
  }
package/lib/http-utils.js CHANGED
@@ -8,12 +8,15 @@ export const MAX_BODY_BYTES = 1024 * 1024;
8
8
  export const SCRIPT_CONFIRM_HEADER = 'x-dsh-cron-confirm';
9
9
 
10
10
  /**
11
- * Parse a JSON request body with a hard size cap.
11
+ * Parse a JSON request body with a hard byte size cap.
12
+ * Chunks are accumulated as Buffers so multi-byte UTF-8 characters split
13
+ * across chunk boundaries decode correctly (#106, #97 follow-up).
12
14
  * Rejects with err.statusCode = 413 when the body exceeds the limit.
13
15
  */
14
16
  export function parseJsonBody(req, limit = MAX_BODY_BYTES) {
15
17
  return new Promise((resolve, reject) => {
16
- let data = '';
18
+ const chunks = [];
19
+ let bytes = 0;
17
20
  let settled = false;
18
21
  const fail = (err) => {
19
22
  if (settled) return;
@@ -22,17 +25,20 @@ export function parseJsonBody(req, limit = MAX_BODY_BYTES) {
22
25
  };
23
26
  req.on('data', (chunk) => {
24
27
  if (settled) return;
25
- data += chunk;
26
- if (data.length > limit) {
28
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
29
+ bytes += buf.length;
30
+ if (bytes > limit) {
27
31
  fail(Object.assign(new Error('Request body exceeds the allowed size'), { statusCode: 413 }));
28
32
  req.destroy();
33
+ return;
29
34
  }
35
+ chunks.push(buf);
30
36
  });
31
37
  req.on('end', () => {
32
38
  if (settled) return;
33
39
  settled = true;
34
40
  try {
35
- resolve(data ? JSON.parse(data) : {});
41
+ resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {});
36
42
  } catch (err) {
37
43
  reject(err);
38
44
  }
package/lib/index.js CHANGED
@@ -71,7 +71,7 @@ async function readBody(req, res) {
71
71
  * snapshot stays authoritative (#102). Returns null when the settings
72
72
  * service is unavailable (headless profiles fall back to store-only writes).
73
73
  */
74
- async function applySettingsToScope(settingsScope, body) {
74
+ export async function applySettingsToScope(settingsScope, body) {
75
75
  if (!settingsScope || typeof settingsScope.set !== 'function') return null;
76
76
  const errors = [];
77
77
  for (const key of ['botToken', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl']) {
@@ -85,6 +85,18 @@ async function applySettingsToScope(settingsScope, body) {
85
85
  return errors;
86
86
  }
87
87
 
88
+ /**
89
+ * Drop values that must never re-enter the store from the client: a masked
90
+ * secret echoed back by the settings UI would overwrite the real token.
91
+ */
92
+ export function sanitizeSettingsPayload(body) {
93
+ const payload = Object.assign({}, body);
94
+ if (typeof payload.botToken === 'string' && (payload.botToken.includes('••') || payload.botToken.includes('****'))) {
95
+ delete payload.botToken;
96
+ }
97
+ return payload;
98
+ }
99
+
88
100
  /**
89
101
  * Shared implementation for the /dsh-cron/tasks... and /dsh-cron/action...
90
102
  * route families — the #66 alias is kept alive by the same code path instead
@@ -92,9 +104,9 @@ async function applySettingsToScope(settingsScope, body) {
92
104
  *
93
105
  * IMPORTANT: prefix routes must be registered WITHOUT a trailing slash —
94
106
  * with one, the core degrades matching to exact-path equality and every
95
- * deeper path unrouted (#96).
107
+ * deeper path unrouted (#96). Exported for unit tests.
96
108
  */
97
- function createCronApiHandler(store, scheduler, collection) {
109
+ export function createCronApiHandler(store, scheduler, collection) {
98
110
  return async function handleCronApi(req, res) {
99
111
  try {
100
112
  const url = new URL(req.url, 'http://127.0.0.1');
@@ -174,6 +186,10 @@ function createCronApiHandler(store, scheduler, collection) {
174
186
  }
175
187
 
176
188
  if (req.method === 'POST' && action === 'run') {
189
+ if (!store.get(id)) {
190
+ sendJson(res, 404, { ok: false, error: 'Task not found' });
191
+ return;
192
+ }
177
193
  await scheduler.triggerManualRun(id);
178
194
  sendJson(res, 200, { ok: true, task: store.get(id) });
179
195
  return;
@@ -259,9 +275,9 @@ function createCronApiHandler(store, scheduler, collection) {
259
275
 
260
276
  /**
261
277
  * Single implementation behind the cron_create_task / cron_schedule_task
262
- * alias pair (#70, #92).
278
+ * alias pair (#70, #92). Exported for unit tests.
263
279
  */
264
- function executeCreateTask(store, scheduler, args) {
280
+ export function executeCreateTask(store, scheduler, args) {
265
281
  const taskType = args.type === 'script' ? 'script' : 'llm';
266
282
  const parsed = parseScheduleExpression(args.schedule);
267
283
  const task = store.set({
@@ -339,6 +355,9 @@ export function apply(ctx, config) {
339
355
  }
340
356
  });
341
357
  }
358
+ sctx.effect(() => () => {
359
+ cronSettingsScope = null;
360
+ });
342
361
  } catch (e) {
343
362
  console.warn('[dsh-cron] settings registration warning:', e.message);
344
363
  }
@@ -432,10 +451,7 @@ export function apply(ctx, config) {
432
451
  if (error) return;
433
452
  // Masked secrets echoed back by the client must never overwrite the
434
453
  // stored token (store.saveSettings guards this too, #86-era).
435
- const payload = Object.assign({}, body);
436
- if (typeof payload.botToken === 'string' && (payload.botToken.includes('••') || payload.botToken.includes('****'))) {
437
- delete payload.botToken;
438
- }
454
+ const payload = sanitizeSettingsPayload(body);
439
455
  const scopeErrors = await applySettingsToScope(cronSettingsScope, payload);
440
456
  if (scopeErrors === null) {
441
457
  store.saveSettings(payload);
package/lib/scheduler.js CHANGED
@@ -434,40 +434,7 @@ export class TaskScheduler {
434
434
  }
435
435
  }
436
436
 
437
- removeTask(taskId) {
438
- if (this.jobs.has(taskId)) {
439
- this.jobs.get(taskId).stop();
440
- this.jobs.delete(taskId);
441
- }
442
- if (this.timers.has(taskId)) {
443
- clearTimeout(this.timers.get(taskId));
444
- this.timers.delete(taskId);
445
- }
446
- const running = this.running.get(taskId);
447
- if (running) {
448
- try {
449
- running.controller.abort(new Error('Task deleted'));
450
- } catch {}
451
- this.running.delete(taskId);
452
- }
453
- return this.store.delete(taskId);
454
- }
455
-
456
437
  triggerManualRun(taskId) {
457
438
  return this.runTask(taskId);
458
439
  }
459
-
460
- getNextRun(schedule) {
461
- try {
462
- const parsed = parseScheduleExpression(schedule);
463
- if (parsed.isOneShot) {
464
- return parsed.targetTimestamp;
465
- }
466
- const tempJob = new Cron(parsed.cronPattern);
467
- const next = tempJob.nextRun();
468
- return next ? next.getTime() : null;
469
- } catch {
470
- return null;
471
- }
472
- }
473
440
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",