scheduler.html 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  6. <title>Task Scheduler</title>
  7. <script src="../../script/jquery.min.js"></script>
  8. <script src="../../script/ao_module.js"></script>
  9. <script src="../../script/applocale.js"></script>
  10. <script src="js/moment.min.js"></script>
  11. <style>
  12. *, *::before, *::after { box-sizing: border-box; }
  13. /* ── Palette — matches system_setting/main.css ── */
  14. :root {
  15. --bg: #f3f3f3;
  16. --sidebar-bg: #ebebeb;
  17. --sidebar-border: #dcdcdc;
  18. --text: #202020;
  19. --text-dim: #555;
  20. --text-muted: #888;
  21. --text-desc: #666;
  22. --nav-hover: rgba(0,0,0,0.055);
  23. --nav-active: rgba(0,0,0,0.09);
  24. --card-bg: #ffffff;
  25. --card-border: #e5e5e5;
  26. --card-hover-bg: #fafafa;
  27. --accent: #0071e3;
  28. --danger: #ff3b30;
  29. --success: #34c759;
  30. --warning: #ff9f0a;
  31. --divider: #e0e0e0;
  32. --scrollbar: #c8c8c8;
  33. }
  34. body.dark {
  35. --bg: #1f1f1f;
  36. --sidebar-bg: #282828;
  37. --sidebar-border: #363636;
  38. --text: #e3e3e3;
  39. --text-dim: #999;
  40. --text-muted: #666;
  41. --text-desc: #aaa;
  42. --nav-hover: rgba(255,255,255,0.06);
  43. --nav-active: rgba(255,255,255,0.10);
  44. --card-bg: #2d2d2d;
  45. --card-border: #3a3a3a;
  46. --card-hover-bg: #333;
  47. --accent: #2997ff;
  48. --danger: #ff453a;
  49. --success: #30d158;
  50. --warning: #ffd60a;
  51. --divider: #3a3a3a;
  52. --scrollbar: #555;
  53. }
  54. html, body {
  55. height: 100%;
  56. margin: 0;
  57. overflow: hidden;
  58. background: var(--bg);
  59. color: var(--text);
  60. font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  61. font-size: 14px;
  62. -webkit-font-smoothing: antialiased;
  63. transition: background 0.15s, color 0.15s;
  64. }
  65. /* ── Layout shell ── */
  66. #app { display: flex; height: 100vh; overflow: hidden; }
  67. /* ── Sidebar ── */
  68. #sidebar {
  69. width: 220px;
  70. min-width: 220px;
  71. background: var(--sidebar-bg);
  72. border-right: 1px solid var(--sidebar-border);
  73. display: flex;
  74. flex-direction: column;
  75. overflow: hidden;
  76. transition: background 0.15s, border-color 0.15s;
  77. }
  78. #sidebar-title {
  79. padding: 18px 16px 10px;
  80. font-size: 17px;
  81. font-weight: 600;
  82. letter-spacing: -0.1px;
  83. display: flex;
  84. align-items: center;
  85. gap: 9px;
  86. user-select: none;
  87. flex-shrink: 0;
  88. color: var(--text);
  89. }
  90. #sidebar-title img { width: 20px; height: 20px; opacity: 0.7; }
  91. #sidebar-nav {
  92. flex: 1;
  93. overflow-y: auto;
  94. padding: 4px 8px 10px;
  95. }
  96. #sidebar-nav::-webkit-scrollbar { width: 3px; }
  97. #sidebar-nav::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 2px; }
  98. .nav-item {
  99. display: flex;
  100. align-items: center;
  101. gap: 10px;
  102. padding: 7px 10px;
  103. border-radius: 6px;
  104. cursor: pointer;
  105. user-select: none;
  106. transition: background 0.08s;
  107. font-size: 13.5px;
  108. color: var(--text);
  109. margin-bottom: 1px;
  110. }
  111. .nav-item:hover { background: var(--nav-hover); }
  112. .nav-item.active { background: var(--nav-active); font-weight: 500; }
  113. .nav-icon { width: 16px; height: 16px; flex-shrink: 0; opacity: 0.72; display: flex; align-items: center; }
  114. .nav-icon svg { width: 16px; height: 16px; }
  115. /* Filter box */
  116. #sidebar-filter {
  117. padding: 0 10px 10px;
  118. flex-shrink: 0;
  119. }
  120. #filter-input {
  121. width: 100%;
  122. padding: 6px 10px;
  123. border-radius: 6px;
  124. border: 1px solid var(--card-border);
  125. background: var(--card-bg);
  126. color: var(--text);
  127. font-size: 12.5px;
  128. font-family: inherit;
  129. outline: none;
  130. transition: border-color 0.1s;
  131. }
  132. #filter-input:focus { border-color: var(--accent); }
  133. body.dark #filter-input { background: #333; border-color: #444; }
  134. /* ── Main content ── */
  135. #main {
  136. flex: 1;
  137. overflow-y: auto;
  138. padding: 20px 24px;
  139. background: var(--bg);
  140. }
  141. #main::-webkit-scrollbar { width: 6px; }
  142. #main::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 3px; }
  143. .view { display: none; }
  144. .view.active { display: block; }
  145. /* ── Section heading ── */
  146. .section-title {
  147. font-size: 18px;
  148. font-weight: 600;
  149. margin-bottom: 14px;
  150. color: var(--text);
  151. }
  152. /* ── Card / list ── */
  153. .card {
  154. background: var(--card-bg);
  155. border: 1px solid var(--card-border);
  156. border-radius: 10px;
  157. overflow: hidden;
  158. margin-bottom: 16px;
  159. }
  160. /* ── Task list rows ── */
  161. .task-row {
  162. display: grid;
  163. grid-template-columns: minmax(120px,1.8fr) minmax(0,2fr) 90px 100px;
  164. align-items: center;
  165. padding: 10px 14px;
  166. border-bottom: 1px solid var(--divider);
  167. gap: 10px;
  168. font-size: 13px;
  169. transition: background 0.08s;
  170. }
  171. .task-row:last-child { border-bottom: none; }
  172. .task-row.header {
  173. font-size: 11px;
  174. font-weight: 600;
  175. text-transform: uppercase;
  176. letter-spacing: 0.04em;
  177. color: var(--text-muted);
  178. padding: 8px 14px;
  179. background: var(--bg);
  180. cursor: default;
  181. }
  182. body.dark .task-row.header { background: #282828; }
  183. .task-row:not(.header):hover { background: var(--card-hover-bg); }
  184. /* Admin "all tasks" has an extra Creator column */
  185. .task-row.all-cols {
  186. grid-template-columns: minmax(100px,1.4fr) minmax(80px,1.2fr) minmax(0,2fr) 80px 90px 100px;
  187. }
  188. /* Remove view has action button column */
  189. .task-row.remove-cols {
  190. grid-template-columns: minmax(100px,1.4fr) minmax(80px,1.2fr) minmax(0,2fr) 80px 90px 80px;
  191. }
  192. .task-name { font-weight: 500; color: var(--text); }
  193. .task-script { color: var(--text-dim); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  194. .task-interval { color: var(--text-desc); font-size: 12.5px; }
  195. .task-basetime { color: var(--text-desc); font-size: 11.5px; }
  196. .app-badge {
  197. display: inline-block;
  198. padding: 2px 8px;
  199. border-radius: 20px;
  200. font-size: 11px;
  201. font-weight: 500;
  202. background: rgba(0,113,227,0.10);
  203. color: var(--accent);
  204. white-space: nowrap;
  205. max-width: 88px;
  206. overflow: hidden;
  207. text-overflow: ellipsis;
  208. }
  209. body.dark .app-badge { background: rgba(41,151,255,0.15); }
  210. /* Empty state */
  211. .empty-state {
  212. padding: 32px 20px;
  213. text-align: center;
  214. color: var(--text-muted);
  215. font-size: 13.5px;
  216. }
  217. /* ── Buttons ── */
  218. .btn {
  219. display: inline-flex;
  220. align-items: center;
  221. gap: 5px;
  222. padding: 6px 14px;
  223. border-radius: 6px;
  224. border: 1px solid var(--card-border);
  225. font-family: inherit;
  226. font-size: 13px;
  227. font-weight: 500;
  228. cursor: pointer;
  229. outline: none;
  230. background: var(--card-bg);
  231. color: var(--text);
  232. transition: background 0.08s, opacity 0.12s;
  233. white-space: nowrap;
  234. }
  235. .btn:hover { background: var(--card-hover-bg); }
  236. .btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
  237. .btn.primary:hover { opacity: 0.85; }
  238. .btn.danger { background: var(--danger); color: #fff; border-color: var(--danger); }
  239. .btn.danger:hover { opacity: 0.85; }
  240. .btn.sm { padding: 4px 10px; font-size: 12px; }
  241. .btn:disabled { opacity: 0.35; cursor: default; pointer-events: none; }
  242. /* ── New Task form ── */
  243. .form-card {
  244. background: var(--card-bg);
  245. border: 1px solid var(--card-border);
  246. border-radius: 10px;
  247. padding: 18px 20px;
  248. margin-bottom: 16px;
  249. }
  250. .form-row { margin-bottom: 14px; }
  251. .form-label {
  252. display: block;
  253. font-size: 12.5px;
  254. font-weight: 500;
  255. color: var(--text-dim);
  256. margin-bottom: 5px;
  257. }
  258. .form-req { color: var(--danger); }
  259. .form-input {
  260. width: 100%;
  261. padding: 7px 11px;
  262. border-radius: 7px;
  263. border: 1px solid var(--card-border);
  264. background: var(--bg);
  265. color: var(--text);
  266. font-size: 13.5px;
  267. font-family: inherit;
  268. outline: none;
  269. transition: border-color 0.12s;
  270. }
  271. body.dark .form-input { background: #333; border-color: #444; }
  272. .form-input:focus { border-color: var(--accent); }
  273. .form-hint { font-size: 11.5px; color: var(--text-muted); margin-top: 4px; }
  274. .form-row-inline { display: flex; gap: 8px; align-items: flex-start; }
  275. .form-row-inline .form-input { flex: 1; }
  276. select.form-input { cursor: pointer; }
  277. .input-with-btn { display: flex; gap: 6px; }
  278. .input-with-btn .form-input { flex: 1; }
  279. /* Permission denied banner */
  280. #noPermMessage {
  281. background: rgba(255,159,10,0.10);
  282. border: 1px solid rgba(255,159,10,0.28);
  283. border-radius: 8px;
  284. padding: 11px 14px;
  285. font-size: 13px;
  286. color: var(--warning);
  287. margin-bottom: 14px;
  288. display: none;
  289. }
  290. /* ── Toast ── */
  291. #toast {
  292. position: fixed;
  293. bottom: 16px;
  294. right: 18px;
  295. background: var(--card-bg);
  296. border: 1px solid var(--card-border);
  297. border-radius: 8px;
  298. padding: 9px 15px;
  299. font-size: 13px;
  300. box-shadow: 0 4px 16px rgba(0,0,0,0.14);
  301. display: none;
  302. z-index: 9999;
  303. color: var(--text);
  304. }
  305. /* ── Warning bar ── */
  306. .warn-bar {
  307. background: rgba(255,159,10,0.10);
  308. border: 1px solid rgba(255,159,10,0.25);
  309. border-radius: 7px;
  310. padding: 8px 12px;
  311. font-size: 12.5px;
  312. color: var(--warning);
  313. margin-top: 6px;
  314. display: none;
  315. }
  316. /* ── Remove view danger header ── */
  317. .danger-header {
  318. background: rgba(255,59,48,0.08);
  319. border-bottom: 1px solid rgba(255,59,48,0.18);
  320. padding: 10px 14px;
  321. font-size: 12.5px;
  322. color: var(--danger);
  323. font-weight: 500;
  324. }
  325. body.dark .danger-header { background: rgba(255,69,58,0.12); }
  326. /* Responsive */
  327. @media (max-width: 640px) {
  328. #sidebar { display: none; }
  329. #main { padding: 14px 12px; }
  330. }
  331. </style>
  332. </head>
  333. <body>
  334. <div id="app">
  335. <!-- ── Sidebar ────────────────────────────────────────── -->
  336. <div id="sidebar">
  337. <div id="sidebar-title">
  338. <img src="img/small_icon.png" onerror="this.style.display='none'">
  339. <span locale="sidebar/title">Scheduler</span>
  340. </div>
  341. <div id="sidebar-nav">
  342. <div class="nav-item active" id="nav-mine" onclick="showView('mine')">
  343. <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><path d="M5 4h7M5 8h7M5 12h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="2.5" cy="4" r="0.75" fill="currentColor"/><circle cx="2.5" cy="8" r="0.75" fill="currentColor"/><circle cx="2.5" cy="12" r="0.75" fill="currentColor"/></svg></span>
  344. <span locale="nav/mine">My Tasks</span>
  345. </div>
  346. <div class="nav-item" id="nav-all" onclick="showView('all')">
  347. <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><rect x="2" y="2" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/><rect x="9" y="2" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/><rect x="2" y="9" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/><rect x="9" y="9" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/></svg></span>
  348. <span locale="nav/all">All Tasks</span>
  349. </div>
  350. <div class="nav-item" id="nav-new" onclick="showView('new')">
  351. <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>
  352. <span locale="nav/new">New Task</span>
  353. </div>
  354. <div class="nav-item" id="nav-remove" onclick="showView('remove')">
  355. <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><path d="M3 5h10M6 5V4h4v1M6.5 8v4M9.5 8v4M4 5l.9 9h6.2L12 5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
  356. <span locale="nav/remove">Remove Task</span>
  357. </div>
  358. </div>
  359. <div id="sidebar-filter">
  360. <input id="filter-input" type="text" placeholder="filter/placeholder"
  361. oninput="applyFilter()" onkeydown="if(event.key==='Enter')applyFilter()">
  362. </div>
  363. </div>
  364. <!-- ── Main ──────────────────────────────────────────── -->
  365. <div id="main">
  366. <!-- My Tasks -->
  367. <div id="view-mine" class="view active">
  368. <div class="section-title" locale="view/mine/title">My Scheduled Tasks</div>
  369. <div class="card" id="card-mine">
  370. <div class="task-row header">
  371. <span locale="header/task-script">Task / Script</span>
  372. <span locale="header/path">Path</span>
  373. <span locale="header/app">App</span>
  374. <span locale="header/interval">Interval</span>
  375. </div>
  376. <div id="list-mine"><div class="empty-state" locale="js/loading">Loading…</div></div>
  377. </div>
  378. </div>
  379. <!-- All Tasks (admin) -->
  380. <div id="view-all" class="view">
  381. <div class="section-title" locale="view/all/title">All Scheduled Tasks</div>
  382. <div class="card">
  383. <div class="task-row all-cols header">
  384. <span locale="header/taskname">Task Name</span>
  385. <span locale="header/creator">Creator</span>
  386. <span locale="header/scriptpath">Script Path</span>
  387. <span locale="header/app">App</span>
  388. <span locale="header/interval">Interval</span>
  389. <span locale="header/basetime">Base Time</span>
  390. </div>
  391. <div id="list-all"><div class="empty-state" locale="js/loading">Loading…</div></div>
  392. </div>
  393. </div>
  394. <!-- New Task -->
  395. <div id="view-new" class="view">
  396. <div class="section-title" locale="view/new/title">New Scheduled Task</div>
  397. <div id="noPermMessage">
  398. <svg width="14" height="14" viewBox="0 0 16 16" fill="none" style="flex-shrink:0;margin-right:6px;vertical-align:-2px"><path d="M8 2L1.5 14h13L8 2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="12" r="0.8" fill="currentColor"/></svg>
  399. <span locale="noperm/message">Your account does not have permission to create scheduled tasks.
  400. Ask an administrator to grant cron job access via <strong>System Settings &rarr; Tasks Scheduler</strong>.</span>
  401. </div>
  402. <div id="newTaskForm">
  403. <div class="form-card">
  404. <div class="form-row">
  405. <label class="form-label"><span locale="form/taskname/label">Task Name</span> <span class="form-req">*</span></label>
  406. <input class="form-input" id="taskname" type="text"
  407. placeholder="taskname/placeholder" maxlength="32" autocomplete="off">
  408. <div class="form-hint" locale="form/taskname/hint">Max 32 characters, must be unique.</div>
  409. </div>
  410. <div class="form-row">
  411. <label class="form-label" locale="form/desc/label">Description</label>
  412. <input class="form-input" id="desc" type="text"
  413. placeholder="desc/placeholder" autocomplete="off">
  414. </div>
  415. <div class="form-row">
  416. <label class="form-label"><span locale="form/scriptpath/label">Script Path</span> <span class="form-req">*</span></label>
  417. <div class="input-with-btn">
  418. <input class="form-input" id="scriptpath" type="text"
  419. placeholder="scriptpath/placeholder" autocomplete="off"
  420. oninput="checkExt(this.value)">
  421. <button class="btn" onclick="openFileSelector()" locale="form/browse">Browse…</button>
  422. </div>
  423. <div class="warn-bar" id="extWarn">
  424. <svg width="13" height="13" viewBox="0 0 16 16" fill="none" style="flex-shrink:0;vertical-align:-2px;margin-right:4px"><path d="M8 2L1.5 14h13L8 2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="12" r="0.8" fill="currentColor"/></svg>
  425. <span locale="form/extwarn">This file extension may not be supported. Only .agi and .js files are executed.</span>
  426. </div>
  427. </div>
  428. <div class="form-row">
  429. <label class="form-label"><span locale="form/runevery/label">Run Every</span> <span class="form-req">*</span></label>
  430. <div class="form-row-inline">
  431. <input class="form-input" id="intervalvalue" type="number"
  432. value="1" min="1" style="max-width:90px;">
  433. <select class="form-input" id="intervalunit" style="max-width:160px;">
  434. <option value="60" locale="form/unit/minutes">Minutes</option>
  435. <option value="3600" locale="form/unit/hours">Hours</option>
  436. <option value="86400" selected locale="form/unit/days">Days</option>
  437. <option value="2628333" locale="form/unit/months">Months (approx.)</option>
  438. </select>
  439. </div>
  440. <div class="form-hint" locale="form/month/hint">Month is approximated as 2 628 333 seconds.</div>
  441. </div>
  442. <div class="form-row">
  443. <label class="form-label"><span locale="form/alignto/label">Align to</span> <span class="form-req">*</span></label>
  444. <select class="form-input" id="intervalbase">
  445. <option value="now" locale="form/base/now">Now (current minute)</option>
  446. <option value="hour" locale="form/base/hour">Start of the Hour</option>
  447. <option value="day" locale="form/base/day">Start of the Day</option>
  448. <option value="month" locale="form/base/month">Start of the Month</option>
  449. <option value="year" locale="form/base/year">Start of the Year</option>
  450. <option value="mon" locale="form/base/mon">Monday midnight</option>
  451. <option value="tue" locale="form/base/tue">Tuesday midnight</option>
  452. <option value="wed" locale="form/base/wed">Wednesday midnight</option>
  453. <option value="thu" locale="form/base/thu">Thursday midnight</option>
  454. <option value="fri" locale="form/base/fri">Friday midnight</option>
  455. <option value="sat" locale="form/base/sat">Saturday midnight</option>
  456. <option value="sun" locale="form/base/sun">Sunday midnight</option>
  457. </select>
  458. <div class="form-hint" locale="form/base/hint">Sets the reference point for interval alignment (useful for weekly/monthly schedules).</div>
  459. </div>
  460. <button class="btn primary" onclick="submitNewTask()" locale="form/submit">Create Task</button>
  461. <span style="margin-left:10px; font-size:12px; color:var(--text-muted);" locale="form/required/hint">
  462. Fields with <span class="form-req">*</span> are required.
  463. </span>
  464. </div>
  465. </div>
  466. </div>
  467. <!-- Remove Task -->
  468. <div id="view-remove" class="view">
  469. <div class="section-title" locale="view/remove/title">Remove Scheduled Tasks</div>
  470. <div class="card">
  471. <div class="danger-header"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" style="flex-shrink:0;vertical-align:-2px;margin-right:5px"><path d="M8 2L1.5 14h13L8 2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="12" r="0.8" fill="currentColor"/></svg><span locale="danger/message">Removal is permanent and cannot be undone.</span></div>
  472. <div class="task-row remove-cols header">
  473. <span locale="header/taskname">Task Name</span>
  474. <span locale="header/creator">Creator</span>
  475. <span locale="header/scriptpath">Script Path</span>
  476. <span locale="header/app">App</span>
  477. <span locale="header/interval">Interval</span>
  478. <span locale="header/action">Action</span>
  479. </div>
  480. <div id="list-remove"><div class="empty-state" locale="js/loading">Loading…</div></div>
  481. </div>
  482. </div>
  483. </div><!-- /#main -->
  484. </div>
  485. <div id="toast"></div>
  486. <script>
  487. /* ── Theme ── */
  488. ao_module_getSystemThemeColor(function(c) {
  489. document.body.classList.toggle('dark', c !== 'whiteTheme');
  490. });
  491. /* ── i18n helper ── */
  492. function t(key, fallback) {
  493. if (typeof applocale !== 'undefined') {
  494. return applocale.getString(key, fallback);
  495. }
  496. return fallback;
  497. }
  498. /* ── Navigation ── */
  499. var currentView = 'mine';
  500. function showView(name) {
  501. document.querySelectorAll('.view').forEach(function(el) { el.classList.remove('active'); });
  502. document.querySelectorAll('.nav-item').forEach(function(el) { el.classList.remove('active'); });
  503. document.getElementById('view-' + name).classList.add('active');
  504. document.getElementById('nav-' + name).classList.add('active');
  505. currentView = name;
  506. if (name === 'mine') loadMine();
  507. if (name === 'all') loadAll();
  508. if (name === 'new') initNewTaskView();
  509. if (name === 'remove') loadRemove();
  510. }
  511. /* ── Helpers ── */
  512. function fmtInterval(s) {
  513. s = Number(s);
  514. var d = Math.floor(s / 86400),
  515. h = Math.floor(s % 86400 / 3600),
  516. m = Math.floor(s % 3600 / 60),
  517. sec = s % 60;
  518. var parts = [];
  519. if (d) parts.push(d + 'd');
  520. if (h) parts.push(h + 'h');
  521. if (m) parts.push(m + 'min');
  522. if (sec) parts.push(sec + 's');
  523. return parts.length ? parts.join(' ') : s + 's';
  524. }
  525. function fmtTime(unix) {
  526. if (!unix) return '—';
  527. return moment.unix(unix).format('MMM D, YYYY HH:mm');
  528. }
  529. function appBadge(name) {
  530. if (!name) return '<span style="color:var(--text-muted)">—</span>';
  531. return '<span class="app-badge" title="' + esc(name) + '">' + esc(name) + '</span>';
  532. }
  533. function esc(s) {
  534. return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  535. }
  536. function filter() { return $('#filter-input').val().trim().toLowerCase(); }
  537. function matchFilter(task) {
  538. var f = filter();
  539. if (!f) return true;
  540. return (task.Name || '').toLowerCase().includes(f)
  541. || (task.Creator || '').toLowerCase().includes(f)
  542. || (task.ScriptVpath|| '').toLowerCase().includes(f)
  543. || (task.Description|| '').toLowerCase().includes(f)
  544. || (task.AppName || '').toLowerCase().includes(f);
  545. }
  546. function applyFilter() {
  547. if (currentView === 'mine') loadMine();
  548. if (currentView === 'all') loadAll();
  549. if (currentView === 'remove') loadRemove();
  550. }
  551. function toast(msg) {
  552. var el = document.getElementById('toast');
  553. el.textContent = msg;
  554. el.style.display = 'block';
  555. setTimeout(function() { el.style.display = 'none'; }, 2500);
  556. }
  557. /* ── My Tasks ── */
  558. function loadMine() {
  559. $.getJSON('../../system/arsm/aecron/list', function(data) {
  560. var el = document.getElementById('list-mine');
  561. var rows = (data || []).filter(matchFilter);
  562. if (!rows.length) {
  563. el.innerHTML = '<div class="empty-state">' + t('js/empty/notfound', 'No scheduled tasks found.') + '</div>';
  564. return;
  565. }
  566. el.innerHTML = rows.map(function(task) {
  567. var script = (task.ScriptVpath || '').split('/').pop();
  568. return '<div class="task-row">'
  569. + '<span class="task-name" title="' + esc(task.Name) + '">' + esc(task.Name)
  570. + (task.Description ? '<br><small style="color:var(--text-muted);font-weight:400">' + esc(task.Description) + '</small>' : '')
  571. + '</span>'
  572. + '<span class="task-script" title="' + esc(task.ScriptVpath) + '">' + esc(task.ScriptVpath) + '</span>'
  573. + '<span>' + appBadge(task.AppName) + '</span>'
  574. + '<span class="task-interval">' + t('js/task/every', 'Every ') + fmtInterval(task.ExecutionInterval) + '</span>'
  575. + '</div>';
  576. }).join('');
  577. });
  578. }
  579. /* ── All Tasks ── */
  580. function loadAll() {
  581. $.getJSON('../../system/arsm/aecron/list?listall=true', function(data) {
  582. var el = document.getElementById('list-all');
  583. var rows = (data || []).filter(matchFilter);
  584. if (!rows.length) {
  585. el.innerHTML = '<div class="empty-state">' + t('js/empty/notfound', 'No scheduled tasks found.') + '</div>';
  586. return;
  587. }
  588. el.innerHTML = rows.map(function(task) {
  589. return '<div class="task-row all-cols">'
  590. + '<span class="task-name" title="' + esc(task.Name) + '">' + esc(task.Name) + '</span>'
  591. + '<span style="color:var(--text-dim);font-size:12.5px">' + esc(task.Creator) + '</span>'
  592. + '<span class="task-script" title="' + esc(task.ScriptVpath) + '">' + esc(task.ScriptVpath) + '</span>'
  593. + '<span>' + appBadge(task.AppName) + '</span>'
  594. + '<span class="task-interval">' + t('js/task/every', 'Every ') + fmtInterval(task.ExecutionInterval) + '</span>'
  595. + '<span class="task-basetime">' + fmtTime(task.BaseTime) + '</span>'
  596. + '</div>';
  597. }).join('');
  598. });
  599. }
  600. /* ── Remove ── */
  601. function loadRemove() {
  602. $.getJSON('../../system/arsm/aecron/list?listall=true', function(data) {
  603. var el = document.getElementById('list-remove');
  604. var rows = (data || []).filter(matchFilter);
  605. if (!rows.length) {
  606. el.innerHTML = '<div class="empty-state">' + t('js/empty/none', 'No scheduled tasks.') + '</div>';
  607. return;
  608. }
  609. el.innerHTML = rows.map(function(task) {
  610. return '<div class="task-row remove-cols">'
  611. + '<span class="task-name" title="' + esc(task.Name) + '">' + esc(task.Name) + '</span>'
  612. + '<span style="color:var(--text-dim);font-size:12.5px">' + esc(task.Creator) + '</span>'
  613. + '<span class="task-script" title="' + esc(task.ScriptVpath) + '">' + esc(task.ScriptVpath) + '</span>'
  614. + '<span>' + appBadge(task.AppName) + '</span>'
  615. + '<span class="task-interval">' + t('js/task/every', 'Every ') + fmtInterval(task.ExecutionInterval) + '</span>'
  616. + '<span><button class="btn danger sm" onclick="removeTask(' + JSON.stringify(esc(task.Name)) + ')">' + t('js/btn/remove', 'Remove') + '</button></span>'
  617. + '</div>';
  618. }).join('');
  619. });
  620. }
  621. function removeTask(name) {
  622. if (!confirm(t('js/confirm/remove/pre', 'Remove task "') + name + t('js/confirm/remove/post', '"? This cannot be undone.'))) return;
  623. $.post('../../system/arsm/aecron/remove', { name: name }, function(data) {
  624. if (data && data.error) { alert(data.error); return; }
  625. toast(t('js/toast/removed', 'Task removed.'));
  626. loadRemove();
  627. });
  628. }
  629. /* ── New Task ── */
  630. function initNewTaskView() {
  631. $.getJSON('../../system/arsm/aecron/permission', function(data) {
  632. if (!data || !data.CanCreate) {
  633. document.getElementById('noPermMessage').style.display = 'block';
  634. document.getElementById('newTaskForm').style.display = 'none';
  635. } else {
  636. document.getElementById('noPermMessage').style.display = 'none';
  637. document.getElementById('newTaskForm').style.display = 'block';
  638. }
  639. });
  640. }
  641. function checkExt(val) {
  642. var ext = (val.split('.').pop() || '').toLowerCase();
  643. var warn = document.getElementById('extWarn');
  644. warn.style.display = (ext === 'agi' || ext === 'js' || val === '') ? 'none' : 'block';
  645. }
  646. // ao_module_openFileSelector requires a named function — it passes callback.name
  647. // as a string to the parent window. An anonymous function has name="" and fails.
  648. function scriptSelected(files) {
  649. if (!files || !files.length) return;
  650. var fp = files[0].filepath;
  651. document.getElementById('scriptpath').value = fp;
  652. checkExt(fp);
  653. }
  654. function openFileSelector() {
  655. ao_module_openFileSelector(scriptSelected, 'user:/', 'file', false);
  656. }
  657. function baseUnix(base) {
  658. switch (base) {
  659. case 'now': return moment().startOf('minute').unix();
  660. case 'hour': return moment().startOf('hour').unix();
  661. case 'day': return moment().startOf('day').unix();
  662. case 'month': return moment().startOf('month').unix();
  663. case 'year': return moment().startOf('year').unix();
  664. case 'mon': return moment().startOf('week').add(1,'days').unix();
  665. case 'tue': return moment().startOf('week').add(2,'days').unix();
  666. case 'wed': return moment().startOf('week').add(3,'days').unix();
  667. case 'thu': return moment().startOf('week').add(4,'days').unix();
  668. case 'fri': return moment().startOf('week').add(5,'days').unix();
  669. case 'sat': return moment().startOf('week').add(6,'days').unix();
  670. case 'sun': return moment().startOf('week').unix();
  671. default: return moment().startOf('minute').unix();
  672. }
  673. }
  674. function submitNewTask() {
  675. var name = $('#taskname').val().trim();
  676. var desc = $('#desc').val().trim();
  677. var scriptPath = $('#scriptpath').val().trim();
  678. var interval = parseFloat($('#intervalvalue').val()) * parseFloat($('#intervalunit').val());
  679. var base = baseUnix($('#intervalbase').val());
  680. if (!name) { alert(t('js/alert/noname', 'Task name is required.')); return; }
  681. if (!scriptPath) { alert(t('js/alert/nopath', 'Script path is required.')); return; }
  682. if (!interval || interval < 60) { alert(t('js/alert/interval', 'Interval must be at least 1 minute.')); return; }
  683. $.post('../../system/arsm/aecron/add', {
  684. name: name, desc: desc, path: scriptPath,
  685. interval: interval, base: base
  686. }, function(data) {
  687. if (data && data.error) { alert(data.error); return; }
  688. $('#taskname').val('');
  689. $('#desc').val('');
  690. $('#scriptpath').val('');
  691. toast(t('js/toast/created', 'Task created!'));
  692. showView('mine');
  693. });
  694. }
  695. /* ── Init ── */
  696. applocale.init("../locale/scheduler.json", function() {
  697. applocale.translate();
  698. loadMine();
  699. });
  700. </script>
  701. </body>
  702. </html>