opencode-supertask 0.1.28 → 0.1.30

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/dist/cli/index.js CHANGED
@@ -12547,8 +12547,8 @@ var require_luxon = __commonJS({
12547
12547
  * Set whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals
12548
12548
  * @type {boolean}
12549
12549
  */
12550
- static set throwOnInvalid(t) {
12551
- throwOnInvalid = t;
12550
+ static set throwOnInvalid(t2) {
12551
+ throwOnInvalid = t2;
12552
12552
  }
12553
12553
  /**
12554
12554
  * Reset Luxon's global caches. Should only be necessary in testing scenarios.
@@ -13441,7 +13441,7 @@ var require_luxon = __commonJS({
13441
13441
  }, tokens = _Formatter.parseFormat(fmt), realTokens = tokens.reduce((found, {
13442
13442
  literal,
13443
13443
  val
13444
- }) => literal ? found : found.concat(val), []), collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t) => t)), durationInfo = {
13444
+ }) => literal ? found : found.concat(val), []), collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t2) => t2)), durationInfo = {
13445
13445
  isNegativeDuration: collapsed < 0,
13446
13446
  // this relies on "collapsed" being based on "shiftTo", which builds up the object
13447
13447
  // in order
@@ -15351,15 +15351,15 @@ var require_luxon = __commonJS({
15351
15351
  return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
15352
15352
  }
15353
15353
  function unitForToken(token, loc) {
15354
- const one = digitRegex(loc), two = digitRegex(loc, "{2}"), three = digitRegex(loc, "{3}"), four = digitRegex(loc, "{4}"), six = digitRegex(loc, "{6}"), oneOrTwo = digitRegex(loc, "{1,2}"), oneToThree = digitRegex(loc, "{1,3}"), oneToSix = digitRegex(loc, "{1,6}"), oneToNine = digitRegex(loc, "{1,9}"), twoToFour = digitRegex(loc, "{2,4}"), fourToSix = digitRegex(loc, "{4,6}"), literal = (t) => ({
15355
- regex: RegExp(escapeToken(t.val)),
15354
+ const one = digitRegex(loc), two = digitRegex(loc, "{2}"), three = digitRegex(loc, "{3}"), four = digitRegex(loc, "{4}"), six = digitRegex(loc, "{6}"), oneOrTwo = digitRegex(loc, "{1,2}"), oneToThree = digitRegex(loc, "{1,3}"), oneToSix = digitRegex(loc, "{1,6}"), oneToNine = digitRegex(loc, "{1,9}"), twoToFour = digitRegex(loc, "{2,4}"), fourToSix = digitRegex(loc, "{4,6}"), literal = (t2) => ({
15355
+ regex: RegExp(escapeToken(t2.val)),
15356
15356
  deser: ([s2]) => s2,
15357
15357
  literal: true
15358
- }), unitate = (t) => {
15358
+ }), unitate = (t2) => {
15359
15359
  if (token.literal) {
15360
- return literal(t);
15360
+ return literal(t2);
15361
15361
  }
15362
- switch (t.val) {
15362
+ switch (t2.val) {
15363
15363
  // era
15364
15364
  case "G":
15365
15365
  return oneOf(loc.eras("short"), 0);
@@ -15474,7 +15474,7 @@ var require_luxon = __commonJS({
15474
15474
  case " ":
15475
15475
  return simple(/[^\S\n\r]/);
15476
15476
  default:
15477
- return literal(t);
15477
+ return literal(t2);
15478
15478
  }
15479
15479
  };
15480
15480
  const unit = unitate(token) || {
@@ -15676,15 +15676,15 @@ var require_luxon = __commonJS({
15676
15676
  return tokens;
15677
15677
  }
15678
15678
  function expandMacroTokens(tokens, locale) {
15679
- return Array.prototype.concat(...tokens.map((t) => maybeExpandMacroToken(t, locale)));
15679
+ return Array.prototype.concat(...tokens.map((t2) => maybeExpandMacroToken(t2, locale)));
15680
15680
  }
15681
15681
  var TokenParser = class {
15682
15682
  constructor(locale, format) {
15683
15683
  this.locale = locale;
15684
15684
  this.format = format;
15685
15685
  this.tokens = expandMacroTokens(Formatter.parseFormat(format), locale);
15686
- this.units = this.tokens.map((t) => unitForToken(t, locale));
15687
- this.disqualifyingUnit = this.units.find((t) => t.invalidReason);
15686
+ this.units = this.tokens.map((t2) => unitForToken(t2, locale));
15687
+ this.disqualifyingUnit = this.units.find((t2) => t2.invalidReason);
15688
15688
  if (!this.disqualifyingUnit) {
15689
15689
  const [regexString, handlers] = buildRegex(this.units);
15690
15690
  this.regex = RegExp(regexString, "i");
@@ -16509,7 +16509,7 @@ var require_luxon = __commonJS({
16509
16509
  */
16510
16510
  static parseFormatForOpts(formatOpts, localeOpts = {}) {
16511
16511
  const tokenList = formatOptsToTokens(formatOpts, Locale.fromObject(localeOpts));
16512
- return !tokenList ? null : tokenList.map((t) => t ? t.val : null).join("");
16512
+ return !tokenList ? null : tokenList.map((t2) => t2 ? t2.val : null).join("");
16513
16513
  }
16514
16514
  /**
16515
16515
  * Produce the the fully expanded format token for the locale
@@ -16520,7 +16520,7 @@ var require_luxon = __commonJS({
16520
16520
  */
16521
16521
  static expandFormat(fmt, localeOpts = {}) {
16522
16522
  const expanded = expandMacroTokens(Formatter.parseFormat(fmt), Locale.fromObject(localeOpts));
16523
- return expanded.map((t) => t.val).join("");
16523
+ return expanded.map((t2) => t2.val).join("");
16524
16524
  }
16525
16525
  static resetCache() {
16526
16526
  zoneOffsetTs = void 0;
@@ -19377,10 +19377,10 @@ var require_random = __commonJS({
19377
19377
  }
19378
19378
  function mulberry32(seed) {
19379
19379
  return () => {
19380
- let t = seed += 1831565813;
19381
- t = Math.imul(t ^ t >>> 15, t | 1);
19382
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
19383
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
19380
+ let t2 = seed += 1831565813;
19381
+ t2 = Math.imul(t2 ^ t2 >>> 15, t2 | 1);
19382
+ t2 ^= t2 + Math.imul(t2 ^ t2 >>> 7, t2 | 61);
19383
+ return ((t2 ^ t2 >>> 14) >>> 0) / 4294967296;
19384
19384
  };
19385
19385
  }
19386
19386
  function seededRandom(str) {
@@ -20749,6 +20749,7 @@ __export(pm2_exports, {
20749
20749
  isMacLaunchAgentConfigured: () => isMacLaunchAgentConfigured,
20750
20750
  isPm2Installed: () => isPm2Installed,
20751
20751
  resolveGatewayEntry: () => resolveGatewayEntry,
20752
+ resolvePm2SupervisorEntry: () => resolvePm2SupervisorEntry,
20752
20753
  restartGatewayAfterMaintenance: () => restartGatewayAfterMaintenance,
20753
20754
  stopGatewayForMaintenance: () => stopGatewayForMaintenance,
20754
20755
  uninstall: () => uninstall,
@@ -20906,15 +20907,17 @@ function launchAgentPath() {
20906
20907
  function launchctlBin() {
20907
20908
  return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
20908
20909
  }
20909
- function resolvePm2SupervisorEntry() {
20910
+ function resolvePm2SupervisorEntry(baseDir = __dirname) {
20910
20911
  const override = process.env.SUPERTASK_PM2_SUPERVISOR_ENTRY;
20911
20912
  if (override) {
20912
20913
  if (!existsSync5(override)) throw new Error(`[supertask] PM2 supervisor entry not found: ${override}`);
20913
20914
  return resolve3(override);
20914
20915
  }
20915
20916
  const candidates = [
20916
- join4(__dirname, "pm2-supervisor.js"),
20917
- join4(__dirname, "pm2-supervisor.ts")
20917
+ join4(baseDir, "pm2-supervisor.js"),
20918
+ join4(baseDir, "pm2-supervisor.ts"),
20919
+ join4(baseDir, "../daemon/pm2-supervisor.js"),
20920
+ join4(baseDir, "../daemon/pm2-supervisor.ts")
20918
20921
  ];
20919
20922
  const entry = candidates.find((candidate) => existsSync5(candidate));
20920
20923
  if (!entry) throw new Error(`[supertask] PM2 supervisor entry not found. Checked: ${candidates.join(", ")}`);
@@ -23688,7 +23691,7 @@ var init_request = __esm({
23688
23691
  });
23689
23692
 
23690
23693
  // node_modules/hono/dist/utils/html.js
23691
- var HtmlEscapedCallbackPhase, raw, escapeRe, stringBufferToString, escapeToBuffer, resolveCallbackSync, resolveCallback;
23694
+ var HtmlEscapedCallbackPhase, raw, resolveCallback;
23692
23695
  var init_html = __esm({
23693
23696
  "node_modules/hono/dist/utils/html.js"() {
23694
23697
  "use strict";
@@ -23703,80 +23706,6 @@ var init_html = __esm({
23703
23706
  escapedString.callbacks = callbacks;
23704
23707
  return escapedString;
23705
23708
  };
23706
- escapeRe = /[&<>'"]/;
23707
- stringBufferToString = async (buffer, callbacks) => {
23708
- let str = "";
23709
- callbacks ||= [];
23710
- const resolvedBuffer = await Promise.all(buffer);
23711
- for (let i = resolvedBuffer.length - 1; ; i--) {
23712
- str += resolvedBuffer[i];
23713
- i--;
23714
- if (i < 0) {
23715
- break;
23716
- }
23717
- let r = resolvedBuffer[i];
23718
- if (typeof r === "object") {
23719
- callbacks.push(...r.callbacks || []);
23720
- }
23721
- const isEscaped = r.isEscaped;
23722
- r = await (typeof r === "object" ? r.toString() : r);
23723
- if (typeof r === "object") {
23724
- callbacks.push(...r.callbacks || []);
23725
- }
23726
- if (r.isEscaped ?? isEscaped) {
23727
- str += r;
23728
- } else {
23729
- const buf = [str];
23730
- escapeToBuffer(r, buf);
23731
- str = buf[0];
23732
- }
23733
- }
23734
- return raw(str, callbacks);
23735
- };
23736
- escapeToBuffer = (str, buffer) => {
23737
- const match2 = str.search(escapeRe);
23738
- if (match2 === -1) {
23739
- buffer[0] += str;
23740
- return;
23741
- }
23742
- let escape;
23743
- let index2;
23744
- let lastIndex = 0;
23745
- for (index2 = match2; index2 < str.length; index2++) {
23746
- switch (str.charCodeAt(index2)) {
23747
- case 34:
23748
- escape = "&quot;";
23749
- break;
23750
- case 39:
23751
- escape = "&#39;";
23752
- break;
23753
- case 38:
23754
- escape = "&amp;";
23755
- break;
23756
- case 60:
23757
- escape = "&lt;";
23758
- break;
23759
- case 62:
23760
- escape = "&gt;";
23761
- break;
23762
- default:
23763
- continue;
23764
- }
23765
- buffer[0] += str.substring(lastIndex, index2) + escape;
23766
- lastIndex = index2 + 1;
23767
- }
23768
- buffer[0] += str.substring(lastIndex, index2);
23769
- };
23770
- resolveCallbackSync = (str) => {
23771
- const callbacks = str.callbacks;
23772
- if (!callbacks?.length) {
23773
- return str;
23774
- }
23775
- const buffer = [str];
23776
- const context = {};
23777
- callbacks.forEach((c) => c({ phase: HtmlEscapedCallbackPhase.Stringify, buffer, context }));
23778
- return buffer[0];
23779
- };
23780
23709
  resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
23781
23710
  if (typeof str === "object" && !(str instanceof String)) {
23782
23711
  if (!(str instanceof Promise)) {
@@ -24174,9 +24103,9 @@ var init_context = __esm({
24174
24103
  setDefaultContentType("application/json", headers)
24175
24104
  );
24176
24105
  };
24177
- html = (html2, arg, headers) => {
24178
- const res = (html22) => this.#newResponse(html22, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
24179
- return typeof html2 === "object" ? resolveCallback(html2, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html2);
24106
+ html = (html, arg, headers) => {
24107
+ const res = (html2) => this.#newResponse(html2, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
24108
+ return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
24180
24109
  };
24181
24110
  /**
24182
24111
  * `.redirect()` can Redirect, default status code is 302.
@@ -25349,46 +25278,816 @@ var init_dist = __esm({
25349
25278
  }
25350
25279
  });
25351
25280
 
25352
- // node_modules/hono/dist/helper/html/index.js
25353
- var html;
25354
- var init_html2 = __esm({
25355
- "node_modules/hono/dist/helper/html/index.js"() {
25281
+ // src/web/ui.ts
25282
+ function t(locale, key, values = {}) {
25283
+ const template = (locale === "en" ? EN : ZH)[key];
25284
+ return template.replace(/\{([a-zA-Z]+)\}/g, (_, name) => String(values[name] ?? `{${name}}`));
25285
+ }
25286
+ function statusText(locale, status) {
25287
+ const key = `status.${status}`;
25288
+ return key in ZH ? t(locale, key) : t(locale, "status.unknown");
25289
+ }
25290
+ function formatRelative(timestamp2, locale) {
25291
+ if (!timestamp2) return "\u2014";
25292
+ const deltaMs = timestamp2 - Date.now();
25293
+ const abs = Math.abs(deltaMs);
25294
+ const language = locale === "en" ? "en" : "zh-CN";
25295
+ const formatter = new Intl.RelativeTimeFormat(language, { numeric: "auto" });
25296
+ if (abs < 6e4) return formatter.format(Math.round(deltaMs / 1e3), "second");
25297
+ if (abs < 36e5) return formatter.format(Math.round(deltaMs / 6e4), "minute");
25298
+ if (abs < 864e5) return formatter.format(Math.round(deltaMs / 36e5), "hour");
25299
+ return formatter.format(Math.round(deltaMs / 864e5), "day");
25300
+ }
25301
+ function formatFuture(timestamp2, locale) {
25302
+ if (!timestamp2) return "\u2014";
25303
+ if (timestamp2 < Date.now()) return t(locale, "schedule.overdue");
25304
+ return formatRelative(timestamp2, locale);
25305
+ }
25306
+ function formatDateTime(value, locale) {
25307
+ if (!value) return "\u2014";
25308
+ const date = value instanceof Date ? value : new Date(value);
25309
+ return new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
25310
+ month: "short",
25311
+ day: "2-digit",
25312
+ hour: "2-digit",
25313
+ minute: "2-digit"
25314
+ }).format(date);
25315
+ }
25316
+ function icon(name, className = "icon") {
25317
+ const paths = {
25318
+ brand: '<path d="M7 4.5h10a2.5 2.5 0 0 1 2.5 2.5v10a2.5 2.5 0 0 1-2.5 2.5H7A2.5 2.5 0 0 1 4.5 17V7A2.5 2.5 0 0 1 7 4.5Z"/><path d="m8 12 2.4 2.4L16.5 8.5"/>',
25319
+ tasks: '<path d="M9 6h11M9 12h11M9 18h11"/><path d="M4 6h.01M4 12h.01M4 18h.01"/>',
25320
+ templates: '<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M16 3v4M8 3v4M3 10h18"/><path d="M12 14v3l2 1"/>',
25321
+ runs: '<path d="M4 19.5V4.5M4 19.5h16"/><path d="m7 15 3-4 3 2 5-6"/>',
25322
+ system: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-1.42 1.42-.06-.06a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.03 1.56V20h-2v-.48a1.7 1.7 0 0 0-1.03-1.56 1.7 1.7 0 0 0-1.88.34l-.06.06-1.42-1.42.06-.06A1.7 1.7 0 0 0 9.4 15a1.7 1.7 0 0 0-1.56-1.03H7.5v-2h.34A1.7 1.7 0 0 0 9.4 10a1.7 1.7 0 0 0-.34-1.88L9 8.06l1.42-1.42.06.06a1.7 1.7 0 0 0 1.88.34A1.7 1.7 0 0 0 13.4 5.5V5h2v.5a1.7 1.7 0 0 0 1.03 1.56 1.7 1.7 0 0 0 1.88-.34l.06-.06 1.42 1.42-.06.06A1.7 1.7 0 0 0 19.4 10a1.7 1.7 0 0 0 1.56 1.03h.54v2h-.54A1.7 1.7 0 0 0 19.4 15Z"/>',
25323
+ refresh: '<path d="M20 6v5h-5"/><path d="M18.5 15a7 7 0 1 1-.8-7.8L20 11"/>',
25324
+ search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/>',
25325
+ sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.42 1.42M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.42-1.42M17.66 6.34l1.41-1.41"/>',
25326
+ globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/>',
25327
+ chevronLeft: '<path d="m15 18-6-6 6-6"/>',
25328
+ chevronRight: '<path d="m9 18 6-6-6-6"/>',
25329
+ copy: '<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>',
25330
+ close: '<path d="m6 6 12 12M18 6 6 18"/>',
25331
+ inbox: '<path d="M4 4h16l2 12h-6l-2 3h-4l-2-3H2L4 4Z"/><path d="M8 9h8"/>',
25332
+ activity: '<path d="M3 12h4l2-6 4 12 2-6h6"/>',
25333
+ check: '<path d="m5 12 4 4L19 6"/>',
25334
+ alert: '<path d="M12 3 2.8 19h18.4L12 3Z"/><path d="M12 9v4M12 17h.01"/>',
25335
+ clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
25336
+ database: '<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.66 3.58 3 8 3s8-1.34 8-3V5M4 11v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6"/>'
25337
+ };
25338
+ return `<svg class="${className}" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${paths[name]}</svg>`;
25339
+ }
25340
+ function clientMessages(locale) {
25341
+ const keys = [
25342
+ "action.cancel",
25343
+ "action.confirm",
25344
+ "action.copy",
25345
+ "action.delete",
25346
+ "action.refresh",
25347
+ "action.logs",
25348
+ "action.hideLogs",
25349
+ "details.copySuccess",
25350
+ "feedback.copyFailed",
25351
+ "dialog.cancelTask",
25352
+ "dialog.cancelTaskBody",
25353
+ "dialog.retryTask",
25354
+ "dialog.retryTaskBody",
25355
+ "dialog.deleteTask",
25356
+ "dialog.deleteTaskBody",
25357
+ "dialog.disableTemplate",
25358
+ "dialog.disableTemplateBody",
25359
+ "dialog.deleteTemplate",
25360
+ "dialog.deleteTemplateBody",
25361
+ "dialog.triggerTemplate",
25362
+ "dialog.triggerTemplateBody",
25363
+ "dialog.clearTitle",
25364
+ "dialog.clearBody",
25365
+ "dialog.clearInstruction",
25366
+ "feedback.retryFailed",
25367
+ "feedback.cancelFailed",
25368
+ "feedback.deleteFailed",
25369
+ "feedback.requestFailed",
25370
+ "feedback.triggered",
25371
+ "feedback.configSaved",
25372
+ "feedback.databaseCleared",
25373
+ "filter.noResults"
25374
+ ];
25375
+ return Object.fromEntries(keys.map((key) => [key, t(locale, key)]));
25376
+ }
25377
+ function renderLayout(options) {
25378
+ const { locale, activeTab, body } = options;
25379
+ const page = PAGE_KEYS[activeTab];
25380
+ const nav = [
25381
+ { id: "tasks", href: "/", icon: "tasks" },
25382
+ { id: "templates", href: "/templates", icon: "templates" },
25383
+ { id: "runs", href: "/runs", icon: "runs" },
25384
+ { id: "system", href: "/system", icon: "system" }
25385
+ ];
25386
+ const ui = JSON.stringify(clientMessages(locale)).replace(/</g, "\\u003c");
25387
+ const language = locale === "en" ? "en" : "zh-CN";
25388
+ return `<!DOCTYPE html>
25389
+ <html lang="${language}">
25390
+ <head>
25391
+ <meta charset="UTF-8">
25392
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
25393
+ <meta name="color-scheme" content="light dark">
25394
+ <meta name="theme-color" content="#f5f7fb">
25395
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='2' y='2' width='20' height='20' rx='6' fill='%235957d9'/%3E%3Cpath d='m7 12 3 3 7-7' fill='none' stroke='white' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
25396
+ <title>${t(locale, page.title)} \xB7 SuperTask</title>
25397
+ <script>(function(){try{var p=localStorage.getItem('supertask-theme')||'system';var d=p==='system'?(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'):p;document.documentElement.dataset.theme=d;document.documentElement.dataset.themePreference=p}catch(e){}})();</script>
25398
+ ${STYLES}
25399
+ </head>
25400
+ <body>
25401
+ <a class="skip-link" href="#main">${t(locale, "a11y.skip")}</a>
25402
+ <div class="app-shell">
25403
+ <header class="topbar">
25404
+ <div class="brand">
25405
+ <div class="brand-mark">${icon("brand")}</div>
25406
+ <div>
25407
+ <div class="brand-name">${t(locale, "app.name")} <span>${t(locale, "app.dashboard")}</span></div>
25408
+ <div class="brand-tagline">${t(locale, "app.tagline")}</div>
25409
+ </div>
25410
+ </div>
25411
+ <div class="top-actions">
25412
+ <div class="local-chip"><span class="live-dot"></span>${t(locale, "app.local")}</div>
25413
+ <div class="control language-switch" role="group" aria-label="${t(locale, "language.label")}">
25414
+ <button type="button" class="${locale === "zh-CN" ? "active" : ""}" onclick="setLocale('zh-CN')" aria-pressed="${locale === "zh-CN"}">\u4E2D</button>
25415
+ <button type="button" class="${locale === "en" ? "active" : ""}" onclick="setLocale('en')" aria-pressed="${locale === "en"}">EN</button>
25416
+ </div>
25417
+ <button type="button" class="icon-button mobile-language" style="display:none" onclick="setLocale('${locale === "en" ? "zh-CN" : "en"}')" aria-label="${t(locale, "language.label")}" title="${t(locale, "language.label")}">${icon("globe")}</button>
25418
+ <label class="select-wrap" aria-label="${t(locale, "theme.label")}">
25419
+ ${icon("sun")}
25420
+ <select id="theme-select" class="control" onchange="setTheme(this.value)" title="${t(locale, "theme.label")}">
25421
+ <option value="system">${t(locale, "theme.system")}</option>
25422
+ <option value="light">${t(locale, "theme.light")}</option>
25423
+ <option value="dark">${t(locale, "theme.dark")}</option>
25424
+ </select>
25425
+ </label>
25426
+ <button type="button" class="icon-button" onclick="refreshPage(this)" aria-label="${t(locale, "action.refresh")}" title="${t(locale, "action.refresh")}">${icon("refresh")}</button>
25427
+ </div>
25428
+ </header>
25429
+ <nav class="tabs" aria-label="Primary">
25430
+ ${nav.map((item) => `<a href="${item.href}" class="${activeTab === item.id ? "active" : ""}" ${activeTab === item.id ? 'aria-current="page"' : ""}>${icon(item.icon)}<span>${t(locale, `nav.${item.id}`)}</span></a>`).join("")}
25431
+ </nav>
25432
+ <main id="main" tabindex="-1">
25433
+ <div class="page-heading reveal">
25434
+ <div><h1>${t(locale, page.title)}</h1><p>${t(locale, page.description)}</p></div>
25435
+ </div>
25436
+ ${body}
25437
+ </main>
25438
+ <footer>${t(locale, "app.footer")}</footer>
25439
+ </div>
25440
+ <div id="toast-region" class="toast-region" role="status" aria-live="polite"></div>
25441
+ <dialog id="detail-dialog">
25442
+ <div class="dialog-head"><div><h2>${t(locale, "details.title")}</h2><p>${t(locale, "details.subtitle")}</p></div><button class="icon-button" onclick="document.getElementById('detail-dialog').close()" aria-label="${t(locale, "action.close")}">${icon("close")}</button></div>
25443
+ <div class="dialog-body"><pre id="detail-content" class="json-view"></pre></div>
25444
+ <div class="dialog-actions"><button class="btn" onclick="copyDetails()">${icon("copy")}${t(locale, "action.copy")}</button><button class="btn btn-primary" onclick="document.getElementById('detail-dialog').close()">${t(locale, "action.close")}</button></div>
25445
+ </dialog>
25446
+ <dialog id="confirm-dialog">
25447
+ <div class="dialog-head"><div><h2 id="confirm-title"></h2></div><button class="icon-button" onclick="document.getElementById('confirm-dialog').close('cancel')" aria-label="${t(locale, "action.close")}">${icon("close")}</button></div>
25448
+ <div class="dialog-body"><p id="confirm-body" class="confirm-copy"></p></div>
25449
+ <div class="dialog-actions"><button class="btn" onclick="document.getElementById('confirm-dialog').close('cancel')">${t(locale, "action.cancel")}</button><button id="confirm-ok" class="btn btn-primary" onclick="document.getElementById('confirm-dialog').close('confirm')">${t(locale, "action.confirm")}</button></div>
25450
+ </dialog>
25451
+ <dialog id="danger-dialog">
25452
+ <div class="dialog-head"><div><h2>${t(locale, "dialog.clearTitle")}</h2></div><button class="icon-button" onclick="document.getElementById('danger-dialog').close('cancel')" aria-label="${t(locale, "action.close")}">${icon("close")}</button></div>
25453
+ <div class="dialog-body"><p class="confirm-copy"><strong>${t(locale, "dialog.clearBody")}</strong>${t(locale, "dialog.clearInstruction")}</p><input id="danger-confirmation" class="danger-input" autocomplete="off" spellcheck="false" placeholder="CLEAR" oninput="document.getElementById('danger-ok').disabled=this.value!=='CLEAR'"></div>
25454
+ <div class="dialog-actions"><button class="btn" onclick="document.getElementById('danger-dialog').close('cancel')">${t(locale, "action.cancel")}</button><button id="danger-ok" class="btn btn-danger" disabled onclick="document.getElementById('danger-dialog').close('confirm')">${t(locale, "action.clearDatabase")}</button></div>
25455
+ </dialog>
25456
+ <script>
25457
+ const UI=${ui};
25458
+ const text=(key,values={})=>(UI[key]||key).replace(/{([a-zA-Z]+)}/g,(_,name)=>String(values[name]??'{'+name+'}'));
25459
+ const themeMedia=matchMedia('(prefers-color-scheme: dark)');
25460
+ function applyTheme(preference){const resolved=preference==='system'?(themeMedia.matches?'dark':'light'):preference;document.documentElement.dataset.theme=resolved;document.documentElement.dataset.themePreference=preference;const select=document.getElementById('theme-select');if(select)select.value=preference;document.querySelector('meta[name="theme-color"]').content=resolved==='dark'?'#090d15':'#f5f7fb';}
25461
+ function setTheme(preference){localStorage.setItem('supertask-theme',preference);applyTheme(preference);}
25462
+ function setLocale(value){document.cookie='supertask_locale='+encodeURIComponent(value)+'; Path=/; Max-Age=31536000; SameSite=Lax';location.reload();}
25463
+ themeMedia.addEventListener?.('change',()=>{if((localStorage.getItem('supertask-theme')||'system')==='system')applyTheme('system');});
25464
+ applyTheme(localStorage.getItem('supertask-theme')||'system');
25465
+ requestAnimationFrame(()=>document.documentElement.classList.add('ui-ready'));
25466
+ function refreshPage(button){button.classList.add('refreshing');button.setAttribute('aria-label','${t(locale, "a11y.refreshing")}');location.reload();}
25467
+ function showToast(message,type='ok'){const region=document.getElementById('toast-region');const node=document.createElement('div');node.className='toast '+type;node.innerHTML=(type==='error'?'${icon("alert")}':'${icon("check")}')+'<span></span>';node.querySelector('span').textContent=message;region.appendChild(node);setTimeout(()=>{node.classList.add('leaving');setTimeout(()=>node.remove(),220)},3600);}
25468
+ async function readJson(response){const data=await response.json().catch(()=>({}));if(!response.ok)throw new Error(data.error||text('feedback.requestFailed'));return data;}
25469
+ async function ask(title,body,danger=false){const dialog=document.getElementById('confirm-dialog');document.getElementById('confirm-title').textContent=title;document.getElementById('confirm-body').textContent=body;document.getElementById('confirm-ok').className='btn '+(danger?'btn-danger':'btn-primary');return new Promise(resolve=>{dialog.addEventListener('close',()=>resolve(dialog.returnValue==='confirm'),{once:true});dialog.showModal();});}
25470
+ async function askDanger(){const dialog=document.getElementById('danger-dialog');const input=document.getElementById('danger-confirmation');input.value='';document.getElementById('danger-ok').disabled=true;return new Promise(resolve=>{dialog.addEventListener('close',()=>resolve(dialog.returnValue==='confirm'),{once:true});dialog.showModal();setTimeout(()=>input.focus(),50);});}
25471
+ async function retryTask(id){if(!await ask(text('dialog.retryTask',{id}),text('dialog.retryTaskBody')))return;try{await readJson(await fetch('/api/tasks/'+id+'/retry',{method:'POST'}));location.reload()}catch(error){showToast(text('feedback.retryFailed')+': '+error.message,'error')}}
25472
+ async function cancelTask(id){if(!await ask(text('dialog.cancelTask',{id}),text('dialog.cancelTaskBody'),true))return;try{await readJson(await fetch('/api/tasks/'+id+'/cancel',{method:'POST'}));location.reload()}catch(error){showToast(text('feedback.cancelFailed')+': '+error.message,'error')}}
25473
+ async function deleteTask(id){if(!await ask(text('dialog.deleteTask',{id}),text('dialog.deleteTaskBody'),true))return;try{await readJson(await fetch('/api/tasks/'+id,{method:'DELETE'}));location.reload()}catch(error){showToast(text('feedback.deleteFailed')+': '+error.message,'error')}}
25474
+ async function showRecord(url){try{const data=await readJson(await fetch(url));document.getElementById('detail-content').textContent=JSON.stringify(data,null,2);document.getElementById('detail-dialog').showModal()}catch(error){showToast(error.message,'error')}}
25475
+ const showDetail=id=>showRecord('/api/tasks/'+id);const showRunDetail=id=>showRecord('/api/runs/'+id);const showTemplateDetail=id=>showRecord('/api/templates/'+id);
25476
+ async function copyDetails(){try{await navigator.clipboard.writeText(document.getElementById('detail-content').textContent);showToast(text('details.copySuccess'))}catch{showToast(text('feedback.copyFailed'),'error')}}
25477
+ async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
25478
+ async function disableTmpl(id){if(!await ask(text('dialog.disableTemplate'),text('dialog.disableTemplateBody')))return;try{await readJson(await fetch('/api/templates/'+id+'/disable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
25479
+ async function deleteTmpl(id){if(!await ask(text('dialog.deleteTemplate'),text('dialog.deleteTemplateBody'),true))return;try{await readJson(await fetch('/api/templates/'+id,{method:'DELETE'}));location.reload()}catch(error){showToast(error.message,'error')}}
25480
+ async function triggerTmpl(id){if(!await ask(text('dialog.triggerTemplate'),text('dialog.triggerTemplateBody')))return;try{const data=await readJson(await fetch('/api/templates/'+id+'/trigger',{method:'POST'}));showToast(text('feedback.triggered',{id:data.taskId}));setTimeout(()=>location.reload(),550)}catch(error){showToast(error.message,'error')}}
25481
+ function toggleLog(id,button){const panel=document.getElementById('log-'+id);const hidden=!panel.hidden;panel.hidden=hidden;button.setAttribute('aria-expanded',String(!hidden));button.textContent=text(hidden?'action.logs':'action.hideLogs');}
25482
+ function filterTasks(value){const query=value.trim().toLocaleLowerCase();let visible=0;document.querySelectorAll('[data-task-row]').forEach(row=>{const match=!query||row.dataset.search.toLocaleLowerCase().includes(query);row.hidden=!match;if(match)visible++});const empty=document.getElementById('search-empty');if(empty)empty.hidden=visible!==0;}
25483
+ async function clearDatabase(){if(!await askDanger())return;try{const data=await readJson(await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})}));showToast(text('feedback.databaseCleared',{path:data.backupPath}));setTimeout(()=>location.reload(),1000)}catch(error){showToast(error.message,'error')}}
25484
+ async function saveConfig(){const form=document.getElementById('config-form');const data={worker:{maxConcurrency:Number(form.mc.value),pollIntervalMs:Number(form.pi.value),heartbeatIntervalMs:Number(form.hi.value)*1000,taskTimeoutMs:Number(form.to.value)*60000},scheduler:{enabled:form.se.checked,checkIntervalMs:Number(form.si.value)},watchdog:{heartbeatTimeoutMs:Number(form.wt.value)*1000,checkIntervalMs:Number(form.wci.value)*1000,cleanupIntervalMs:Number(form.wcl.value)*3600000,retentionDays:Number(form.rd.value)}};try{await readJson(await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}));showToast(text('feedback.configSaved'))}catch(error){showToast(error.message,'error')}}
25485
+ </script>
25486
+ </body>
25487
+ </html>`;
25488
+ }
25489
+ var ZH, EN, STYLES, PAGE_KEYS;
25490
+ var init_ui = __esm({
25491
+ "src/web/ui.ts"() {
25356
25492
  "use strict";
25357
- init_html();
25358
- html = (strings, ...values) => {
25359
- const buffer = [""];
25360
- for (let i = 0, len = strings.length - 1; i < len; i++) {
25361
- buffer[0] += strings[i];
25362
- const children = Array.isArray(values[i]) ? values[i].flat(Infinity) : [values[i]];
25363
- for (let i2 = 0, len2 = children.length; i2 < len2; i2++) {
25364
- const child = children[i2];
25365
- if (typeof child === "string") {
25366
- escapeToBuffer(child, buffer);
25367
- } else if (typeof child === "number") {
25368
- ;
25369
- buffer[0] += child;
25370
- } else if (typeof child === "boolean" || child === null || child === void 0) {
25371
- continue;
25372
- } else if (typeof child === "object" && child.isEscaped) {
25373
- if (child.callbacks) {
25374
- buffer.unshift("", child);
25375
- } else {
25376
- const tmp = child.toString();
25377
- if (tmp instanceof Promise) {
25378
- buffer.unshift("", tmp);
25379
- } else {
25380
- buffer[0] += tmp;
25381
- }
25382
- }
25383
- } else if (child instanceof Promise) {
25384
- buffer.unshift("", child);
25385
- } else {
25386
- escapeToBuffer(child.toString(), buffer);
25387
- }
25388
- }
25389
- }
25390
- buffer[0] += strings.at(-1);
25391
- return buffer.length === 1 ? "callbacks" in buffer ? raw(resolveCallbackSync(raw(buffer[0], buffer.callbacks))) : raw(buffer[0]) : stringBufferToString(buffer, buffer.callbacks);
25493
+ ZH = {
25494
+ "app.name": "SuperTask",
25495
+ "app.dashboard": "\u63A7\u5236\u53F0",
25496
+ "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u8C03\u5EA6\u4E2D\u5FC3",
25497
+ "app.local": "\u672C\u5730\u8FD0\u884C",
25498
+ "app.footer": "Local-first \xB7 \u6570\u636E\u7559\u5728\u4F60\u7684\u8BBE\u5907\u4E0A",
25499
+ "nav.tasks": "\u4EFB\u52A1",
25500
+ "nav.templates": "\u8C03\u5EA6",
25501
+ "nav.runs": "\u6267\u884C\u8BB0\u5F55",
25502
+ "nav.system": "\u7CFB\u7EDF",
25503
+ "page.tasks.title": "\u4EFB\u52A1\u961F\u5217",
25504
+ "page.tasks.description": "\u67E5\u770B\u4F18\u5148\u7EA7\u3001\u6267\u884C\u72B6\u6001\u4E0E\u91CD\u8BD5\u60C5\u51B5\uFF0C\u5FEB\u901F\u5904\u7406\u9700\u8981\u5173\u6CE8\u7684\u4EFB\u52A1\u3002",
25505
+ "page.templates.title": "\u8C03\u5EA6\u6A21\u677F",
25506
+ "page.templates.description": "\u7BA1\u7406 Cron\u3001\u5EF6\u8FDF\u548C\u5FAA\u73AF\u4EFB\u52A1\uFF0C\u8BA9\u91CD\u590D\u5DE5\u4F5C\u6309\u8BA1\u5212\u81EA\u52A8\u8FD0\u884C\u3002",
25507
+ "page.runs.title": "\u6267\u884C\u8BB0\u5F55",
25508
+ "page.runs.description": "\u8FFD\u8E2A\u6BCF\u6B21 Agent \u6267\u884C\u7684\u72B6\u6001\u3001\u8017\u65F6\u3001\u5FC3\u8DF3\u4E0E\u8F93\u51FA\u3002",
25509
+ "page.system.title": "\u7CFB\u7EDF\u8BBE\u7F6E",
25510
+ "page.system.description": "\u8C03\u6574 Gateway \u8FD0\u884C\u53C2\u6570\uFF0C\u68C0\u67E5\u5B9E\u65F6\u4EFB\u52A1\u5E76\u7BA1\u7406\u672C\u5730\u6570\u636E\u3002",
25511
+ "action.refresh": "\u5237\u65B0",
25512
+ "action.details": "\u8BE6\u60C5",
25513
+ "action.retry": "\u91CD\u8BD5",
25514
+ "action.cancel": "\u53D6\u6D88",
25515
+ "action.delete": "\u5220\u9664",
25516
+ "action.enable": "\u542F\u7528",
25517
+ "action.disable": "\u7981\u7528",
25518
+ "action.trigger": "\u7ACB\u5373\u89E6\u53D1",
25519
+ "action.logs": "\u67E5\u770B\u65E5\u5FD7",
25520
+ "action.hideLogs": "\u6536\u8D77\u65E5\u5FD7",
25521
+ "action.save": "\u4FDD\u5B58\u8BBE\u7F6E",
25522
+ "action.copy": "\u590D\u5236 JSON",
25523
+ "action.close": "\u5173\u95ED",
25524
+ "action.confirm": "\u786E\u8BA4",
25525
+ "action.clearDatabase": "\u6E05\u7A7A\u6570\u636E\u5E93",
25526
+ "status.pending": "\u5F85\u6267\u884C",
25527
+ "status.running": "\u8FD0\u884C\u4E2D",
25528
+ "status.done": "\u5DF2\u5B8C\u6210",
25529
+ "status.failed": "\u5931\u8D25",
25530
+ "status.dead_letter": "\u6B7B\u4FE1",
25531
+ "status.cancelled": "\u5DF2\u53D6\u6D88",
25532
+ "status.unknown": "\u672A\u77E5",
25533
+ "stats.total": "\u603B\u4EFB\u52A1",
25534
+ "stats.pending": "\u5F85\u6267\u884C",
25535
+ "stats.running": "\u8FD0\u884C\u4E2D",
25536
+ "stats.done": "\u5DF2\u5B8C\u6210",
25537
+ "stats.failedDead": "\u5931\u8D25\u4E0E\u6B7B\u4FE1",
25538
+ "stats.templates": "\u6A21\u677F\u603B\u6570",
25539
+ "stats.enabled": "\u5DF2\u542F\u7528",
25540
+ "stats.disabled": "\u5DF2\u7981\u7528",
25541
+ "stats.records": "\u6267\u884C\u603B\u6570",
25542
+ "stats.pageDone": "\u672C\u9875\u6210\u529F",
25543
+ "stats.pageFailed": "\u672C\u9875\u5931\u8D25",
25544
+ "stats.pageRunning": "\u672C\u9875\u8FD0\u884C\u4E2D",
25545
+ "filter.all": "\u5168\u90E8",
25546
+ "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001Agent \u6216\u63D0\u793A\u8BCD",
25547
+ "filter.noResults": "\u6CA1\u6709\u7B26\u5408\u5F53\u524D\u641C\u7D22\u6761\u4EF6\u7684\u4EFB\u52A1",
25548
+ "table.id": "ID",
25549
+ "table.task": "\u4EFB\u52A1",
25550
+ "table.name": "\u540D\u79F0",
25551
+ "table.agent": "Agent",
25552
+ "table.status": "\u72B6\u6001",
25553
+ "table.duration": "\u8017\u65F6",
25554
+ "table.retries": "\u91CD\u8BD5",
25555
+ "table.actions": "\u64CD\u4F5C",
25556
+ "table.type": "\u7C7B\u578B",
25557
+ "table.rule": "\u89C4\u5219",
25558
+ "table.lastRun": "\u4E0A\u6B21\u6267\u884C",
25559
+ "table.nextRun": "\u4E0B\u6B21\u6267\u884C",
25560
+ "table.run": "Run",
25561
+ "table.heartbeat": "\u5FC3\u8DF3",
25562
+ "table.session": "Session",
25563
+ "table.model": "\u6A21\u578B",
25564
+ "table.startedAt": "\u542F\u52A8\u65F6\u95F4",
25565
+ "table.pid": "PID",
25566
+ "pagination.previous": "\u4E0A\u4E00\u9875",
25567
+ "pagination.next": "\u4E0B\u4E00\u9875",
25568
+ "pagination.summary": "\u7B2C {page} \u9875\uFF0C\u5171 {pages} \u9875 \xB7 {total} \u6761",
25569
+ "empty.tasks": "\u961F\u5217\u91CC\u8FD8\u6CA1\u6709\u4EFB\u52A1",
25570
+ "empty.tasksHint": "\u901A\u8FC7 OpenCode \u63D2\u4EF6\u6216 supertask add \u521B\u5EFA\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u3002",
25571
+ "empty.templates": "\u8FD8\u6CA1\u6709\u8C03\u5EA6\u6A21\u677F",
25572
+ "empty.templatesHint": "\u4F7F\u7528 CLI \u521B\u5EFA\uFF1Asupertask template add",
25573
+ "empty.runs": "\u8FD8\u6CA1\u6709\u6267\u884C\u8BB0\u5F55",
25574
+ "empty.running": "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1",
25575
+ "schedule.cron": "Cron",
25576
+ "schedule.recurring": "\u5FAA\u73AF",
25577
+ "schedule.delayed": "\u5EF6\u8FDF",
25578
+ "schedule.unknown": "\u672A\u77E5",
25579
+ "schedule.enabled": "\u5DF2\u542F\u7528",
25580
+ "schedule.disabled": "\u5DF2\u7981\u7528",
25581
+ "schedule.minutes": "{count} \u5206\u949F",
25582
+ "schedule.seconds": "{count} \u79D2",
25583
+ "schedule.hours": "{count} \u5C0F\u65F6",
25584
+ "schedule.days": "{count} \u5929",
25585
+ "schedule.overdue": "\u5DF2\u5230\u671F",
25586
+ "system.worker": "Worker",
25587
+ "system.scheduler": "Scheduler",
25588
+ "system.watchdog": "Watchdog",
25589
+ "system.maxConcurrency": "\u6700\u5927\u5E76\u53D1",
25590
+ "system.pollInterval": "\u8F6E\u8BE2\u95F4\u9694",
25591
+ "system.heartbeatInterval": "\u5FC3\u8DF3\u95F4\u9694",
25592
+ "system.taskTimeout": "\u4EFB\u52A1\u8D85\u65F6",
25593
+ "system.schedulerEnabled": "\u542F\u7528\u8C03\u5EA6",
25594
+ "system.checkInterval": "\u68C0\u67E5\u95F4\u9694",
25595
+ "system.heartbeatTimeout": "\u5FC3\u8DF3\u8D85\u65F6",
25596
+ "system.cleanupInterval": "\u6E05\u7406\u95F4\u9694",
25597
+ "system.retentionDays": "\u6570\u636E\u4FDD\u7559",
25598
+ "system.milliseconds": "\u6BEB\u79D2",
25599
+ "system.seconds": "\u79D2",
25600
+ "system.minutes": "\u5206\u949F",
25601
+ "system.hours": "\u5C0F\u65F6",
25602
+ "system.days": "\u5929",
25603
+ "system.activeTemplates": "\u6D3B\u8DC3\u6A21\u677F",
25604
+ "system.saveHint": "\u4FDD\u5B58\u540E\u9700\u91CD\u542F Gateway \u751F\u6548",
25605
+ "system.runningTasks": "\u5F53\u524D\u8FD0\u884C\u4EFB\u52A1\uFF08{running} / {limit} \u5E76\u53D1\uFF09",
25606
+ "system.taskStats": "\u4EFB\u52A1\u6982\u89C8",
25607
+ "system.configFile": "\u914D\u7F6E\u6587\u4EF6",
25608
+ "system.path": "\u8DEF\u5F84",
25609
+ "system.fileExists": "\u6587\u4EF6\u5B58\u5728",
25610
+ "system.yes": "\u662F",
25611
+ "system.noDefault": "\u5426\uFF0C\u5F53\u524D\u4F7F\u7528\u9ED8\u8BA4\u503C",
25612
+ "system.danger": "\u5371\u9669\u64CD\u4F5C",
25613
+ "system.dangerDescription": "\u7CFB\u7EDF\u4F1A\u5148\u521B\u5EFA\u53EF\u6821\u9A8C\u5907\u4EFD\uFF0C\u518D\u4E8B\u52A1\u6027\u6E05\u7A7A\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\uFF1B\u5B58\u5728\u8FD0\u884C\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u64CD\u4F5C\u3002",
25614
+ "theme.label": "\u4E3B\u9898",
25615
+ "theme.system": "\u8DDF\u968F\u7CFB\u7EDF",
25616
+ "theme.light": "\u6D45\u8272",
25617
+ "theme.dark": "\u6DF1\u8272",
25618
+ "language.label": "\u8BED\u8A00",
25619
+ "details.title": "\u6570\u636E\u8BE6\u60C5",
25620
+ "details.subtitle": "\u539F\u59CB\u8BB0\u5F55\uFF08JSON\uFF09",
25621
+ "details.copySuccess": "JSON \u5DF2\u590D\u5236",
25622
+ "dialog.cancelTask": "\u53D6\u6D88\u4EFB\u52A1 #{id}\uFF1F",
25623
+ "dialog.cancelTaskBody": "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u5728\u4E0B\u4E00\u4E2A\u8F6E\u8BE2\u5468\u671F\u7EC8\u6B62\u5BF9\u5E94\u8FDB\u7A0B\u6811\u3002",
25624
+ "dialog.retryTask": "\u91CD\u8BD5\u4EFB\u52A1 #{id}\uFF1F",
25625
+ "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
25626
+ "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
25627
+ "dialog.deleteTaskBody": "\u4EFB\u52A1\u53CA\u5173\u8054\u6267\u884C\u8BB0\u5F55\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
25628
+ "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
25629
+ "dialog.disableTemplateBody": "\u6A21\u677F\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
25630
+ "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
25631
+ "dialog.deleteTemplateBody": "\u6A21\u677F\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
25632
+ "dialog.triggerTemplate": "\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21\uFF1F",
25633
+ "dialog.triggerTemplateBody": "\u7CFB\u7EDF\u4F1A\u6309\u5F53\u524D\u6A21\u677F\u521B\u5EFA\u4E00\u4E2A\u65B0\u4EFB\u52A1\uFF0C\u5E76\u9075\u5B88\u6700\u5927\u5B9E\u4F8B\u9650\u5236\u3002",
25634
+ "dialog.clearTitle": "\u786E\u8BA4\u6E05\u7A7A\u6570\u636E\u5E93",
25635
+ "dialog.clearBody": "\u8FD9\u4F1A\u5220\u9664\u5168\u90E8\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u5907\u4EFD\u3002",
25636
+ "dialog.clearInstruction": "\u8F93\u5165 CLEAR \u4EE5\u786E\u8BA4",
25637
+ "feedback.retryFailed": "\u91CD\u8BD5\u5931\u8D25",
25638
+ "feedback.cancelFailed": "\u53D6\u6D88\u5931\u8D25",
25639
+ "feedback.deleteFailed": "\u5220\u9664\u5931\u8D25",
25640
+ "feedback.requestFailed": "\u8BF7\u6C42\u5931\u8D25",
25641
+ "feedback.triggered": "\u5DF2\u521B\u5EFA\u4EFB\u52A1 #{id}",
25642
+ "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F Gateway \u540E\u751F\u6548",
25643
+ "feedback.databaseCleared": "\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u5907\u4EFD\u4F4D\u4E8E\uFF1A{path}",
25644
+ "feedback.copyFailed": "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u5185\u5BB9",
25645
+ "a11y.skip": "\u8DF3\u5230\u4E3B\u8981\u5185\u5BB9",
25646
+ "a11y.refreshing": "\u6B63\u5728\u5237\u65B0"
25647
+ };
25648
+ EN = {
25649
+ "app.name": "SuperTask",
25650
+ "app.dashboard": "Dashboard",
25651
+ "app.tagline": "Reliable local agent orchestration",
25652
+ "app.local": "Running locally",
25653
+ "app.footer": "Local-first \xB7 Your data stays on this device",
25654
+ "nav.tasks": "Tasks",
25655
+ "nav.templates": "Schedules",
25656
+ "nav.runs": "Runs",
25657
+ "nav.system": "System",
25658
+ "page.tasks.title": "Task queue",
25659
+ "page.tasks.description": "Track priority, execution state, and retries, then act on tasks that need attention.",
25660
+ "page.templates.title": "Schedule templates",
25661
+ "page.templates.description": "Manage cron, delayed, and recurring work so repeated tasks run on time.",
25662
+ "page.runs.title": "Execution history",
25663
+ "page.runs.description": "Inspect the status, duration, heartbeat, and output of every agent run.",
25664
+ "page.system.title": "System settings",
25665
+ "page.system.description": "Tune Gateway behavior, inspect active work, and manage local data.",
25666
+ "action.refresh": "Refresh",
25667
+ "action.details": "Details",
25668
+ "action.retry": "Retry",
25669
+ "action.cancel": "Cancel",
25670
+ "action.delete": "Delete",
25671
+ "action.enable": "Enable",
25672
+ "action.disable": "Disable",
25673
+ "action.trigger": "Run now",
25674
+ "action.logs": "View log",
25675
+ "action.hideLogs": "Hide log",
25676
+ "action.save": "Save settings",
25677
+ "action.copy": "Copy JSON",
25678
+ "action.close": "Close",
25679
+ "action.confirm": "Confirm",
25680
+ "action.clearDatabase": "Clear database",
25681
+ "status.pending": "Pending",
25682
+ "status.running": "Running",
25683
+ "status.done": "Done",
25684
+ "status.failed": "Failed",
25685
+ "status.dead_letter": "Dead letter",
25686
+ "status.cancelled": "Cancelled",
25687
+ "status.unknown": "Unknown",
25688
+ "stats.total": "Total tasks",
25689
+ "stats.pending": "Pending",
25690
+ "stats.running": "Running",
25691
+ "stats.done": "Completed",
25692
+ "stats.failedDead": "Failed & dead",
25693
+ "stats.templates": "Templates",
25694
+ "stats.enabled": "Enabled",
25695
+ "stats.disabled": "Disabled",
25696
+ "stats.records": "Total runs",
25697
+ "stats.pageDone": "Succeeded here",
25698
+ "stats.pageFailed": "Failed here",
25699
+ "stats.pageRunning": "Running here",
25700
+ "filter.all": "All",
25701
+ "filter.searchTasks": "Search tasks, agents, or prompts on this page",
25702
+ "filter.noResults": "No tasks match this search",
25703
+ "table.id": "ID",
25704
+ "table.task": "Task",
25705
+ "table.name": "Name",
25706
+ "table.agent": "Agent",
25707
+ "table.status": "Status",
25708
+ "table.duration": "Duration",
25709
+ "table.retries": "Retries",
25710
+ "table.actions": "Actions",
25711
+ "table.type": "Type",
25712
+ "table.rule": "Rule",
25713
+ "table.lastRun": "Last run",
25714
+ "table.nextRun": "Next run",
25715
+ "table.run": "Run",
25716
+ "table.heartbeat": "Heartbeat",
25717
+ "table.session": "Session",
25718
+ "table.model": "Model",
25719
+ "table.startedAt": "Started",
25720
+ "table.pid": "PID",
25721
+ "pagination.previous": "Previous",
25722
+ "pagination.next": "Next",
25723
+ "pagination.summary": "Page {page} of {pages} \xB7 {total} items",
25724
+ "empty.tasks": "Your queue is empty",
25725
+ "empty.tasksHint": "Create the first task with the OpenCode plugin or supertask add.",
25726
+ "empty.templates": "No schedule templates yet",
25727
+ "empty.templatesHint": "Create one with: supertask template add",
25728
+ "empty.runs": "No execution history yet",
25729
+ "empty.running": "No tasks are running right now",
25730
+ "schedule.cron": "Cron",
25731
+ "schedule.recurring": "Recurring",
25732
+ "schedule.delayed": "Delayed",
25733
+ "schedule.unknown": "Unknown",
25734
+ "schedule.enabled": "Enabled",
25735
+ "schedule.disabled": "Disabled",
25736
+ "schedule.minutes": "{count} min",
25737
+ "schedule.seconds": "{count} sec",
25738
+ "schedule.hours": "{count} hr",
25739
+ "schedule.days": "{count} days",
25740
+ "schedule.overdue": "Overdue",
25741
+ "system.worker": "Worker",
25742
+ "system.scheduler": "Scheduler",
25743
+ "system.watchdog": "Watchdog",
25744
+ "system.maxConcurrency": "Max concurrency",
25745
+ "system.pollInterval": "Poll interval",
25746
+ "system.heartbeatInterval": "Heartbeat interval",
25747
+ "system.taskTimeout": "Task timeout",
25748
+ "system.schedulerEnabled": "Enable scheduler",
25749
+ "system.checkInterval": "Check interval",
25750
+ "system.heartbeatTimeout": "Heartbeat timeout",
25751
+ "system.cleanupInterval": "Cleanup interval",
25752
+ "system.retentionDays": "Data retention",
25753
+ "system.milliseconds": "ms",
25754
+ "system.seconds": "seconds",
25755
+ "system.minutes": "minutes",
25756
+ "system.hours": "hours",
25757
+ "system.days": "days",
25758
+ "system.activeTemplates": "Active templates",
25759
+ "system.saveHint": "Restart Gateway to apply saved settings",
25760
+ "system.runningTasks": "Active tasks ({running} / {limit} concurrent)",
25761
+ "system.taskStats": "Task overview",
25762
+ "system.configFile": "Configuration file",
25763
+ "system.path": "Path",
25764
+ "system.fileExists": "File exists",
25765
+ "system.yes": "Yes",
25766
+ "system.noDefault": "No, using defaults",
25767
+ "system.danger": "Danger zone",
25768
+ "system.dangerDescription": "A verified backup is created first, then tasks, runs, and templates are cleared transactionally. Active work blocks this operation.",
25769
+ "theme.label": "Theme",
25770
+ "theme.system": "System",
25771
+ "theme.light": "Light",
25772
+ "theme.dark": "Dark",
25773
+ "language.label": "Language",
25774
+ "details.title": "Data details",
25775
+ "details.subtitle": "Raw record (JSON)",
25776
+ "details.copySuccess": "JSON copied",
25777
+ "dialog.cancelTask": "Cancel task #{id}?",
25778
+ "dialog.cancelTaskBody": "A running task will terminate its process tree on the next worker poll.",
25779
+ "dialog.retryTask": "Retry task #{id}?",
25780
+ "dialog.retryTaskBody": "The task returns to pending and its automatic retry budget is reset.",
25781
+ "dialog.deleteTask": "Delete task #{id}?",
25782
+ "dialog.deleteTaskBody": "The task and its execution history will be permanently deleted.",
25783
+ "dialog.disableTemplate": "Disable this schedule?",
25784
+ "dialog.disableTemplateBody": "It will stop creating new tasks automatically. Existing tasks are unchanged.",
25785
+ "dialog.deleteTemplate": "Delete this schedule?",
25786
+ "dialog.deleteTemplateBody": "This template configuration will be permanently deleted.",
25787
+ "dialog.triggerTemplate": "Run this schedule now?",
25788
+ "dialog.triggerTemplateBody": "A new task is created from the template, subject to its instance limit.",
25789
+ "dialog.clearTitle": "Confirm database clear",
25790
+ "dialog.clearBody": "This deletes every task, run, and schedule template after creating a backup.",
25791
+ "dialog.clearInstruction": "Type CLEAR to confirm",
25792
+ "feedback.retryFailed": "Retry failed",
25793
+ "feedback.cancelFailed": "Cancellation failed",
25794
+ "feedback.deleteFailed": "Delete failed",
25795
+ "feedback.requestFailed": "Request failed",
25796
+ "feedback.triggered": "Task #{id} created",
25797
+ "feedback.configSaved": "Settings saved. Restart Gateway to apply them.",
25798
+ "feedback.databaseCleared": "Database cleared. Backup: {path}",
25799
+ "feedback.copyFailed": "Copy failed. Select the content manually.",
25800
+ "a11y.skip": "Skip to main content",
25801
+ "a11y.refreshing": "Refreshing"
25802
+ };
25803
+ STYLES = `
25804
+ <style>
25805
+ :root {
25806
+ color-scheme: light;
25807
+ --bg:#f5f7fb; --bg-glow:rgba(99,102,241,.12); --surface:#ffffff; --surface-2:#f8fafc;
25808
+ --surface-3:#eef2f7; --text:#172033; --text-2:#58657a; --text-3:#8792a5;
25809
+ --border:#dfe5ee; --border-strong:#cbd4e1; --primary:#5957d9; --primary-hover:#4846c7;
25810
+ --primary-soft:#eeedff; --green:#15805d; --green-soft:#e7f7f0; --red:#c63f4f; --red-soft:#fdecef;
25811
+ --yellow:#a66608; --yellow-soft:#fff4d9; --blue:#2563b8; --blue-soft:#e8f1ff; --purple:#7552c8;
25812
+ --shadow-sm:0 1px 2px rgba(16,24,40,.04); --shadow-md:0 12px 30px rgba(30,41,59,.08);
25813
+ --shadow-lg:0 24px 60px rgba(30,41,59,.18); --radius:14px; --radius-sm:9px;
25814
+ --focus:0 0 0 3px rgba(89,87,217,.22);
25815
+ }
25816
+ :root[data-theme="dark"] {
25817
+ color-scheme: dark;
25818
+ --bg:#090d15; --bg-glow:rgba(99,102,241,.18); --surface:#111722; --surface-2:#151d2a;
25819
+ --surface-3:#1c2635; --text:#edf2f8; --text-2:#a6b1c2; --text-3:#738095;
25820
+ --border:#273244; --border-strong:#344258; --primary:#8b87ff; --primary-hover:#a19eff;
25821
+ --primary-soft:#242347; --green:#48c78e; --green-soft:#16362b; --red:#ff7180; --red-soft:#3b1e27;
25822
+ --yellow:#f0b34b; --yellow-soft:#392d18; --blue:#6ea8ff; --blue-soft:#192d4b; --purple:#b89cff;
25823
+ --shadow-sm:0 1px 2px rgba(0,0,0,.25); --shadow-md:0 16px 36px rgba(0,0,0,.28);
25824
+ --shadow-lg:0 28px 70px rgba(0,0,0,.45); --focus:0 0 0 3px rgba(139,135,255,.25);
25825
+ }
25826
+ * { box-sizing:border-box; }
25827
+ html { min-height:100%; background:var(--bg); }
25828
+ body { min-height:100vh; margin:0; color:var(--text); background:
25829
+ radial-gradient(circle at 10% -10%,var(--bg-glow),transparent 34rem),var(--bg);
25830
+ font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
25831
+ font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; }
25832
+ button,input,select { font:inherit; }
25833
+ button,a,select,input { -webkit-tap-highlight-color:transparent; }
25834
+ a { color:inherit; }
25835
+ code,.mono,.m { font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace; }
25836
+ .skip-link { position:fixed; left:16px; top:-60px; z-index:200; padding:10px 14px; border-radius:8px;
25837
+ color:#fff; background:var(--primary); transition:top .2s ease; }
25838
+ .skip-link:focus { top:16px; }
25839
+ .app-shell { width:min(1440px,100%); margin:0 auto; padding:0 28px 28px; }
25840
+ .topbar { min-height:80px; display:flex; align-items:center; justify-content:space-between; gap:20px;
25841
+ border-bottom:1px solid var(--border); }
25842
+ .brand { display:flex; align-items:center; gap:12px; min-width:0; }
25843
+ .brand-mark { width:38px; height:38px; display:grid; place-items:center; color:#fff; border-radius:11px;
25844
+ background:linear-gradient(145deg,#7773ff,#514ec8); box-shadow:0 9px 22px rgba(89,87,217,.28); }
25845
+ .brand-mark .icon { width:23px; height:23px; }
25846
+ .brand-name { display:flex; align-items:baseline; gap:7px; font-size:17px; font-weight:760; letter-spacing:-.025em; }
25847
+ .brand-name span { color:var(--text-3); font-size:12px; font-weight:650; letter-spacing:.02em; text-transform:uppercase; }
25848
+ .brand-tagline { color:var(--text-2); font-size:12px; margin-top:1px; }
25849
+ .top-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
25850
+ .local-chip { display:inline-flex; align-items:center; gap:7px; color:var(--text-2); font-size:12px; padding:7px 10px;
25851
+ border:1px solid var(--border); border-radius:999px; background:color-mix(in srgb,var(--surface) 78%,transparent); }
25852
+ .live-dot { width:7px; height:7px; border-radius:50%; background:var(--green); box-shadow:0 0 0 4px var(--green-soft); }
25853
+ .control { height:36px; border:1px solid var(--border); border-radius:9px; background:var(--surface);
25854
+ color:var(--text-2); box-shadow:var(--shadow-sm); }
25855
+ .select-wrap { position:relative; display:flex; align-items:center; }
25856
+ .select-wrap>.icon { width:15px; height:15px; position:absolute; left:10px; pointer-events:none; color:var(--text-3); }
25857
+ .select-wrap select { appearance:none; padding:0 29px 0 31px; cursor:pointer; outline:none; }
25858
+ .select-wrap::after { content:""; position:absolute; right:11px; width:6px; height:6px; border-right:1.5px solid currentColor;
25859
+ border-bottom:1.5px solid currentColor; transform:rotate(45deg) translateY(-2px); pointer-events:none; color:var(--text-3); }
25860
+ .language-switch { display:flex; padding:3px; gap:2px; }
25861
+ .language-switch button { height:28px; min-width:34px; padding:0 8px; border:0; border-radius:6px; color:var(--text-3);
25862
+ background:transparent; cursor:pointer; font-size:12px; font-weight:650; }
25863
+ .language-switch button.active { background:var(--surface-3); color:var(--text); }
25864
+ .icon-button { width:36px; height:36px; display:grid; place-items:center; border:1px solid var(--border); border-radius:9px;
25865
+ color:var(--text-2); background:var(--surface); box-shadow:var(--shadow-sm); cursor:pointer; }
25866
+ .icon-button .icon { width:17px; height:17px; }
25867
+ .icon-button:hover,.control:hover { border-color:var(--border-strong); color:var(--text); }
25868
+ .icon-button.refreshing .icon { animation:spin .7s linear infinite; }
25869
+ .tabs { display:flex; gap:6px; margin:18px 0 30px; padding:5px; width:max-content; max-width:100%; overflow-x:auto;
25870
+ border:1px solid var(--border); border-radius:12px; background:color-mix(in srgb,var(--surface) 82%,transparent); box-shadow:var(--shadow-sm); }
25871
+ .tabs a { display:flex; align-items:center; gap:8px; min-height:36px; padding:0 14px; border-radius:8px; color:var(--text-2);
25872
+ font-weight:650; font-size:13px; text-decoration:none; white-space:nowrap; transition:background .16s ease,color .16s ease,box-shadow .16s ease; }
25873
+ .tabs a .icon { width:16px; height:16px; }
25874
+ .tabs a:hover { color:var(--text); background:var(--surface-2); }
25875
+ .tabs a.active { color:var(--primary); background:var(--surface); box-shadow:0 1px 4px rgba(16,24,40,.08); }
25876
+ main { outline:none; }
25877
+ .page-heading { display:flex; justify-content:space-between; align-items:flex-end; gap:20px; margin-bottom:22px; }
25878
+ .page-heading h1 { margin:0; font-size:28px; line-height:1.2; letter-spacing:-.035em; }
25879
+ .page-heading p { margin:7px 0 0; color:var(--text-2); max-width:720px; }
25880
+ .stats-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:14px; margin-bottom:20px; }
25881
+ .stats-grid.three { grid-template-columns:repeat(3,minmax(0,1fr)); }
25882
+ .stat-card { position:relative; min-height:118px; padding:19px; overflow:hidden; border:1px solid var(--border); border-radius:var(--radius);
25883
+ background:linear-gradient(145deg,var(--surface),color-mix(in srgb,var(--surface-2) 72%,var(--surface)));
25884
+ box-shadow:var(--shadow-sm); transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease; }
25885
+ .stat-card:hover { transform:translateY(-2px); border-color:var(--border-strong); box-shadow:var(--shadow-md); }
25886
+ .stat-card::after { content:""; position:absolute; width:90px; height:90px; right:-38px; top:-45px; border-radius:50%; background:var(--tone-soft); }
25887
+ .stat-top { display:flex; justify-content:space-between; align-items:center; }
25888
+ .stat-icon { width:32px; height:32px; display:grid; place-items:center; border-radius:9px; color:var(--tone); background:var(--tone-soft); }
25889
+ .stat-icon .icon { width:17px; height:17px; }
25890
+ .stat-value { margin-top:13px; font-size:28px; font-weight:760; line-height:1; letter-spacing:-.04em; color:var(--tone); }
25891
+ .stat-label { margin-top:7px; color:var(--text-2); font-size:12px; font-weight:650; }
25892
+ .tone-neutral { --tone:var(--text-2); --tone-soft:var(--surface-3); }
25893
+ .tone-blue { --tone:var(--blue); --tone-soft:var(--blue-soft); }
25894
+ .tone-green { --tone:var(--green); --tone-soft:var(--green-soft); }
25895
+ .tone-red { --tone:var(--red); --tone-soft:var(--red-soft); }
25896
+ .tone-purple { --tone:var(--purple); --tone-soft:var(--primary-soft); }
25897
+ .toolbar { display:flex; justify-content:space-between; align-items:center; gap:14px; margin:0 0 12px; }
25898
+ .filters { display:flex; gap:6px; overflow-x:auto; padding:2px; }
25899
+ .filter-chip { display:inline-flex; align-items:center; min-height:34px; padding:0 12px; border:1px solid var(--border); border-radius:9px;
25900
+ color:var(--text-2); background:var(--surface); text-decoration:none; font-size:12px; font-weight:650; white-space:nowrap; }
25901
+ .filter-chip:hover { border-color:var(--border-strong); color:var(--text); }
25902
+ .filter-chip.active { border-color:color-mix(in srgb,var(--primary) 38%,var(--border)); color:var(--primary); background:var(--primary-soft); }
25903
+ .search-box { position:relative; width:min(320px,100%); flex:0 1 320px; }
25904
+ .search-box .icon { position:absolute; left:11px; top:50%; width:16px; height:16px; color:var(--text-3); transform:translateY(-50%); }
25905
+ .search-box input { width:100%; height:36px; padding:0 12px 0 36px; border:1px solid var(--border); border-radius:9px;
25906
+ outline:none; color:var(--text); background:var(--surface); box-shadow:var(--shadow-sm); }
25907
+ .search-box input::placeholder { color:var(--text-3); }
25908
+ .search-box input:focus { border-color:var(--primary); box-shadow:var(--focus); }
25909
+ .panel,.card { border:1px solid var(--border); border-radius:var(--radius); background:var(--surface); box-shadow:var(--shadow-sm); }
25910
+ .panel { overflow:hidden; margin-bottom:16px; }
25911
+ .panel-head { min-height:52px; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:0 18px;
25912
+ border-bottom:1px solid var(--border); }
25913
+ .panel-head h2,.panel-head h3 { margin:0; font-size:14px; letter-spacing:-.01em; }
25914
+ .table-wrap { width:100%; overflow-x:auto; }
25915
+ table { width:100%; border-collapse:separate; border-spacing:0; font-size:13px; }
25916
+ th { height:42px; padding:0 13px; color:var(--text-3); background:var(--surface-2); border-bottom:1px solid var(--border);
25917
+ font-size:11px; font-weight:730; letter-spacing:.045em; text-align:left; text-transform:uppercase; white-space:nowrap; }
25918
+ td { padding:12px 13px; border-bottom:1px solid var(--border); vertical-align:middle; }
25919
+ tbody tr:last-child td { border-bottom:0; }
25920
+ tbody tr { transition:background .14s ease; }
25921
+ tbody tr:hover { background:color-mix(in srgb,var(--primary-soft) 30%,transparent); }
25922
+ .task-name { font-weight:680; color:var(--text); }
25923
+ .task-prompt { max-width:520px; margin-top:3px; color:var(--text-2); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
25924
+ .muted,.mu { color:var(--text-2); }
25925
+ .faint { color:var(--text-3); }
25926
+ .small,.sm { font-size:12px; }
25927
+ .tag { display:inline-flex; align-items:center; min-height:23px; max-width:180px; padding:0 8px; border:1px solid var(--border);
25928
+ border-radius:7px; color:var(--text-2); background:var(--surface-2); font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
25929
+ .badge { display:inline-flex; align-items:center; gap:6px; min-height:24px; padding:0 9px; border-radius:999px; font-size:11px; font-weight:720; white-space:nowrap; }
25930
+ .badge::before { content:""; width:6px; height:6px; border-radius:50%; background:currentColor; }
25931
+ .b-pending { color:var(--text-2); background:var(--surface-3); }
25932
+ .b-running { color:var(--blue); background:var(--blue-soft); }
25933
+ .b-running::before { animation:pulse 1.7s ease-in-out infinite; }
25934
+ .b-done { color:var(--green); background:var(--green-soft); }
25935
+ .b-failed { color:var(--red); background:var(--red-soft); }
25936
+ .b-dead_letter { color:var(--yellow); background:var(--yellow-soft); }
25937
+ .b-cancelled,.b-unknown { color:var(--text-3); background:var(--surface-3); }
25938
+ .t-cron { color:var(--purple); } .t-recurring { color:var(--blue); } .t-delayed { color:var(--yellow); }
25939
+ .actions { display:flex; align-items:center; flex-wrap:wrap; gap:5px; }
25940
+ .btn { min-height:32px; display:inline-flex; align-items:center; justify-content:center; gap:6px; padding:0 11px; border:1px solid var(--border);
25941
+ border-radius:8px; color:var(--text-2); background:var(--surface); text-decoration:none; cursor:pointer; font-size:12px; font-weight:650;
25942
+ transition:transform .12s ease,background .15s ease,border-color .15s ease,color .15s ease,box-shadow .15s ease; }
25943
+ .btn:hover { color:var(--text); border-color:var(--border-strong); background:var(--surface-2); }
25944
+ .btn:active { transform:scale(.97); }
25945
+ .btn:disabled { opacity:.5; cursor:not-allowed; transform:none; }
25946
+ .btn-primary { color:#fff; border-color:var(--primary); background:var(--primary); }
25947
+ .btn-primary:hover { color:#fff; border-color:var(--primary-hover); background:var(--primary-hover); }
25948
+ .btn-danger { color:var(--red); }
25949
+ .btn-danger:hover { color:var(--red); border-color:color-mix(in srgb,var(--red) 55%,var(--border)); background:var(--red-soft); }
25950
+ .btn-warning:hover { color:var(--yellow); border-color:color-mix(in srgb,var(--yellow) 55%,var(--border)); background:var(--yellow-soft); }
25951
+ .btn .icon { width:14px; height:14px; }
25952
+ .pagination { display:flex; justify-content:center; align-items:center; gap:10px; margin:18px 0 4px; }
25953
+ .pagination .summary { color:var(--text-2); font-size:12px; }
25954
+ .empty-state { display:grid; place-items:center; min-height:230px; padding:36px; text-align:center; }
25955
+ .empty-icon { width:48px; height:48px; display:grid; place-items:center; border-radius:14px; color:var(--primary); background:var(--primary-soft); }
25956
+ .empty-icon .icon { width:23px; height:23px; }
25957
+ .empty-state h3 { margin:13px 0 4px; font-size:15px; }
25958
+ .empty-state p { margin:0; color:var(--text-2); font-size:12px; }
25959
+ .empty-state code { display:inline-block; margin-top:10px; padding:5px 8px; border-radius:6px; background:var(--surface-3); }
25960
+ .settings-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:14px; margin-bottom:16px; }
25961
+ .settings-card { padding:18px; }
25962
+ .settings-title { display:flex; align-items:center; justify-content:space-between; margin:0 0 16px; font-size:14px; }
25963
+ .settings-title span:first-child { display:flex; align-items:center; gap:8px; }
25964
+ .settings-title .icon { width:17px; height:17px; color:var(--primary); }
25965
+ .field { display:grid; grid-template-columns:minmax(0,1fr) 112px; align-items:center; gap:12px; margin:11px 0; }
25966
+ .field label { color:var(--text-2); font-size:12px; }
25967
+ .input-unit { position:relative; }
25968
+ .input-unit input { width:100%; height:36px; padding:0 47px 0 10px; border:1px solid var(--border); border-radius:8px; outline:none;
25969
+ color:var(--text); background:var(--surface-2); }
25970
+ .input-unit span { position:absolute; right:9px; top:50%; transform:translateY(-50%); color:var(--text-3); font-size:10px; pointer-events:none; }
25971
+ .input-unit input:focus { border-color:var(--primary); box-shadow:var(--focus); background:var(--surface); }
25972
+ .switch-field { display:flex; align-items:center; justify-content:space-between; gap:12px; margin:11px 0; color:var(--text-2); font-size:12px; }
25973
+ .switch { position:relative; width:42px; height:24px; flex:0 0 auto; }
25974
+ .switch input { position:absolute; opacity:0; }
25975
+ .switch span { position:absolute; inset:0; border-radius:999px; background:var(--surface-3); border:1px solid var(--border-strong); cursor:pointer; transition:.2s ease; }
25976
+ .switch span::after { content:""; position:absolute; width:17px; height:17px; left:2px; top:2px; border-radius:50%; background:var(--surface);
25977
+ box-shadow:0 1px 3px rgba(0,0,0,.22); transition:transform .2s ease; }
25978
+ .switch input:checked+span { background:var(--primary); border-color:var(--primary); }
25979
+ .switch input:checked+span::after { transform:translateX(18px); }
25980
+ .save-row { display:flex; align-items:center; justify-content:flex-end; gap:12px; margin:0 0 24px; }
25981
+ .info-list { padding:4px 18px; }
25982
+ .info-row { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; padding:13px 0; border-bottom:1px solid var(--border); }
25983
+ .info-row:last-child { border-bottom:0; }
25984
+ .info-key { color:var(--text-2); }
25985
+ .info-value { font-weight:650; text-align:right; overflow-wrap:anywhere; }
25986
+ .overview-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; padding:18px; }
25987
+ .overview-item { padding:13px; border-radius:10px; background:var(--surface-2); }
25988
+ .overview-item span { color:var(--text-2); font-size:11px; }
25989
+ .overview-item strong { display:block; margin-top:5px; font-size:19px; }
25990
+ .danger-card { margin-top:16px; padding:18px; border-color:color-mix(in srgb,var(--red) 40%,var(--border)); background:linear-gradient(145deg,var(--surface),var(--red-soft)); }
25991
+ .danger-card h2 { display:flex; align-items:center; gap:8px; margin:0 0 5px; color:var(--red); font-size:14px; }
25992
+ .danger-card h2 .icon { width:17px; height:17px; }
25993
+ .danger-card p { max-width:800px; margin:0 0 14px; color:var(--text-2); font-size:12px; }
25994
+ .log-panel { margin:12px 0; animation:reveal .18s ease both; }
25995
+ .log-box { max-height:360px; overflow:auto; padding:16px; color:var(--text-2); background:#0b1018; font-family:"SFMono-Regular",Consolas,monospace;
25996
+ font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
25997
+ :root[data-theme="light"] .log-box { color:#dbe5f3; }
25998
+ dialog { width:min(760px,calc(100% - 32px)); padding:0; border:1px solid var(--border); border-radius:16px; color:var(--text);
25999
+ background:var(--surface); box-shadow:var(--shadow-lg); }
26000
+ dialog[open] { animation:dialog-in .18s ease both; }
26001
+ dialog::backdrop { background:rgba(8,12,20,.62); backdrop-filter:blur(3px); animation:fade-in .18s ease both; }
26002
+ .dialog-head { display:flex; align-items:center; justify-content:space-between; gap:14px; padding:17px 18px; border-bottom:1px solid var(--border); }
26003
+ .dialog-head h2 { margin:0; font-size:15px; }
26004
+ .dialog-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
26005
+ .dialog-body { max-height:70vh; overflow:auto; padding:18px; }
26006
+ .dialog-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; padding:14px 18px; border-top:1px solid var(--border); }
26007
+ .json-view { min-height:160px; margin:0; padding:15px; overflow:auto; border:1px solid var(--border); border-radius:10px; color:var(--text-2);
26008
+ background:var(--surface-2); font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
26009
+ .confirm-copy { color:var(--text-2); margin:0; }
26010
+ .confirm-copy strong { display:block; margin-bottom:5px; color:var(--text); font-size:15px; }
26011
+ .danger-input { width:100%; height:40px; margin-top:14px; padding:0 12px; border:1px solid var(--border); border-radius:9px; outline:none;
26012
+ color:var(--text); background:var(--surface-2); font-family:"SFMono-Regular",Consolas,monospace; text-transform:uppercase; }
26013
+ .danger-input:focus { border-color:var(--red); box-shadow:0 0 0 3px color-mix(in srgb,var(--red) 22%,transparent); }
26014
+ .toast-region { position:fixed; top:18px; right:18px; z-index:300; display:grid; gap:8px; pointer-events:none; }
26015
+ .toast { min-width:260px; max-width:min(420px,calc(100vw - 36px)); display:flex; align-items:flex-start; gap:10px; padding:12px 14px;
26016
+ border:1px solid var(--border); border-radius:11px; color:var(--text); background:var(--surface); box-shadow:var(--shadow-lg); animation:toast-in .22s ease both; }
26017
+ .toast .icon { width:18px; height:18px; flex:0 0 auto; margin-top:1px; }
26018
+ .toast.ok .icon { color:var(--green); } .toast.error .icon { color:var(--red); }
26019
+ .toast.leaving { animation:toast-out .18s ease both; }
26020
+ footer { display:flex; justify-content:center; padding:24px 0 4px; color:var(--text-3); font-size:11px; }
26021
+ [hidden] { display:none!important; }
26022
+ :focus-visible { outline:none; box-shadow:var(--focus); }
26023
+ .ui-ready,.ui-ready * { transition-property:background-color,border-color,color,box-shadow; transition-duration:.16s; transition-timing-function:ease; }
26024
+ .reveal { animation:reveal .28s ease both; }
26025
+ .reveal-delay-1 { animation-delay:.04s; } .reveal-delay-2 { animation-delay:.08s; }
26026
+ @keyframes spin { to { transform:rotate(360deg); } }
26027
+ @keyframes pulse { 0%,100% { opacity:1; box-shadow:0 0 0 0 currentColor; } 50% { opacity:.7; box-shadow:0 0 0 4px transparent; } }
26028
+ @keyframes reveal { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }
26029
+ @keyframes dialog-in { from { opacity:0; transform:translateY(8px) scale(.985); } to { opacity:1; transform:none; } }
26030
+ @keyframes fade-in { from { opacity:0; } to { opacity:1; } }
26031
+ @keyframes toast-in { from { opacity:0; transform:translateY(-8px) scale(.98); } to { opacity:1; transform:none; } }
26032
+ @keyframes toast-out { to { opacity:0; transform:translateY(-6px) scale(.98); } }
26033
+ @media (max-width:1000px) {
26034
+ .stats-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
26035
+ .settings-grid { grid-template-columns:1fr; }
26036
+ .local-chip { display:none; }
26037
+ }
26038
+ @media (max-width:720px) {
26039
+ .app-shell { padding:0 16px 20px; }
26040
+ .topbar { min-height:72px; }
26041
+ .brand-tagline,.brand-name span { display:none; }
26042
+ .top-actions { gap:5px; }
26043
+ .select-wrap select { width:42px; color:transparent; padding:0; }
26044
+ .select-wrap>.icon { left:12px; color:var(--text-2); }
26045
+ .select-wrap::after { display:none; }
26046
+ .language-switch { display:none; }
26047
+ .mobile-language { display:grid!important; }
26048
+ .tabs { width:100%; margin:14px 0 24px; }
26049
+ .tabs a { flex:1; justify-content:center; padding:0 10px; }
26050
+ .tabs a span { display:none; }
26051
+ .page-heading { align-items:flex-start; }
26052
+ .page-heading h1 { font-size:24px; }
26053
+ .page-heading p { font-size:13px; }
26054
+ .stats-grid,.stats-grid.three { grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
26055
+ .stat-card { min-height:105px; padding:15px; }
26056
+ .toolbar { align-items:stretch; flex-direction:column; }
26057
+ .search-box { width:100%; flex-basis:auto; }
26058
+ .filters { order:2; }
26059
+ .overview-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
26060
+ .save-row { align-items:flex-end; flex-direction:column-reverse; }
26061
+ .save-row .btn { width:100%; }
26062
+ .responsive-table { display:block; padding:10px; }
26063
+ .responsive-table thead { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
26064
+ .responsive-table tbody { display:grid; gap:10px; }
26065
+ .responsive-table tr { display:grid; gap:0; padding:10px 12px; border:1px solid var(--border); border-radius:11px; background:var(--surface-2); }
26066
+ .responsive-table td { min-width:0; display:grid; grid-template-columns:88px minmax(0,1fr); align-items:start; gap:10px; padding:6px 0; border:0; }
26067
+ .responsive-table td::before { content:attr(data-label); color:var(--text-3); font-size:10px; font-weight:730; letter-spacing:.045em; text-transform:uppercase; }
26068
+ .responsive-table td[data-primary] { display:block; padding-bottom:10px; margin-bottom:4px; border-bottom:1px solid var(--border); }
26069
+ .responsive-table td[data-primary]::before { display:none; }
26070
+ .responsive-table .task-prompt { max-width:100%; }
26071
+ .responsive-table .actions { justify-content:flex-start; }
26072
+ }
26073
+ @media (max-width:520px) {
26074
+ .stats-grid,.stats-grid.three { grid-template-columns:1fr 1fr; }
26075
+ .stat-card { min-height:98px; }
26076
+ .stat-value { font-size:24px; }
26077
+ .page-heading p { max-width:95%; }
26078
+ .field { grid-template-columns:minmax(0,1fr) 105px; }
26079
+ .overview-grid { grid-template-columns:1fr 1fr; padding:12px; }
26080
+ .pagination .summary { max-width:150px; text-align:center; }
26081
+ }
26082
+ @media (prefers-reduced-motion:reduce) {
26083
+ *,*::before,*::after { scroll-behavior:auto!important; animation-duration:.01ms!important; animation-iteration-count:1!important; transition-duration:.01ms!important; }
26084
+ }
26085
+ </style>`;
26086
+ PAGE_KEYS = {
26087
+ tasks: { title: "page.tasks.title", description: "page.tasks.description" },
26088
+ templates: { title: "page.templates.title", description: "page.templates.description" },
26089
+ runs: { title: "page.runs.title", description: "page.runs.description" },
26090
+ system: { title: "page.system.title", description: "page.system.description" }
25392
26091
  };
25393
26092
  }
25394
26093
  });
@@ -25399,7 +26098,7 @@ __export(web_exports, {
25399
26098
  dashboardApp: () => dashboardApp,
25400
26099
  default: () => web_default
25401
26100
  });
25402
- import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync5, renameSync as renameSync2 } from "fs";
26101
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
25403
26102
  import { dirname as dirname8 } from "path";
25404
26103
  function parsePositiveInteger2(value) {
25405
26104
  if (!/^\d+$/.test(value)) return null;
@@ -25412,41 +26111,34 @@ function parseTaskStatus2(value) {
25412
26111
  function safeStatus(value) {
25413
26112
  return value && TASK_STATUSES2.has(value) ? value : "unknown";
25414
26113
  }
26114
+ function resolveLocale(c) {
26115
+ const requested = c.req.query("lang");
26116
+ if (requested === "en" || requested === "zh-CN") return requested;
26117
+ const cookie = c.req.header("Cookie") ?? "";
26118
+ const match2 = /(?:^|;\s*)supertask_locale=([^;]+)/.exec(cookie);
26119
+ if (match2) {
26120
+ try {
26121
+ const saved = decodeURIComponent(match2[1]);
26122
+ if (saved === "en" || saved === "zh-CN") return saved;
26123
+ } catch {
26124
+ }
26125
+ }
26126
+ const accepted = c.req.header("Accept-Language")?.toLowerCase() ?? "";
26127
+ return accepted.startsWith("en") ? "en" : "zh-CN";
26128
+ }
25415
26129
  function formatDuration(startAt, endAt) {
25416
- if (!startAt) return "-";
26130
+ if (!startAt) return "\u2014";
25417
26131
  const start = new Date(startAt).getTime();
25418
26132
  const end = endAt ? new Date(endAt).getTime() : Date.now();
25419
26133
  const seconds = Math.floor((end - start) / 1e3);
25420
26134
  if (seconds < 0) return "0s";
25421
26135
  if (seconds < 60) return `${seconds}s`;
25422
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m${seconds % 60}s`;
25423
- return `${Math.floor(seconds / 3600)}h${Math.floor(seconds % 3600 / 60)}m`;
25424
- }
25425
- function timeAgo(ms) {
25426
- if (!ms) return "-";
25427
- const diff = Date.now() - ms;
25428
- if (diff < 6e4) return `${Math.floor(diff / 1e3)}\u79D2\u524D`;
25429
- if (diff < 36e5) return `${Math.floor(diff / 6e4)}\u5206\u949F\u524D`;
25430
- if (diff < 864e5) return `${Math.floor(diff / 36e5)}\u5C0F\u65F6\u524D`;
25431
- return `${Math.floor(diff / 864e5)}\u5929\u524D`;
25432
- }
25433
- function timeUntil(ms) {
25434
- if (!ms) return "-";
25435
- const diff = ms - Date.now();
25436
- if (diff < 0) return "\u5DF2\u5230\u671F";
25437
- if (diff < 6e4) return `${Math.floor(diff / 1e3)}\u79D2\u540E`;
25438
- if (diff < 36e5) return `${Math.floor(diff / 6e4)}\u5206\u949F\u540E`;
25439
- if (diff < 864e5) return `${Math.floor(diff / 36e5)}\u5C0F\u65F6\u540E`;
25440
- return `${Math.floor(diff / 864e5)}\u5929\u540E`;
25441
- }
25442
- function formatDate(ts) {
25443
- if (!ts) return "-";
25444
- const d = ts instanceof Date ? ts : new Date(ts);
25445
- return d.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
25446
- }
25447
- function esc(s) {
25448
- if (!s) return "";
25449
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
26136
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
26137
+ return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m`;
26138
+ }
26139
+ function esc(value) {
26140
+ if (!value) return "";
26141
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
25450
26142
  }
25451
26143
  function readCurrentConfig() {
25452
26144
  const configPath = getConfigPath();
@@ -25465,94 +26157,51 @@ function writeConfig(cfg) {
25465
26157
  writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
25466
26158
  renameSync2(tempPath, configPath);
25467
26159
  }
25468
- function renderLayout(title, activeTab, body) {
25469
- return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
25470
- <script>
25471
- async function retryTask(id){if(!confirm('\u786E\u5B9A\u91CD\u8BD5\u4EFB\u52A1 #'+id+'?'))return;const r=await fetch('/api/tasks/'+id+'/retry',{method:'POST'});const d=await r.json();if(!r.ok){alert('\u91CD\u8BD5\u5931\u8D25: '+d.error);return;}location.reload();}
25472
- async function cancelTask(id){if(!confirm('\u786E\u5B9A\u53D6\u6D88\u4EFB\u52A1 #'+id+'?'))return;const r=await fetch('/api/tasks/'+id+'/cancel',{method:'POST'});const d=await r.json();if(!r.ok){alert('\u53D6\u6D88\u5931\u8D25: '+d.error);return;}location.reload();}
25473
- async function deleteTask(id){if(!confirm('\u786E\u5B9A\u5220\u9664\u4EFB\u52A1 #'+id+'?'))return;const r=await fetch('/api/tasks/'+id,{method:'DELETE'});const d=await r.json();if(!r.ok){alert('\u5220\u9664\u5931\u8D25: '+d.error);return;}location.reload();}
25474
- async function showDetail(id){try{const r=await fetch('/api/tasks/'+id);const t=await r.json();document.getElementById('dc').textContent=JSON.stringify(t,null,2);document.getElementById('dd').showModal();}catch(e){alert('\u83B7\u53D6\u8BE6\u60C5\u5931\u8D25');}}
25475
- async function showRunDetail(id){try{const r=await fetch('/api/runs/'+id);const t=await r.json();document.getElementById('dc').textContent=JSON.stringify(t,null,2);document.getElementById('dd').showModal();}catch(e){alert('\u83B7\u53D6\u8BE6\u60C5\u5931\u8D25');}}
25476
- async function showTemplateDetail(id){try{const r=await fetch('/api/templates/'+id);const t=await r.json();document.getElementById('dc').textContent=JSON.stringify(t,null,2);document.getElementById('dd').showModal();}catch(e){alert('\u83B7\u53D6\u8BE6\u60C5\u5931\u8D25');}}
25477
- async function enableTmpl(id){await fetch('/api/templates/'+id+'/enable',{method:'POST'});location.reload();}
25478
- async function disableTmpl(id){if(!confirm('\u786E\u5B9A\u7981\u7528\u6B64\u6A21\u677F?'))return;await fetch('/api/templates/'+id+'/disable',{method:'POST'});location.reload();}
25479
- async function deleteTmpl(id){if(!confirm('\u786E\u5B9A\u5220\u9664\u6B64\u6A21\u677F? \u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D!'))return;await fetch('/api/templates/'+id,{method:'DELETE'});location.reload();}
25480
- async function triggerTmpl(id){if(!confirm('\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21?'))return;const r=await fetch('/api/templates/'+id+'/trigger',{method:'POST'});const d=await r.json();if(d.success){alert('\u5DF2\u521B\u5EFA\u4EFB\u52A1 #'+d.taskId);location.reload();}else{alert('\u89E6\u53D1\u5931\u8D25');}}
25481
- function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
25482
-
25483
- async function clearDatabase(){
25484
- if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u521B\u5EFA\u5907\u4EFD\u3002'))return;
25485
- if(!confirm('\u518D\u6B21\u786E\u8BA4\uFF1A\u5C06\u4E8B\u52A1\u6027\u5220\u9664\u6240\u6709\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002'))return;
25486
- try{
25487
- const r=await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})});
25488
- const d=await r.json();
25489
- if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u81EA\u52A8\u5907\u4EFD\uFF1A'+d.backupPath);location.reload();}
25490
- else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
25491
- }catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
26160
+ function statCard(value, label, tone, cardIcon, delay = "") {
26161
+ return `<div class="stat-card ${tone} reveal ${delay}">
26162
+ <div class="stat-top"><div class="stat-icon">${cardIcon}</div></div>
26163
+ <div class="stat-value">${value}</div><div class="stat-label">${label}</div>
26164
+ </div>`;
25492
26165
  }
25493
-
25494
- async function saveConfig(){
25495
- const form=document.getElementById('config-form');
25496
- const data={
25497
- worker:{
25498
- maxConcurrency:Number(form.mc.value),
25499
- pollIntervalMs:Number(form.pi.value),
25500
- heartbeatIntervalMs:Number(form.hi.value)*1000,
25501
- taskTimeoutMs:Number(form.to.value)*60000,
25502
- },
25503
- scheduler:{
25504
- enabled:form.se.checked,
25505
- checkIntervalMs:Number(form.si.value),
25506
- },
25507
- watchdog:{
25508
- heartbeatTimeoutMs:Number(form.wt.value)*1000,
25509
- checkIntervalMs:Number(form.wci.value)*1000,
25510
- cleanupIntervalMs:Number(form.wcl.value)*3600000,
25511
- retentionDays:Number(form.rd.value),
25512
- }
25513
- };
25514
- try{
25515
- const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
25516
- const d=await r.json();
25517
- if(d.success){document.getElementById('toast').textContent='\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F Gateway \u540E\u751F\u6548';document.getElementById('toast').style.display='block';setTimeout(()=>document.getElementById('toast').style.display='none',3000);}
25518
- else{alert('\u4FDD\u5B58\u5931\u8D25: '+d.error);}
25519
- }catch(e){alert('\u4FDD\u5B58\u5931\u8D25: '+e.message);}
25520
- }
25521
- </script>
25522
- </head>
25523
- <body>
25524
- <div id="toast" class="toast toast-ok"></div>
25525
- <div class="c">
25526
- <header>
25527
- <div><h1>SuperTask Dashboard</h1><span class="mu sm">\u4EFB\u52A1\u8C03\u5EA6\u7BA1\u7406\u4E2D\u5FC3</span></div>
25528
- <div><a href="${activeTab === "tasks" ? "/" : "/" + activeTab}" class="rf">\u5237\u65B0</a></div>
25529
- </header>
25530
- <nav class="tabs">
25531
- <a href="/" class="${activeTab === "tasks" ? "active" : ""}">\u4EFB\u52A1\u961F\u5217</a>
25532
- <a href="/templates" class="${activeTab === "templates" ? "active" : ""}">\u5B9A\u65F6\u4EFB\u52A1</a>
25533
- <a href="/runs" class="${activeTab === "runs" ? "active" : ""}">\u6267\u884C\u65E5\u5FD7</a>
25534
- <a href="/system" class="${activeTab === "system" ? "active" : ""}">\u7CFB\u7EDF\u72B6\u6001</a>
25535
- </nav>
25536
- ${body}
25537
- </div>
25538
- <dialog id="dd"><div class="dh"><h3 style="margin:0">\u8BE6\u60C5</h3><button class="cb" onclick="document.getElementById('dd').close()">&times;</button></div><div class="db"><pre id="dc"></pre></div></dialog>
25539
- </body></html>`;
25540
- }
25541
- var app, TASK_STATUSES2, SHARED_STYLES, dashboardApp, web_default;
26166
+ function emptyState(title, hint, code = "") {
26167
+ return `<div class="empty-state"><div><div class="empty-icon">${icon("inbox")}</div>
26168
+ <h3>${title}</h3><p>${hint}</p>${code ? `<code>${code}</code>` : ""}</div></div>`;
26169
+ }
26170
+ function formatInterval(milliseconds, locale) {
26171
+ const formatter = new Intl.NumberFormat(locale === "en" ? "en-US" : "zh-CN", {
26172
+ maximumFractionDigits: 1
26173
+ });
26174
+ if (milliseconds < 6e4) {
26175
+ return t(locale, "schedule.seconds", { count: formatter.format(milliseconds / 1e3) });
26176
+ }
26177
+ if (milliseconds < 36e5) {
26178
+ return t(locale, "schedule.minutes", { count: formatter.format(milliseconds / 6e4) });
26179
+ }
26180
+ if (milliseconds < 864e5) {
26181
+ return t(locale, "schedule.hours", { count: formatter.format(milliseconds / 36e5) });
26182
+ }
26183
+ return t(locale, "schedule.days", { count: formatter.format(milliseconds / 864e5) });
26184
+ }
26185
+ function pagination(locale, basePath, page, pages, total, suffix = "") {
26186
+ const previous = page > 1 ? `<a class="btn" href="${basePath}?page=${page - 1}${suffix}">${icon("chevronLeft")}${t(locale, "pagination.previous")}</a>` : "";
26187
+ const next = page < pages ? `<a class="btn" href="${basePath}?page=${page + 1}${suffix}">${t(locale, "pagination.next")}${icon("chevronRight")}</a>` : "";
26188
+ return `<div class="pagination">${previous}<span class="summary">${t(locale, "pagination.summary", { page, pages, total })}</span>${next}</div>`;
26189
+ }
26190
+ var app, TASK_STATUSES2, dashboardApp, web_default;
25542
26191
  var init_web = __esm({
25543
26192
  "src/web/index.tsx"() {
25544
26193
  "use strict";
25545
26194
  init_dist();
25546
- init_html2();
25547
- init_task_service();
25548
- init_task_run_service();
25549
- init_task_template_service();
25550
- init_database_maintenance_service();
25551
26195
  init_drizzle_orm();
25552
26196
  init_db2();
26197
+ init_database_maintenance_service();
26198
+ init_task_run_service();
26199
+ init_task_service();
26200
+ init_task_template_service();
25553
26201
  init_config();
25554
26202
  init_health();
25555
26203
  init_job_templates();
26204
+ init_ui();
25556
26205
  app = new Hono2();
25557
26206
  TASK_STATUSES2 = /* @__PURE__ */ new Set([
25558
26207
  "pending",
@@ -25593,84 +26242,9 @@ var init_web = __esm({
25593
26242
  const health = getGatewayHealth();
25594
26243
  return c.json(health, health.status === "ok" ? 200 : 503);
25595
26244
  });
25596
- SHARED_STYLES = html`
25597
- <style>
25598
- :root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
25599
- * { box-sizing:border-box; }
25600
- body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; background:var(--bg); color:var(--t1); margin:0; line-height:1.5; }
25601
- .c { max-width:1280px; margin:0 auto; padding:20px; }
25602
- header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; border-bottom:1px solid var(--border); padding-bottom:16px; }
25603
- h1 { font-size:22px; margin:0; color:#fff; }
25604
- nav.tabs { display:flex; gap:0; margin-bottom:20px; border-bottom:1px solid var(--border); }
25605
- nav.tabs a { display:block; padding:10px 20px; color:var(--t2); text-decoration:none; font-size:14px; font-weight:500; border-bottom:2px solid transparent; }
25606
- nav.tabs a:hover { color:var(--t1); }
25607
- nav.tabs a.active { color:#fff; border-bottom-color:var(--blue); }
25608
- .g4 { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:16px; margin-bottom:24px; }
25609
- .g3 { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:16px; margin-bottom:24px; }
25610
- .card { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:16px; }
25611
- .sv { font-size:28px; font-weight:bold; }
25612
- .sl { color:var(--t2); font-size:12px; text-transform:uppercase; margin-top:4px; }
25613
- .panel { background:var(--card); border:1px solid var(--border); border-radius:8px; overflow:hidden; margin-bottom:16px; }
25614
- .ph { padding:12px 16px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; }
25615
- .ph h3 { margin:0; font-size:14px; }
25616
- table { width:100%; border-collapse:collapse; font-size:13px; }
25617
- th { background:#21262d; color:var(--t2); font-weight:600; text-align:left; padding:8px 12px; white-space:nowrap; }
25618
- td { padding:8px 12px; border-bottom:1px solid var(--border); vertical-align:top; }
25619
- tr:last-child td { border-bottom:none; }
25620
- tr:hover { background:rgba(255,255,255,0.02); }
25621
- .badge { display:inline-block; padding:2px 8px; border-radius:12px; font-size:11px; font-weight:600; }
25622
- .b-pending { background:rgba(110,118,129,0.3); color:#8b949e; }
25623
- .b-running { background:rgba(56,139,253,0.15); color:#58a6ff; }
25624
- .b-done { background:rgba(46,160,67,0.15); color:#3fb950; }
25625
- .b-failed { background:rgba(248,81,73,0.15); color:#f85149; }
25626
- .b-dead_letter { background:rgba(210,153,34,0.15); color:#d29922; }
25627
- .b-cancelled { background:rgba(110,118,129,0.2); color:#6e7681; }
25628
- .btn { appearance:none; background:transparent; border:1px solid var(--border); color:var(--t2); padding:4px 10px; border-radius:4px; cursor:pointer; font-size:12px; margin-right:4px; text-decoration:none; }
25629
- .btn:hover { background:#30363d; color:#fff; }
25630
- .btn-sm { padding:2px 6px; font-size:11px; }
25631
- .btn-primary { background:var(--green); border-color:var(--green); color:#fff; }
25632
- .btn-primary:hover { opacity:0.85; color:#fff; }
25633
- .btn-danger:hover { color:var(--red); border-color:var(--red); }
25634
- .btn-warn:hover { color:var(--yellow); border-color:var(--yellow); }
25635
- .rf { background:var(--green); color:white; border:none; padding:6px 16px; border-radius:6px; font-weight:600; cursor:pointer; text-decoration:none; }
25636
- .rf:hover { opacity:0.9; }
25637
- .m { font-family:monospace; font-size:12px; }
25638
- .mu { color:var(--t2); }
25639
- .sm { font-size:12px; }
25640
- .el { max-width:400px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:inline-block; vertical-align:middle; }
25641
- .tag { display:inline-block; background:#21262d; padding:1px 6px; border-radius:4px; font-size:11px; }
25642
- .t-cron { color:var(--purple); }
25643
- .t-recurring { color:var(--blue); }
25644
- .t-delayed { color:var(--yellow); }
25645
- .pn { margin-top:16px; display:flex; justify-content:center; gap:10px; align-items:center; }
25646
- .ir { display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px solid var(--border); font-size:13px; }
25647
- .ir:last-child { border-bottom:none; }
25648
- .ik { color:var(--t2); }
25649
- .iv { font-weight:500; }
25650
- .form-row { display:flex; gap:12px; align-items:center; margin-bottom:12px; }
25651
- .form-row label { color:var(--t2); font-size:13px; min-width:100px; }
25652
- .form-row input, .form-row select { background:#0d1117; border:1px solid var(--border); color:var(--t1); padding:6px 10px; border-radius:4px; font-size:13px; }
25653
- .form-row input:focus, .form-row select:focus { outline:none; border-color:var(--blue); }
25654
- .log-box { background:#0d1117; border:1px solid var(--border); border-radius:4px; padding:12px; font-family:monospace; font-size:12px; max-height:300px; overflow-y:auto; white-space:pre-wrap; color:var(--t1); margin-top:8px; }
25655
- dialog { background:var(--card); color:var(--t1); border:1px solid var(--border); border-radius:8px; padding:0; max-width:900px; width:90%; }
25656
- dialog::backdrop { background:rgba(0,0,0,0.6); }
25657
- .dh { padding:16px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; }
25658
- .db { padding:16px; max-height:70vh; overflow-y:auto; }
25659
- pre { margin:0; white-space:pre-wrap; font-size:12px; }
25660
- .cb { background:transparent; border:none; color:var(--t2); cursor:pointer; font-size:20px; }
25661
- .mt8 { margin-top:8px; }
25662
- .mt16 { margin-top:16px; }
25663
- .mb0 { margin-bottom:0; }
25664
- .ta-center { text-align:center; }
25665
- .p30 { padding:30px; }
25666
- .toast { position:fixed; top:20px; right:20px; padding:12px 20px; border-radius:6px; color:#fff; font-size:14px; z-index:9999; display:none; }
25667
- .toast-ok { background:var(--green); }
25668
- .toast-err { background:var(--red); }
25669
- </style>
25670
- `;
25671
26245
  app.get("/", async (c) => {
25672
- const pageParam = c.req.query("page") || "1";
25673
- const page = parsePositiveInteger2(pageParam);
26246
+ const locale = resolveLocale(c);
26247
+ const page = parsePositiveInteger2(c.req.query("page") || "1");
25674
26248
  if (page === null) return c.text("invalid page", 400);
25675
26249
  const statusFilter = c.req.query("status") || "";
25676
26250
  const parsedStatus = statusFilter ? parseTaskStatus2(statusFilter) : null;
@@ -25681,8 +26255,7 @@ var init_web = __esm({
25681
26255
  TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
25682
26256
  TaskService.stats({})
25683
26257
  ]);
25684
- const taskIds = tasks4.map((t) => t.id);
25685
- const latestRuns = await TaskRunService.getLatestByTaskIds(taskIds);
26258
+ const latestRuns = await TaskRunService.getLatestByTaskIds(tasks4.map((task) => task.id));
25686
26259
  const counts = {
25687
26260
  pending: statsData.pending || 0,
25688
26261
  running: statsData.running || 0,
@@ -25690,253 +26263,211 @@ var init_web = __esm({
25690
26263
  failed: (statsData.failed || 0) + (statsData.dead_letter || 0),
25691
26264
  total: statsData.total || 0
25692
26265
  };
25693
- const totalPages = Math.ceil(counts.total / limit);
25694
- let filterBtns = `<div style="margin-bottom:12px;display:flex;gap:6px;">
25695
- <a href="/" class="btn ${!statusFilter ? "btn-primary" : ""}">\u5168\u90E8</a>
25696
- <a href="/?status=pending" class="btn ${statusFilter === "pending" ? "btn-primary" : ""}">Pending</a>
25697
- <a href="/?status=running" class="btn ${statusFilter === "running" ? "btn-primary" : ""}">Running</a>
25698
- <a href="/?status=done" class="btn ${statusFilter === "done" ? "btn-primary" : ""}">Done</a>
25699
- <a href="/?status=failed" class="btn ${statusFilter === "failed" ? "btn-primary" : ""}">Failed</a>
25700
- <a href="/?status=dead_letter" class="btn ${statusFilter === "dead_letter" ? "btn-primary" : ""}">Dead Letter</a>
25701
- </div>`;
25702
- let rows = "";
25703
- for (const task of tasks4) {
26266
+ const filteredTotal = parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
26267
+ const totalPages = Math.max(1, Math.ceil(filteredTotal / limit));
26268
+ const filterItems = [
26269
+ { status: "", label: t(locale, "filter.all") },
26270
+ { status: "pending", label: statusText(locale, "pending") },
26271
+ { status: "running", label: statusText(locale, "running") },
26272
+ { status: "done", label: statusText(locale, "done") },
26273
+ { status: "failed", label: statusText(locale, "failed") },
26274
+ { status: "dead_letter", label: statusText(locale, "dead_letter") },
26275
+ { status: "cancelled", label: statusText(locale, "cancelled") }
26276
+ ];
26277
+ const filters = filterItems.map(({ status, label }) => {
26278
+ const href = status ? `/?status=${status}` : "/";
26279
+ return `<a href="${href}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
26280
+ }).join("");
26281
+ const rows = tasks4.map((task) => {
25704
26282
  const status = safeStatus(task.status);
25705
- const st = status.toUpperCase();
25706
26283
  const executionActive = latestRuns.get(task.id)?.status === "running";
25707
- rows += `<tr>
25708
- <td class="mu">#${task.id}</td>
25709
- <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
25710
- <td><span class="tag">${esc(task.agent)}</span></td>
25711
- <td><span class="badge b-${status}">${st}</span></td>
25712
- <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
25713
- <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
25714
- <td>
25715
- <button class="btn btn-sm" onclick="showDetail(${task.id})">\u8BE6\u60C5</button>
25716
- ${task.status === "failed" || task.status === "dead_letter" ? `<button class="btn btn-sm btn-warn" onclick="retryTask(${task.id})">\u91CD\u8BD5</button>` : ""}
25717
- ${["pending", "running", "failed"].includes(task.status ?? "") ? `<button class="btn btn-sm btn-warn" onclick="cancelTask(${task.id})">\u53D6\u6D88</button>` : ""}
25718
- ${task.status === "running" || executionActive ? "" : `<button class="btn btn-sm btn-danger" onclick="deleteTask(${task.id})">\u5220\u9664</button>`}
25719
- </td></tr>`;
25720
- }
25721
- const qp = statusFilter ? `&status=${statusFilter}` : "";
25722
- let paging = `<div class="pn">`;
25723
- if (page > 1) paging += `<a href="/?page=${page - 1}${qp}" class="btn">\u4E0A\u4E00\u9875</a>`;
25724
- paging += `<span class="mu sm">\u7B2C ${page} \u9875 / \u5171 ${totalPages} \u9875 (${counts.total} \u6761)</span>`;
25725
- if (page < totalPages) paging += `<a href="/?page=${page + 1}${qp}" class="btn">\u4E0B\u4E00\u9875</a>`;
25726
- paging += `</div>`;
26284
+ const searchable = esc(`${task.name} ${task.agent} ${task.prompt}`);
26285
+ return `<tr data-task-row data-search="${searchable}">
26286
+ <td class="faint" data-label="${t(locale, "table.id")}">#${task.id}</td>
26287
+ <td data-primary data-label="${t(locale, "table.task")}"><div class="task-name">${esc(task.name)}</div><div class="task-prompt" title="${esc(task.prompt)}">${esc(task.prompt.substring(0, 160))}</div></td>
26288
+ <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(task.agent)}</span></td>
26289
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
26290
+ <td data-label="${t(locale, "table.duration")}" class="small ${task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
26291
+ <td data-label="${t(locale, "table.retries")}" class="muted small">${(task.retryCount ?? 0) > 0 ? task.retryCount : "\u2014"}</td>
26292
+ <td data-label="${t(locale, "table.actions")}"><div class="actions">
26293
+ <button type="button" class="btn" onclick="showDetail(${task.id})">${t(locale, "action.details")}</button>
26294
+ ${task.status === "failed" || task.status === "dead_letter" ? `<button type="button" class="btn btn-warning" onclick="retryTask(${task.id})">${t(locale, "action.retry")}</button>` : ""}
26295
+ ${["pending", "running", "failed"].includes(task.status ?? "") ? `<button type="button" class="btn btn-warning" onclick="cancelTask(${task.id})">${t(locale, "action.cancel")}</button>` : ""}
26296
+ ${task.status === "running" || executionActive ? "" : `<button type="button" class="btn btn-danger" onclick="deleteTask(${task.id})">${t(locale, "action.delete")}</button>`}
26297
+ </div></td>
26298
+ </tr>`;
26299
+ }).join("");
26300
+ const table = tasks4.length === 0 ? emptyState(t(locale, "empty.tasks"), t(locale, "empty.tasksHint")) : `<div class="table-wrap"><table class="responsive-table">
26301
+ <thead><tr><th>ID</th><th>${t(locale, "table.task")}</th><th>${t(locale, "table.agent")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.duration")}</th><th>${t(locale, "table.retries")}</th><th>${t(locale, "table.actions")}</th></tr></thead>
26302
+ <tbody>${rows}</tbody>
26303
+ </table></div>
26304
+ <div id="search-empty" hidden>${emptyState(t(locale, "filter.noResults"), "")}</div>`;
26305
+ const suffix = statusFilter ? `&status=${statusFilter}` : "";
25727
26306
  const body = `
25728
- <div class="g4">
25729
- <div class="card"><div class="sv" style="color:var(--t2)">${counts.pending}</div><div class="sl">Pending</div></div>
25730
- <div class="card"><div class="sv" style="color:var(--blue)">${counts.running}</div><div class="sl">Running</div></div>
25731
- <div class="card"><div class="sv" style="color:var(--green)">${counts.done}</div><div class="sl">Done</div></div>
25732
- <div class="card"><div class="sv" style="color:var(--red)">${counts.failed}</div><div class="sl">Failed / Dead</div></div>
26307
+ <div class="stats-grid">
26308
+ ${statCard(counts.pending, t(locale, "stats.pending"), "tone-neutral", icon("clock"))}
26309
+ ${statCard(counts.running, t(locale, "stats.running"), "tone-blue", icon("activity"), "reveal-delay-1")}
26310
+ ${statCard(counts.done, t(locale, "stats.done"), "tone-green", icon("check"), "reveal-delay-1")}
26311
+ ${statCard(counts.failed, t(locale, "stats.failedDead"), "tone-red", icon("alert"), "reveal-delay-2")}
25733
26312
  </div>
25734
- ${filterBtns}
25735
- <div class="panel"><table>
25736
- <thead><tr><th width="50">ID</th><th>\u4EFB\u52A1</th><th>Agent</th><th width="90">\u72B6\u6001</th><th width="70">\u8017\u65F6</th><th width="60">\u91CD\u8BD5</th><th>\u64CD\u4F5C</th></tr></thead>
25737
- <tbody>${rows}</tbody>
25738
- </table></div>
25739
- ${paging}`;
25740
- return c.html(renderLayout("\u4EFB\u52A1\u961F\u5217", "tasks", body));
26313
+ <div class="toolbar reveal reveal-delay-1">
26314
+ <div class="filters">${filters}</div>
26315
+ <label class="search-box">${icon("search")}<input type="search" oninput="filterTasks(this.value)" placeholder="${t(locale, "filter.searchTasks")}" aria-label="${t(locale, "filter.searchTasks")}"></label>
26316
+ </div>
26317
+ <section class="panel reveal reveal-delay-2">${table}</section>
26318
+ ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}`;
26319
+ return c.html(renderLayout({ locale, activeTab: "tasks", body }));
25741
26320
  });
25742
26321
  app.get("/templates", async (c) => {
26322
+ const locale = resolveLocale(c);
25743
26323
  const templates = await TaskTemplateService.list(100);
25744
- const enabled = templates.filter((t) => t.enabled).length;
26324
+ const enabled = templates.filter((template) => template.enabled).length;
25745
26325
  const disabled = templates.length - enabled;
25746
- let rows = "";
25747
- for (const t of templates) {
25748
- const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
25749
- const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
25750
- const typeClass = "tag t-" + scheduleType;
25751
- let rule = "-";
25752
- if (t.scheduleType === "cron") rule = t.cronExpr || "-";
25753
- else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
25754
- else if (t.scheduleType === "delayed") rule = formatDate(t.runAt);
25755
- const statusBadge = t.enabled ? '<span class="badge b-done">\u542F\u7528</span>' : '<span class="badge b-cancelled">\u7981\u7528</span>';
25756
- const toggleBtn = t.enabled ? `<button class="btn btn-sm btn-warn" onclick="disableTmpl(${t.id})">\u7981\u7528</button>` : `<button class="btn btn-sm" onclick="enableTmpl(${t.id})">\u542F\u7528</button>`;
25757
- rows += `<tr>
25758
- <td class="mu">#${t.id}</td>
25759
- <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
25760
- <div class="sm" style="margin-top:2px"><span class="tag">${esc(t.agent)}</span>${t.model && t.model !== "default" ? ` <span class="tag">${esc(t.model)}</span>` : ""}</div></td>
25761
- <td><span class="${typeClass}">${typeLabel}</span></td>
25762
- <td class="m sm">${esc(rule)}</td>
25763
- <td>${statusBadge}</td>
25764
- <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
25765
- <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
25766
- <td>
25767
- <button class="btn btn-sm" onclick="showTemplateDetail(${t.id})">\u8BE6\u60C5</button>
25768
- <button class="btn btn-sm btn-primary" onclick="triggerTmpl(${t.id})">\u89E6\u53D1</button>
25769
- ${toggleBtn}
25770
- <button class="btn btn-sm btn-danger" onclick="deleteTmpl(${t.id})">\u5220\u9664</button>
25771
- </td></tr>`;
25772
- }
25773
- const emptyRow = templates.length === 0 ? `<tr><td colspan="8" class="ta-center mu p30">\u6682\u65E0\u5B9A\u65F6\u4EFB\u52A1\u6A21\u677F\u3002\u4F7F\u7528 CLI \u521B\u5EFA\uFF1A<code>supertask template add</code></td></tr>` : "";
26326
+ const rows = templates.map((template) => {
26327
+ const scheduleType = ["cron", "recurring", "delayed"].includes(template.scheduleType) ? template.scheduleType : "unknown";
26328
+ const typeLabel = scheduleType === "cron" ? t(locale, "schedule.cron") : scheduleType === "recurring" ? t(locale, "schedule.recurring") : scheduleType === "delayed" ? t(locale, "schedule.delayed") : t(locale, "schedule.unknown");
26329
+ let rule = "\u2014";
26330
+ if (template.scheduleType === "cron") rule = template.cronExpr || "\u2014";
26331
+ if (template.scheduleType === "recurring" && template.intervalMs) rule = formatInterval(template.intervalMs, locale);
26332
+ if (template.scheduleType === "delayed") rule = formatDateTime(template.runAt, locale);
26333
+ const toggle = template.enabled ? `<button type="button" class="btn btn-warning" onclick="disableTmpl(${template.id})">${t(locale, "action.disable")}</button>` : `<button type="button" class="btn" onclick="enableTmpl(${template.id})">${t(locale, "action.enable")}</button>`;
26334
+ return `<tr>
26335
+ <td class="faint" data-label="${t(locale, "table.id")}">#${template.id}</td>
26336
+ <td data-primary data-label="${t(locale, "table.name")}"><div class="task-name">${esc(template.name)}</div><div class="task-prompt" title="${esc(template.prompt)}">${esc(template.prompt.substring(0, 140))}</div>
26337
+ <div class="actions" style="margin-top:5px"><span class="tag">${esc(template.agent)}</span>${template.model && template.model !== "default" ? `<span class="tag">${esc(template.model)}</span>` : ""}</div></td>
26338
+ <td data-label="${t(locale, "table.type")}"><span class="tag t-${scheduleType}">${typeLabel}</span></td>
26339
+ <td data-label="${t(locale, "table.rule")}" class="m small">${esc(rule)}</td>
26340
+ <td data-label="${t(locale, "table.status")}"><span class="badge ${template.enabled ? "b-done" : "b-cancelled"}">${t(locale, template.enabled ? "schedule.enabled" : "schedule.disabled")}</span></td>
26341
+ <td data-label="${t(locale, "table.lastRun")}" class="small muted">${formatRelative(template.lastRunAt, locale)}</td>
26342
+ <td data-label="${t(locale, "table.nextRun")}" class="small">${formatFuture(template.nextRunAt, locale)}</td>
26343
+ <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showTemplateDetail(${template.id})">${t(locale, "action.details")}</button>
26344
+ <button type="button" class="btn btn-primary" onclick="triggerTmpl(${template.id})">${t(locale, "action.trigger")}</button>${toggle}
26345
+ <button type="button" class="btn btn-danger" onclick="deleteTmpl(${template.id})">${t(locale, "action.delete")}</button></div></td>
26346
+ </tr>`;
26347
+ }).join("");
25774
26348
  const body = `
25775
- <div class="g3">
25776
- <div class="card"><div class="sv" style="color:var(--purple)">${templates.length}</div><div class="sl">\u6A21\u677F\u603B\u6570</div></div>
25777
- <div class="card"><div class="sv" style="color:var(--green)">${enabled}</div><div class="sl">\u5DF2\u542F\u7528</div></div>
25778
- <div class="card"><div class="sv" style="color:var(--t2)">${disabled}</div><div class="sl">\u5DF2\u7981\u7528</div></div>
26349
+ <div class="stats-grid three">
26350
+ ${statCard(templates.length, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
26351
+ ${statCard(enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
26352
+ ${statCard(disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
25779
26353
  </div>
25780
- <div class="panel">
25781
- <div class="ph"><h3>\u8C03\u5EA6\u6A21\u677F</h3></div>
25782
- <table>
25783
- <thead><tr><th width="50">ID</th><th>\u540D\u79F0</th><th>\u7C7B\u578B</th><th>\u89C4\u5219</th><th width="90">\u72B6\u6001</th><th>\u4E0A\u6B21\u6267\u884C</th><th>\u4E0B\u6B21\u6267\u884C</th><th>\u64CD\u4F5C</th></tr></thead>
25784
- <tbody>${rows}${emptyRow}</tbody>
25785
- </table>
25786
- </div>`;
25787
- return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
26354
+ <section class="panel reveal reveal-delay-2">
26355
+ <div class="panel-head"><h2>${t(locale, "page.templates.title")}</h2></div>
26356
+ ${templates.length === 0 ? emptyState(t(locale, "empty.templates"), t(locale, "empty.templatesHint"), "supertask template add") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>ID</th><th>${t(locale, "table.name")}</th><th>${t(locale, "table.type")}</th><th>${t(locale, "table.rule")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.lastRun")}</th><th>${t(locale, "table.nextRun")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}
26357
+ </section>`;
26358
+ return c.html(renderLayout({ locale, activeTab: "templates", body }));
25788
26359
  });
25789
26360
  app.get("/runs", async (c) => {
26361
+ const locale = resolveLocale(c);
25790
26362
  const page = parsePositiveInteger2(c.req.query("page") || "1");
25791
26363
  if (page === null) return c.text("invalid page", 400);
25792
26364
  const limit = 50;
25793
26365
  const offset = (page - 1) * limit;
25794
- const { taskRuns: tr, tasks: tk } = schema_exports;
26366
+ const { taskRuns: taskRuns4, tasks: tasks4 } = schema_exports;
25795
26367
  const runs = await db.select({
25796
- id: tr.id,
25797
- taskId: tr.taskId,
25798
- sessionId: tr.sessionId,
25799
- model: tr.model,
25800
- status: tr.status,
25801
- startedAt: tr.startedAt,
25802
- finishedAt: tr.finishedAt,
25803
- log: tr.log,
25804
- heartbeatAt: tr.heartbeatAt,
25805
- workerPid: tr.workerPid,
25806
- childPid: tr.childPid,
25807
- taskName: tk.name,
25808
- taskAgent: tk.agent
25809
- }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
25810
- const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
26368
+ id: taskRuns4.id,
26369
+ taskId: taskRuns4.taskId,
26370
+ sessionId: taskRuns4.sessionId,
26371
+ model: taskRuns4.model,
26372
+ status: taskRuns4.status,
26373
+ startedAt: taskRuns4.startedAt,
26374
+ finishedAt: taskRuns4.finishedAt,
26375
+ log: taskRuns4.log,
26376
+ heartbeatAt: taskRuns4.heartbeatAt,
26377
+ workerPid: taskRuns4.workerPid,
26378
+ childPid: taskRuns4.childPid,
26379
+ taskName: tasks4.name,
26380
+ taskAgent: tasks4.agent
26381
+ }).from(taskRuns4).innerJoin(tasks4, eq(taskRuns4.taskId, tasks4.id)).orderBy(desc(taskRuns4.startedAt), desc(taskRuns4.id)).limit(limit).offset(offset);
26382
+ const totalResult = await db.select({ count: sql`count(*)` }).from(taskRuns4);
25811
26383
  const total = Number(totalResult[0]?.count ?? 0);
25812
- const totalPages = Math.ceil(total / limit);
25813
- let rows = "";
25814
- const logsHtml = [];
25815
- for (const run of runs) {
26384
+ const totalPages = Math.max(1, Math.ceil(total / limit));
26385
+ const logs = [];
26386
+ const rows = runs.map((run) => {
25816
26387
  const status = safeStatus(run.status);
25817
- const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
25818
- const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
25819
- rows += `<tr>
25820
- <td class="mu">#${run.id}</td>
25821
- <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
25822
- ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
25823
- <td><span class="tag">${esc(run.taskAgent)}</span></td>
25824
- <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
25825
- <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
25826
- <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
25827
- <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
25828
- </tr>`;
25829
26388
  if (run.log) {
25830
- logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
25831
- <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
25832
- <div class="log-box">${esc(run.log)}</div></div></div>`);
25833
- }
25834
- }
25835
- let paging = `<div class="pn">`;
25836
- if (page > 1) paging += `<a href="/runs?page=${page - 1}" class="btn">\u4E0A\u4E00\u9875</a>`;
25837
- paging += `<span class="mu sm">\u7B2C ${page} \u9875 / \u5171 ${totalPages} \u9875 (${total} \u6761)</span>`;
25838
- if (page < totalPages) paging += `<a href="/runs?page=${page + 1}" class="btn">\u4E0B\u4E00\u9875</a>`;
25839
- paging += `</div>`;
25840
- const emptyRow = runs.length === 0 ? `<tr><td colspan="7" class="ta-center mu p30">\u6682\u65E0\u6267\u884C\u8BB0\u5F55</td></tr>` : "";
26389
+ logs.push(`<section id="log-${run.id}" class="panel log-panel" hidden><div class="panel-head"><h3>Run #${run.id} \xB7 ${esc(run.taskName)}</h3></div><div class="log-box">${esc(run.log)}</div></section>`);
26390
+ }
26391
+ return `<tr>
26392
+ <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
26393
+ <td data-primary data-label="${t(locale, "table.task")}"><div class="task-name">${esc(run.taskName)} <span class="faint">#${run.taskId}</span></div>${run.model ? `<div style="margin-top:4px"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
26394
+ <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(run.taskAgent)}</span></td>
26395
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
26396
+ <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, run.finishedAt)}</td>
26397
+ <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td>
26398
+ <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showRunDetail(${run.id})">${t(locale, "action.details")}</button>
26399
+ ${run.log ? `<button type="button" class="btn" aria-expanded="false" onclick="toggleLog(${run.id},this)">${t(locale, "action.logs")}</button>` : ""}</div></td>
26400
+ </tr>`;
26401
+ }).join("");
25841
26402
  const body = `
25842
- <div class="g4">
25843
- <div class="card"><div class="sv">${total}</div><div class="sl">\u603B\u8BB0\u5F55</div></div>
25844
- <div class="card"><div class="sv" style="color:var(--green)">${runs.filter((r) => r.status === "done").length}</div><div class="sl">\u672C\u9875\u6210\u529F</div></div>
25845
- <div class="card"><div class="sv" style="color:var(--red)">${runs.filter((r) => r.status === "failed").length}</div><div class="sl">\u672C\u9875\u5931\u8D25</div></div>
25846
- <div class="card"><div class="sv" style="color:var(--blue)">${runs.filter((r) => r.status === "running").length}</div><div class="sl">\u672C\u9875\u8FD0\u884C\u4E2D</div></div>
26403
+ <div class="stats-grid">
26404
+ ${statCard(total, t(locale, "stats.records"), "tone-purple", icon("runs"))}
26405
+ ${statCard(runs.filter((run) => run.status === "done").length, t(locale, "stats.pageDone"), "tone-green", icon("check"), "reveal-delay-1")}
26406
+ ${statCard(runs.filter((run) => run.status === "failed").length, t(locale, "stats.pageFailed"), "tone-red", icon("alert"), "reveal-delay-1")}
26407
+ ${statCard(runs.filter((run) => run.status === "running").length, t(locale, "stats.pageRunning"), "tone-blue", icon("activity"), "reveal-delay-2")}
25847
26408
  </div>
25848
- <div class="panel"><table>
25849
- <thead><tr><th width="50">Run</th><th>\u4EFB\u52A1</th><th>Agent</th><th width="90">\u72B6\u6001</th><th width="70">\u8017\u65F6</th><th>\u5FC3\u8DF3</th><th>\u64CD\u4F5C</th></tr></thead>
25850
- <tbody>${rows}${emptyRow}</tbody>
25851
- </table></div>
25852
- ${logsHtml.join("")}
25853
- ${paging}`;
25854
- return c.html(renderLayout("\u6267\u884C\u65E5\u5FD7", "runs", body));
26409
+ <section class="panel reveal reveal-delay-2">${runs.length === 0 ? emptyState(t(locale, "empty.runs"), "") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>${t(locale, "table.run")}</th><th>${t(locale, "table.task")}</th><th>${t(locale, "table.agent")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.duration")}</th><th>${t(locale, "table.heartbeat")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}</section>
26410
+ ${logs.join("")}${pagination(locale, "/runs", page, totalPages, total)}`;
26411
+ return c.html(renderLayout({ locale, activeTab: "runs", body }));
25855
26412
  });
25856
26413
  app.get("/system", async (c) => {
26414
+ const locale = resolveLocale(c);
25857
26415
  const config = loadConfig();
25858
26416
  const configPath = getConfigPath();
25859
- const stats = await TaskService.stats({});
25860
- const runningRuns = await TaskRunService.getAllRunningRuns();
25861
- const templates = await TaskTemplateService.list(100);
26417
+ const [stats, runningRuns, templates] = await Promise.all([
26418
+ TaskService.stats({}),
26419
+ TaskRunService.getAllRunningRuns(),
26420
+ TaskTemplateService.list(100)
26421
+ ]);
25862
26422
  const configExists = existsSync7(configPath);
25863
- let runRows = "";
25864
- if (runningRuns.length > 0) {
25865
- for (const run of runningRuns) {
25866
- const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
25867
- runRows += `<tr>
25868
- <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
25869
- <td class="m sm">${esc(shortS)}</td>
25870
- <td class="sm">${esc(run.model) || "-"}</td>
25871
- <td class="sm">${formatDate(run.startedAt)}</td>
25872
- <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
25873
- <td class="m sm">W:${run.workerPid ?? "-"} C:${run.childPid ?? "-"}</td>
25874
- <td class="sm">${formatDuration(run.startedAt, null)}</td>
25875
- </tr>`;
25876
- }
25877
- }
25878
- const schedulerStatus = config.scheduler.enabled ? '<span class="badge b-done">\u5DF2\u542F\u7528</span>' : '<span class="badge b-cancelled">\u5DF2\u7981\u7528</span>';
25879
- const configFileStatus = configExists ? '<span class="badge b-done">\u662F</span>' : '<span class="badge b-cancelled">\u5426 (\u4F7F\u7528\u9ED8\u8BA4\u503C)</span>';
26423
+ const runRows = runningRuns.map((run) => {
26424
+ const session = run.sessionId ? `${run.sessionId.slice(4, 7)}***${run.sessionId.slice(-3)}` : "\u2014";
26425
+ return `<tr><td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td><td data-primary data-label="${t(locale, "table.task")}">#${run.taskId}</td><td data-label="${t(locale, "table.session")}" class="m small">${esc(session)}</td>
26426
+ <td data-label="${t(locale, "table.model")}" class="small">${esc(run.model) || "\u2014"}</td><td data-label="${t(locale, "table.startedAt")}" class="small">${formatDateTime(run.startedAt, locale)}</td>
26427
+ <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td><td data-label="${t(locale, "table.pid")}" class="m small">W:${run.workerPid ?? "\u2014"} C:${run.childPid ?? "\u2014"}</td>
26428
+ <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, null)}</td></tr>`;
26429
+ }).join("");
26430
+ const unitInput = (name, value, min, unit, max) => `<div class="input-unit"><input id="${name}" type="number" name="${name}" value="${value}" min="${min}" ${max ? `max="${max}"` : ""}><span>${unit}</span></div>`;
25880
26431
  const body = `
25881
- <form id="config-form" onsubmit="event.preventDefault();saveConfig();">
25882
- <div class="g3">
25883
- <div class="card">
25884
- <h3 style="margin:0 0 12px;font-size:14px">Worker \u914D\u7F6E</h3>
25885
- <div class="form-row"><label>\u6700\u5927\u5E76\u53D1</label><input type="number" name="mc" value="${config.worker.maxConcurrency}" min="1" max="20" style="width:80px"></div>
25886
- <div class="form-row"><label>\u8F6E\u8BE2\u95F4\u9694(ms)</label><input type="number" name="pi" value="${config.worker.pollIntervalMs}" min="100" style="width:100px"></div>
25887
- <div class="form-row"><label>\u5FC3\u8DF3\u95F4\u9694(\u79D2)</label><input type="number" name="hi" value="${config.worker.heartbeatIntervalMs / 1e3}" min="5" style="width:100px"></div>
25888
- <div class="form-row"><label>\u4EFB\u52A1\u8D85\u65F6(\u5206\u949F)</label><input type="number" name="to" value="${config.worker.taskTimeoutMs / 6e4}" min="1" style="width:100px"></div>
26432
+ <form id="config-form" onsubmit="event.preventDefault();saveConfig()">
26433
+ <div class="settings-grid reveal">
26434
+ <section class="card settings-card"><h2 class="settings-title"><span>${icon("activity")}${t(locale, "system.worker")}</span></h2>
26435
+ <div class="field"><label for="mc">${t(locale, "system.maxConcurrency")}</label>${unitInput("mc", config.worker.maxConcurrency, 1, "\xD7", 20)}</div>
26436
+ <div class="field"><label for="pi">${t(locale, "system.pollInterval")}</label>${unitInput("pi", config.worker.pollIntervalMs, 100, t(locale, "system.milliseconds"))}</div>
26437
+ <div class="field"><label for="hi">${t(locale, "system.heartbeatInterval")}</label>${unitInput("hi", config.worker.heartbeatIntervalMs / 1e3, 5, t(locale, "system.seconds"))}</div>
26438
+ <div class="field"><label for="to">${t(locale, "system.taskTimeout")}</label>${unitInput("to", config.worker.taskTimeoutMs / 6e4, 1, t(locale, "system.minutes"))}</div>
26439
+ </section>
26440
+ <section class="card settings-card reveal-delay-1"><h2 class="settings-title"><span>${icon("templates")}${t(locale, "system.scheduler")}</span><span class="badge ${config.scheduler.enabled ? "b-done" : "b-cancelled"}">${t(locale, config.scheduler.enabled ? "schedule.enabled" : "schedule.disabled")}</span></h2>
26441
+ <div class="switch-field"><label for="se">${t(locale, "system.schedulerEnabled")}</label><label class="switch"><input id="se" type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}><span></span></label></div>
26442
+ <div class="field"><label for="si">${t(locale, "system.checkInterval")}</label>${unitInput("si", config.scheduler.checkIntervalMs, 100, t(locale, "system.milliseconds"))}</div>
26443
+ <div class="info-row"><span class="info-key">${t(locale, "system.activeTemplates")}</span><span class="info-value">${templates.filter((template) => template.enabled).length} / ${templates.length}</span></div>
26444
+ </section>
26445
+ <section class="card settings-card reveal-delay-2"><h2 class="settings-title"><span>${icon("system")}${t(locale, "system.watchdog")}</span></h2>
26446
+ <div class="field"><label for="wt">${t(locale, "system.heartbeatTimeout")}</label>${unitInput("wt", config.watchdog.heartbeatTimeoutMs / 1e3, 10, t(locale, "system.seconds"))}</div>
26447
+ <div class="field"><label for="wci">${t(locale, "system.checkInterval")}</label>${unitInput("wci", config.watchdog.checkIntervalMs / 1e3, 1, t(locale, "system.seconds"))}</div>
26448
+ <div class="field"><label for="wcl">${t(locale, "system.cleanupInterval")}</label>${unitInput("wcl", config.watchdog.cleanupIntervalMs / 36e5, 1, t(locale, "system.hours"))}</div>
26449
+ <div class="field"><label for="rd">${t(locale, "system.retentionDays")}</label>${unitInput("rd", config.watchdog.retentionDays, 1, t(locale, "system.days"))}</div>
26450
+ </section>
25889
26451
  </div>
25890
- <div class="card">
25891
- <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
25892
- <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
25893
- <div class="form-row"><label>\u68C0\u67E5\u95F4\u9694(ms)</label><input type="number" name="si" value="${config.scheduler.checkIntervalMs}" min="100" style="width:100px"></div>
25894
- <div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
25895
- </div>
25896
- <div class="card">
25897
- <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
25898
- <div class="form-row"><label>\u5FC3\u8DF3\u8D85\u65F6(\u79D2)</label><input type="number" name="wt" value="${config.watchdog.heartbeatTimeoutMs / 1e3}" min="10" style="width:100px"></div>
25899
- <div class="form-row"><label>\u68C0\u67E5\u95F4\u9694(\u79D2)</label><input type="number" name="wci" value="${config.watchdog.checkIntervalMs / 1e3}" min="1" style="width:100px"></div>
25900
- <div class="form-row"><label>\u6E05\u7406\u95F4\u9694(\u5C0F\u65F6)</label><input type="number" name="wcl" value="${config.watchdog.cleanupIntervalMs / 36e5}" min="1" style="width:100px"></div>
25901
- <div class="form-row"><label>\u6570\u636E\u4FDD\u7559(\u5929)</label><input type="number" name="rd" value="${config.watchdog.retentionDays}" min="1" style="width:100px"></div>
25902
- </div>
25903
- </div>
25904
- <div style="text-align:center;margin-bottom:24px">
25905
- <button type="submit" class="rf" style="font-size:14px;padding:10px 30px">\u4FDD\u5B58\u914D\u7F6E</button>
25906
- <span class="mu sm" style="margin-left:12px">\u4FDD\u5B58\u540E\u9700\u91CD\u542F Gateway \u751F\u6548</span>
25907
- </div>
26452
+ <div class="save-row"><span class="muted small">${t(locale, "system.saveHint")}</span><button type="submit" class="btn btn-primary">${t(locale, "action.save")}</button></div>
25908
26453
  </form>
25909
-
25910
- <div class="panel mt8">
25911
- <div class="ph"><h3>\u5F53\u524D\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1 (${runningRuns.length} / ${config.worker.maxConcurrency} \u5E76\u53D1)</h3></div>
25912
- ${runningRuns.length > 0 ? `<table>
25913
- <thead><tr><th>Run</th><th>\u4EFB\u52A1</th><th>Session</th><th>\u6A21\u578B</th><th>\u542F\u52A8\u65F6\u95F4</th><th>\u6700\u540E\u5FC3\u8DF3</th><th>PID</th><th>\u8017\u65F6</th></tr></thead>
25914
- <tbody>${runRows}</tbody>
25915
- </table>` : `<div class="ta-center mu p30">\u5F53\u524D\u65E0\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1</div>`}
25916
- </div>
25917
-
25918
- <div class="card mt16">
25919
- <h3 style="margin:0 0 12px;font-size:14px">\u4EFB\u52A1\u7EDF\u8BA1</h3>
25920
- <div class="g4 mb0">
25921
- <div><span class="mu sm">Pending:</span> <strong>${stats.pending || 0}</strong></div>
25922
- <div><span class="mu sm">Running:</span> <strong style="color:var(--blue)">${stats.running || 0}</strong></div>
25923
- <div><span class="mu sm">Done:</span> <strong style="color:var(--green)">${stats.done || 0}</strong></div>
25924
- <div><span class="mu sm">Failed/Dead:</span> <strong style="color:var(--red)">${(stats.failed || 0) + (stats.dead_letter || 0)}</strong></div>
25925
- </div>
25926
- </div>
25927
-
25928
- <div class="card mt16">
25929
- <h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
25930
- <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
25931
- <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
25932
- </div>
25933
-
25934
- <div class="card mt16" style="border-color:var(--red)">
25935
- <h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
25936
- <p class="sm mu" style="margin:0 0 12px">\u81EA\u52A8\u5907\u4EFD\u540E\u4E8B\u52A1\u6027\u6E05\u7A7A tasks\u3001task_runs \u548C task_templates\uFF1B\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u6267\u884C\u3002</p>
25937
- <button class="btn btn-danger" style="border-color:var(--red);color:var(--red);padding:6px 16px" onclick="clearDatabase()">\u6E05\u7A7A\u6570\u636E\u5E93</button>
25938
- </div>`;
25939
- return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
26454
+ <section class="panel reveal">
26455
+ <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: config.worker.maxConcurrency })}</h2></div>
26456
+ ${runningRuns.length === 0 ? emptyState(t(locale, "empty.running"), "") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>${t(locale, "table.run")}</th><th>${t(locale, "table.task")}</th><th>${t(locale, "table.session")}</th><th>${t(locale, "table.model")}</th><th>${t(locale, "table.startedAt")}</th><th>${t(locale, "table.heartbeat")}</th><th>${t(locale, "table.pid")}</th><th>${t(locale, "table.duration")}</th></tr></thead><tbody>${runRows}</tbody></table></div>`}
26457
+ </section>
26458
+ <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.taskStats")}</h2></div><div class="overview-grid">
26459
+ <div class="overview-item"><span>${statusText(locale, "pending")}</span><strong>${stats.pending || 0}</strong></div>
26460
+ <div class="overview-item"><span>${statusText(locale, "running")}</span><strong style="color:var(--blue)">${stats.running || 0}</strong></div>
26461
+ <div class="overview-item"><span>${statusText(locale, "done")}</span><strong style="color:var(--green)">${stats.done || 0}</strong></div>
26462
+ <div class="overview-item"><span>${t(locale, "stats.failedDead")}</span><strong style="color:var(--red)">${(stats.failed || 0) + (stats.dead_letter || 0)}</strong></div>
26463
+ </div></section>
26464
+ <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.configFile")}</h2></div><div class="info-list">
26465
+ <div class="info-row"><span class="info-key">${t(locale, "system.path")}</span><span class="info-value m small">${esc(configPath)}</span></div>
26466
+ <div class="info-row"><span class="info-key">${t(locale, "system.fileExists")}</span><span class="badge ${configExists ? "b-done" : "b-cancelled"}">${t(locale, configExists ? "system.yes" : "system.noDefault")}</span></div>
26467
+ </div></section>
26468
+ <section class="card danger-card reveal reveal-delay-2"><h2>${icon("alert")}${t(locale, "system.danger")}</h2><p>${t(locale, "system.dangerDescription")}</p>
26469
+ <button type="button" class="btn btn-danger" onclick="clearDatabase()">${icon("database")}${t(locale, "action.clearDatabase")}</button></section>`;
26470
+ return c.html(renderLayout({ locale, activeTab: "system", body }));
25940
26471
  });
25941
26472
  app.get("/api/tasks/:id", async (c) => {
25942
26473
  const id = parsePositiveInteger2(c.req.param("id"));
@@ -25956,9 +26487,9 @@ var init_web = __esm({
25956
26487
  app.get("/api/templates/:id", async (c) => {
25957
26488
  const id = parsePositiveInteger2(c.req.param("id"));
25958
26489
  if (id === null) return c.json({ error: "invalid id" }, 400);
25959
- const tmpl = await TaskTemplateService.getById(id);
25960
- if (!tmpl) return c.json({ error: "not found" }, 404);
25961
- return c.json(tmpl);
26490
+ const template = await TaskTemplateService.getById(id);
26491
+ if (!template) return c.json({ error: "not found" }, 404);
26492
+ return c.json(template);
25962
26493
  });
25963
26494
  app.post("/api/tasks/:id/retry", async (c) => {
25964
26495
  const id = parsePositiveInteger2(c.req.param("id"));
@@ -26002,14 +26533,14 @@ var init_web = __esm({
26002
26533
  app.delete("/api/templates/:id", async (c) => {
26003
26534
  const id = parsePositiveInteger2(c.req.param("id"));
26004
26535
  if (id === null) return c.json({ error: "invalid id" }, 400);
26005
- const ok = await TaskTemplateService.delete(id);
26006
- return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
26536
+ const deleted = await TaskTemplateService.delete(id);
26537
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
26007
26538
  });
26008
26539
  app.post("/api/templates/:id/trigger", async (c) => {
26009
26540
  const id = parsePositiveInteger2(c.req.param("id"));
26010
26541
  if (id === null) return c.json({ error: "invalid id" }, 400);
26011
- const tmpl = await TaskTemplateService.getById(id);
26012
- if (!tmpl) return c.json({ error: "not found" }, 404);
26542
+ const template = await TaskTemplateService.getById(id);
26543
+ if (!template) return c.json({ error: "not found" }, 404);
26013
26544
  const task = await triggerTaskFromTemplate(id);
26014
26545
  if (!task) return c.json({ error: "maxInstances reached" }, 409);
26015
26546
  return c.json({ success: true, taskId: task.id });
@@ -26018,24 +26549,27 @@ var init_web = __esm({
26018
26549
  try {
26019
26550
  const body = await c.req.json();
26020
26551
  const current = readCurrentConfig();
26021
- const curW = current.worker ?? {};
26022
- const curS = current.scheduler ?? {};
26023
- const curD = current.watchdog ?? {};
26024
- const bW = body.worker ?? {};
26025
- const bS = body.scheduler ?? {};
26026
- const bD = body.watchdog ?? {};
26552
+ const currentWorker = current.worker ?? {};
26553
+ const currentScheduler = current.scheduler ?? {};
26554
+ const currentWatchdog = current.watchdog ?? {};
26555
+ const bodyWorker = body.worker ?? {};
26556
+ const bodyScheduler = body.scheduler ?? {};
26557
+ const bodyWatchdog = body.watchdog ?? {};
26027
26558
  const merged = {
26028
26559
  ...current,
26029
26560
  ...body,
26030
26561
  configVersion: 2,
26031
- worker: { ...curW, ...bW },
26032
- scheduler: { ...curS, ...bS },
26033
- watchdog: { ...curD, ...bD }
26562
+ worker: { ...currentWorker, ...bodyWorker },
26563
+ scheduler: { ...currentScheduler, ...bodyScheduler },
26564
+ watchdog: { ...currentWatchdog, ...bodyWatchdog }
26034
26565
  };
26035
26566
  writeConfig(validateConfig(merged));
26036
26567
  return c.json({ success: true });
26037
- } catch (err) {
26038
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
26568
+ } catch (error) {
26569
+ return c.json({
26570
+ success: false,
26571
+ error: error instanceof Error ? error.message : String(error)
26572
+ }, 400);
26039
26573
  }
26040
26574
  });
26041
26575
  app.post("/api/database/clear", async (c) => {
@@ -26046,9 +26580,12 @@ var init_web = __esm({
26046
26580
  try {
26047
26581
  const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
26048
26582
  return c.json({ success: true, ...result });
26049
- } catch (err) {
26050
- const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
26051
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
26583
+ } catch (error) {
26584
+ const status = error instanceof DatabaseMaintenanceConflictError ? 409 : 500;
26585
+ return c.json({
26586
+ success: false,
26587
+ error: error instanceof Error ? error.message : String(error)
26588
+ }, status);
26052
26589
  }
26053
26590
  });
26054
26591
  dashboardApp = app;