@goodandready/dsh-cron 0.2.48 → 0.2.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +92 -47
- package/lib/http-utils.js +7 -1
- package/lib/runtimes.js +0 -2
- package/lib/store.js +8 -2
- package/lib/task-create.js +9 -2
- package/lib/task-patch.js +7 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1102,6 +1102,54 @@ window.__ModuleLoader__.load({
|
|
|
1102
1102
|
return (typeof t === 'function' ? t : translate)(key, vars);
|
|
1103
1103
|
}
|
|
1104
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* Issue #307: universal fetch with AbortController timeout (default 15s).
|
|
1107
|
+
* Connects with caller signal if supplied and fails closed on timeouts.
|
|
1108
|
+
*/
|
|
1109
|
+
const fetchWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1110
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1111
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
1112
|
+
if (options && options.signal && controller) {
|
|
1113
|
+
if (options.signal.aborted) {
|
|
1114
|
+
controller.abort();
|
|
1115
|
+
} else {
|
|
1116
|
+
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
const res = await fetch(url, {
|
|
1121
|
+
...options,
|
|
1122
|
+
signal: controller ? controller.signal : (options ? options.signal : undefined),
|
|
1123
|
+
});
|
|
1124
|
+
return res;
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
if (err && (err.name === 'AbortError' || controller?.signal?.aborted)) {
|
|
1127
|
+
const timeoutErr = new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
1128
|
+
timeoutErr.isTimeout = true;
|
|
1129
|
+
throw timeoutErr;
|
|
1130
|
+
}
|
|
1131
|
+
throw err;
|
|
1132
|
+
} finally {
|
|
1133
|
+
if (timer) clearTimeout(timer);
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Issue #307: universal JSON fetch helper with timeout and aborted body protection.
|
|
1139
|
+
*/
|
|
1140
|
+
const fetchJsonWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1141
|
+
const res = await fetchWithTimeout(url, options, timeoutMs);
|
|
1142
|
+
let data = {};
|
|
1143
|
+
try {
|
|
1144
|
+
data = await res.json();
|
|
1145
|
+
} catch (jsonErr) {
|
|
1146
|
+
if (jsonErr && jsonErr.name === 'AbortError') {
|
|
1147
|
+
throw jsonErr;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return { res, data };
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1105
1153
|
function pickDeliverySettings(source) {
|
|
1106
1154
|
const out = {};
|
|
1107
1155
|
for (const key of DELIVERY_SETTING_KEYS.concat(TEMPLATE_SETTING_KEYS)) {
|
|
@@ -1391,8 +1439,12 @@ window.__ModuleLoader__.load({
|
|
|
1391
1439
|
const gen = ++fetchGenRef.current;
|
|
1392
1440
|
setLoading(true);
|
|
1393
1441
|
setFetchError(null);
|
|
1442
|
+
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1443
|
+
const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
|
|
1394
1444
|
try {
|
|
1395
|
-
const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query)
|
|
1445
|
+
const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query), {
|
|
1446
|
+
signal: controller ? controller.signal : undefined,
|
|
1447
|
+
});
|
|
1396
1448
|
const data = await res.json();
|
|
1397
1449
|
if (gen !== fetchGenRef.current) return;
|
|
1398
1450
|
if (data && data.ok) {
|
|
@@ -1407,6 +1459,7 @@ window.__ModuleLoader__.load({
|
|
|
1407
1459
|
console.error('[dsh-cron] fetch tasks error:', err);
|
|
1408
1460
|
setFetchError(err.message || T_KEY(t, 'list.loadErrorPrefix'));
|
|
1409
1461
|
} finally {
|
|
1462
|
+
if (timer) clearTimeout(timer);
|
|
1410
1463
|
if (gen === fetchGenRef.current) {
|
|
1411
1464
|
setLoading(false);
|
|
1412
1465
|
}
|
|
@@ -1414,9 +1467,11 @@ window.__ModuleLoader__.load({
|
|
|
1414
1467
|
};
|
|
1415
1468
|
|
|
1416
1469
|
const loadModels = async (providerName) => {
|
|
1470
|
+
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1471
|
+
const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
|
|
1417
1472
|
try {
|
|
1418
1473
|
const url = providerName ? '/dsh-cron/models?provider=' + encodeURIComponent(providerName) : '/dsh-cron/models';
|
|
1419
|
-
const res = await fetch(url);
|
|
1474
|
+
const res = await fetch(url, { signal: controller ? controller.signal : undefined });
|
|
1420
1475
|
const data = await res.json();
|
|
1421
1476
|
if (data && data.ok) {
|
|
1422
1477
|
if (!providerName) {
|
|
@@ -1431,6 +1486,8 @@ window.__ModuleLoader__.load({
|
|
|
1431
1486
|
}
|
|
1432
1487
|
} catch (err) {
|
|
1433
1488
|
console.error('[dsh-cron] load models error:', err);
|
|
1489
|
+
} finally {
|
|
1490
|
+
if (timer) clearTimeout(timer);
|
|
1434
1491
|
}
|
|
1435
1492
|
};
|
|
1436
1493
|
|
|
@@ -1455,6 +1512,7 @@ window.__ModuleLoader__.load({
|
|
|
1455
1512
|
const fetchOpts = { headers };
|
|
1456
1513
|
if (pollController) fetchOpts.signal = pollController.signal;
|
|
1457
1514
|
|
|
1515
|
+
const pollTimer = pollController ? setTimeout(() => { try { pollController.abort(); } catch (abortErr) { void abortErr; } }, 15000) : null;
|
|
1458
1516
|
fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query), fetchOpts)
|
|
1459
1517
|
.then(res => {
|
|
1460
1518
|
if (isDestroyed || pollGen !== fetchGenRef.current) return null;
|
|
@@ -1478,6 +1536,7 @@ window.__ModuleLoader__.load({
|
|
|
1478
1536
|
if (err && err.name === 'AbortError') return;
|
|
1479
1537
|
})
|
|
1480
1538
|
.finally(() => {
|
|
1539
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1481
1540
|
if (isDestroyed) return;
|
|
1482
1541
|
// Adaptive interval: 30s when tab is hidden, 8s when active (#134)
|
|
1483
1542
|
const intervalMs = (typeof document !== 'undefined' && document.hidden) ? 30000 : 8000;
|
|
@@ -1515,10 +1574,17 @@ window.__ModuleLoader__.load({
|
|
|
1515
1574
|
const fetchJsonWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1516
1575
|
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1517
1576
|
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
1577
|
+
if (options && options.signal && controller) {
|
|
1578
|
+
if (options.signal.aborted) {
|
|
1579
|
+
controller.abort();
|
|
1580
|
+
} else {
|
|
1581
|
+
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1518
1584
|
try {
|
|
1519
1585
|
const res = await fetch(url, {
|
|
1520
1586
|
...options,
|
|
1521
|
-
signal: controller ? controller.signal : options.signal,
|
|
1587
|
+
signal: controller ? controller.signal : (options ? options.signal : undefined),
|
|
1522
1588
|
});
|
|
1523
1589
|
let data = {};
|
|
1524
1590
|
try {
|
|
@@ -1589,8 +1655,7 @@ window.__ModuleLoader__.load({
|
|
|
1589
1655
|
/** #41: duplicate a task; the server copies config, resets run state (#41). */
|
|
1590
1656
|
const handleDryRun = async (taskId) => {
|
|
1591
1657
|
try {
|
|
1592
|
-
const res = await
|
|
1593
|
-
const data = await res.json();
|
|
1658
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/dry-run', { method: 'POST' });
|
|
1594
1659
|
setDryRunResult(data);
|
|
1595
1660
|
} catch (err) {
|
|
1596
1661
|
alert('Dry run error: ' + err.message);
|
|
@@ -1603,8 +1668,7 @@ window.__ModuleLoader__.load({
|
|
|
1603
1668
|
setArchiveLoading(true);
|
|
1604
1669
|
setArchiveModalOpen(true);
|
|
1605
1670
|
try {
|
|
1606
|
-
const res = await
|
|
1607
|
-
const data = await res.json();
|
|
1671
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/archive?limit=15&offset=' + offset);
|
|
1608
1672
|
if (data.ok) {
|
|
1609
1673
|
setArchiveRuns(prev => offset === 0 ? (data.runs || []) : [...prev, ...(data.runs || [])]);
|
|
1610
1674
|
setArchiveTotal(data.total || 0);
|
|
@@ -1618,12 +1682,11 @@ window.__ModuleLoader__.load({
|
|
|
1618
1682
|
|
|
1619
1683
|
const handlePreviewSchedule = async (scheduleExpr) => {
|
|
1620
1684
|
try {
|
|
1621
|
-
const res = await
|
|
1685
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/schedule/preview', {
|
|
1622
1686
|
method: 'POST',
|
|
1623
1687
|
headers: { 'Content-Type': 'application/json' },
|
|
1624
1688
|
body: JSON.stringify({ schedule: scheduleExpr, timezone: formTimezone })
|
|
1625
1689
|
});
|
|
1626
|
-
const data = await res.json();
|
|
1627
1690
|
if (data.ok && data.runs) {
|
|
1628
1691
|
setSchedulePreviewRuns(data.runs);
|
|
1629
1692
|
} else {
|
|
@@ -1636,8 +1699,7 @@ window.__ModuleLoader__.load({
|
|
|
1636
1699
|
|
|
1637
1700
|
const handleDuplicate = async (task) => {
|
|
1638
1701
|
try {
|
|
1639
|
-
const res = await
|
|
1640
|
-
const data = await res.json();
|
|
1702
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/duplicate', { method: 'POST' });
|
|
1641
1703
|
if (!data.ok) {
|
|
1642
1704
|
alert(T_KEY(t, 'duplicate.failed', { error: (data && data.error) || '' }));
|
|
1643
1705
|
return;
|
|
@@ -1678,8 +1740,7 @@ window.__ModuleLoader__.load({
|
|
|
1678
1740
|
const promptMsg = T_KEY(t, 'transfer.exportPrompt') || 'Choose export mode:\nOK = Sanitized (removes custom env vars & headers)\nCancel = Full raw export';
|
|
1679
1741
|
const wantSanitized = confirm(promptMsg);
|
|
1680
1742
|
const exportUrl = '/dsh-cron/tasks/export' + (wantSanitized ? '?sanitize=true' : '');
|
|
1681
|
-
const res = await
|
|
1682
|
-
const data = await res.json();
|
|
1743
|
+
const { res, data } = await fetchJsonWithTimeout(exportUrl);
|
|
1683
1744
|
if (!data.ok) {
|
|
1684
1745
|
alert(T_KEY(t, 'transfer.exportFailed', { error: (data && data.error) || '' }));
|
|
1685
1746
|
return;
|
|
@@ -1717,12 +1778,11 @@ window.__ModuleLoader__.load({
|
|
|
1717
1778
|
alert(T_KEY(t, 'transfer.badFile', { error: parseErr.message }));
|
|
1718
1779
|
return;
|
|
1719
1780
|
}
|
|
1720
|
-
const res = await
|
|
1781
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/import', {
|
|
1721
1782
|
method: 'POST',
|
|
1722
1783
|
headers: importHeaders(document_),
|
|
1723
1784
|
body: JSON.stringify({ document: document_, dryRun: true, strategy: importStrategy })
|
|
1724
1785
|
});
|
|
1725
|
-
const data = await res.json();
|
|
1726
1786
|
if (!data.ok) {
|
|
1727
1787
|
alert(T_KEY(t, 'transfer.importFailed', { error: (data && data.error) || '' }));
|
|
1728
1788
|
return;
|
|
@@ -1740,12 +1800,11 @@ window.__ModuleLoader__.load({
|
|
|
1740
1800
|
if (!importDoc) return;
|
|
1741
1801
|
setTransferBusy(true);
|
|
1742
1802
|
try {
|
|
1743
|
-
const res = await
|
|
1803
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/import', {
|
|
1744
1804
|
method: 'POST',
|
|
1745
1805
|
headers: importHeaders(importDoc),
|
|
1746
1806
|
body: JSON.stringify({ document: importDoc, strategy: importStrategy })
|
|
1747
1807
|
});
|
|
1748
|
-
const data = await res.json();
|
|
1749
1808
|
if (!data.ok) {
|
|
1750
1809
|
alert(T_KEY(t, 'transfer.importFailed', { error: (data && data.error) || '' }));
|
|
1751
1810
|
return;
|
|
@@ -1777,12 +1836,11 @@ window.__ModuleLoader__.load({
|
|
|
1777
1836
|
'If the change makes the task execute code (script/node/python/ssh/docker), confirm it with me explicitly before applying it.',
|
|
1778
1837
|
].filter(Boolean);
|
|
1779
1838
|
try {
|
|
1780
|
-
const res = await
|
|
1839
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chat/start', {
|
|
1781
1840
|
method: 'POST',
|
|
1782
1841
|
headers: { 'Content-Type': 'application/json' },
|
|
1783
1842
|
body: JSON.stringify({ prompt: lines.join(String.fromCharCode(10)) })
|
|
1784
1843
|
});
|
|
1785
|
-
const data = await res.json();
|
|
1786
1844
|
if (!data.ok || !data.sessionId) {
|
|
1787
1845
|
alert(T_KEY(t, 'tune.startFailed', { error: (data && data.error) || '' }));
|
|
1788
1846
|
return;
|
|
@@ -1798,7 +1856,7 @@ window.__ModuleLoader__.load({
|
|
|
1798
1856
|
const handleDelete = async (id) => {
|
|
1799
1857
|
if (!confirm(T_KEY(t, 'confirm.delete'))) return;
|
|
1800
1858
|
try {
|
|
1801
|
-
await
|
|
1859
|
+
await fetchWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
1802
1860
|
fetchTasks();
|
|
1803
1861
|
} catch (err) {
|
|
1804
1862
|
console.debug('[dsh-cron] delete failed', err);
|
|
@@ -1809,8 +1867,7 @@ window.__ModuleLoader__.load({
|
|
|
1809
1867
|
setTestStatus(null);
|
|
1810
1868
|
setSettingsModalOpen(true);
|
|
1811
1869
|
try {
|
|
1812
|
-
const res = await
|
|
1813
|
-
const data = await res.json();
|
|
1870
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings');
|
|
1814
1871
|
if (data && data.ok && data.settings) {
|
|
1815
1872
|
setSettingsBotToken(data.settings.botToken || '');
|
|
1816
1873
|
setSettingsChatId(data.settings.chatId || '');
|
|
@@ -1829,7 +1886,7 @@ window.__ModuleLoader__.load({
|
|
|
1829
1886
|
const handleSaveSettings = async (e) => {
|
|
1830
1887
|
e.preventDefault();
|
|
1831
1888
|
try {
|
|
1832
|
-
const res = await
|
|
1889
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
|
|
1833
1890
|
method: 'POST',
|
|
1834
1891
|
headers: { 'Content-Type': 'application/json' },
|
|
1835
1892
|
body: JSON.stringify(Object.assign({
|
|
@@ -1840,7 +1897,6 @@ window.__ModuleLoader__.load({
|
|
|
1840
1897
|
kanbanBaseUrl: settingsKanbanUrl,
|
|
1841
1898
|
}, settingsDelivery))
|
|
1842
1899
|
});
|
|
1843
|
-
const data = await res.json();
|
|
1844
1900
|
if (data && data.ok) {
|
|
1845
1901
|
setSettingsModalOpen(false);
|
|
1846
1902
|
} else {
|
|
@@ -1881,7 +1937,7 @@ window.__ModuleLoader__.load({
|
|
|
1881
1937
|
|
|
1882
1938
|
const handleTestTelegram = async () => { setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
|
|
1883
1939
|
try {
|
|
1884
|
-
const res = await
|
|
1940
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
|
|
1885
1941
|
method: 'POST',
|
|
1886
1942
|
headers: { 'Content-Type': 'application/json' },
|
|
1887
1943
|
body: JSON.stringify({
|
|
@@ -1889,7 +1945,6 @@ window.__ModuleLoader__.load({
|
|
|
1889
1945
|
chatId: settingsChatId
|
|
1890
1946
|
})
|
|
1891
1947
|
});
|
|
1892
|
-
const data = await res.json();
|
|
1893
1948
|
if (data && data.ok) {
|
|
1894
1949
|
setTestStatus({ loading: false, success: true, message: T_KEY(t, 'settings.testOk') });
|
|
1895
1950
|
} else {
|
|
@@ -1969,8 +2024,7 @@ window.__ModuleLoader__.load({
|
|
|
1969
2024
|
const loadTaskHistory = async (taskId) => {
|
|
1970
2025
|
setHistoryLoading(true);
|
|
1971
2026
|
try {
|
|
1972
|
-
const res = await
|
|
1973
|
-
const data = await res.json();
|
|
2027
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/history');
|
|
1974
2028
|
if (data.ok) {
|
|
1975
2029
|
setTaskHistory(data.history || []);
|
|
1976
2030
|
}
|
|
@@ -2119,12 +2173,11 @@ window.__ModuleLoader__.load({
|
|
|
2119
2173
|
Boolean(payload.preflightCommand && payload.preflightCommand.trim()) ||
|
|
2120
2174
|
Boolean(payload.selfHealingCommand && payload.selfHealingCommand.trim());
|
|
2121
2175
|
if (hasCodeExec) headers[SCRIPT_CONFIRM_HEADER] = 'script';
|
|
2122
|
-
const res = await
|
|
2176
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks', {
|
|
2123
2177
|
method: 'POST',
|
|
2124
2178
|
headers,
|
|
2125
2179
|
body: JSON.stringify(payload)
|
|
2126
2180
|
});
|
|
2127
|
-
const data = await res.json();
|
|
2128
2181
|
if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
|
|
2129
2182
|
setManualModalOpen(false);
|
|
2130
2183
|
fetchTasks();
|
|
@@ -2143,12 +2196,11 @@ window.__ModuleLoader__.load({
|
|
|
2143
2196
|
|
|
2144
2197
|
setDshLoading(true);
|
|
2145
2198
|
try {
|
|
2146
|
-
const res = await
|
|
2199
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chat/start', {
|
|
2147
2200
|
method: 'POST',
|
|
2148
2201
|
headers: { 'Content-Type': 'application/json' },
|
|
2149
2202
|
body: JSON.stringify({ prompt: text })
|
|
2150
2203
|
});
|
|
2151
|
-
const data = await res.json();
|
|
2152
2204
|
if (!data.ok || !data.sessionId) {
|
|
2153
2205
|
alert(T_KEY(t, 'dsh.startFailed', { error: data.error || '' }));
|
|
2154
2206
|
setDshLoading(false);
|
|
@@ -3532,8 +3584,7 @@ window.__ModuleLoader__.load({
|
|
|
3532
3584
|
const checkUpdates = async () => {
|
|
3533
3585
|
setUpdateState(prev => ({ ...prev, checking: true, error: null, notice: null }));
|
|
3534
3586
|
try {
|
|
3535
|
-
const res = await
|
|
3536
|
-
const data = await res.json();
|
|
3587
|
+
const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update');
|
|
3537
3588
|
if (data && data.currentVersion) {
|
|
3538
3589
|
setUpdateState(prev => ({
|
|
3539
3590
|
...prev,
|
|
@@ -3554,11 +3605,10 @@ window.__ModuleLoader__.load({
|
|
|
3554
3605
|
const runUpdate = async () => {
|
|
3555
3606
|
setUpdateState(prev => ({ ...prev, updating: true, error: null, notice: null }));
|
|
3556
3607
|
try {
|
|
3557
|
-
const res = await
|
|
3608
|
+
const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update', {
|
|
3558
3609
|
method: 'POST',
|
|
3559
3610
|
headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' }
|
|
3560
3611
|
});
|
|
3561
|
-
const data = await res.json();
|
|
3562
3612
|
if (data && data.updated) {
|
|
3563
3613
|
setUpdateState(prev => ({
|
|
3564
3614
|
...prev,
|
|
@@ -3587,9 +3637,8 @@ window.__ModuleLoader__.load({
|
|
|
3587
3637
|
React.useEffect(() => {
|
|
3588
3638
|
let active = true;
|
|
3589
3639
|
setLoadState('loading');
|
|
3590
|
-
|
|
3591
|
-
.then(
|
|
3592
|
-
.then(data => {
|
|
3640
|
+
fetchJsonWithTimeout('/dsh-cron/settings')
|
|
3641
|
+
.then(({ data }) => {
|
|
3593
3642
|
if (!active) return;
|
|
3594
3643
|
if (data && data.ok && data.settings) {
|
|
3595
3644
|
setToken(data.settings.botToken || '');
|
|
@@ -3615,7 +3664,7 @@ window.__ModuleLoader__.load({
|
|
|
3615
3664
|
const saveSettings = async (e) => {
|
|
3616
3665
|
if (e) e.preventDefault();
|
|
3617
3666
|
try {
|
|
3618
|
-
const res = await
|
|
3667
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
|
|
3619
3668
|
method: 'POST',
|
|
3620
3669
|
headers: { 'Content-Type': 'application/json' },
|
|
3621
3670
|
body: JSON.stringify(Object.assign({
|
|
@@ -3627,7 +3676,6 @@ window.__ModuleLoader__.load({
|
|
|
3627
3676
|
kanbanBaseUrl: kanbanUrl
|
|
3628
3677
|
}, delivery))
|
|
3629
3678
|
});
|
|
3630
|
-
const data = await res.json();
|
|
3631
3679
|
if (data && data.ok) {
|
|
3632
3680
|
setSaved(true);
|
|
3633
3681
|
setTimeout(() => setSaved(false), 2500);
|
|
@@ -3640,12 +3688,11 @@ window.__ModuleLoader__.load({
|
|
|
3640
3688
|
const testTelegram = async () => {
|
|
3641
3689
|
setTestInfo({ loading: true, message: T_KEY(t, 'settings.sending') });
|
|
3642
3690
|
try {
|
|
3643
|
-
const res = await
|
|
3691
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
|
|
3644
3692
|
method: 'POST',
|
|
3645
3693
|
headers: { 'Content-Type': 'application/json' },
|
|
3646
3694
|
body: JSON.stringify({ botToken: token, chatId })
|
|
3647
3695
|
});
|
|
3648
|
-
const data = await res.json();
|
|
3649
3696
|
if (data && data.ok) {
|
|
3650
3697
|
setTestInfo({ success: true, message: T_KEY(t, 'settings.testOk') });
|
|
3651
3698
|
} else {
|
|
@@ -3659,12 +3706,11 @@ window.__ModuleLoader__.load({
|
|
|
3659
3706
|
const testKanban = async () => {
|
|
3660
3707
|
setTestInfo({ loading: true, message: T_KEY(t, 'settings.checkingKanban') });
|
|
3661
3708
|
try {
|
|
3662
|
-
const res = await
|
|
3709
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/kanban/test', {
|
|
3663
3710
|
method: 'POST',
|
|
3664
3711
|
headers: { 'Content-Type': 'application/json' },
|
|
3665
3712
|
body: JSON.stringify({ kanbanBaseUrl: kanbanUrl })
|
|
3666
3713
|
});
|
|
3667
|
-
const data = await res.json();
|
|
3668
3714
|
if (data && data.ok) {
|
|
3669
3715
|
setTestInfo({ success: true, message: T_KEY(t, 'settings.kanbanOk') });
|
|
3670
3716
|
} else {
|
|
@@ -3971,8 +4017,7 @@ window.__ModuleLoader__.load({
|
|
|
3971
4017
|
|
|
3972
4018
|
async function loadSidebarJobs(state) {
|
|
3973
4019
|
try {
|
|
3974
|
-
const res = await
|
|
3975
|
-
const data = await res.json();
|
|
4020
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks?status=active');
|
|
3976
4021
|
if (data && data.ok) renderSidebarJobs(state, data.tasks || []);
|
|
3977
4022
|
} catch (err) {
|
|
3978
4023
|
console.debug('[dsh-cron] load sidebar jobs failed', err);
|
package/lib/http-utils.js
CHANGED
|
@@ -180,7 +180,13 @@ export async function readBody(req, res) {
|
|
|
180
180
|
*/
|
|
181
181
|
export function redactTaskSecrets(task) {
|
|
182
182
|
if (!task || typeof task !== 'object') return task;
|
|
183
|
-
|
|
183
|
+
// Issue #310 / GH #5: sanitize to lossless JSON by omitting any undefined properties
|
|
184
|
+
const clone = {};
|
|
185
|
+
for (const [k, v] of Object.entries(task)) {
|
|
186
|
+
if (v !== undefined) {
|
|
187
|
+
clone[k] = v;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
184
190
|
|
|
185
191
|
if (clone.env && typeof clone.env === 'object') {
|
|
186
192
|
const redactedEnv = {};
|
package/lib/runtimes.js
CHANGED
|
@@ -124,8 +124,6 @@ export function buildSshInvocation(task) {
|
|
|
124
124
|
return { file: 'ssh', args };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
export const DOCKER_HOST_ENV_ALLOWLIST = ['TZ', 'LANG', 'LC_ALL'];
|
|
128
|
-
|
|
129
127
|
/** #8/#226: build Docker run invocation without leaking host process secrets into container. */
|
|
130
128
|
export function buildDockerInvocation(task, explicitEnv) {
|
|
131
129
|
const image = String(task.dockerImage || '').trim();
|
package/lib/store.js
CHANGED
|
@@ -555,8 +555,8 @@ export class TaskStore {
|
|
|
555
555
|
type: task.type !== undefined ? task.type : (prev.type || 'llm'),
|
|
556
556
|
status: task.status !== undefined ? task.status : (prev.status || 'active'),
|
|
557
557
|
delivery: task.delivery !== undefined ? task.delivery : (prev.delivery || 'isolated'),
|
|
558
|
-
provider: task.provider !== undefined ? task.provider : prev.provider,
|
|
559
|
-
model: task.model !== undefined ? task.model : prev.model,
|
|
558
|
+
provider: task.provider !== undefined ? task.provider : (prev.provider || null),
|
|
559
|
+
model: task.model !== undefined ? task.model : (prev.model || null),
|
|
560
560
|
notifyTelegram: task.notifyTelegram !== undefined
|
|
561
561
|
? (task.notifyTelegram === null || task.notifyTelegram === 'inherit' ? undefined : Boolean(task.notifyTelegram))
|
|
562
562
|
: prev.notifyTelegram,
|
|
@@ -591,6 +591,12 @@ export class TaskStore {
|
|
|
591
591
|
lastDurationMs: task.lastDurationMs !== undefined ? task.lastDurationMs : (prev.lastDurationMs || 0),
|
|
592
592
|
nextRunAt: task.nextRunAt !== undefined ? task.nextRunAt : (prev.nextRunAt || null),
|
|
593
593
|
};
|
|
594
|
+
// Issue #310 / GH #5: strip any undefined properties to guarantee lossless JSON
|
|
595
|
+
for (const key of Object.keys(record)) {
|
|
596
|
+
if (record[key] === undefined) {
|
|
597
|
+
delete record[key];
|
|
598
|
+
}
|
|
599
|
+
}
|
|
594
600
|
this.tasks.set(id, record);
|
|
595
601
|
try {
|
|
596
602
|
this.save();
|
package/lib/task-create.js
CHANGED
|
@@ -9,7 +9,7 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
9
9
|
const typeError = validateTaskType(taskType, args);
|
|
10
10
|
if (typeError) throw new Error(typeError);
|
|
11
11
|
const parsed = parseScheduleExpression(args.schedule);
|
|
12
|
-
const
|
|
12
|
+
const taskData = {
|
|
13
13
|
title: args.title,
|
|
14
14
|
schedule: parsed.cronPattern || args.schedule,
|
|
15
15
|
scheduleText: parsed.humanText,
|
|
@@ -72,7 +72,14 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
72
72
|
costLimitUsd: Number(args.costLimitUsd) > 0 ? Number(args.costLimitUsd) : undefined,
|
|
73
73
|
dailyCostLimitUsd: Number(args.dailyCostLimitUsd) > 0 ? Number(args.dailyCostLimitUsd) : undefined,
|
|
74
74
|
tokenLimit: Number(args.tokenLimit) > 0 ? Number(args.tokenLimit) : undefined,
|
|
75
|
-
}
|
|
75
|
+
};
|
|
76
|
+
// Issue #310 / GH #5: strip any undefined properties before store.set()
|
|
77
|
+
for (const k of Object.keys(taskData)) {
|
|
78
|
+
if (taskData[k] === undefined) {
|
|
79
|
+
delete taskData[k];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const task = store.set(taskData);
|
|
76
83
|
scheduler.scheduleTask(task);
|
|
77
84
|
return {
|
|
78
85
|
success: true,
|
package/lib/task-patch.js
CHANGED
|
@@ -80,7 +80,7 @@ export function buildTaskPatch(current, body) {
|
|
|
80
80
|
patch.oneShot = Boolean(parsed.isOneShot || (body && body.oneShot));
|
|
81
81
|
if (scheduleChanged) {
|
|
82
82
|
patch.nextRunAt = parsed.isOneShot ? parsed.targetTimestamp : (parsed.nextRun || null);
|
|
83
|
-
patch.targetTimestamp = parsed.isOneShot ? parsed.targetTimestamp :
|
|
83
|
+
patch.targetTimestamp = parsed.isOneShot ? parsed.targetTimestamp : null;
|
|
84
84
|
}
|
|
85
85
|
} catch (err) {
|
|
86
86
|
return { ok: false, error: err.message };
|
|
@@ -95,6 +95,12 @@ export function buildTaskPatch(current, body) {
|
|
|
95
95
|
if (patch.tokenLimit !== undefined) {
|
|
96
96
|
patch.tokenLimit = patch.tokenLimit === null ? null : (Number(patch.tokenLimit) > 0 ? Number(patch.tokenLimit) : null);
|
|
97
97
|
}
|
|
98
|
+
// Issue #310 / GH #5: strip undefined properties from patch
|
|
99
|
+
for (const k of Object.keys(patch)) {
|
|
100
|
+
if (patch[k] === undefined) {
|
|
101
|
+
delete patch[k];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
98
104
|
return { ok: true, patch };
|
|
99
105
|
}
|
|
100
106
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.49",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|