@goodandready/dsh-cron 0.2.49 → 0.2.51

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
@@ -6,20 +6,14 @@ window.__ModuleLoader__.load({
6
6
  const React = window.React || require('react');
7
7
  const ReactDOM = window.ReactDOM || require('react-dom/client');
8
8
 
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
9
  const PLUGIN_ID = '@goodandready/dsh-cron';
13
10
  const NS = 'dsh-cron';
14
- // Plugins page row seat (DSH 0.1.6-alpha.2): key = '<package name>#<row id>'.
15
11
  const ROW_ID = 'dsh-cron';
16
12
  const ROW_CONFIG_KEY = PLUGIN_ID + '#' + ROW_ID;
17
13
  const PANEL = 'cron';
18
14
  const ACTIVATE_EVENT = 'dsh-panel-activate';
19
15
  const ACTIVE_ATTR = 'data-dsh-cron-active';
20
16
  const ENTRY_ATTR = 'data-dsh-cron-entry';
21
- // Set by the sidebar job list (#34) so the panel can highlight the task the
22
- // user clicked; cleared once the panel has scrolled to it.
23
17
  let pendingTaskHighlight = null;
24
18
  const VIEW_ATTR = 'data-dsh-cron-view';
25
19
  const COLUMN_SELECTOR = '[data-pane=conversation], [class*=centerCol]';
@@ -44,10 +38,6 @@ window.__ModuleLoader__.load({
44
38
  const ICON_UPLOAD = '<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>';
45
39
  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>';
46
40
 
47
- // Locale dictionary: English is the canonical source language (Issue #87).
48
- // Russian and other languages are provided at runtime by the translation
49
- // plugin; no per-language duplicates are stored here.
50
- // Core chevron primitive when the build ships it; own SVG fallback otherwise.
51
41
  let CoreChevron = null;
52
42
  try {
53
43
  const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
@@ -57,9 +47,6 @@ window.__ModuleLoader__.load({
57
47
  }
58
48
  const chevronNode = () => (CoreChevron ? React.createElement(CoreChevron) : React.createElement('span', { style: { display: 'flex', alignItems: 'center' }, dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } }));
59
49
 
60
- // Own React error boundary (#106): the core's SlotErrorBoundary renders an
61
- // empty div on crash, so a slot failure would silently vanish. Ours keeps
62
- // the failure visible with a retry.
63
50
  const STRINGS = {
64
51
  en: {
65
52
  'sidebar.label': 'Scheduled tasks',
@@ -137,12 +124,31 @@ window.__ModuleLoader__.load({
137
124
  'modal.params': 'Parameters',
138
125
  'modal.history': 'Run history ({count})',
139
126
  'modal.cancel': 'Cancel',
127
+ 'modal.confirmDiscard': 'Discard unsaved changes?',
140
128
  'modal.save': 'Save changes',
141
129
  'modal.create': 'Create task',
142
130
  'modal.close': 'Close',
143
131
  'history.loading': 'Loading history...',
144
132
  'history.empty': 'No runs recorded yet',
133
+ 'history.stoppedReasonTitle': 'Stopped reason:',
134
+ 'history.retryDelivery': 'Retry delivery',
135
+ 'history.retryingDelivery': 'Retrying...',
136
+ 'history.statusValidationFailed': 'validation failed',
137
+ 'history.stage_queue': 'Queue',
138
+ 'history.stage_preflight': 'Preflight',
139
+ 'history.stage_execution': 'Execution',
140
+ 'history.stage_validation': 'Validation',
141
+ 'history.stage_delivery': 'Delivery',
145
142
  'history.refresh': 'Refresh history',
143
+ 'history.stoppedReasonTitle': '停止原因:',
144
+ 'history.retryDelivery': '重试推送',
145
+ 'history.retryingDelivery': '重试中...',
146
+ 'history.statusValidationFailed': '校验未通过',
147
+ 'history.stage_queue': '排队',
148
+ 'history.stage_preflight': '前置检查',
149
+ 'history.stage_execution': '执行',
150
+ 'history.stage_validation': '校验',
151
+ 'history.stage_delivery': '推送',
146
152
  'history.statusSuccess': 'Success',
147
153
  'history.statusSkipped': 'Skipped',
148
154
  'history.statusMissed': 'Missed',
@@ -492,6 +498,7 @@ window.__ModuleLoader__.load({
492
498
  'modal.params': '配置参数',
493
499
  'modal.history': '运行历史 ({count})',
494
500
  'modal.cancel': '取消',
501
+ 'modal.confirmDiscard': '放弃未保存的内容?',
495
502
  'modal.save': '保存修改',
496
503
  'modal.create': '创建任务',
497
504
  'history.loading': '正在加载历史记录...',
@@ -613,8 +620,6 @@ window.__ModuleLoader__.load({
613
620
  }
614
621
  };
615
622
 
616
- // Module-level translator. apply() rebinds it to the DSH locale service so
617
- // the active language and live language switches are honored.
618
623
  let translate = (key, vars) => {
619
624
  let s = (STRINGS.en && STRINGS.en[key]) || key;
620
625
  if (vars) {
@@ -772,6 +777,12 @@ window.__ModuleLoader__.load({
772
777
  .dsh-cron-history-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
773
778
  .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); }
774
779
  .dsh-cron-history-status[data-status="success"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
780
+ .dsh-cron-history-status[data-status="validation_failed"] { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger); }
781
+ .dsh-cron-stage-pill { font-size: 10.5px; padding: 1px 7px; border-radius: 4px; border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); }
782
+ .dsh-cron-stage-pill[data-status="success"], .dsh-cron-stage-pill[data-status="delivered"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
783
+ .dsh-cron-stage-pill[data-status="failed"], .dsh-cron-stage-pill[data-status="error"] { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger); }
784
+ .dsh-cron-stage-pill[data-status="partial"] { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
785
+ .dsh-cron-stage-pill[data-status="skipped"] { opacity: 0.6; }
775
786
  .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); }
776
787
  .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); }
777
788
  .dsh-cron-history-time { color: var(--dsw-alias-label-tertiary); font-size: 12px; }
@@ -859,7 +870,6 @@ window.__ModuleLoader__.load({
859
870
  if (!head) return null;
860
871
  el = document.createElement('style');
861
872
  el.id = STYLE_ID;
862
- // Mark ownership before insertion so neighbor cleanups leave it alone (#91)
863
873
  el.dataset.dshPlugin = 'dsh-cron';
864
874
  el.textContent = STYLES;
865
875
  head.appendChild(el);
@@ -869,8 +879,6 @@ window.__ModuleLoader__.load({
869
879
  return el;
870
880
  }
871
881
 
872
- // Registry is shared across hot reloads so the newest instance owns exactly
873
- // one watcher instead of stacking one per module evaluation.
874
882
  const STYLE_HEAL_KEY = '__dshCronStyleHeal';
875
883
  const STYLE_HEAL_INTERVAL_MS = 2000;
876
884
 
@@ -945,7 +953,6 @@ window.__ModuleLoader__.load({
945
953
  const AGENT_FORM_TYPES = ['llm', 'skill', 'workflow'];
946
954
  const CODE_FORM_TYPES = ['script', 'node', 'python', 'ssh', 'docker'];
947
955
 
948
- // Delivery channels (#26) — must stay in sync with lib/channels.js.
949
956
  const DELIVERY_CHANNELS = ['telegram', 'kanban', 'discord', 'slack', 'ntfy', 'bark', 'pushplus', 'tts', 'gitea'];
950
957
  const CHANNEL_LABEL_KEYS = {
951
958
  telegram: 'form.channelTelegram',
@@ -959,15 +966,11 @@ window.__ModuleLoader__.load({
959
966
  gitea: 'form.channelGitea',
960
967
  };
961
968
 
962
- // Non-secret delivery settings shown in the settings panel. Secrets are
963
- // referenced by credential name only (#51) — never typed as values.
964
969
  const DELIVERY_SETTING_FIELDS = [
965
970
  { key: 'botTokenRef', labelKey: 'settings.botTokenRefLabel', placeholderKey: 'settings.botTokenRefPlaceholder', group: 'secrets' },
966
971
  { key: 'ntfyTokenRef', labelKey: 'settings.ntfyTokenRefLabel', group: 'secrets' },
967
972
  { key: 'pushplusTokenRef', labelKey: 'settings.pushplusTokenRefLabel', group: 'secrets' },
968
973
  { key: 'giteaTokenRef', labelKey: 'settings.giteaTokenRefLabel', group: 'secrets' },
969
- // The external API is the one surface where a typed secret is the point:
970
- // a CI job cannot present a credential reference (#54).
971
974
  { key: 'apiToken', labelKey: 'settings.apiTokenLabel', placeholderKey: 'settings.apiTokenPlaceholder', group: 'secrets' },
972
975
  { key: 'discordWebhookUrl', labelKey: 'settings.discordLabel', group: 'channels' },
973
976
  { key: 'slackWebhookUrl', labelKey: 'settings.slackLabel', group: 'channels' },
@@ -984,7 +987,6 @@ window.__ModuleLoader__.load({
984
987
  { key: 'inspectorModel', labelKey: 'settings.inspectorModelLabel', group: 'automation' },
985
988
  ];
986
989
  const DELIVERY_SETTING_KEYS = DELIVERY_SETTING_FIELDS.map((f) => f.key);
987
- // The global template plus the per-channel override map (server model).
988
990
  const TEMPLATE_SETTING_KEYS = ['template', 'channelTemplates'];
989
991
 
990
992
  /** Collect every delivery key the settings endpoints may return. */
@@ -1007,8 +1009,6 @@ window.__ModuleLoader__.load({
1007
1009
  return Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n');
1008
1010
  }
1009
1011
 
1010
- // Status colors: core semantic state tokens first (same set dsh-clinebot
1011
- // uses), plugin fallbacks as a single point of change (#105).
1012
1012
  function createToggle() {
1013
1013
  let open = false;
1014
1014
  const listeners = new Set();
@@ -1035,7 +1035,6 @@ window.__ModuleLoader__.load({
1035
1035
  const getSessions = () => (ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : (ctx && ctx.sessions));
1036
1036
  let sessions = getSessions();
1037
1037
 
1038
- // 1. Try sessions.open(sessionId) immediately
1039
1038
  if (sessions && typeof sessions.open === 'function') {
1040
1039
  try {
1041
1040
  sessions.open(sessionId);
@@ -1045,7 +1044,6 @@ window.__ModuleLoader__.load({
1045
1044
  }
1046
1045
  }
1047
1046
 
1048
- // 2. If the session is not in manager.summaries yet, refresh from the backend
1049
1047
  if (sessions && typeof sessions.refresh === 'function') {
1050
1048
  try {
1051
1049
  await sessions.refresh();
@@ -1056,7 +1054,6 @@ window.__ModuleLoader__.load({
1056
1054
  }
1057
1055
  }
1058
1056
 
1059
- // 3. Async retries until the WebSocket/SSE event adds the session to the list
1060
1057
  for (let i = 0; i < 15; i++) {
1061
1058
  await new Promise(resolve => setTimeout(resolve, 120));
1062
1059
  sessions = getSessions();
@@ -1068,7 +1065,6 @@ window.__ModuleLoader__.load({
1068
1065
  /* retry polling sessions.open threw */
1069
1066
  }
1070
1067
  }
1071
- // DOM fallback: click the session in the sidebar if it has rendered
1072
1068
  if (typeof document !== 'undefined') {
1073
1069
  const domItem = document.querySelector('[data-session-id="' + sessionId + '"], [role="treeitem"][data-id="' + sessionId + '"], [data-id="' + sessionId + '"]');
1074
1070
  if (domItem) {
@@ -1083,6 +1079,7 @@ window.__ModuleLoader__.load({
1083
1079
 
1084
1080
  function statusLabel(t, status) {
1085
1081
  if (status === 'success') return T_KEY(t, 'history.statusSuccess');
1082
+ if (status === 'validation_failed') return T_KEY(t, 'history.statusValidationFailed');
1086
1083
  if (status === 'skipped') return T_KEY(t, 'history.statusSkipped');
1087
1084
  if (status === 'missed') return T_KEY(t, 'history.statusMissed');
1088
1085
  if (status === 'timeout') return T_KEY(t, 'history.statusTimeout');
@@ -1176,9 +1173,6 @@ window.__ModuleLoader__.load({
1176
1173
  step: 1000,
1177
1174
  value: val(f.key),
1178
1175
  placeholder: String(f.defaultValue !== undefined ? f.defaultValue : 15000),
1179
- // #115: clearing the field must send the explicit default. Deleting the
1180
- // key would leave the previously stored value in place (the settings
1181
- // scope skips absent keys), so the field would silently keep 1 ms.
1182
1176
  onChange: (e) => onChange(f.key, e.target.value === ''
1183
1177
  ? (f.defaultValue !== undefined ? f.defaultValue : 15000)
1184
1178
  : Number(e.target.value)),
@@ -1251,8 +1245,6 @@ window.__ModuleLoader__.load({
1251
1245
  const t = props.t || translate;
1252
1246
  const [tab, setTab] = React.useState('all');
1253
1247
  const [query, setQuery] = React.useState('');
1254
- // Facet filters (#40): applied client-side over the already-loaded list so
1255
- // toggling them is instant and does not touch the polling request.
1256
1248
  const [filterType, setFilterType] = React.useState('');
1257
1249
  const [filterModel, setFilterModel] = React.useState('');
1258
1250
  const [filterChannel, setFilterChannel] = React.useState('');
@@ -1263,14 +1255,12 @@ window.__ModuleLoader__.load({
1263
1255
  const [fetchError, setFetchError] = React.useState(null);
1264
1256
  const [dropdownOpen, setDropdownOpen] = React.useState(false);
1265
1257
 
1266
- // Export/import state (#42)
1267
1258
  const [transferBusy, setTransferBusy] = React.useState(false);
1268
1259
  const [importDoc, setImportDoc] = React.useState(null);
1269
1260
  const [importSummary, setImportSummary] = React.useState(null);
1270
1261
  const [importStrategy, setImportStrategy] = React.useState('skip');
1271
1262
  const importFileRef = React.useRef(null);
1272
1263
 
1273
- // Manual / Edit modal state
1274
1264
  const [manualModalOpen, setManualModalOpen] = React.useState(false);
1275
1265
  const [formId, setFormId] = React.useState(null);
1276
1266
  const [formTitle, setFormTitle] = React.useState('');
@@ -1287,6 +1277,7 @@ window.__ModuleLoader__.load({
1287
1277
  const [modalTab, setModalTab] = React.useState('params');
1288
1278
  const [taskHistory, setTaskHistory] = React.useState([]);
1289
1279
  const [historyLoading, setHistoryLoading] = React.useState(false);
1280
+ const [retryingDeliveryId, setRetryingDeliveryId] = React.useState(null);
1290
1281
  const [formNotifyTelegram, setFormNotifyTelegram] = React.useState(false);
1291
1282
  const [formChannels, setFormChannels] = React.useState([]);
1292
1283
  const [formTemplate, setFormTemplate] = React.useState('');
@@ -1338,8 +1329,80 @@ window.__ModuleLoader__.load({
1338
1329
  const [formWorkflowName, setFormWorkflowName] = React.useState('');
1339
1330
  const [stats, setStats] = React.useState(null);
1340
1331
 
1341
- // Live execution indicator (#35): a light 1s tick only while some task
1342
- // is actually running.
1332
+ const initialFormSnapshotRef = React.useRef(null);
1333
+
1334
+ const getFormSnapshot = () => JSON.stringify({
1335
+ title: String(formTitle || '').trim(),
1336
+ schedule: String(formSchedule || '').trim(),
1337
+ type: String(formType || 'llm'),
1338
+ prompt: String(formPrompt || '').trim(),
1339
+ provider: String(formProvider || ''),
1340
+ model: String(formModel || ''),
1341
+ fallbackModel: String(formFallbackModel || ''),
1342
+ silentRule: String(formSilentRule || ''),
1343
+ inspectOnFailure: Boolean(formInspectOnFailure),
1344
+ delivery: String(formDelivery || 'isolated'),
1345
+ notifyTelegram: Boolean(formNotifyTelegram),
1346
+ channels: Array.isArray(formChannels) ? [...formChannels].sort() : [],
1347
+ template: String(formTemplate || '').trim(),
1348
+ onlyOnFailure: Boolean(formOnlyOnFailure),
1349
+ timeoutSeconds: Number(formTimeoutSeconds) || 1800,
1350
+ overlapPolicy: String(formOverlapPolicy || 'skip'),
1351
+ kanbanMode: String(formKanbanMode || 'none'),
1352
+ onSuccess: String(formOnSuccess || '').trim(),
1353
+ onFailure: String(formOnFailure || '').trim(),
1354
+ heartbeatIntervalSeconds: Number(formHeartbeatIntervalSeconds) || 0,
1355
+ gracePeriodSeconds: Number(formGracePeriodSeconds) || 300,
1356
+ preflightType: String(formPreflightType || 'none'),
1357
+ preflightTarget: String(formPreflightTarget || '').trim(),
1358
+ priority: Number(formPriority) || 5,
1359
+ concurrencyGroup: String(formConcurrencyGroup || 'default').trim(),
1360
+ selfHealingCommand: String(formSelfHealingCommand || '').trim(),
1361
+ autoDiagnose: Boolean(formAutoDiagnose),
1362
+ timezone: String(formTimezone || '').trim(),
1363
+ misfirePolicy: String(formMisfirePolicy || 'skip'),
1364
+ maxRetries: Number(formMaxRetries) || 0,
1365
+ permissionPreset: String(formPermissionPreset || 'default'),
1366
+ agentPreset: String(formAgentPreset || '').trim(),
1367
+ targetSessionId: String(formTargetSessionId || '').trim(),
1368
+ targetSessionReset: String(formTargetSessionReset || 'never'),
1369
+ env: String(formEnv || '').trim(),
1370
+ cwd: String(formCwd || '').trim(),
1371
+ workspaceId: String(formWorkspaceId || '').trim(),
1372
+ worktree: Boolean(formWorktree),
1373
+ keepWorktree: Boolean(formKeepWorktree),
1374
+ httpMethod: String(formHttpMethod || 'GET'),
1375
+ httpUrl: String(formHttpUrl || '').trim(),
1376
+ httpHttpHeaders: String(formHttpHeaders || '').trim(),
1377
+ httpBody: String(formHttpBody || '').trim(),
1378
+ sshProfileId: String(formSshProfileId || '').trim(),
1379
+ sshTarget: String(formSshTarget || '').trim(),
1380
+ sshKeyPath: String(formSshKeyPath || '').trim(),
1381
+ dockerImage: String(formDockerImage || '').trim(),
1382
+ nodePath: String(formNodePath || '').trim(),
1383
+ pythonPath: String(formPythonPath || '').trim(),
1384
+ skillName: String(formSkillName || '').trim(),
1385
+ workflowName: String(formWorkflowName || '').trim(),
1386
+ });
1387
+
1388
+ const isFormDirty = () => {
1389
+ if (!manualModalOpen) return false;
1390
+ if (initialFormSnapshotRef.current !== null) {
1391
+ return getFormSnapshot() !== initialFormSnapshotRef.current;
1392
+ }
1393
+ return Boolean(formTitle.trim() || formPrompt.trim());
1394
+ };
1395
+
1396
+ const closeManualModalWithConfirm = () => {
1397
+ if (isFormDirty()) {
1398
+ if (!confirm(T_KEY(t, 'modal.confirmDiscard') || 'Discard unsaved changes?')) {
1399
+ return;
1400
+ }
1401
+ }
1402
+ initialFormSnapshotRef.current = null;
1403
+ setManualModalOpen(false);
1404
+ };
1405
+
1343
1406
  const tasksRef = React.useRef([]);
1344
1407
  const [nowTick, setNowTick] = React.useState(0);
1345
1408
  React.useEffect(() => {
@@ -1352,7 +1415,6 @@ window.__ModuleLoader__.load({
1352
1415
  return () => clearInterval(tick);
1353
1416
  }, []);
1354
1417
 
1355
- // Settings modal state
1356
1418
  const [settingsModalOpen, setSettingsModalOpen] = React.useState(false);
1357
1419
  const [settingsBotToken, setSettingsBotToken] = React.useState('');
1358
1420
  const [settingsChatId, setSettingsChatId] = React.useState('');
@@ -1365,12 +1427,10 @@ window.__ModuleLoader__.load({
1365
1427
  const [settingsHasCustomBotToken, setSettingsHasCustomBotToken] = React.useState(false);
1366
1428
  const [testStatus, setTestStatus] = React.useState(null);
1367
1429
 
1368
- // DSH dialog state
1369
1430
  const [dshModalOpen, setDshModalOpen] = React.useState(false);
1370
1431
  const [dshPrompt, setDshPrompt] = React.useState('');
1371
1432
  const [dshLoading, setDshLoading] = React.useState(false);
1372
1433
 
1373
- // Models list state
1374
1434
  const [providers, setProviders] = React.useState([]);
1375
1435
  const [models, setModels] = React.useState([]);
1376
1436
  const [defaultModelInfo, setDefaultModelInfo] = React.useState(null);
@@ -1379,7 +1439,7 @@ window.__ModuleLoader__.load({
1379
1439
  const onKey = (e) => {
1380
1440
  if (e.key === 'Escape') {
1381
1441
  if (dropdownOpen) { setDropdownOpen(false); return; }
1382
- if (manualModalOpen) { setManualModalOpen(false); return; }
1442
+ if (manualModalOpen) { closeManualModalWithConfirm(); return; }
1383
1443
  if (settingsModalOpen) { setSettingsModalOpen(false); return; }
1384
1444
  if (dshModalOpen && !dshLoading) { setDshModalOpen(false); return; }
1385
1445
  if (onClose) onClose();
@@ -1517,7 +1577,6 @@ window.__ModuleLoader__.load({
1517
1577
  .then(res => {
1518
1578
  if (isDestroyed || pollGen !== fetchGenRef.current) return null;
1519
1579
  if (res.status === 304) {
1520
- // Not modified: skip parsing JSON
1521
1580
  return null;
1522
1581
  }
1523
1582
  const etag = res.headers.get('ETag');
@@ -1538,13 +1597,11 @@ window.__ModuleLoader__.load({
1538
1597
  .finally(() => {
1539
1598
  if (pollTimer) clearTimeout(pollTimer);
1540
1599
  if (isDestroyed) return;
1541
- // Adaptive interval: 30s when tab is hidden, 8s when active (#134)
1542
1600
  const intervalMs = (typeof document !== 'undefined' && document.hidden) ? 30000 : 8000;
1543
1601
  timerId = setTimeout(doPoll, intervalMs);
1544
1602
  });
1545
1603
  };
1546
1604
 
1547
- // Trigger immediate fetch when visibility returns
1548
1605
  const onVisibilityChange = () => {
1549
1606
  if (typeof document !== 'undefined' && !document.hidden) {
1550
1607
  if (timerId) clearTimeout(timerId);
@@ -1590,7 +1647,6 @@ window.__ModuleLoader__.load({
1590
1647
  try {
1591
1648
  data = await res.json();
1592
1649
  } catch (jsonErr) {
1593
- // Re-throw if body reading aborted/timed out (#242)
1594
1650
  if (jsonErr && (jsonErr.name === 'AbortError' || controller?.signal.aborted)) {
1595
1651
  throw jsonErr;
1596
1652
  }
@@ -1705,8 +1761,6 @@ window.__ModuleLoader__.load({
1705
1761
  return;
1706
1762
  }
1707
1763
  await fetchTasks();
1708
- // A one-shot copy keeps its original schedule string, so warn instead
1709
- // of letting the user resume a task pointing at a past moment.
1710
1764
  alert(task.oneShot ? T_KEY(t, 'duplicate.oneShotHint') : T_KEY(t, 'duplicate.done'));
1711
1765
  } catch (err) {
1712
1766
  alert(T_KEY(t, 'duplicate.failed', { error: err.message }));
@@ -1992,7 +2046,6 @@ window.__ModuleLoader__.load({
1992
2046
  setFormPythonPath('');
1993
2047
  setFormSkillName('');
1994
2048
  setFormWorkflowName('');
1995
- // Reset before the preset branch, so a recipe keeps what it sets.
1996
2049
  setFormFallbackModel('');
1997
2050
  setFormSilentRule('');
1998
2051
  setFormInspectOnFailure(false);
@@ -2000,7 +2053,6 @@ window.__ModuleLoader__.load({
2000
2053
  setFormTitle(preset.title || '');
2001
2054
  setFormSchedule(preset.schedule || '');
2002
2055
  setFormPrompt(preset.prompt || '');
2003
- // #48: a recipe preset also carries its runtime and delivery shape.
2004
2056
  if (preset.type) setFormType(preset.type);
2005
2057
  if (Array.isArray(preset.channels) && preset.channels.length) setFormChannels(preset.channels.filter((c) => DELIVERY_CHANNELS.includes(c)));
2006
2058
  if (preset.silentRule !== undefined) setFormSilentRule(preset.silentRule || '');
@@ -2018,9 +2070,82 @@ window.__ModuleLoader__.load({
2018
2070
  setFormModel(defaultModelInfo?.model || '');
2019
2071
  if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
2020
2072
  }
2073
+ initialFormSnapshotRef.current = JSON.stringify({
2074
+ title: String((preset && preset.title) || '').trim(),
2075
+ schedule: String((preset && preset.schedule) || (preset ? '' : '0 9 * * 1-5')).trim(),
2076
+ type: String((preset && preset.type) || 'llm'),
2077
+ prompt: String((preset && preset.prompt) || '').trim(),
2078
+ provider: String(defaultModelInfo?.provider || ''),
2079
+ model: String(defaultModelInfo?.model || ''),
2080
+ fallbackModel: String((preset && preset.fallbackModel) || '').trim(),
2081
+ silentRule: String((preset && preset.silentRule) || '').trim(),
2082
+ inspectOnFailure: Boolean(preset && preset.inspectOnFailure),
2083
+ delivery: 'isolated',
2084
+ notifyTelegram: false,
2085
+ channels: Array.isArray(preset && preset.channels) ? [...preset.channels].filter((c) => DELIVERY_CHANNELS.includes(c)).sort() : [],
2086
+ template: '',
2087
+ onlyOnFailure: Boolean(preset && preset.onlyOnFailure),
2088
+ timeoutSeconds: 1800,
2089
+ overlapPolicy: 'skip',
2090
+ kanbanMode: 'none',
2091
+ onSuccess: '',
2092
+ onFailure: '',
2093
+ heartbeatIntervalSeconds: 0,
2094
+ gracePeriodSeconds: 300,
2095
+ preflightType: 'none',
2096
+ preflightTarget: '',
2097
+ priority: 5,
2098
+ concurrencyGroup: 'default',
2099
+ selfHealingCommand: '',
2100
+ autoDiagnose: false,
2101
+ timezone: '',
2102
+ misfirePolicy: 'skip',
2103
+ maxRetries: 0,
2104
+ permissionPreset: 'default',
2105
+ agentPreset: '',
2106
+ targetSessionId: '',
2107
+ targetSessionReset: 'never',
2108
+ env: '',
2109
+ cwd: '',
2110
+ workspaceId: '',
2111
+ worktree: false,
2112
+ keepWorktree: false,
2113
+ httpMethod: 'GET',
2114
+ httpUrl: '',
2115
+ httpHttpHeaders: '',
2116
+ httpBody: '',
2117
+ sshProfileId: '',
2118
+ sshTarget: '',
2119
+ sshKeyPath: '',
2120
+ dockerImage: '',
2121
+ nodePath: '',
2122
+ pythonPath: '',
2123
+ skillName: '',
2124
+ workflowName: '',
2125
+ });
2021
2126
  setManualModalOpen(true);
2022
2127
  };
2023
2128
 
2129
+ const handleRetryDelivery = async (taskId, runId) => {
2130
+ setRetryingDeliveryId(runId);
2131
+ try {
2132
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/retry-delivery', {
2133
+ method: 'POST',
2134
+ headers: { 'Content-Type': 'application/json' },
2135
+ body: JSON.stringify({ runId }),
2136
+ });
2137
+ if (data && data.ok) {
2138
+ await loadTaskHistory(taskId);
2139
+ } else {
2140
+ alert(data?.error || 'Failed to retry delivery');
2141
+ }
2142
+ } catch (e) {
2143
+ alert(e.message || 'Error retrying delivery');
2144
+ } finally {
2145
+ setRetryingDeliveryId(null);
2146
+ }
2147
+ };
2148
+
2024
2149
  const loadTaskHistory = async (taskId) => {
2025
2150
  setHistoryLoading(true);
2026
2151
  try {
@@ -2131,7 +2256,7 @@ window.__ModuleLoader__.load({
2131
2256
  timezone: formTimezone.trim(),
2132
2257
  misfirePolicy: formMisfirePolicy,
2133
2258
  maxRetries: Number(formMaxRetries) || 0,
2134
- permissionPreset: formPermissionPreset,
2259
+ permissionPreset: isAgentType ? formPermissionPreset : 'default',
2135
2260
  agentPreset: formAgentPreset.trim() || undefined,
2136
2261
  targetSessionId: formTargetSessionId.trim() || undefined,
2137
2262
  targetSessionReset: formTargetSessionReset || 'never',
@@ -2164,8 +2289,6 @@ window.__ModuleLoader__.load({
2164
2289
  skillName: formSkillName.trim(),
2165
2290
  workflowName: formWorkflowName.trim(),
2166
2291
  };
2167
- // Cross-origin hardening: every code-executing type needs the explicit
2168
- // confirm header, which forged cross-site posts cannot attach (#86)
2169
2292
  const headers = { 'Content-Type': 'application/json' };
2170
2293
  const hasCodeExec = CODE_FORM_TYPES.includes(formType) ||
2171
2294
  Boolean(payload.command && payload.command.trim()) ||
@@ -2179,6 +2302,7 @@ window.__ModuleLoader__.load({
2179
2302
  body: JSON.stringify(payload)
2180
2303
  });
2181
2304
  if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
2305
+ initialFormSnapshotRef.current = null;
2182
2306
  setManualModalOpen(false);
2183
2307
  fetchTasks();
2184
2308
  } catch (err) { alert(err.message); }
@@ -2261,7 +2385,6 @@ window.__ModuleLoader__.load({
2261
2385
  React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, T_KEY(t, 'actions.setupManually'))
2262
2386
  )
2263
2387
  ),
2264
- // #42: export/import as a small pair of icon buttons.
2265
2388
  React.createElement('button', {
2266
2389
  type: 'button',
2267
2390
  className: 'dsh-cron-icon-btn',
@@ -2330,7 +2453,6 @@ window.__ModuleLoader__.load({
2330
2453
  : T_KEY(t, 'tabs.completed'))
2331
2454
  )
2332
2455
  ),
2333
- // #40: facet filters over the loaded list.
2334
2456
  React.createElement('div', { className: 'dsh-cron-filters' },
2335
2457
  React.createElement('button', {
2336
2458
  type: 'button',
@@ -2562,8 +2684,12 @@ window.__ModuleLoader__.load({
2562
2684
  )
2563
2685
  ),
2564
2686
 
2565
- // 1. Manual creation / Edit modal
2566
- manualModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setManualModalOpen(false) },
2687
+ manualModalOpen && React.createElement('div', {
2688
+ className: 'dsh-cron-modal-overlay',
2689
+ onClick: (e) => {
2690
+ if (e.target === e.currentTarget) closeManualModalWithConfirm();
2691
+ }
2692
+ },
2567
2693
  React.createElement('div', {
2568
2694
  className: 'dsh-cron-modal dsh-cron-modal-scroll',
2569
2695
  style: { maxWidth: '640px' },
@@ -2618,8 +2744,39 @@ window.__ModuleLoader__.load({
2618
2744
  React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary, #888)', fontSize: '11.5px' } }, (run.durationMs || 0) + ' ms')
2619
2745
  )
2620
2746
  ),
2621
- // #43: a diagnosed failure shows what went wrong and
2622
- // offers to prefill the prompt with the suggestion.
2747
+ run.stoppedReason && React.createElement('div', {
2748
+ className: 'dsh-cron-hint',
2749
+ style: { display: 'flex', gap: '6px', alignItems: 'center', marginTop: '6px' }
2750
+ },
2751
+ React.createElement('span', { className: 'dsh-cron-badge-ref' }, T_KEY(t, 'history.stoppedReasonTitle')),
2752
+ React.createElement('span', null, run.stoppedReason)
2753
+ ),
2754
+ Array.isArray(run.stages) && run.stages.length > 0 && React.createElement('div', {
2755
+ className: 'dsh-cron-stages-flow',
2756
+ style: { display: 'flex', gap: '6px', flexWrap: 'wrap', marginTop: '6px' }
2757
+ },
2758
+ run.stages.map((st) => React.createElement('span', {
2759
+ key: st.name,
2760
+ className: 'dsh-cron-stage-pill',
2761
+ 'data-status': st.status || 'unknown',
2762
+ title: st.error ? (st.name + ': ' + st.error) : (st.name + (st.durationMs !== undefined ? ' (' + st.durationMs + 'ms)' : ''))
2763
+ }, (T_KEY(t, 'history.stage_' + st.name) || st.name) + (st.durationMs !== undefined ? ' ' + st.durationMs + 'ms' : '')))
2764
+ ),
2765
+ run.delivery && React.createElement('div', {
2766
+ style: { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '6px' }
2767
+ },
2768
+ React.createElement('span', {
2769
+ className: 'dsh-cron-badge-ref',
2770
+ 'data-status': run.delivery.status
2771
+ }, 'Delivery: ' + run.delivery.status),
2772
+ (run.delivery.status === 'failed' || run.delivery.status === 'partial') && React.createElement('button', {
2773
+ type: 'button',
2774
+ className: 'dsh-cron-btn-secondary',
2775
+ style: { padding: '2px 8px', fontSize: '11px' },
2776
+ disabled: retryingDeliveryId === run.id,
2777
+ onClick: () => handleRetryDelivery(formId, run.id)
2778
+ }, retryingDeliveryId === run.id ? T_KEY(t, 'history.retryingDelivery') : T_KEY(t, 'history.retryDelivery'))
2779
+ ),
2623
2780
  run.diagnosis && React.createElement('div', {
2624
2781
  className: 'dsh-cron-status-banner',
2625
2782
  'data-type': 'info',
@@ -2639,7 +2796,6 @@ window.__ModuleLoader__.load({
2639
2796
  }, T_KEY(t, 'history.applySuggestion'))
2640
2797
  )
2641
2798
  ),
2642
- // #44: a run suppressed by its silent rule says why.
2643
2799
  run.silentSkip && React.createElement('div', {
2644
2800
  className: 'dsh-cron-hint',
2645
2801
  style: { display: 'flex', gap: '6px', alignItems: 'center' }
@@ -2666,7 +2822,7 @@ window.__ModuleLoader__.load({
2666
2822
  React.createElement('button', {
2667
2823
  type: 'button',
2668
2824
  className: 'dsh-cron-btn-primary',
2669
- onClick: () => setManualModalOpen(false)
2825
+ onClick: closeManualModalWithConfirm
2670
2826
  }, T_KEY(t, 'modal.close'))
2671
2827
  )
2672
2828
  ) :
@@ -2780,7 +2936,32 @@ window.__ModuleLoader__.load({
2780
2936
  )
2781
2937
  )
2782
2938
  ),
2783
- React.createElement('div', { className: 'dsh-cron-form-row' },
2939
+ AGENT_FORM_TYPES.includes(formType) ? (
2940
+ React.createElement('div', { className: 'dsh-cron-form-row' },
2941
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2942
+ React.createElement('label', null, T_KEY(t, 'form.retriesLabel')),
2943
+ React.createElement('input', {
2944
+ type: 'number',
2945
+ min: 0,
2946
+ value: formMaxRetries,
2947
+ placeholder: '0',
2948
+ onChange: (e) => setFormMaxRetries(e.target.value)
2949
+ })
2950
+ ),
2951
+ React.createElement('div', { className: 'dsh-cron-form-group' },
2952
+ React.createElement('label', null, T_KEY(t, 'form.permissionLabel')),
2953
+ React.createElement('select', {
2954
+ value: formPermissionPreset,
2955
+ onChange: (e) => setFormPermissionPreset(e.target.value)
2956
+ },
2957
+ React.createElement('option', { value: 'default' }, T_KEY(t, 'form.permissionDefault')),
2958
+ React.createElement('option', { value: 'read-only' }, T_KEY(t, 'form.permissionReadonly')),
2959
+ React.createElement('option', { value: 'workspace-write' }, T_KEY(t, 'form.permissionWrite')),
2960
+ React.createElement('option', { value: 'full' }, T_KEY(t, 'form.permissionFull'))
2961
+ )
2962
+ )
2963
+ )
2964
+ ) : (
2784
2965
  React.createElement('div', { className: 'dsh-cron-form-group' },
2785
2966
  React.createElement('label', null, T_KEY(t, 'form.retriesLabel')),
2786
2967
  React.createElement('input', {
@@ -2790,18 +2971,6 @@ window.__ModuleLoader__.load({
2790
2971
  placeholder: '0',
2791
2972
  onChange: (e) => setFormMaxRetries(e.target.value)
2792
2973
  })
2793
- ),
2794
- React.createElement('div', { className: 'dsh-cron-form-group' },
2795
- React.createElement('label', null, T_KEY(t, 'form.permissionLabel')),
2796
- React.createElement('select', {
2797
- value: formPermissionPreset,
2798
- onChange: (e) => setFormPermissionPreset(e.target.value)
2799
- },
2800
- React.createElement('option', { value: 'default' }, T_KEY(t, 'form.permissionDefault')),
2801
- React.createElement('option', { value: 'read-only' }, T_KEY(t, 'form.permissionReadonly')),
2802
- React.createElement('option', { value: 'workspace-write' }, T_KEY(t, 'form.permissionWrite')),
2803
- React.createElement('option', { value: 'full' }, T_KEY(t, 'form.permissionFull'))
2804
- )
2805
2974
  )
2806
2975
  ),
2807
2976
  React.createElement('div', { className: 'dsh-cron-form-group' },
@@ -2870,7 +3039,6 @@ window.__ModuleLoader__.load({
2870
3039
  models.map((m) => React.createElement('option', { key: m.id, value: m.id }, m.name || m.id))
2871
3040
  )
2872
3041
  ),
2873
- // #45: one retry on a stronger model when the cheap one fails.
2874
3042
  React.createElement('div', { className: 'dsh-cron-form-group' },
2875
3043
  React.createElement('label', null, T_KEY(t, 'form.fallbackModelLabel')),
2876
3044
  React.createElement('input', {
@@ -2881,7 +3049,6 @@ window.__ModuleLoader__.load({
2881
3049
  }),
2882
3050
  React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.fallbackModelHint'))
2883
3051
  ),
2884
- // #43: diagnose failures with a model.
2885
3052
  React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginTop: '4px' } },
2886
3053
  React.createElement('input', {
2887
3054
  type: 'checkbox',
@@ -3113,7 +3280,6 @@ window.__ModuleLoader__.load({
3113
3280
  }),
3114
3281
  React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.templateHint'))
3115
3282
  ),
3116
- // #44: output-producing runtimes may stay silent by rule.
3117
3283
  !AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-group', style: { marginTop: '12px', marginBottom: 0 } },
3118
3284
  React.createElement('label', null, T_KEY(t, 'form.silentRuleLabel')),
3119
3285
  React.createElement('input', {
@@ -3126,7 +3292,6 @@ window.__ModuleLoader__.load({
3126
3292
  )
3127
3293
  ),
3128
3294
 
3129
- // Reliability, Heartbeat & Self-Healing section
3130
3295
  React.createElement('div', {
3131
3296
  style: {
3132
3297
  marginTop: '16px',
@@ -3249,7 +3414,7 @@ window.__ModuleLoader__.load({
3249
3414
  React.createElement('button', {
3250
3415
  type: 'button',
3251
3416
  className: 'dsh-cron-btn-secondary',
3252
- onClick: () => setManualModalOpen(false)
3417
+ onClick: closeManualModalWithConfirm
3253
3418
  }, T_KEY(t, 'modal.cancel')),
3254
3419
  React.createElement('button', {
3255
3420
  type: 'submit',
@@ -3261,7 +3426,6 @@ window.__ModuleLoader__.load({
3261
3426
  ),
3262
3427
 
3263
3428
 
3264
- // Dry Run simulation modal
3265
3429
  dryRunResult && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setDryRunResult(null) },
3266
3430
  React.createElement('div', {
3267
3431
  className: 'dsh-cron-modal',
@@ -3308,7 +3472,6 @@ window.__ModuleLoader__.load({
3308
3472
  )
3309
3473
  ),
3310
3474
 
3311
- // Archive runs modal
3312
3475
  archiveModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setArchiveModalOpen(false) },
3313
3476
  React.createElement('div', {
3314
3477
  className: 'dsh-cron-modal',
@@ -3376,7 +3539,6 @@ window.__ModuleLoader__.load({
3376
3539
  )
3377
3540
  ),
3378
3541
 
3379
- // 2. Settings modal
3380
3542
  settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
3381
3543
  React.createElement('div', { className: 'dsh-cron-modal dsh-cron-modal-scroll', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
3382
3544
  React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
@@ -3463,7 +3625,6 @@ window.__ModuleLoader__.load({
3463
3625
  )
3464
3626
  ),
3465
3627
 
3466
- // 3. Import modal (#42) — summary first, then an explicit strategy.
3467
3628
  importDoc && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !transferBusy && setImportDoc(null) },
3468
3629
  React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
3469
3630
  React.createElement('div', { className: 'dsh-cron-modal-title' }, T_KEY(t, 'transfer.importTitleModal')),
@@ -3506,7 +3667,6 @@ window.__ModuleLoader__.load({
3506
3667
  )
3507
3668
  ),
3508
3669
 
3509
- // 4. "Create with DSH" modal
3510
3670
  dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
3511
3671
  React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
3512
3672
  React.createElement('div', { className: 'dsh-cron-modal-title' }, T_KEY(t, 'dsh.modalTitle')),
@@ -3569,7 +3729,6 @@ window.__ModuleLoader__.load({
3569
3729
  const [hasDefault, setHasDefault] = React.useState(false);
3570
3730
  const [saved, setSaved] = React.useState(false);
3571
3731
  const [testInfo, setTestInfo] = React.useState(null);
3572
- // Collapsed by default, head is the toggle (canonical card contract, #100)
3573
3732
  const [cardOpen, setCardOpen] = React.useState(false);
3574
3733
  const [updateState, setUpdateState] = React.useState({
3575
3734
  checking: false,
@@ -3629,8 +3788,6 @@ window.__ModuleLoader__.load({
3629
3788
  }
3630
3789
  };
3631
3790
 
3632
- // Settings snapshot status (#102): never render phantom inputs before
3633
- // the snapshot arrives; offer a retry when it is unavailable.
3634
3791
  const [loadState, setLoadState] = React.useState('loading');
3635
3792
  const [reloadTick, setReloadTick] = React.useState(0);
3636
3793
 
@@ -3739,8 +3896,6 @@ window.__ModuleLoader__.load({
3739
3896
  );
3740
3897
  }
3741
3898
 
3742
- // Row seat (plugins.row.config): the host page draws title/icon/crumb and the
3743
- // padding, so the summary is a one-liner and the page drops our card chrome.
3744
3899
  if (props && props.view === 'summary') {
3745
3900
  return React.createElement('span', { className: 'dsh-cron-card-desc' }, T_KEY(t, 'settings.cardDesc'));
3746
3901
  }
@@ -3968,8 +4123,6 @@ window.__ModuleLoader__.load({
3968
4123
  const row = document.createElement('button');
3969
4124
  row.type = 'button';
3970
4125
  row.className = 'dsh-cron-sidebar-job';
3971
- // A distinct attribute: the panel uses data-task-id, and the highlight
3972
- // lookup must never pick a sidebar row instead of a task card.
3973
4126
  row.setAttribute('data-sidebar-task-id', task.id);
3974
4127
  row.setAttribute('aria-label', task.title || task.id);
3975
4128
 
@@ -4070,7 +4223,6 @@ window.__ModuleLoader__.load({
4070
4223
  entry.insertAdjacentElement('afterend', buildSidebarJobsRoot(state, label));
4071
4224
  } else {
4072
4225
  entry.insertAdjacentElement('afterend', state.root);
4073
- // Re-query the refs: the previous nodes may belong to a detached tree.
4074
4226
  state.root = document.querySelector('[data-dsh-part="sidebar-jobs"]') || state.root;
4075
4227
  state.listEl = state.root.querySelector('.dsh-cron-sidebar-jobs-list');
4076
4228
  state.headEl = state.root.querySelector('.dsh-cron-sidebar-jobs-head');
@@ -4187,8 +4339,6 @@ window.__ModuleLoader__.load({
4187
4339
 
4188
4340
  const applyActive = () => {
4189
4341
  if (toggle.isOpen()) {
4190
- // Repair a dropped stylesheet before the panel paints, so opening the
4191
- // screen never shows an unstyled page (see startStyleSelfHeal).
4192
4342
  ensureStyles();
4193
4343
  document.documentElement.setAttribute(ACTIVE_ATTR, '');
4194
4344
  document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL }));
@@ -4248,14 +4398,12 @@ window.__ModuleLoader__.load({
4248
4398
  function apply(ctx) {
4249
4399
  const stopStyleSelfHeal = startStyleSelfHeal();
4250
4400
 
4251
- // Register the English canonical strings; rebinding the translator lets
4252
- // the translation plugin drive the active language at runtime (#87).
4253
4401
  try {
4254
4402
  if (ctx.locale && typeof ctx.locale.register === 'function') {
4255
4403
  ctx.locale.register(NS, STRINGS);
4256
4404
  }
4257
4405
  } catch (e) {
4258
- // Re-registration in the same session throws — the dictionary is live already.
4406
+ void e;
4259
4407
  }
4260
4408
  try {
4261
4409
  if (ctx.locale && typeof ctx.locale.bind === 'function') {
@@ -4267,16 +4415,9 @@ window.__ModuleLoader__.load({
4267
4415
 
4268
4416
  const toggle = createToggle();
4269
4417
 
4270
- // The settings card lives ONLY in the plugins settings tab (#102): no
4271
- // top-level section fallback — the flat sidebar list is a shared core
4272
- // resource, and the card slot is proven on current builds (#85).
4273
4418
  function SettingsCardWithBoundary(props) {
4274
4419
  return React.createElement(ErrorBoundary, null, CronSettingsCard(props));
4275
4420
  }
4276
- // List seat (plugins.item): the seat the Plugins page renders as the plugin's own
4277
- // page with its configuration. The label is a static string on purpose — it is
4278
- // resolved while the page renders, and a locale lookup there would take the whole
4279
- // client batch down with it.
4280
4421
  const itemOk = registerIntoMount(ctx, 'plugins.item', {
4281
4422
  name: 'plugins.item',
4282
4423
  id: ROW_ID,
@@ -4288,8 +4429,6 @@ window.__ModuleLoader__.load({
4288
4429
  if (!itemOk) {
4289
4430
  console.warn('[dsh-cron] plugins.item slot unavailable — plugin page card not registered');
4290
4431
  }
4291
- // Row seat and the legacy settings.plugin.item seat stay as fallbacks
4292
- // (#102: still no top-level section fallback).
4293
4432
  const rowOk = registerIntoMount(ctx, 'plugins.row.config', {
4294
4433
  name: 'plugins.row.config',
4295
4434
  key: ROW_CONFIG_KEY,