@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/api.js +32 -2
- package/lib/channels.js +4 -2
- package/lib/client.js +246 -107
- package/lib/cron-tool.js +21 -1
- package/lib/index.js +4 -1
- package/lib/scheduler-execution.js +99 -5
- package/lib/scheduler.js +12 -0
- package/lib/settings.js +1 -1
- package/lib/store.js +10 -1
- package/lib/task-create.js +4 -2
- package/lib/task-patch.js +9 -1
- package/lib/task-transfer.js +1 -0
- package/lib/telegram.js +1 -1
- package/lib/validators.js +192 -0
- package/package.json +1 -1
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
|
-
|
|
1342
|
-
|
|
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) {
|
|
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
|
-
|
|
2566
|
-
|
|
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
|
-
|
|
2622
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|