@goodandready/dsh-cron 0.2.50 → 0.2.52
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 +66 -0
- package/lib/approval.js +143 -0
- package/lib/channels.js +4 -2
- package/lib/client.js +141 -90
- package/lib/cron-tool.js +24 -0
- package/lib/global-budgets.js +137 -0
- package/lib/index.js +4 -1
- package/lib/recipes-catalog.js +107 -0
- package/lib/routes.js +3 -1
- package/lib/scheduler-execution.js +132 -6
- package/lib/scheduler.js +12 -0
- package/lib/settings.js +6 -1
- package/lib/store.js +10 -1
- package/lib/task-create.js +3 -0
- package/lib/task-patch.js +7 -0
- package/lib/task-transfer.js +2 -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',
|
|
@@ -143,7 +130,17 @@ window.__ModuleLoader__.load({
|
|
|
143
130
|
'modal.close': 'Close',
|
|
144
131
|
'history.loading': 'Loading history...',
|
|
145
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',
|
|
146
142
|
'history.refresh': 'Refresh history',
|
|
143
|
+
'history.stage_delivery': '推送',
|
|
147
144
|
'history.statusSuccess': 'Success',
|
|
148
145
|
'history.statusSkipped': 'Skipped',
|
|
149
146
|
'history.statusMissed': 'Missed',
|
|
@@ -362,6 +359,14 @@ window.__ModuleLoader__.load({
|
|
|
362
359
|
'form.targetSessionResetDaily': 'Daily rotation (new session each day)',
|
|
363
360
|
'form.targetSessionResetWeekly': 'Weekly rotation (new session each week)',
|
|
364
361
|
'form.targetSessionResetHint': 'Automatically rotates the session identifier to prevent context window explosion over long periods.',
|
|
362
|
+
'form.requireApprovalLabel': 'Require human approval before execution',
|
|
363
|
+
'form.requireApprovalHint': 'When enabled, the task will not execute until an authorized user approves the run proposal (#318).',
|
|
364
|
+
'actions.approve': 'Approve',
|
|
365
|
+
'actions.reject': 'Reject',
|
|
366
|
+
'approval.waiting': 'Awaiting human approval',
|
|
367
|
+
'approval.approved': 'Approved',
|
|
368
|
+
'approval.rejected': 'Rejected',
|
|
369
|
+
'recipe.dependenciesMissing': 'Missing required integrations or credentials',
|
|
365
370
|
'form.previewSchedule': 'Preview next runs',
|
|
366
371
|
'form.previewScheduleTitle': 'Next 5 scheduled runs:',
|
|
367
372
|
},
|
|
@@ -611,12 +616,18 @@ window.__ModuleLoader__.load({
|
|
|
611
616
|
'dsh.startFailed': '启动智能体对话失败: {error}',
|
|
612
617
|
'dsh.launchError': '执行错误: {error}',
|
|
613
618
|
'confirm.delete': '确定删除该任务吗?',
|
|
619
|
+
'form.requireApprovalLabel': '执行前需人工确认审批',
|
|
620
|
+
'form.requireApprovalHint': '开启后,任务执行前将挂起并等待授权用户批准提议 (#318)。',
|
|
621
|
+
'actions.approve': '批准',
|
|
622
|
+
'actions.reject': '拒绝',
|
|
623
|
+
'approval.waiting': '等待人工审批',
|
|
624
|
+
'approval.approved': '已批准',
|
|
625
|
+
'approval.rejected': '已拒绝',
|
|
626
|
+
'recipe.dependenciesMissing': '缺少必要集成服务或凭据',
|
|
614
627
|
'list.loadErrorPrefix': '加载任务列表失败'
|
|
615
628
|
}
|
|
616
629
|
};
|
|
617
630
|
|
|
618
|
-
// Module-level translator. apply() rebinds it to the DSH locale service so
|
|
619
|
-
// the active language and live language switches are honored.
|
|
620
631
|
let translate = (key, vars) => {
|
|
621
632
|
let s = (STRINGS.en && STRINGS.en[key]) || key;
|
|
622
633
|
if (vars) {
|
|
@@ -774,6 +785,12 @@ window.__ModuleLoader__.load({
|
|
|
774
785
|
.dsh-cron-history-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
|
775
786
|
.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); }
|
|
776
787
|
.dsh-cron-history-status[data-status="success"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
|
|
788
|
+
.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); }
|
|
789
|
+
.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); }
|
|
790
|
+
.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); }
|
|
791
|
+
.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); }
|
|
792
|
+
.dsh-cron-stage-pill[data-status="partial"] { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
|
|
793
|
+
.dsh-cron-stage-pill[data-status="skipped"] { opacity: 0.6; }
|
|
777
794
|
.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); }
|
|
778
795
|
.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); }
|
|
779
796
|
.dsh-cron-history-time { color: var(--dsw-alias-label-tertiary); font-size: 12px; }
|
|
@@ -861,7 +878,6 @@ window.__ModuleLoader__.load({
|
|
|
861
878
|
if (!head) return null;
|
|
862
879
|
el = document.createElement('style');
|
|
863
880
|
el.id = STYLE_ID;
|
|
864
|
-
// Mark ownership before insertion so neighbor cleanups leave it alone (#91)
|
|
865
881
|
el.dataset.dshPlugin = 'dsh-cron';
|
|
866
882
|
el.textContent = STYLES;
|
|
867
883
|
head.appendChild(el);
|
|
@@ -871,8 +887,6 @@ window.__ModuleLoader__.load({
|
|
|
871
887
|
return el;
|
|
872
888
|
}
|
|
873
889
|
|
|
874
|
-
// Registry is shared across hot reloads so the newest instance owns exactly
|
|
875
|
-
// one watcher instead of stacking one per module evaluation.
|
|
876
890
|
const STYLE_HEAL_KEY = '__dshCronStyleHeal';
|
|
877
891
|
const STYLE_HEAL_INTERVAL_MS = 2000;
|
|
878
892
|
|
|
@@ -947,7 +961,6 @@ window.__ModuleLoader__.load({
|
|
|
947
961
|
const AGENT_FORM_TYPES = ['llm', 'skill', 'workflow'];
|
|
948
962
|
const CODE_FORM_TYPES = ['script', 'node', 'python', 'ssh', 'docker'];
|
|
949
963
|
|
|
950
|
-
// Delivery channels (#26) — must stay in sync with lib/channels.js.
|
|
951
964
|
const DELIVERY_CHANNELS = ['telegram', 'kanban', 'discord', 'slack', 'ntfy', 'bark', 'pushplus', 'tts', 'gitea'];
|
|
952
965
|
const CHANNEL_LABEL_KEYS = {
|
|
953
966
|
telegram: 'form.channelTelegram',
|
|
@@ -961,15 +974,11 @@ window.__ModuleLoader__.load({
|
|
|
961
974
|
gitea: 'form.channelGitea',
|
|
962
975
|
};
|
|
963
976
|
|
|
964
|
-
// Non-secret delivery settings shown in the settings panel. Secrets are
|
|
965
|
-
// referenced by credential name only (#51) — never typed as values.
|
|
966
977
|
const DELIVERY_SETTING_FIELDS = [
|
|
967
978
|
{ key: 'botTokenRef', labelKey: 'settings.botTokenRefLabel', placeholderKey: 'settings.botTokenRefPlaceholder', group: 'secrets' },
|
|
968
979
|
{ key: 'ntfyTokenRef', labelKey: 'settings.ntfyTokenRefLabel', group: 'secrets' },
|
|
969
980
|
{ key: 'pushplusTokenRef', labelKey: 'settings.pushplusTokenRefLabel', group: 'secrets' },
|
|
970
981
|
{ key: 'giteaTokenRef', labelKey: 'settings.giteaTokenRefLabel', group: 'secrets' },
|
|
971
|
-
// The external API is the one surface where a typed secret is the point:
|
|
972
|
-
// a CI job cannot present a credential reference (#54).
|
|
973
982
|
{ key: 'apiToken', labelKey: 'settings.apiTokenLabel', placeholderKey: 'settings.apiTokenPlaceholder', group: 'secrets' },
|
|
974
983
|
{ key: 'discordWebhookUrl', labelKey: 'settings.discordLabel', group: 'channels' },
|
|
975
984
|
{ key: 'slackWebhookUrl', labelKey: 'settings.slackLabel', group: 'channels' },
|
|
@@ -986,7 +995,6 @@ window.__ModuleLoader__.load({
|
|
|
986
995
|
{ key: 'inspectorModel', labelKey: 'settings.inspectorModelLabel', group: 'automation' },
|
|
987
996
|
];
|
|
988
997
|
const DELIVERY_SETTING_KEYS = DELIVERY_SETTING_FIELDS.map((f) => f.key);
|
|
989
|
-
// The global template plus the per-channel override map (server model).
|
|
990
998
|
const TEMPLATE_SETTING_KEYS = ['template', 'channelTemplates'];
|
|
991
999
|
|
|
992
1000
|
/** Collect every delivery key the settings endpoints may return. */
|
|
@@ -1009,8 +1017,6 @@ window.__ModuleLoader__.load({
|
|
|
1009
1017
|
return Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n');
|
|
1010
1018
|
}
|
|
1011
1019
|
|
|
1012
|
-
// Status colors: core semantic state tokens first (same set dsh-clinebot
|
|
1013
|
-
// uses), plugin fallbacks as a single point of change (#105).
|
|
1014
1020
|
function createToggle() {
|
|
1015
1021
|
let open = false;
|
|
1016
1022
|
const listeners = new Set();
|
|
@@ -1037,7 +1043,6 @@ window.__ModuleLoader__.load({
|
|
|
1037
1043
|
const getSessions = () => (ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : (ctx && ctx.sessions));
|
|
1038
1044
|
let sessions = getSessions();
|
|
1039
1045
|
|
|
1040
|
-
// 1. Try sessions.open(sessionId) immediately
|
|
1041
1046
|
if (sessions && typeof sessions.open === 'function') {
|
|
1042
1047
|
try {
|
|
1043
1048
|
sessions.open(sessionId);
|
|
@@ -1047,7 +1052,6 @@ window.__ModuleLoader__.load({
|
|
|
1047
1052
|
}
|
|
1048
1053
|
}
|
|
1049
1054
|
|
|
1050
|
-
// 2. If the session is not in manager.summaries yet, refresh from the backend
|
|
1051
1055
|
if (sessions && typeof sessions.refresh === 'function') {
|
|
1052
1056
|
try {
|
|
1053
1057
|
await sessions.refresh();
|
|
@@ -1058,7 +1062,6 @@ window.__ModuleLoader__.load({
|
|
|
1058
1062
|
}
|
|
1059
1063
|
}
|
|
1060
1064
|
|
|
1061
|
-
// 3. Async retries until the WebSocket/SSE event adds the session to the list
|
|
1062
1065
|
for (let i = 0; i < 15; i++) {
|
|
1063
1066
|
await new Promise(resolve => setTimeout(resolve, 120));
|
|
1064
1067
|
sessions = getSessions();
|
|
@@ -1070,7 +1073,6 @@ window.__ModuleLoader__.load({
|
|
|
1070
1073
|
/* retry polling sessions.open threw */
|
|
1071
1074
|
}
|
|
1072
1075
|
}
|
|
1073
|
-
// DOM fallback: click the session in the sidebar if it has rendered
|
|
1074
1076
|
if (typeof document !== 'undefined') {
|
|
1075
1077
|
const domItem = document.querySelector('[data-session-id="' + sessionId + '"], [role="treeitem"][data-id="' + sessionId + '"], [data-id="' + sessionId + '"]');
|
|
1076
1078
|
if (domItem) {
|
|
@@ -1085,6 +1087,7 @@ window.__ModuleLoader__.load({
|
|
|
1085
1087
|
|
|
1086
1088
|
function statusLabel(t, status) {
|
|
1087
1089
|
if (status === 'success') return T_KEY(t, 'history.statusSuccess');
|
|
1090
|
+
if (status === 'validation_failed') return T_KEY(t, 'history.statusValidationFailed');
|
|
1088
1091
|
if (status === 'skipped') return T_KEY(t, 'history.statusSkipped');
|
|
1089
1092
|
if (status === 'missed') return T_KEY(t, 'history.statusMissed');
|
|
1090
1093
|
if (status === 'timeout') return T_KEY(t, 'history.statusTimeout');
|
|
@@ -1178,9 +1181,6 @@ window.__ModuleLoader__.load({
|
|
|
1178
1181
|
step: 1000,
|
|
1179
1182
|
value: val(f.key),
|
|
1180
1183
|
placeholder: String(f.defaultValue !== undefined ? f.defaultValue : 15000),
|
|
1181
|
-
// #115: clearing the field must send the explicit default. Deleting the
|
|
1182
|
-
// key would leave the previously stored value in place (the settings
|
|
1183
|
-
// scope skips absent keys), so the field would silently keep 1 ms.
|
|
1184
1184
|
onChange: (e) => onChange(f.key, e.target.value === ''
|
|
1185
1185
|
? (f.defaultValue !== undefined ? f.defaultValue : 15000)
|
|
1186
1186
|
: Number(e.target.value)),
|
|
@@ -1253,8 +1253,6 @@ window.__ModuleLoader__.load({
|
|
|
1253
1253
|
const t = props.t || translate;
|
|
1254
1254
|
const [tab, setTab] = React.useState('all');
|
|
1255
1255
|
const [query, setQuery] = React.useState('');
|
|
1256
|
-
// Facet filters (#40): applied client-side over the already-loaded list so
|
|
1257
|
-
// toggling them is instant and does not touch the polling request.
|
|
1258
1256
|
const [filterType, setFilterType] = React.useState('');
|
|
1259
1257
|
const [filterModel, setFilterModel] = React.useState('');
|
|
1260
1258
|
const [filterChannel, setFilterChannel] = React.useState('');
|
|
@@ -1265,14 +1263,12 @@ window.__ModuleLoader__.load({
|
|
|
1265
1263
|
const [fetchError, setFetchError] = React.useState(null);
|
|
1266
1264
|
const [dropdownOpen, setDropdownOpen] = React.useState(false);
|
|
1267
1265
|
|
|
1268
|
-
// Export/import state (#42)
|
|
1269
1266
|
const [transferBusy, setTransferBusy] = React.useState(false);
|
|
1270
1267
|
const [importDoc, setImportDoc] = React.useState(null);
|
|
1271
1268
|
const [importSummary, setImportSummary] = React.useState(null);
|
|
1272
1269
|
const [importStrategy, setImportStrategy] = React.useState('skip');
|
|
1273
1270
|
const importFileRef = React.useRef(null);
|
|
1274
1271
|
|
|
1275
|
-
// Manual / Edit modal state
|
|
1276
1272
|
const [manualModalOpen, setManualModalOpen] = React.useState(false);
|
|
1277
1273
|
const [formId, setFormId] = React.useState(null);
|
|
1278
1274
|
const [formTitle, setFormTitle] = React.useState('');
|
|
@@ -1284,11 +1280,13 @@ window.__ModuleLoader__.load({
|
|
|
1284
1280
|
const [formFallbackModel, setFormFallbackModel] = React.useState('');
|
|
1285
1281
|
const [formSilentRule, setFormSilentRule] = React.useState('');
|
|
1286
1282
|
const [formInspectOnFailure, setFormInspectOnFailure] = React.useState(false);
|
|
1283
|
+
const [formRequireApproval, setFormRequireApproval] = React.useState(false);
|
|
1287
1284
|
const [formDelivery, setFormDelivery] = React.useState('isolated');
|
|
1288
1285
|
const [selectedTaskMeta, setSelectedTaskMeta] = React.useState(null);
|
|
1289
1286
|
const [modalTab, setModalTab] = React.useState('params');
|
|
1290
1287
|
const [taskHistory, setTaskHistory] = React.useState([]);
|
|
1291
1288
|
const [historyLoading, setHistoryLoading] = React.useState(false);
|
|
1289
|
+
const [retryingDeliveryId, setRetryingDeliveryId] = React.useState(null);
|
|
1292
1290
|
const [formNotifyTelegram, setFormNotifyTelegram] = React.useState(false);
|
|
1293
1291
|
const [formChannels, setFormChannels] = React.useState([]);
|
|
1294
1292
|
const [formTemplate, setFormTemplate] = React.useState('');
|
|
@@ -1352,6 +1350,7 @@ window.__ModuleLoader__.load({
|
|
|
1352
1350
|
fallbackModel: String(formFallbackModel || ''),
|
|
1353
1351
|
silentRule: String(formSilentRule || ''),
|
|
1354
1352
|
inspectOnFailure: Boolean(formInspectOnFailure),
|
|
1353
|
+
requireApproval: Boolean(formRequireApproval),
|
|
1355
1354
|
delivery: String(formDelivery || 'isolated'),
|
|
1356
1355
|
notifyTelegram: Boolean(formNotifyTelegram),
|
|
1357
1356
|
channels: Array.isArray(formChannels) ? [...formChannels].sort() : [],
|
|
@@ -1414,8 +1413,6 @@ window.__ModuleLoader__.load({
|
|
|
1414
1413
|
setManualModalOpen(false);
|
|
1415
1414
|
};
|
|
1416
1415
|
|
|
1417
|
-
// Live execution indicator (#35): a light 1s tick only while some task
|
|
1418
|
-
// is actually running.
|
|
1419
1416
|
const tasksRef = React.useRef([]);
|
|
1420
1417
|
const [nowTick, setNowTick] = React.useState(0);
|
|
1421
1418
|
React.useEffect(() => {
|
|
@@ -1428,7 +1425,6 @@ window.__ModuleLoader__.load({
|
|
|
1428
1425
|
return () => clearInterval(tick);
|
|
1429
1426
|
}, []);
|
|
1430
1427
|
|
|
1431
|
-
// Settings modal state
|
|
1432
1428
|
const [settingsModalOpen, setSettingsModalOpen] = React.useState(false);
|
|
1433
1429
|
const [settingsBotToken, setSettingsBotToken] = React.useState('');
|
|
1434
1430
|
const [settingsChatId, setSettingsChatId] = React.useState('');
|
|
@@ -1441,12 +1437,10 @@ window.__ModuleLoader__.load({
|
|
|
1441
1437
|
const [settingsHasCustomBotToken, setSettingsHasCustomBotToken] = React.useState(false);
|
|
1442
1438
|
const [testStatus, setTestStatus] = React.useState(null);
|
|
1443
1439
|
|
|
1444
|
-
// DSH dialog state
|
|
1445
1440
|
const [dshModalOpen, setDshModalOpen] = React.useState(false);
|
|
1446
1441
|
const [dshPrompt, setDshPrompt] = React.useState('');
|
|
1447
1442
|
const [dshLoading, setDshLoading] = React.useState(false);
|
|
1448
1443
|
|
|
1449
|
-
// Models list state
|
|
1450
1444
|
const [providers, setProviders] = React.useState([]);
|
|
1451
1445
|
const [models, setModels] = React.useState([]);
|
|
1452
1446
|
const [defaultModelInfo, setDefaultModelInfo] = React.useState(null);
|
|
@@ -1593,7 +1587,6 @@ window.__ModuleLoader__.load({
|
|
|
1593
1587
|
.then(res => {
|
|
1594
1588
|
if (isDestroyed || pollGen !== fetchGenRef.current) return null;
|
|
1595
1589
|
if (res.status === 304) {
|
|
1596
|
-
// Not modified: skip parsing JSON
|
|
1597
1590
|
return null;
|
|
1598
1591
|
}
|
|
1599
1592
|
const etag = res.headers.get('ETag');
|
|
@@ -1614,13 +1607,11 @@ window.__ModuleLoader__.load({
|
|
|
1614
1607
|
.finally(() => {
|
|
1615
1608
|
if (pollTimer) clearTimeout(pollTimer);
|
|
1616
1609
|
if (isDestroyed) return;
|
|
1617
|
-
// Adaptive interval: 30s when tab is hidden, 8s when active (#134)
|
|
1618
1610
|
const intervalMs = (typeof document !== 'undefined' && document.hidden) ? 30000 : 8000;
|
|
1619
1611
|
timerId = setTimeout(doPoll, intervalMs);
|
|
1620
1612
|
});
|
|
1621
1613
|
};
|
|
1622
1614
|
|
|
1623
|
-
// Trigger immediate fetch when visibility returns
|
|
1624
1615
|
const onVisibilityChange = () => {
|
|
1625
1616
|
if (typeof document !== 'undefined' && !document.hidden) {
|
|
1626
1617
|
if (timerId) clearTimeout(timerId);
|
|
@@ -1666,7 +1657,6 @@ window.__ModuleLoader__.load({
|
|
|
1666
1657
|
try {
|
|
1667
1658
|
data = await res.json();
|
|
1668
1659
|
} catch (jsonErr) {
|
|
1669
|
-
// Re-throw if body reading aborted/timed out (#242)
|
|
1670
1660
|
if (jsonErr && (jsonErr.name === 'AbortError' || controller?.signal.aborted)) {
|
|
1671
1661
|
throw jsonErr;
|
|
1672
1662
|
}
|
|
@@ -1781,8 +1771,6 @@ window.__ModuleLoader__.load({
|
|
|
1781
1771
|
return;
|
|
1782
1772
|
}
|
|
1783
1773
|
await fetchTasks();
|
|
1784
|
-
// A one-shot copy keeps its original schedule string, so warn instead
|
|
1785
|
-
// of letting the user resume a task pointing at a past moment.
|
|
1786
1774
|
alert(task.oneShot ? T_KEY(t, 'duplicate.oneShotHint') : T_KEY(t, 'duplicate.done'));
|
|
1787
1775
|
} catch (err) {
|
|
1788
1776
|
alert(T_KEY(t, 'duplicate.failed', { error: err.message }));
|
|
@@ -1929,6 +1917,42 @@ window.__ModuleLoader__.load({
|
|
|
1929
1917
|
}
|
|
1930
1918
|
};
|
|
1931
1919
|
|
|
1920
|
+
const handleApproveProposal = async (taskId, proposalId) => {
|
|
1921
|
+
try {
|
|
1922
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/approve', {
|
|
1923
|
+
method: 'POST',
|
|
1924
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1925
|
+
body: JSON.stringify({ proposalId, decidedBy: 'web-user' })
|
|
1926
|
+
});
|
|
1927
|
+
if (data && data.ok) {
|
|
1928
|
+
alert(T_KEY(t, 'approval.approved') || 'Approved');
|
|
1929
|
+
setTimeout(() => fetchTasks(), 1000);
|
|
1930
|
+
} else {
|
|
1931
|
+
alert('Approval failed: ' + ((data && data.error) || ''));
|
|
1932
|
+
}
|
|
1933
|
+
} catch (err) {
|
|
1934
|
+
alert('Approval request failed: ' + err.message);
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
|
|
1938
|
+
const handleRejectProposal = async (taskId, proposalId) => {
|
|
1939
|
+
try {
|
|
1940
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/reject', {
|
|
1941
|
+
method: 'POST',
|
|
1942
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1943
|
+
body: JSON.stringify({ proposalId, decidedBy: 'web-user' })
|
|
1944
|
+
});
|
|
1945
|
+
if (data && data.ok) {
|
|
1946
|
+
alert(T_KEY(t, 'approval.rejected') || 'Rejected');
|
|
1947
|
+
setTimeout(() => fetchTasks(), 1000);
|
|
1948
|
+
} else {
|
|
1949
|
+
alert('Rejection failed: ' + ((data && data.error) || ''));
|
|
1950
|
+
}
|
|
1951
|
+
} catch (err) {
|
|
1952
|
+
alert('Rejection request failed: ' + err.message);
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1932
1956
|
const handleDelete = async (id) => {
|
|
1933
1957
|
if (!confirm(T_KEY(t, 'confirm.delete'))) return;
|
|
1934
1958
|
try {
|
|
@@ -2068,20 +2092,20 @@ window.__ModuleLoader__.load({
|
|
|
2068
2092
|
setFormPythonPath('');
|
|
2069
2093
|
setFormSkillName('');
|
|
2070
2094
|
setFormWorkflowName('');
|
|
2071
|
-
// Reset before the preset branch, so a recipe keeps what it sets.
|
|
2072
2095
|
setFormFallbackModel('');
|
|
2073
2096
|
setFormSilentRule('');
|
|
2074
2097
|
setFormInspectOnFailure(false);
|
|
2098
|
+
setFormRequireApproval(false);
|
|
2075
2099
|
if (preset) {
|
|
2076
2100
|
setFormTitle(preset.title || '');
|
|
2077
2101
|
setFormSchedule(preset.schedule || '');
|
|
2078
2102
|
setFormPrompt(preset.prompt || '');
|
|
2079
|
-
// #48: a recipe preset also carries its runtime and delivery shape.
|
|
2080
2103
|
if (preset.type) setFormType(preset.type);
|
|
2081
2104
|
if (Array.isArray(preset.channels) && preset.channels.length) setFormChannels(preset.channels.filter((c) => DELIVERY_CHANNELS.includes(c)));
|
|
2082
2105
|
if (preset.silentRule !== undefined) setFormSilentRule(preset.silentRule || '');
|
|
2083
2106
|
if (preset.fallbackModel !== undefined) setFormFallbackModel(preset.fallbackModel || '');
|
|
2084
2107
|
if (preset.inspectOnFailure !== undefined) setFormInspectOnFailure(Boolean(preset.inspectOnFailure));
|
|
2108
|
+
if (preset.requireApproval !== undefined) setFormRequireApproval(Boolean(preset.requireApproval));
|
|
2085
2109
|
setFormOnlyOnFailure(Boolean(preset.onlyOnFailure));
|
|
2086
2110
|
setFormProvider(defaultModelInfo?.provider || '');
|
|
2087
2111
|
setFormModel(defaultModelInfo?.model || '');
|
|
@@ -2104,6 +2128,7 @@ window.__ModuleLoader__.load({
|
|
|
2104
2128
|
fallbackModel: String((preset && preset.fallbackModel) || '').trim(),
|
|
2105
2129
|
silentRule: String((preset && preset.silentRule) || '').trim(),
|
|
2106
2130
|
inspectOnFailure: Boolean(preset && preset.inspectOnFailure),
|
|
2131
|
+
requireApproval: Boolean(preset && preset.requireApproval),
|
|
2107
2132
|
delivery: 'isolated',
|
|
2108
2133
|
notifyTelegram: false,
|
|
2109
2134
|
channels: Array.isArray(preset && preset.channels) ? [...preset.channels].filter((c) => DELIVERY_CHANNELS.includes(c)).sort() : [],
|
|
@@ -2150,6 +2175,26 @@ window.__ModuleLoader__.load({
|
|
|
2150
2175
|
setManualModalOpen(true);
|
|
2151
2176
|
};
|
|
2152
2177
|
|
|
2178
|
+
const handleRetryDelivery = async (taskId, runId) => {
|
|
2179
|
+
setRetryingDeliveryId(runId);
|
|
2180
|
+
try {
|
|
2181
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/retry-delivery', {
|
|
2182
|
+
method: 'POST',
|
|
2183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2184
|
+
body: JSON.stringify({ runId }),
|
|
2185
|
+
});
|
|
2186
|
+
if (data && data.ok) {
|
|
2187
|
+
await loadTaskHistory(taskId);
|
|
2188
|
+
} else {
|
|
2189
|
+
alert(data?.error || 'Failed to retry delivery');
|
|
2190
|
+
}
|
|
2191
|
+
} catch (e) {
|
|
2192
|
+
alert(e.message || 'Error retrying delivery');
|
|
2193
|
+
} finally {
|
|
2194
|
+
setRetryingDeliveryId(null);
|
|
2195
|
+
}
|
|
2196
|
+
};
|
|
2197
|
+
|
|
2153
2198
|
const loadTaskHistory = async (taskId) => {
|
|
2154
2199
|
setHistoryLoading(true);
|
|
2155
2200
|
try {
|
|
@@ -2178,6 +2223,7 @@ window.__ModuleLoader__.load({
|
|
|
2178
2223
|
setFormFallbackModel(task.fallbackModel || '');
|
|
2179
2224
|
setFormSilentRule(task.silentRule || '');
|
|
2180
2225
|
setFormInspectOnFailure(Boolean(task.inspectOnFailure));
|
|
2226
|
+
setFormRequireApproval(Boolean(task.requireApproval));
|
|
2181
2227
|
setFormDelivery(task.delivery || 'isolated');
|
|
2182
2228
|
setFormNotifyTelegram(Boolean(task.notifyTelegram));
|
|
2183
2229
|
setFormChannels(Array.isArray(task.channels) ? task.channels.filter((c) => DELIVERY_CHANNELS.includes(c)) : []);
|
|
@@ -2275,6 +2321,7 @@ window.__ModuleLoader__.load({
|
|
|
2275
2321
|
concurrencyGroup: formConcurrencyGroup.trim() || 'default',
|
|
2276
2322
|
selfHealingCommand: formSelfHealingCommand.trim() || undefined,
|
|
2277
2323
|
autoDiagnose: Boolean(formAutoDiagnose),
|
|
2324
|
+
requireApproval: Boolean(formRequireApproval),
|
|
2278
2325
|
env: parseEnvText(formEnv),
|
|
2279
2326
|
cwd: formCwd.trim(),
|
|
2280
2327
|
workspaceId: formWorkspaceId.trim(),
|
|
@@ -2293,8 +2340,6 @@ window.__ModuleLoader__.load({
|
|
|
2293
2340
|
skillName: formSkillName.trim(),
|
|
2294
2341
|
workflowName: formWorkflowName.trim(),
|
|
2295
2342
|
};
|
|
2296
|
-
// Cross-origin hardening: every code-executing type needs the explicit
|
|
2297
|
-
// confirm header, which forged cross-site posts cannot attach (#86)
|
|
2298
2343
|
const headers = { 'Content-Type': 'application/json' };
|
|
2299
2344
|
const hasCodeExec = CODE_FORM_TYPES.includes(formType) ||
|
|
2300
2345
|
Boolean(payload.command && payload.command.trim()) ||
|
|
@@ -2391,7 +2436,6 @@ window.__ModuleLoader__.load({
|
|
|
2391
2436
|
React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, T_KEY(t, 'actions.setupManually'))
|
|
2392
2437
|
)
|
|
2393
2438
|
),
|
|
2394
|
-
// #42: export/import as a small pair of icon buttons.
|
|
2395
2439
|
React.createElement('button', {
|
|
2396
2440
|
type: 'button',
|
|
2397
2441
|
className: 'dsh-cron-icon-btn',
|
|
@@ -2460,7 +2504,6 @@ window.__ModuleLoader__.load({
|
|
|
2460
2504
|
: T_KEY(t, 'tabs.completed'))
|
|
2461
2505
|
)
|
|
2462
2506
|
),
|
|
2463
|
-
// #40: facet filters over the loaded list.
|
|
2464
2507
|
React.createElement('div', { className: 'dsh-cron-filters' },
|
|
2465
2508
|
React.createElement('button', {
|
|
2466
2509
|
type: 'button',
|
|
@@ -2692,7 +2735,6 @@ window.__ModuleLoader__.load({
|
|
|
2692
2735
|
)
|
|
2693
2736
|
),
|
|
2694
2737
|
|
|
2695
|
-
// 1. Manual creation / Edit modal
|
|
2696
2738
|
manualModalOpen && React.createElement('div', {
|
|
2697
2739
|
className: 'dsh-cron-modal-overlay',
|
|
2698
2740
|
onClick: (e) => {
|
|
@@ -2753,8 +2795,39 @@ window.__ModuleLoader__.load({
|
|
|
2753
2795
|
React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary, #888)', fontSize: '11.5px' } }, (run.durationMs || 0) + ' ms')
|
|
2754
2796
|
)
|
|
2755
2797
|
),
|
|
2756
|
-
|
|
2757
|
-
|
|
2798
|
+
run.stoppedReason && React.createElement('div', {
|
|
2799
|
+
className: 'dsh-cron-hint',
|
|
2800
|
+
style: { display: 'flex', gap: '6px', alignItems: 'center', marginTop: '6px' }
|
|
2801
|
+
},
|
|
2802
|
+
React.createElement('span', { className: 'dsh-cron-badge-ref' }, T_KEY(t, 'history.stoppedReasonTitle')),
|
|
2803
|
+
React.createElement('span', null, run.stoppedReason)
|
|
2804
|
+
),
|
|
2805
|
+
Array.isArray(run.stages) && run.stages.length > 0 && React.createElement('div', {
|
|
2806
|
+
className: 'dsh-cron-stages-flow',
|
|
2807
|
+
style: { display: 'flex', gap: '6px', flexWrap: 'wrap', marginTop: '6px' }
|
|
2808
|
+
},
|
|
2809
|
+
run.stages.map((st) => React.createElement('span', {
|
|
2810
|
+
key: st.name,
|
|
2811
|
+
className: 'dsh-cron-stage-pill',
|
|
2812
|
+
'data-status': st.status || 'unknown',
|
|
2813
|
+
title: st.error ? (st.name + ': ' + st.error) : (st.name + (st.durationMs !== undefined ? ' (' + st.durationMs + 'ms)' : ''))
|
|
2814
|
+
}, (T_KEY(t, 'history.stage_' + st.name) || st.name) + (st.durationMs !== undefined ? ' ' + st.durationMs + 'ms' : '')))
|
|
2815
|
+
),
|
|
2816
|
+
run.delivery && React.createElement('div', {
|
|
2817
|
+
style: { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '6px' }
|
|
2818
|
+
},
|
|
2819
|
+
React.createElement('span', {
|
|
2820
|
+
className: 'dsh-cron-badge-ref',
|
|
2821
|
+
'data-status': run.delivery.status
|
|
2822
|
+
}, 'Delivery: ' + run.delivery.status),
|
|
2823
|
+
(run.delivery.status === 'failed' || run.delivery.status === 'partial') && React.createElement('button', {
|
|
2824
|
+
type: 'button',
|
|
2825
|
+
className: 'dsh-cron-btn-secondary',
|
|
2826
|
+
style: { padding: '2px 8px', fontSize: '11px' },
|
|
2827
|
+
disabled: retryingDeliveryId === run.id,
|
|
2828
|
+
onClick: () => handleRetryDelivery(formId, run.id)
|
|
2829
|
+
}, retryingDeliveryId === run.id ? T_KEY(t, 'history.retryingDelivery') : T_KEY(t, 'history.retryDelivery'))
|
|
2830
|
+
),
|
|
2758
2831
|
run.diagnosis && React.createElement('div', {
|
|
2759
2832
|
className: 'dsh-cron-status-banner',
|
|
2760
2833
|
'data-type': 'info',
|
|
@@ -2774,7 +2847,6 @@ window.__ModuleLoader__.load({
|
|
|
2774
2847
|
}, T_KEY(t, 'history.applySuggestion'))
|
|
2775
2848
|
)
|
|
2776
2849
|
),
|
|
2777
|
-
// #44: a run suppressed by its silent rule says why.
|
|
2778
2850
|
run.silentSkip && React.createElement('div', {
|
|
2779
2851
|
className: 'dsh-cron-hint',
|
|
2780
2852
|
style: { display: 'flex', gap: '6px', alignItems: 'center' }
|
|
@@ -3018,7 +3090,6 @@ window.__ModuleLoader__.load({
|
|
|
3018
3090
|
models.map((m) => React.createElement('option', { key: m.id, value: m.id }, m.name || m.id))
|
|
3019
3091
|
)
|
|
3020
3092
|
),
|
|
3021
|
-
// #45: one retry on a stronger model when the cheap one fails.
|
|
3022
3093
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
3023
3094
|
React.createElement('label', null, T_KEY(t, 'form.fallbackModelLabel')),
|
|
3024
3095
|
React.createElement('input', {
|
|
@@ -3029,7 +3100,6 @@ window.__ModuleLoader__.load({
|
|
|
3029
3100
|
}),
|
|
3030
3101
|
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.fallbackModelHint'))
|
|
3031
3102
|
),
|
|
3032
|
-
// #43: diagnose failures with a model.
|
|
3033
3103
|
React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginTop: '4px' } },
|
|
3034
3104
|
React.createElement('input', {
|
|
3035
3105
|
type: 'checkbox',
|
|
@@ -3038,7 +3108,16 @@ window.__ModuleLoader__.load({
|
|
|
3038
3108
|
}),
|
|
3039
3109
|
React.createElement('span', null, T_KEY(t, 'form.inspectOnFailureLabel'))
|
|
3040
3110
|
),
|
|
3041
|
-
formInspectOnFailure && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.inspectOnFailureHint'))
|
|
3111
|
+
formInspectOnFailure && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.inspectOnFailureHint')),
|
|
3112
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginTop: '8px' } },
|
|
3113
|
+
React.createElement('input', {
|
|
3114
|
+
type: 'checkbox',
|
|
3115
|
+
checked: formRequireApproval,
|
|
3116
|
+
onChange: (e) => setFormRequireApproval(e.target.checked)
|
|
3117
|
+
}),
|
|
3118
|
+
React.createElement('span', null, T_KEY(t, 'form.requireApprovalLabel'))
|
|
3119
|
+
),
|
|
3120
|
+
formRequireApproval && React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.requireApprovalHint'))
|
|
3042
3121
|
),
|
|
3043
3122
|
formType === 'http' && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
3044
3123
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
@@ -3261,7 +3340,6 @@ window.__ModuleLoader__.load({
|
|
|
3261
3340
|
}),
|
|
3262
3341
|
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.templateHint'))
|
|
3263
3342
|
),
|
|
3264
|
-
// #44: output-producing runtimes may stay silent by rule.
|
|
3265
3343
|
!AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-group', style: { marginTop: '12px', marginBottom: 0 } },
|
|
3266
3344
|
React.createElement('label', null, T_KEY(t, 'form.silentRuleLabel')),
|
|
3267
3345
|
React.createElement('input', {
|
|
@@ -3274,7 +3352,6 @@ window.__ModuleLoader__.load({
|
|
|
3274
3352
|
)
|
|
3275
3353
|
),
|
|
3276
3354
|
|
|
3277
|
-
// Reliability, Heartbeat & Self-Healing section
|
|
3278
3355
|
React.createElement('div', {
|
|
3279
3356
|
style: {
|
|
3280
3357
|
marginTop: '16px',
|
|
@@ -3409,7 +3486,6 @@ window.__ModuleLoader__.load({
|
|
|
3409
3486
|
),
|
|
3410
3487
|
|
|
3411
3488
|
|
|
3412
|
-
// Dry Run simulation modal
|
|
3413
3489
|
dryRunResult && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setDryRunResult(null) },
|
|
3414
3490
|
React.createElement('div', {
|
|
3415
3491
|
className: 'dsh-cron-modal',
|
|
@@ -3456,7 +3532,6 @@ window.__ModuleLoader__.load({
|
|
|
3456
3532
|
)
|
|
3457
3533
|
),
|
|
3458
3534
|
|
|
3459
|
-
// Archive runs modal
|
|
3460
3535
|
archiveModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setArchiveModalOpen(false) },
|
|
3461
3536
|
React.createElement('div', {
|
|
3462
3537
|
className: 'dsh-cron-modal',
|
|
@@ -3524,7 +3599,6 @@ window.__ModuleLoader__.load({
|
|
|
3524
3599
|
)
|
|
3525
3600
|
),
|
|
3526
3601
|
|
|
3527
|
-
// 2. Settings modal
|
|
3528
3602
|
settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
|
|
3529
3603
|
React.createElement('div', { className: 'dsh-cron-modal dsh-cron-modal-scroll', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
|
|
3530
3604
|
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
|
|
@@ -3611,7 +3685,6 @@ window.__ModuleLoader__.load({
|
|
|
3611
3685
|
)
|
|
3612
3686
|
),
|
|
3613
3687
|
|
|
3614
|
-
// 3. Import modal (#42) — summary first, then an explicit strategy.
|
|
3615
3688
|
importDoc && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !transferBusy && setImportDoc(null) },
|
|
3616
3689
|
React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
|
|
3617
3690
|
React.createElement('div', { className: 'dsh-cron-modal-title' }, T_KEY(t, 'transfer.importTitleModal')),
|
|
@@ -3654,7 +3727,6 @@ window.__ModuleLoader__.load({
|
|
|
3654
3727
|
)
|
|
3655
3728
|
),
|
|
3656
3729
|
|
|
3657
|
-
// 4. "Create with DSH" modal
|
|
3658
3730
|
dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
|
|
3659
3731
|
React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
|
|
3660
3732
|
React.createElement('div', { className: 'dsh-cron-modal-title' }, T_KEY(t, 'dsh.modalTitle')),
|
|
@@ -3717,7 +3789,6 @@ window.__ModuleLoader__.load({
|
|
|
3717
3789
|
const [hasDefault, setHasDefault] = React.useState(false);
|
|
3718
3790
|
const [saved, setSaved] = React.useState(false);
|
|
3719
3791
|
const [testInfo, setTestInfo] = React.useState(null);
|
|
3720
|
-
// Collapsed by default, head is the toggle (canonical card contract, #100)
|
|
3721
3792
|
const [cardOpen, setCardOpen] = React.useState(false);
|
|
3722
3793
|
const [updateState, setUpdateState] = React.useState({
|
|
3723
3794
|
checking: false,
|
|
@@ -3777,8 +3848,6 @@ window.__ModuleLoader__.load({
|
|
|
3777
3848
|
}
|
|
3778
3849
|
};
|
|
3779
3850
|
|
|
3780
|
-
// Settings snapshot status (#102): never render phantom inputs before
|
|
3781
|
-
// the snapshot arrives; offer a retry when it is unavailable.
|
|
3782
3851
|
const [loadState, setLoadState] = React.useState('loading');
|
|
3783
3852
|
const [reloadTick, setReloadTick] = React.useState(0);
|
|
3784
3853
|
|
|
@@ -3887,8 +3956,6 @@ window.__ModuleLoader__.load({
|
|
|
3887
3956
|
);
|
|
3888
3957
|
}
|
|
3889
3958
|
|
|
3890
|
-
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
3891
|
-
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
3892
3959
|
if (props && props.view === 'summary') {
|
|
3893
3960
|
return React.createElement('span', { className: 'dsh-cron-card-desc' }, T_KEY(t, 'settings.cardDesc'));
|
|
3894
3961
|
}
|
|
@@ -4116,8 +4183,6 @@ window.__ModuleLoader__.load({
|
|
|
4116
4183
|
const row = document.createElement('button');
|
|
4117
4184
|
row.type = 'button';
|
|
4118
4185
|
row.className = 'dsh-cron-sidebar-job';
|
|
4119
|
-
// A distinct attribute: the panel uses data-task-id, and the highlight
|
|
4120
|
-
// lookup must never pick a sidebar row instead of a task card.
|
|
4121
4186
|
row.setAttribute('data-sidebar-task-id', task.id);
|
|
4122
4187
|
row.setAttribute('aria-label', task.title || task.id);
|
|
4123
4188
|
|
|
@@ -4218,7 +4283,6 @@ window.__ModuleLoader__.load({
|
|
|
4218
4283
|
entry.insertAdjacentElement('afterend', buildSidebarJobsRoot(state, label));
|
|
4219
4284
|
} else {
|
|
4220
4285
|
entry.insertAdjacentElement('afterend', state.root);
|
|
4221
|
-
// Re-query the refs: the previous nodes may belong to a detached tree.
|
|
4222
4286
|
state.root = document.querySelector('[data-dsh-part="sidebar-jobs"]') || state.root;
|
|
4223
4287
|
state.listEl = state.root.querySelector('.dsh-cron-sidebar-jobs-list');
|
|
4224
4288
|
state.headEl = state.root.querySelector('.dsh-cron-sidebar-jobs-head');
|
|
@@ -4335,8 +4399,6 @@ window.__ModuleLoader__.load({
|
|
|
4335
4399
|
|
|
4336
4400
|
const applyActive = () => {
|
|
4337
4401
|
if (toggle.isOpen()) {
|
|
4338
|
-
// Repair a dropped stylesheet before the panel paints, so opening the
|
|
4339
|
-
// screen never shows an unstyled page (see startStyleSelfHeal).
|
|
4340
4402
|
ensureStyles();
|
|
4341
4403
|
document.documentElement.setAttribute(ACTIVE_ATTR, '');
|
|
4342
4404
|
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL }));
|
|
@@ -4396,14 +4458,12 @@ window.__ModuleLoader__.load({
|
|
|
4396
4458
|
function apply(ctx) {
|
|
4397
4459
|
const stopStyleSelfHeal = startStyleSelfHeal();
|
|
4398
4460
|
|
|
4399
|
-
// Register the English canonical strings; rebinding the translator lets
|
|
4400
|
-
// the translation plugin drive the active language at runtime (#87).
|
|
4401
4461
|
try {
|
|
4402
4462
|
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
4403
4463
|
ctx.locale.register(NS, STRINGS);
|
|
4404
4464
|
}
|
|
4405
4465
|
} catch (e) {
|
|
4406
|
-
|
|
4466
|
+
void e;
|
|
4407
4467
|
}
|
|
4408
4468
|
try {
|
|
4409
4469
|
if (ctx.locale && typeof ctx.locale.bind === 'function') {
|
|
@@ -4415,16 +4475,9 @@ window.__ModuleLoader__.load({
|
|
|
4415
4475
|
|
|
4416
4476
|
const toggle = createToggle();
|
|
4417
4477
|
|
|
4418
|
-
// The settings card lives ONLY in the plugins settings tab (#102): no
|
|
4419
|
-
// top-level section fallback — the flat sidebar list is a shared core
|
|
4420
|
-
// resource, and the card slot is proven on current builds (#85).
|
|
4421
4478
|
function SettingsCardWithBoundary(props) {
|
|
4422
4479
|
return React.createElement(ErrorBoundary, null, CronSettingsCard(props));
|
|
4423
4480
|
}
|
|
4424
|
-
// List seat (plugins.item): the seat the Plugins page renders as the plugin's own
|
|
4425
|
-
// page with its configuration. The label is a static string on purpose — it is
|
|
4426
|
-
// resolved while the page renders, and a locale lookup there would take the whole
|
|
4427
|
-
// client batch down with it.
|
|
4428
4481
|
const itemOk = registerIntoMount(ctx, 'plugins.item', {
|
|
4429
4482
|
name: 'plugins.item',
|
|
4430
4483
|
id: ROW_ID,
|
|
@@ -4436,8 +4489,6 @@ window.__ModuleLoader__.load({
|
|
|
4436
4489
|
if (!itemOk) {
|
|
4437
4490
|
console.warn('[dsh-cron] plugins.item slot unavailable — plugin page card not registered');
|
|
4438
4491
|
}
|
|
4439
|
-
// Row seat and the legacy settings.plugin.item seat stay as fallbacks
|
|
4440
|
-
// (#102: still no top-level section fallback).
|
|
4441
4492
|
const rowOk = registerIntoMount(ctx, 'plugins.row.config', {
|
|
4442
4493
|
name: 'plugins.row.config',
|
|
4443
4494
|
key: ROW_CONFIG_KEY,
|