opencode-supertask 0.1.27 → 0.1.29

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) {
@@ -23688,7 +23688,7 @@ var init_request = __esm({
23688
23688
  });
23689
23689
 
23690
23690
  // node_modules/hono/dist/utils/html.js
23691
- var HtmlEscapedCallbackPhase, raw, escapeRe, stringBufferToString, escapeToBuffer, resolveCallbackSync, resolveCallback;
23691
+ var HtmlEscapedCallbackPhase, raw, resolveCallback;
23692
23692
  var init_html = __esm({
23693
23693
  "node_modules/hono/dist/utils/html.js"() {
23694
23694
  "use strict";
@@ -23703,80 +23703,6 @@ var init_html = __esm({
23703
23703
  escapedString.callbacks = callbacks;
23704
23704
  return escapedString;
23705
23705
  };
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
23706
  resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
23781
23707
  if (typeof str === "object" && !(str instanceof String)) {
23782
23708
  if (!(str instanceof Promise)) {
@@ -24174,9 +24100,9 @@ var init_context = __esm({
24174
24100
  setDefaultContentType("application/json", headers)
24175
24101
  );
24176
24102
  };
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);
24103
+ html = (html, arg, headers) => {
24104
+ const res = (html2) => this.#newResponse(html2, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
24105
+ return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
24180
24106
  };
24181
24107
  /**
24182
24108
  * `.redirect()` can Redirect, default status code is 302.
@@ -25349,46 +25275,816 @@ var init_dist = __esm({
25349
25275
  }
25350
25276
  });
25351
25277
 
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"() {
25278
+ // src/web/ui.ts
25279
+ function t(locale, key, values = {}) {
25280
+ const template = (locale === "en" ? EN : ZH)[key];
25281
+ return template.replace(/\{([a-zA-Z]+)\}/g, (_, name) => String(values[name] ?? `{${name}}`));
25282
+ }
25283
+ function statusText(locale, status) {
25284
+ const key = `status.${status}`;
25285
+ return key in ZH ? t(locale, key) : t(locale, "status.unknown");
25286
+ }
25287
+ function formatRelative(timestamp2, locale) {
25288
+ if (!timestamp2) return "\u2014";
25289
+ const deltaMs = timestamp2 - Date.now();
25290
+ const abs = Math.abs(deltaMs);
25291
+ const language = locale === "en" ? "en" : "zh-CN";
25292
+ const formatter = new Intl.RelativeTimeFormat(language, { numeric: "auto" });
25293
+ if (abs < 6e4) return formatter.format(Math.round(deltaMs / 1e3), "second");
25294
+ if (abs < 36e5) return formatter.format(Math.round(deltaMs / 6e4), "minute");
25295
+ if (abs < 864e5) return formatter.format(Math.round(deltaMs / 36e5), "hour");
25296
+ return formatter.format(Math.round(deltaMs / 864e5), "day");
25297
+ }
25298
+ function formatFuture(timestamp2, locale) {
25299
+ if (!timestamp2) return "\u2014";
25300
+ if (timestamp2 < Date.now()) return t(locale, "schedule.overdue");
25301
+ return formatRelative(timestamp2, locale);
25302
+ }
25303
+ function formatDateTime(value, locale) {
25304
+ if (!value) return "\u2014";
25305
+ const date = value instanceof Date ? value : new Date(value);
25306
+ return new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
25307
+ month: "short",
25308
+ day: "2-digit",
25309
+ hour: "2-digit",
25310
+ minute: "2-digit"
25311
+ }).format(date);
25312
+ }
25313
+ function icon(name, className = "icon") {
25314
+ const paths = {
25315
+ 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"/>',
25316
+ tasks: '<path d="M9 6h11M9 12h11M9 18h11"/><path d="M4 6h.01M4 12h.01M4 18h.01"/>',
25317
+ templates: '<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M16 3v4M8 3v4M3 10h18"/><path d="M12 14v3l2 1"/>',
25318
+ runs: '<path d="M4 19.5V4.5M4 19.5h16"/><path d="m7 15 3-4 3 2 5-6"/>',
25319
+ 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"/>',
25320
+ refresh: '<path d="M20 6v5h-5"/><path d="M18.5 15a7 7 0 1 1-.8-7.8L20 11"/>',
25321
+ search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/>',
25322
+ 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"/>',
25323
+ 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"/>',
25324
+ chevronLeft: '<path d="m15 18-6-6 6-6"/>',
25325
+ chevronRight: '<path d="m9 18 6-6-6-6"/>',
25326
+ 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"/>',
25327
+ close: '<path d="m6 6 12 12M18 6 6 18"/>',
25328
+ inbox: '<path d="M4 4h16l2 12h-6l-2 3h-4l-2-3H2L4 4Z"/><path d="M8 9h8"/>',
25329
+ activity: '<path d="M3 12h4l2-6 4 12 2-6h6"/>',
25330
+ check: '<path d="m5 12 4 4L19 6"/>',
25331
+ alert: '<path d="M12 3 2.8 19h18.4L12 3Z"/><path d="M12 9v4M12 17h.01"/>',
25332
+ clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
25333
+ 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"/>'
25334
+ };
25335
+ 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>`;
25336
+ }
25337
+ function clientMessages(locale) {
25338
+ const keys = [
25339
+ "action.cancel",
25340
+ "action.confirm",
25341
+ "action.copy",
25342
+ "action.delete",
25343
+ "action.refresh",
25344
+ "action.logs",
25345
+ "action.hideLogs",
25346
+ "details.copySuccess",
25347
+ "feedback.copyFailed",
25348
+ "dialog.cancelTask",
25349
+ "dialog.cancelTaskBody",
25350
+ "dialog.retryTask",
25351
+ "dialog.retryTaskBody",
25352
+ "dialog.deleteTask",
25353
+ "dialog.deleteTaskBody",
25354
+ "dialog.disableTemplate",
25355
+ "dialog.disableTemplateBody",
25356
+ "dialog.deleteTemplate",
25357
+ "dialog.deleteTemplateBody",
25358
+ "dialog.triggerTemplate",
25359
+ "dialog.triggerTemplateBody",
25360
+ "dialog.clearTitle",
25361
+ "dialog.clearBody",
25362
+ "dialog.clearInstruction",
25363
+ "feedback.retryFailed",
25364
+ "feedback.cancelFailed",
25365
+ "feedback.deleteFailed",
25366
+ "feedback.requestFailed",
25367
+ "feedback.triggered",
25368
+ "feedback.configSaved",
25369
+ "feedback.databaseCleared",
25370
+ "filter.noResults"
25371
+ ];
25372
+ return Object.fromEntries(keys.map((key) => [key, t(locale, key)]));
25373
+ }
25374
+ function renderLayout(options) {
25375
+ const { locale, activeTab, body } = options;
25376
+ const page = PAGE_KEYS[activeTab];
25377
+ const nav = [
25378
+ { id: "tasks", href: "/", icon: "tasks" },
25379
+ { id: "templates", href: "/templates", icon: "templates" },
25380
+ { id: "runs", href: "/runs", icon: "runs" },
25381
+ { id: "system", href: "/system", icon: "system" }
25382
+ ];
25383
+ const ui = JSON.stringify(clientMessages(locale)).replace(/</g, "\\u003c");
25384
+ const language = locale === "en" ? "en" : "zh-CN";
25385
+ return `<!DOCTYPE html>
25386
+ <html lang="${language}">
25387
+ <head>
25388
+ <meta charset="UTF-8">
25389
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
25390
+ <meta name="color-scheme" content="light dark">
25391
+ <meta name="theme-color" content="#f5f7fb">
25392
+ <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">
25393
+ <title>${t(locale, page.title)} \xB7 SuperTask</title>
25394
+ <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>
25395
+ ${STYLES}
25396
+ </head>
25397
+ <body>
25398
+ <a class="skip-link" href="#main">${t(locale, "a11y.skip")}</a>
25399
+ <div class="app-shell">
25400
+ <header class="topbar">
25401
+ <div class="brand">
25402
+ <div class="brand-mark">${icon("brand")}</div>
25403
+ <div>
25404
+ <div class="brand-name">${t(locale, "app.name")} <span>${t(locale, "app.dashboard")}</span></div>
25405
+ <div class="brand-tagline">${t(locale, "app.tagline")}</div>
25406
+ </div>
25407
+ </div>
25408
+ <div class="top-actions">
25409
+ <div class="local-chip"><span class="live-dot"></span>${t(locale, "app.local")}</div>
25410
+ <div class="control language-switch" role="group" aria-label="${t(locale, "language.label")}">
25411
+ <button type="button" class="${locale === "zh-CN" ? "active" : ""}" onclick="setLocale('zh-CN')" aria-pressed="${locale === "zh-CN"}">\u4E2D</button>
25412
+ <button type="button" class="${locale === "en" ? "active" : ""}" onclick="setLocale('en')" aria-pressed="${locale === "en"}">EN</button>
25413
+ </div>
25414
+ <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>
25415
+ <label class="select-wrap" aria-label="${t(locale, "theme.label")}">
25416
+ ${icon("sun")}
25417
+ <select id="theme-select" class="control" onchange="setTheme(this.value)" title="${t(locale, "theme.label")}">
25418
+ <option value="system">${t(locale, "theme.system")}</option>
25419
+ <option value="light">${t(locale, "theme.light")}</option>
25420
+ <option value="dark">${t(locale, "theme.dark")}</option>
25421
+ </select>
25422
+ </label>
25423
+ <button type="button" class="icon-button" onclick="refreshPage(this)" aria-label="${t(locale, "action.refresh")}" title="${t(locale, "action.refresh")}">${icon("refresh")}</button>
25424
+ </div>
25425
+ </header>
25426
+ <nav class="tabs" aria-label="Primary">
25427
+ ${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("")}
25428
+ </nav>
25429
+ <main id="main" tabindex="-1">
25430
+ <div class="page-heading reveal">
25431
+ <div><h1>${t(locale, page.title)}</h1><p>${t(locale, page.description)}</p></div>
25432
+ </div>
25433
+ ${body}
25434
+ </main>
25435
+ <footer>${t(locale, "app.footer")}</footer>
25436
+ </div>
25437
+ <div id="toast-region" class="toast-region" role="status" aria-live="polite"></div>
25438
+ <dialog id="detail-dialog">
25439
+ <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>
25440
+ <div class="dialog-body"><pre id="detail-content" class="json-view"></pre></div>
25441
+ <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>
25442
+ </dialog>
25443
+ <dialog id="confirm-dialog">
25444
+ <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>
25445
+ <div class="dialog-body"><p id="confirm-body" class="confirm-copy"></p></div>
25446
+ <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>
25447
+ </dialog>
25448
+ <dialog id="danger-dialog">
25449
+ <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>
25450
+ <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>
25451
+ <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>
25452
+ </dialog>
25453
+ <script>
25454
+ const UI=${ui};
25455
+ const text=(key,values={})=>(UI[key]||key).replace(/{([a-zA-Z]+)}/g,(_,name)=>String(values[name]??'{'+name+'}'));
25456
+ const themeMedia=matchMedia('(prefers-color-scheme: dark)');
25457
+ 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';}
25458
+ function setTheme(preference){localStorage.setItem('supertask-theme',preference);applyTheme(preference);}
25459
+ function setLocale(value){document.cookie='supertask_locale='+encodeURIComponent(value)+'; Path=/; Max-Age=31536000; SameSite=Lax';location.reload();}
25460
+ themeMedia.addEventListener?.('change',()=>{if((localStorage.getItem('supertask-theme')||'system')==='system')applyTheme('system');});
25461
+ applyTheme(localStorage.getItem('supertask-theme')||'system');
25462
+ requestAnimationFrame(()=>document.documentElement.classList.add('ui-ready'));
25463
+ function refreshPage(button){button.classList.add('refreshing');button.setAttribute('aria-label','${t(locale, "a11y.refreshing")}');location.reload();}
25464
+ 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);}
25465
+ async function readJson(response){const data=await response.json().catch(()=>({}));if(!response.ok)throw new Error(data.error||text('feedback.requestFailed'));return data;}
25466
+ 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();});}
25467
+ 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);});}
25468
+ 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')}}
25469
+ 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')}}
25470
+ 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')}}
25471
+ 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')}}
25472
+ const showDetail=id=>showRecord('/api/tasks/'+id);const showRunDetail=id=>showRecord('/api/runs/'+id);const showTemplateDetail=id=>showRecord('/api/templates/'+id);
25473
+ async function copyDetails(){try{await navigator.clipboard.writeText(document.getElementById('detail-content').textContent);showToast(text('details.copySuccess'))}catch{showToast(text('feedback.copyFailed'),'error')}}
25474
+ async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
25475
+ 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')}}
25476
+ 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')}}
25477
+ 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')}}
25478
+ 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');}
25479
+ 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;}
25480
+ 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')}}
25481
+ 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')}}
25482
+ </script>
25483
+ </body>
25484
+ </html>`;
25485
+ }
25486
+ var ZH, EN, STYLES, PAGE_KEYS;
25487
+ var init_ui = __esm({
25488
+ "src/web/ui.ts"() {
25356
25489
  "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);
25490
+ ZH = {
25491
+ "app.name": "SuperTask",
25492
+ "app.dashboard": "\u63A7\u5236\u53F0",
25493
+ "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u8C03\u5EA6\u4E2D\u5FC3",
25494
+ "app.local": "\u672C\u5730\u8FD0\u884C",
25495
+ "app.footer": "Local-first \xB7 \u6570\u636E\u7559\u5728\u4F60\u7684\u8BBE\u5907\u4E0A",
25496
+ "nav.tasks": "\u4EFB\u52A1",
25497
+ "nav.templates": "\u8C03\u5EA6",
25498
+ "nav.runs": "\u6267\u884C\u8BB0\u5F55",
25499
+ "nav.system": "\u7CFB\u7EDF",
25500
+ "page.tasks.title": "\u4EFB\u52A1\u961F\u5217",
25501
+ "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",
25502
+ "page.templates.title": "\u8C03\u5EA6\u6A21\u677F",
25503
+ "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",
25504
+ "page.runs.title": "\u6267\u884C\u8BB0\u5F55",
25505
+ "page.runs.description": "\u8FFD\u8E2A\u6BCF\u6B21 Agent \u6267\u884C\u7684\u72B6\u6001\u3001\u8017\u65F6\u3001\u5FC3\u8DF3\u4E0E\u8F93\u51FA\u3002",
25506
+ "page.system.title": "\u7CFB\u7EDF\u8BBE\u7F6E",
25507
+ "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",
25508
+ "action.refresh": "\u5237\u65B0",
25509
+ "action.details": "\u8BE6\u60C5",
25510
+ "action.retry": "\u91CD\u8BD5",
25511
+ "action.cancel": "\u53D6\u6D88",
25512
+ "action.delete": "\u5220\u9664",
25513
+ "action.enable": "\u542F\u7528",
25514
+ "action.disable": "\u7981\u7528",
25515
+ "action.trigger": "\u7ACB\u5373\u89E6\u53D1",
25516
+ "action.logs": "\u67E5\u770B\u65E5\u5FD7",
25517
+ "action.hideLogs": "\u6536\u8D77\u65E5\u5FD7",
25518
+ "action.save": "\u4FDD\u5B58\u8BBE\u7F6E",
25519
+ "action.copy": "\u590D\u5236 JSON",
25520
+ "action.close": "\u5173\u95ED",
25521
+ "action.confirm": "\u786E\u8BA4",
25522
+ "action.clearDatabase": "\u6E05\u7A7A\u6570\u636E\u5E93",
25523
+ "status.pending": "\u5F85\u6267\u884C",
25524
+ "status.running": "\u8FD0\u884C\u4E2D",
25525
+ "status.done": "\u5DF2\u5B8C\u6210",
25526
+ "status.failed": "\u5931\u8D25",
25527
+ "status.dead_letter": "\u6B7B\u4FE1",
25528
+ "status.cancelled": "\u5DF2\u53D6\u6D88",
25529
+ "status.unknown": "\u672A\u77E5",
25530
+ "stats.total": "\u603B\u4EFB\u52A1",
25531
+ "stats.pending": "\u5F85\u6267\u884C",
25532
+ "stats.running": "\u8FD0\u884C\u4E2D",
25533
+ "stats.done": "\u5DF2\u5B8C\u6210",
25534
+ "stats.failedDead": "\u5931\u8D25\u4E0E\u6B7B\u4FE1",
25535
+ "stats.templates": "\u6A21\u677F\u603B\u6570",
25536
+ "stats.enabled": "\u5DF2\u542F\u7528",
25537
+ "stats.disabled": "\u5DF2\u7981\u7528",
25538
+ "stats.records": "\u6267\u884C\u603B\u6570",
25539
+ "stats.pageDone": "\u672C\u9875\u6210\u529F",
25540
+ "stats.pageFailed": "\u672C\u9875\u5931\u8D25",
25541
+ "stats.pageRunning": "\u672C\u9875\u8FD0\u884C\u4E2D",
25542
+ "filter.all": "\u5168\u90E8",
25543
+ "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001Agent \u6216\u63D0\u793A\u8BCD",
25544
+ "filter.noResults": "\u6CA1\u6709\u7B26\u5408\u5F53\u524D\u641C\u7D22\u6761\u4EF6\u7684\u4EFB\u52A1",
25545
+ "table.id": "ID",
25546
+ "table.task": "\u4EFB\u52A1",
25547
+ "table.name": "\u540D\u79F0",
25548
+ "table.agent": "Agent",
25549
+ "table.status": "\u72B6\u6001",
25550
+ "table.duration": "\u8017\u65F6",
25551
+ "table.retries": "\u91CD\u8BD5",
25552
+ "table.actions": "\u64CD\u4F5C",
25553
+ "table.type": "\u7C7B\u578B",
25554
+ "table.rule": "\u89C4\u5219",
25555
+ "table.lastRun": "\u4E0A\u6B21\u6267\u884C",
25556
+ "table.nextRun": "\u4E0B\u6B21\u6267\u884C",
25557
+ "table.run": "Run",
25558
+ "table.heartbeat": "\u5FC3\u8DF3",
25559
+ "table.session": "Session",
25560
+ "table.model": "\u6A21\u578B",
25561
+ "table.startedAt": "\u542F\u52A8\u65F6\u95F4",
25562
+ "table.pid": "PID",
25563
+ "pagination.previous": "\u4E0A\u4E00\u9875",
25564
+ "pagination.next": "\u4E0B\u4E00\u9875",
25565
+ "pagination.summary": "\u7B2C {page} \u9875\uFF0C\u5171 {pages} \u9875 \xB7 {total} \u6761",
25566
+ "empty.tasks": "\u961F\u5217\u91CC\u8FD8\u6CA1\u6709\u4EFB\u52A1",
25567
+ "empty.tasksHint": "\u901A\u8FC7 OpenCode \u63D2\u4EF6\u6216 supertask add \u521B\u5EFA\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u3002",
25568
+ "empty.templates": "\u8FD8\u6CA1\u6709\u8C03\u5EA6\u6A21\u677F",
25569
+ "empty.templatesHint": "\u4F7F\u7528 CLI \u521B\u5EFA\uFF1Asupertask template add",
25570
+ "empty.runs": "\u8FD8\u6CA1\u6709\u6267\u884C\u8BB0\u5F55",
25571
+ "empty.running": "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1",
25572
+ "schedule.cron": "Cron",
25573
+ "schedule.recurring": "\u5FAA\u73AF",
25574
+ "schedule.delayed": "\u5EF6\u8FDF",
25575
+ "schedule.unknown": "\u672A\u77E5",
25576
+ "schedule.enabled": "\u5DF2\u542F\u7528",
25577
+ "schedule.disabled": "\u5DF2\u7981\u7528",
25578
+ "schedule.minutes": "{count} \u5206\u949F",
25579
+ "schedule.seconds": "{count} \u79D2",
25580
+ "schedule.hours": "{count} \u5C0F\u65F6",
25581
+ "schedule.days": "{count} \u5929",
25582
+ "schedule.overdue": "\u5DF2\u5230\u671F",
25583
+ "system.worker": "Worker",
25584
+ "system.scheduler": "Scheduler",
25585
+ "system.watchdog": "Watchdog",
25586
+ "system.maxConcurrency": "\u6700\u5927\u5E76\u53D1",
25587
+ "system.pollInterval": "\u8F6E\u8BE2\u95F4\u9694",
25588
+ "system.heartbeatInterval": "\u5FC3\u8DF3\u95F4\u9694",
25589
+ "system.taskTimeout": "\u4EFB\u52A1\u8D85\u65F6",
25590
+ "system.schedulerEnabled": "\u542F\u7528\u8C03\u5EA6",
25591
+ "system.checkInterval": "\u68C0\u67E5\u95F4\u9694",
25592
+ "system.heartbeatTimeout": "\u5FC3\u8DF3\u8D85\u65F6",
25593
+ "system.cleanupInterval": "\u6E05\u7406\u95F4\u9694",
25594
+ "system.retentionDays": "\u6570\u636E\u4FDD\u7559",
25595
+ "system.milliseconds": "\u6BEB\u79D2",
25596
+ "system.seconds": "\u79D2",
25597
+ "system.minutes": "\u5206\u949F",
25598
+ "system.hours": "\u5C0F\u65F6",
25599
+ "system.days": "\u5929",
25600
+ "system.activeTemplates": "\u6D3B\u8DC3\u6A21\u677F",
25601
+ "system.saveHint": "\u4FDD\u5B58\u540E\u9700\u91CD\u542F Gateway \u751F\u6548",
25602
+ "system.runningTasks": "\u5F53\u524D\u8FD0\u884C\u4EFB\u52A1\uFF08{running} / {limit} \u5E76\u53D1\uFF09",
25603
+ "system.taskStats": "\u4EFB\u52A1\u6982\u89C8",
25604
+ "system.configFile": "\u914D\u7F6E\u6587\u4EF6",
25605
+ "system.path": "\u8DEF\u5F84",
25606
+ "system.fileExists": "\u6587\u4EF6\u5B58\u5728",
25607
+ "system.yes": "\u662F",
25608
+ "system.noDefault": "\u5426\uFF0C\u5F53\u524D\u4F7F\u7528\u9ED8\u8BA4\u503C",
25609
+ "system.danger": "\u5371\u9669\u64CD\u4F5C",
25610
+ "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",
25611
+ "theme.label": "\u4E3B\u9898",
25612
+ "theme.system": "\u8DDF\u968F\u7CFB\u7EDF",
25613
+ "theme.light": "\u6D45\u8272",
25614
+ "theme.dark": "\u6DF1\u8272",
25615
+ "language.label": "\u8BED\u8A00",
25616
+ "details.title": "\u6570\u636E\u8BE6\u60C5",
25617
+ "details.subtitle": "\u539F\u59CB\u8BB0\u5F55\uFF08JSON\uFF09",
25618
+ "details.copySuccess": "JSON \u5DF2\u590D\u5236",
25619
+ "dialog.cancelTask": "\u53D6\u6D88\u4EFB\u52A1 #{id}\uFF1F",
25620
+ "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",
25621
+ "dialog.retryTask": "\u91CD\u8BD5\u4EFB\u52A1 #{id}\uFF1F",
25622
+ "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
25623
+ "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
25624
+ "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",
25625
+ "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
25626
+ "dialog.disableTemplateBody": "\u6A21\u677F\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
25627
+ "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
25628
+ "dialog.deleteTemplateBody": "\u6A21\u677F\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
25629
+ "dialog.triggerTemplate": "\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21\uFF1F",
25630
+ "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",
25631
+ "dialog.clearTitle": "\u786E\u8BA4\u6E05\u7A7A\u6570\u636E\u5E93",
25632
+ "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",
25633
+ "dialog.clearInstruction": "\u8F93\u5165 CLEAR \u4EE5\u786E\u8BA4",
25634
+ "feedback.retryFailed": "\u91CD\u8BD5\u5931\u8D25",
25635
+ "feedback.cancelFailed": "\u53D6\u6D88\u5931\u8D25",
25636
+ "feedback.deleteFailed": "\u5220\u9664\u5931\u8D25",
25637
+ "feedback.requestFailed": "\u8BF7\u6C42\u5931\u8D25",
25638
+ "feedback.triggered": "\u5DF2\u521B\u5EFA\u4EFB\u52A1 #{id}",
25639
+ "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F Gateway \u540E\u751F\u6548",
25640
+ "feedback.databaseCleared": "\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u5907\u4EFD\u4F4D\u4E8E\uFF1A{path}",
25641
+ "feedback.copyFailed": "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u5185\u5BB9",
25642
+ "a11y.skip": "\u8DF3\u5230\u4E3B\u8981\u5185\u5BB9",
25643
+ "a11y.refreshing": "\u6B63\u5728\u5237\u65B0"
25644
+ };
25645
+ EN = {
25646
+ "app.name": "SuperTask",
25647
+ "app.dashboard": "Dashboard",
25648
+ "app.tagline": "Reliable local agent orchestration",
25649
+ "app.local": "Running locally",
25650
+ "app.footer": "Local-first \xB7 Your data stays on this device",
25651
+ "nav.tasks": "Tasks",
25652
+ "nav.templates": "Schedules",
25653
+ "nav.runs": "Runs",
25654
+ "nav.system": "System",
25655
+ "page.tasks.title": "Task queue",
25656
+ "page.tasks.description": "Track priority, execution state, and retries, then act on tasks that need attention.",
25657
+ "page.templates.title": "Schedule templates",
25658
+ "page.templates.description": "Manage cron, delayed, and recurring work so repeated tasks run on time.",
25659
+ "page.runs.title": "Execution history",
25660
+ "page.runs.description": "Inspect the status, duration, heartbeat, and output of every agent run.",
25661
+ "page.system.title": "System settings",
25662
+ "page.system.description": "Tune Gateway behavior, inspect active work, and manage local data.",
25663
+ "action.refresh": "Refresh",
25664
+ "action.details": "Details",
25665
+ "action.retry": "Retry",
25666
+ "action.cancel": "Cancel",
25667
+ "action.delete": "Delete",
25668
+ "action.enable": "Enable",
25669
+ "action.disable": "Disable",
25670
+ "action.trigger": "Run now",
25671
+ "action.logs": "View log",
25672
+ "action.hideLogs": "Hide log",
25673
+ "action.save": "Save settings",
25674
+ "action.copy": "Copy JSON",
25675
+ "action.close": "Close",
25676
+ "action.confirm": "Confirm",
25677
+ "action.clearDatabase": "Clear database",
25678
+ "status.pending": "Pending",
25679
+ "status.running": "Running",
25680
+ "status.done": "Done",
25681
+ "status.failed": "Failed",
25682
+ "status.dead_letter": "Dead letter",
25683
+ "status.cancelled": "Cancelled",
25684
+ "status.unknown": "Unknown",
25685
+ "stats.total": "Total tasks",
25686
+ "stats.pending": "Pending",
25687
+ "stats.running": "Running",
25688
+ "stats.done": "Completed",
25689
+ "stats.failedDead": "Failed & dead",
25690
+ "stats.templates": "Templates",
25691
+ "stats.enabled": "Enabled",
25692
+ "stats.disabled": "Disabled",
25693
+ "stats.records": "Total runs",
25694
+ "stats.pageDone": "Succeeded here",
25695
+ "stats.pageFailed": "Failed here",
25696
+ "stats.pageRunning": "Running here",
25697
+ "filter.all": "All",
25698
+ "filter.searchTasks": "Search tasks, agents, or prompts on this page",
25699
+ "filter.noResults": "No tasks match this search",
25700
+ "table.id": "ID",
25701
+ "table.task": "Task",
25702
+ "table.name": "Name",
25703
+ "table.agent": "Agent",
25704
+ "table.status": "Status",
25705
+ "table.duration": "Duration",
25706
+ "table.retries": "Retries",
25707
+ "table.actions": "Actions",
25708
+ "table.type": "Type",
25709
+ "table.rule": "Rule",
25710
+ "table.lastRun": "Last run",
25711
+ "table.nextRun": "Next run",
25712
+ "table.run": "Run",
25713
+ "table.heartbeat": "Heartbeat",
25714
+ "table.session": "Session",
25715
+ "table.model": "Model",
25716
+ "table.startedAt": "Started",
25717
+ "table.pid": "PID",
25718
+ "pagination.previous": "Previous",
25719
+ "pagination.next": "Next",
25720
+ "pagination.summary": "Page {page} of {pages} \xB7 {total} items",
25721
+ "empty.tasks": "Your queue is empty",
25722
+ "empty.tasksHint": "Create the first task with the OpenCode plugin or supertask add.",
25723
+ "empty.templates": "No schedule templates yet",
25724
+ "empty.templatesHint": "Create one with: supertask template add",
25725
+ "empty.runs": "No execution history yet",
25726
+ "empty.running": "No tasks are running right now",
25727
+ "schedule.cron": "Cron",
25728
+ "schedule.recurring": "Recurring",
25729
+ "schedule.delayed": "Delayed",
25730
+ "schedule.unknown": "Unknown",
25731
+ "schedule.enabled": "Enabled",
25732
+ "schedule.disabled": "Disabled",
25733
+ "schedule.minutes": "{count} min",
25734
+ "schedule.seconds": "{count} sec",
25735
+ "schedule.hours": "{count} hr",
25736
+ "schedule.days": "{count} days",
25737
+ "schedule.overdue": "Overdue",
25738
+ "system.worker": "Worker",
25739
+ "system.scheduler": "Scheduler",
25740
+ "system.watchdog": "Watchdog",
25741
+ "system.maxConcurrency": "Max concurrency",
25742
+ "system.pollInterval": "Poll interval",
25743
+ "system.heartbeatInterval": "Heartbeat interval",
25744
+ "system.taskTimeout": "Task timeout",
25745
+ "system.schedulerEnabled": "Enable scheduler",
25746
+ "system.checkInterval": "Check interval",
25747
+ "system.heartbeatTimeout": "Heartbeat timeout",
25748
+ "system.cleanupInterval": "Cleanup interval",
25749
+ "system.retentionDays": "Data retention",
25750
+ "system.milliseconds": "ms",
25751
+ "system.seconds": "seconds",
25752
+ "system.minutes": "minutes",
25753
+ "system.hours": "hours",
25754
+ "system.days": "days",
25755
+ "system.activeTemplates": "Active templates",
25756
+ "system.saveHint": "Restart Gateway to apply saved settings",
25757
+ "system.runningTasks": "Active tasks ({running} / {limit} concurrent)",
25758
+ "system.taskStats": "Task overview",
25759
+ "system.configFile": "Configuration file",
25760
+ "system.path": "Path",
25761
+ "system.fileExists": "File exists",
25762
+ "system.yes": "Yes",
25763
+ "system.noDefault": "No, using defaults",
25764
+ "system.danger": "Danger zone",
25765
+ "system.dangerDescription": "A verified backup is created first, then tasks, runs, and templates are cleared transactionally. Active work blocks this operation.",
25766
+ "theme.label": "Theme",
25767
+ "theme.system": "System",
25768
+ "theme.light": "Light",
25769
+ "theme.dark": "Dark",
25770
+ "language.label": "Language",
25771
+ "details.title": "Data details",
25772
+ "details.subtitle": "Raw record (JSON)",
25773
+ "details.copySuccess": "JSON copied",
25774
+ "dialog.cancelTask": "Cancel task #{id}?",
25775
+ "dialog.cancelTaskBody": "A running task will terminate its process tree on the next worker poll.",
25776
+ "dialog.retryTask": "Retry task #{id}?",
25777
+ "dialog.retryTaskBody": "The task returns to pending and its automatic retry budget is reset.",
25778
+ "dialog.deleteTask": "Delete task #{id}?",
25779
+ "dialog.deleteTaskBody": "The task and its execution history will be permanently deleted.",
25780
+ "dialog.disableTemplate": "Disable this schedule?",
25781
+ "dialog.disableTemplateBody": "It will stop creating new tasks automatically. Existing tasks are unchanged.",
25782
+ "dialog.deleteTemplate": "Delete this schedule?",
25783
+ "dialog.deleteTemplateBody": "This template configuration will be permanently deleted.",
25784
+ "dialog.triggerTemplate": "Run this schedule now?",
25785
+ "dialog.triggerTemplateBody": "A new task is created from the template, subject to its instance limit.",
25786
+ "dialog.clearTitle": "Confirm database clear",
25787
+ "dialog.clearBody": "This deletes every task, run, and schedule template after creating a backup.",
25788
+ "dialog.clearInstruction": "Type CLEAR to confirm",
25789
+ "feedback.retryFailed": "Retry failed",
25790
+ "feedback.cancelFailed": "Cancellation failed",
25791
+ "feedback.deleteFailed": "Delete failed",
25792
+ "feedback.requestFailed": "Request failed",
25793
+ "feedback.triggered": "Task #{id} created",
25794
+ "feedback.configSaved": "Settings saved. Restart Gateway to apply them.",
25795
+ "feedback.databaseCleared": "Database cleared. Backup: {path}",
25796
+ "feedback.copyFailed": "Copy failed. Select the content manually.",
25797
+ "a11y.skip": "Skip to main content",
25798
+ "a11y.refreshing": "Refreshing"
25799
+ };
25800
+ STYLES = `
25801
+ <style>
25802
+ :root {
25803
+ color-scheme: light;
25804
+ --bg:#f5f7fb; --bg-glow:rgba(99,102,241,.12); --surface:#ffffff; --surface-2:#f8fafc;
25805
+ --surface-3:#eef2f7; --text:#172033; --text-2:#58657a; --text-3:#8792a5;
25806
+ --border:#dfe5ee; --border-strong:#cbd4e1; --primary:#5957d9; --primary-hover:#4846c7;
25807
+ --primary-soft:#eeedff; --green:#15805d; --green-soft:#e7f7f0; --red:#c63f4f; --red-soft:#fdecef;
25808
+ --yellow:#a66608; --yellow-soft:#fff4d9; --blue:#2563b8; --blue-soft:#e8f1ff; --purple:#7552c8;
25809
+ --shadow-sm:0 1px 2px rgba(16,24,40,.04); --shadow-md:0 12px 30px rgba(30,41,59,.08);
25810
+ --shadow-lg:0 24px 60px rgba(30,41,59,.18); --radius:14px; --radius-sm:9px;
25811
+ --focus:0 0 0 3px rgba(89,87,217,.22);
25812
+ }
25813
+ :root[data-theme="dark"] {
25814
+ color-scheme: dark;
25815
+ --bg:#090d15; --bg-glow:rgba(99,102,241,.18); --surface:#111722; --surface-2:#151d2a;
25816
+ --surface-3:#1c2635; --text:#edf2f8; --text-2:#a6b1c2; --text-3:#738095;
25817
+ --border:#273244; --border-strong:#344258; --primary:#8b87ff; --primary-hover:#a19eff;
25818
+ --primary-soft:#242347; --green:#48c78e; --green-soft:#16362b; --red:#ff7180; --red-soft:#3b1e27;
25819
+ --yellow:#f0b34b; --yellow-soft:#392d18; --blue:#6ea8ff; --blue-soft:#192d4b; --purple:#b89cff;
25820
+ --shadow-sm:0 1px 2px rgba(0,0,0,.25); --shadow-md:0 16px 36px rgba(0,0,0,.28);
25821
+ --shadow-lg:0 28px 70px rgba(0,0,0,.45); --focus:0 0 0 3px rgba(139,135,255,.25);
25822
+ }
25823
+ * { box-sizing:border-box; }
25824
+ html { min-height:100%; background:var(--bg); }
25825
+ body { min-height:100vh; margin:0; color:var(--text); background:
25826
+ radial-gradient(circle at 10% -10%,var(--bg-glow),transparent 34rem),var(--bg);
25827
+ font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
25828
+ font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; }
25829
+ button,input,select { font:inherit; }
25830
+ button,a,select,input { -webkit-tap-highlight-color:transparent; }
25831
+ a { color:inherit; }
25832
+ code,.mono,.m { font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace; }
25833
+ .skip-link { position:fixed; left:16px; top:-60px; z-index:200; padding:10px 14px; border-radius:8px;
25834
+ color:#fff; background:var(--primary); transition:top .2s ease; }
25835
+ .skip-link:focus { top:16px; }
25836
+ .app-shell { width:min(1440px,100%); margin:0 auto; padding:0 28px 28px; }
25837
+ .topbar { min-height:80px; display:flex; align-items:center; justify-content:space-between; gap:20px;
25838
+ border-bottom:1px solid var(--border); }
25839
+ .brand { display:flex; align-items:center; gap:12px; min-width:0; }
25840
+ .brand-mark { width:38px; height:38px; display:grid; place-items:center; color:#fff; border-radius:11px;
25841
+ background:linear-gradient(145deg,#7773ff,#514ec8); box-shadow:0 9px 22px rgba(89,87,217,.28); }
25842
+ .brand-mark .icon { width:23px; height:23px; }
25843
+ .brand-name { display:flex; align-items:baseline; gap:7px; font-size:17px; font-weight:760; letter-spacing:-.025em; }
25844
+ .brand-name span { color:var(--text-3); font-size:12px; font-weight:650; letter-spacing:.02em; text-transform:uppercase; }
25845
+ .brand-tagline { color:var(--text-2); font-size:12px; margin-top:1px; }
25846
+ .top-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
25847
+ .local-chip { display:inline-flex; align-items:center; gap:7px; color:var(--text-2); font-size:12px; padding:7px 10px;
25848
+ border:1px solid var(--border); border-radius:999px; background:color-mix(in srgb,var(--surface) 78%,transparent); }
25849
+ .live-dot { width:7px; height:7px; border-radius:50%; background:var(--green); box-shadow:0 0 0 4px var(--green-soft); }
25850
+ .control { height:36px; border:1px solid var(--border); border-radius:9px; background:var(--surface);
25851
+ color:var(--text-2); box-shadow:var(--shadow-sm); }
25852
+ .select-wrap { position:relative; display:flex; align-items:center; }
25853
+ .select-wrap>.icon { width:15px; height:15px; position:absolute; left:10px; pointer-events:none; color:var(--text-3); }
25854
+ .select-wrap select { appearance:none; padding:0 29px 0 31px; cursor:pointer; outline:none; }
25855
+ .select-wrap::after { content:""; position:absolute; right:11px; width:6px; height:6px; border-right:1.5px solid currentColor;
25856
+ border-bottom:1.5px solid currentColor; transform:rotate(45deg) translateY(-2px); pointer-events:none; color:var(--text-3); }
25857
+ .language-switch { display:flex; padding:3px; gap:2px; }
25858
+ .language-switch button { height:28px; min-width:34px; padding:0 8px; border:0; border-radius:6px; color:var(--text-3);
25859
+ background:transparent; cursor:pointer; font-size:12px; font-weight:650; }
25860
+ .language-switch button.active { background:var(--surface-3); color:var(--text); }
25861
+ .icon-button { width:36px; height:36px; display:grid; place-items:center; border:1px solid var(--border); border-radius:9px;
25862
+ color:var(--text-2); background:var(--surface); box-shadow:var(--shadow-sm); cursor:pointer; }
25863
+ .icon-button .icon { width:17px; height:17px; }
25864
+ .icon-button:hover,.control:hover { border-color:var(--border-strong); color:var(--text); }
25865
+ .icon-button.refreshing .icon { animation:spin .7s linear infinite; }
25866
+ .tabs { display:flex; gap:6px; margin:18px 0 30px; padding:5px; width:max-content; max-width:100%; overflow-x:auto;
25867
+ border:1px solid var(--border); border-radius:12px; background:color-mix(in srgb,var(--surface) 82%,transparent); box-shadow:var(--shadow-sm); }
25868
+ .tabs a { display:flex; align-items:center; gap:8px; min-height:36px; padding:0 14px; border-radius:8px; color:var(--text-2);
25869
+ font-weight:650; font-size:13px; text-decoration:none; white-space:nowrap; transition:background .16s ease,color .16s ease,box-shadow .16s ease; }
25870
+ .tabs a .icon { width:16px; height:16px; }
25871
+ .tabs a:hover { color:var(--text); background:var(--surface-2); }
25872
+ .tabs a.active { color:var(--primary); background:var(--surface); box-shadow:0 1px 4px rgba(16,24,40,.08); }
25873
+ main { outline:none; }
25874
+ .page-heading { display:flex; justify-content:space-between; align-items:flex-end; gap:20px; margin-bottom:22px; }
25875
+ .page-heading h1 { margin:0; font-size:28px; line-height:1.2; letter-spacing:-.035em; }
25876
+ .page-heading p { margin:7px 0 0; color:var(--text-2); max-width:720px; }
25877
+ .stats-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:14px; margin-bottom:20px; }
25878
+ .stats-grid.three { grid-template-columns:repeat(3,minmax(0,1fr)); }
25879
+ .stat-card { position:relative; min-height:118px; padding:19px; overflow:hidden; border:1px solid var(--border); border-radius:var(--radius);
25880
+ background:linear-gradient(145deg,var(--surface),color-mix(in srgb,var(--surface-2) 72%,var(--surface)));
25881
+ box-shadow:var(--shadow-sm); transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease; }
25882
+ .stat-card:hover { transform:translateY(-2px); border-color:var(--border-strong); box-shadow:var(--shadow-md); }
25883
+ .stat-card::after { content:""; position:absolute; width:90px; height:90px; right:-38px; top:-45px; border-radius:50%; background:var(--tone-soft); }
25884
+ .stat-top { display:flex; justify-content:space-between; align-items:center; }
25885
+ .stat-icon { width:32px; height:32px; display:grid; place-items:center; border-radius:9px; color:var(--tone); background:var(--tone-soft); }
25886
+ .stat-icon .icon { width:17px; height:17px; }
25887
+ .stat-value { margin-top:13px; font-size:28px; font-weight:760; line-height:1; letter-spacing:-.04em; color:var(--tone); }
25888
+ .stat-label { margin-top:7px; color:var(--text-2); font-size:12px; font-weight:650; }
25889
+ .tone-neutral { --tone:var(--text-2); --tone-soft:var(--surface-3); }
25890
+ .tone-blue { --tone:var(--blue); --tone-soft:var(--blue-soft); }
25891
+ .tone-green { --tone:var(--green); --tone-soft:var(--green-soft); }
25892
+ .tone-red { --tone:var(--red); --tone-soft:var(--red-soft); }
25893
+ .tone-purple { --tone:var(--purple); --tone-soft:var(--primary-soft); }
25894
+ .toolbar { display:flex; justify-content:space-between; align-items:center; gap:14px; margin:0 0 12px; }
25895
+ .filters { display:flex; gap:6px; overflow-x:auto; padding:2px; }
25896
+ .filter-chip { display:inline-flex; align-items:center; min-height:34px; padding:0 12px; border:1px solid var(--border); border-radius:9px;
25897
+ color:var(--text-2); background:var(--surface); text-decoration:none; font-size:12px; font-weight:650; white-space:nowrap; }
25898
+ .filter-chip:hover { border-color:var(--border-strong); color:var(--text); }
25899
+ .filter-chip.active { border-color:color-mix(in srgb,var(--primary) 38%,var(--border)); color:var(--primary); background:var(--primary-soft); }
25900
+ .search-box { position:relative; width:min(320px,100%); flex:0 1 320px; }
25901
+ .search-box .icon { position:absolute; left:11px; top:50%; width:16px; height:16px; color:var(--text-3); transform:translateY(-50%); }
25902
+ .search-box input { width:100%; height:36px; padding:0 12px 0 36px; border:1px solid var(--border); border-radius:9px;
25903
+ outline:none; color:var(--text); background:var(--surface); box-shadow:var(--shadow-sm); }
25904
+ .search-box input::placeholder { color:var(--text-3); }
25905
+ .search-box input:focus { border-color:var(--primary); box-shadow:var(--focus); }
25906
+ .panel,.card { border:1px solid var(--border); border-radius:var(--radius); background:var(--surface); box-shadow:var(--shadow-sm); }
25907
+ .panel { overflow:hidden; margin-bottom:16px; }
25908
+ .panel-head { min-height:52px; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:0 18px;
25909
+ border-bottom:1px solid var(--border); }
25910
+ .panel-head h2,.panel-head h3 { margin:0; font-size:14px; letter-spacing:-.01em; }
25911
+ .table-wrap { width:100%; overflow-x:auto; }
25912
+ table { width:100%; border-collapse:separate; border-spacing:0; font-size:13px; }
25913
+ th { height:42px; padding:0 13px; color:var(--text-3); background:var(--surface-2); border-bottom:1px solid var(--border);
25914
+ font-size:11px; font-weight:730; letter-spacing:.045em; text-align:left; text-transform:uppercase; white-space:nowrap; }
25915
+ td { padding:12px 13px; border-bottom:1px solid var(--border); vertical-align:middle; }
25916
+ tbody tr:last-child td { border-bottom:0; }
25917
+ tbody tr { transition:background .14s ease; }
25918
+ tbody tr:hover { background:color-mix(in srgb,var(--primary-soft) 30%,transparent); }
25919
+ .task-name { font-weight:680; color:var(--text); }
25920
+ .task-prompt { max-width:520px; margin-top:3px; color:var(--text-2); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
25921
+ .muted,.mu { color:var(--text-2); }
25922
+ .faint { color:var(--text-3); }
25923
+ .small,.sm { font-size:12px; }
25924
+ .tag { display:inline-flex; align-items:center; min-height:23px; max-width:180px; padding:0 8px; border:1px solid var(--border);
25925
+ border-radius:7px; color:var(--text-2); background:var(--surface-2); font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
25926
+ .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; }
25927
+ .badge::before { content:""; width:6px; height:6px; border-radius:50%; background:currentColor; }
25928
+ .b-pending { color:var(--text-2); background:var(--surface-3); }
25929
+ .b-running { color:var(--blue); background:var(--blue-soft); }
25930
+ .b-running::before { animation:pulse 1.7s ease-in-out infinite; }
25931
+ .b-done { color:var(--green); background:var(--green-soft); }
25932
+ .b-failed { color:var(--red); background:var(--red-soft); }
25933
+ .b-dead_letter { color:var(--yellow); background:var(--yellow-soft); }
25934
+ .b-cancelled,.b-unknown { color:var(--text-3); background:var(--surface-3); }
25935
+ .t-cron { color:var(--purple); } .t-recurring { color:var(--blue); } .t-delayed { color:var(--yellow); }
25936
+ .actions { display:flex; align-items:center; flex-wrap:wrap; gap:5px; }
25937
+ .btn { min-height:32px; display:inline-flex; align-items:center; justify-content:center; gap:6px; padding:0 11px; border:1px solid var(--border);
25938
+ border-radius:8px; color:var(--text-2); background:var(--surface); text-decoration:none; cursor:pointer; font-size:12px; font-weight:650;
25939
+ transition:transform .12s ease,background .15s ease,border-color .15s ease,color .15s ease,box-shadow .15s ease; }
25940
+ .btn:hover { color:var(--text); border-color:var(--border-strong); background:var(--surface-2); }
25941
+ .btn:active { transform:scale(.97); }
25942
+ .btn:disabled { opacity:.5; cursor:not-allowed; transform:none; }
25943
+ .btn-primary { color:#fff; border-color:var(--primary); background:var(--primary); }
25944
+ .btn-primary:hover { color:#fff; border-color:var(--primary-hover); background:var(--primary-hover); }
25945
+ .btn-danger { color:var(--red); }
25946
+ .btn-danger:hover { color:var(--red); border-color:color-mix(in srgb,var(--red) 55%,var(--border)); background:var(--red-soft); }
25947
+ .btn-warning:hover { color:var(--yellow); border-color:color-mix(in srgb,var(--yellow) 55%,var(--border)); background:var(--yellow-soft); }
25948
+ .btn .icon { width:14px; height:14px; }
25949
+ .pagination { display:flex; justify-content:center; align-items:center; gap:10px; margin:18px 0 4px; }
25950
+ .pagination .summary { color:var(--text-2); font-size:12px; }
25951
+ .empty-state { display:grid; place-items:center; min-height:230px; padding:36px; text-align:center; }
25952
+ .empty-icon { width:48px; height:48px; display:grid; place-items:center; border-radius:14px; color:var(--primary); background:var(--primary-soft); }
25953
+ .empty-icon .icon { width:23px; height:23px; }
25954
+ .empty-state h3 { margin:13px 0 4px; font-size:15px; }
25955
+ .empty-state p { margin:0; color:var(--text-2); font-size:12px; }
25956
+ .empty-state code { display:inline-block; margin-top:10px; padding:5px 8px; border-radius:6px; background:var(--surface-3); }
25957
+ .settings-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:14px; margin-bottom:16px; }
25958
+ .settings-card { padding:18px; }
25959
+ .settings-title { display:flex; align-items:center; justify-content:space-between; margin:0 0 16px; font-size:14px; }
25960
+ .settings-title span:first-child { display:flex; align-items:center; gap:8px; }
25961
+ .settings-title .icon { width:17px; height:17px; color:var(--primary); }
25962
+ .field { display:grid; grid-template-columns:minmax(0,1fr) 112px; align-items:center; gap:12px; margin:11px 0; }
25963
+ .field label { color:var(--text-2); font-size:12px; }
25964
+ .input-unit { position:relative; }
25965
+ .input-unit input { width:100%; height:36px; padding:0 47px 0 10px; border:1px solid var(--border); border-radius:8px; outline:none;
25966
+ color:var(--text); background:var(--surface-2); }
25967
+ .input-unit span { position:absolute; right:9px; top:50%; transform:translateY(-50%); color:var(--text-3); font-size:10px; pointer-events:none; }
25968
+ .input-unit input:focus { border-color:var(--primary); box-shadow:var(--focus); background:var(--surface); }
25969
+ .switch-field { display:flex; align-items:center; justify-content:space-between; gap:12px; margin:11px 0; color:var(--text-2); font-size:12px; }
25970
+ .switch { position:relative; width:42px; height:24px; flex:0 0 auto; }
25971
+ .switch input { position:absolute; opacity:0; }
25972
+ .switch span { position:absolute; inset:0; border-radius:999px; background:var(--surface-3); border:1px solid var(--border-strong); cursor:pointer; transition:.2s ease; }
25973
+ .switch span::after { content:""; position:absolute; width:17px; height:17px; left:2px; top:2px; border-radius:50%; background:var(--surface);
25974
+ box-shadow:0 1px 3px rgba(0,0,0,.22); transition:transform .2s ease; }
25975
+ .switch input:checked+span { background:var(--primary); border-color:var(--primary); }
25976
+ .switch input:checked+span::after { transform:translateX(18px); }
25977
+ .save-row { display:flex; align-items:center; justify-content:flex-end; gap:12px; margin:0 0 24px; }
25978
+ .info-list { padding:4px 18px; }
25979
+ .info-row { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; padding:13px 0; border-bottom:1px solid var(--border); }
25980
+ .info-row:last-child { border-bottom:0; }
25981
+ .info-key { color:var(--text-2); }
25982
+ .info-value { font-weight:650; text-align:right; overflow-wrap:anywhere; }
25983
+ .overview-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; padding:18px; }
25984
+ .overview-item { padding:13px; border-radius:10px; background:var(--surface-2); }
25985
+ .overview-item span { color:var(--text-2); font-size:11px; }
25986
+ .overview-item strong { display:block; margin-top:5px; font-size:19px; }
25987
+ .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)); }
25988
+ .danger-card h2 { display:flex; align-items:center; gap:8px; margin:0 0 5px; color:var(--red); font-size:14px; }
25989
+ .danger-card h2 .icon { width:17px; height:17px; }
25990
+ .danger-card p { max-width:800px; margin:0 0 14px; color:var(--text-2); font-size:12px; }
25991
+ .log-panel { margin:12px 0; animation:reveal .18s ease both; }
25992
+ .log-box { max-height:360px; overflow:auto; padding:16px; color:var(--text-2); background:#0b1018; font-family:"SFMono-Regular",Consolas,monospace;
25993
+ font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
25994
+ :root[data-theme="light"] .log-box { color:#dbe5f3; }
25995
+ dialog { width:min(760px,calc(100% - 32px)); padding:0; border:1px solid var(--border); border-radius:16px; color:var(--text);
25996
+ background:var(--surface); box-shadow:var(--shadow-lg); }
25997
+ dialog[open] { animation:dialog-in .18s ease both; }
25998
+ dialog::backdrop { background:rgba(8,12,20,.62); backdrop-filter:blur(3px); animation:fade-in .18s ease both; }
25999
+ .dialog-head { display:flex; align-items:center; justify-content:space-between; gap:14px; padding:17px 18px; border-bottom:1px solid var(--border); }
26000
+ .dialog-head h2 { margin:0; font-size:15px; }
26001
+ .dialog-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
26002
+ .dialog-body { max-height:70vh; overflow:auto; padding:18px; }
26003
+ .dialog-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; padding:14px 18px; border-top:1px solid var(--border); }
26004
+ .json-view { min-height:160px; margin:0; padding:15px; overflow:auto; border:1px solid var(--border); border-radius:10px; color:var(--text-2);
26005
+ background:var(--surface-2); font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
26006
+ .confirm-copy { color:var(--text-2); margin:0; }
26007
+ .confirm-copy strong { display:block; margin-bottom:5px; color:var(--text); font-size:15px; }
26008
+ .danger-input { width:100%; height:40px; margin-top:14px; padding:0 12px; border:1px solid var(--border); border-radius:9px; outline:none;
26009
+ color:var(--text); background:var(--surface-2); font-family:"SFMono-Regular",Consolas,monospace; text-transform:uppercase; }
26010
+ .danger-input:focus { border-color:var(--red); box-shadow:0 0 0 3px color-mix(in srgb,var(--red) 22%,transparent); }
26011
+ .toast-region { position:fixed; top:18px; right:18px; z-index:300; display:grid; gap:8px; pointer-events:none; }
26012
+ .toast { min-width:260px; max-width:min(420px,calc(100vw - 36px)); display:flex; align-items:flex-start; gap:10px; padding:12px 14px;
26013
+ 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; }
26014
+ .toast .icon { width:18px; height:18px; flex:0 0 auto; margin-top:1px; }
26015
+ .toast.ok .icon { color:var(--green); } .toast.error .icon { color:var(--red); }
26016
+ .toast.leaving { animation:toast-out .18s ease both; }
26017
+ footer { display:flex; justify-content:center; padding:24px 0 4px; color:var(--text-3); font-size:11px; }
26018
+ [hidden] { display:none!important; }
26019
+ :focus-visible { outline:none; box-shadow:var(--focus); }
26020
+ .ui-ready,.ui-ready * { transition-property:background-color,border-color,color,box-shadow; transition-duration:.16s; transition-timing-function:ease; }
26021
+ .reveal { animation:reveal .28s ease both; }
26022
+ .reveal-delay-1 { animation-delay:.04s; } .reveal-delay-2 { animation-delay:.08s; }
26023
+ @keyframes spin { to { transform:rotate(360deg); } }
26024
+ @keyframes pulse { 0%,100% { opacity:1; box-shadow:0 0 0 0 currentColor; } 50% { opacity:.7; box-shadow:0 0 0 4px transparent; } }
26025
+ @keyframes reveal { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }
26026
+ @keyframes dialog-in { from { opacity:0; transform:translateY(8px) scale(.985); } to { opacity:1; transform:none; } }
26027
+ @keyframes fade-in { from { opacity:0; } to { opacity:1; } }
26028
+ @keyframes toast-in { from { opacity:0; transform:translateY(-8px) scale(.98); } to { opacity:1; transform:none; } }
26029
+ @keyframes toast-out { to { opacity:0; transform:translateY(-6px) scale(.98); } }
26030
+ @media (max-width:1000px) {
26031
+ .stats-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
26032
+ .settings-grid { grid-template-columns:1fr; }
26033
+ .local-chip { display:none; }
26034
+ }
26035
+ @media (max-width:720px) {
26036
+ .app-shell { padding:0 16px 20px; }
26037
+ .topbar { min-height:72px; }
26038
+ .brand-tagline,.brand-name span { display:none; }
26039
+ .top-actions { gap:5px; }
26040
+ .select-wrap select { width:42px; color:transparent; padding:0; }
26041
+ .select-wrap>.icon { left:12px; color:var(--text-2); }
26042
+ .select-wrap::after { display:none; }
26043
+ .language-switch { display:none; }
26044
+ .mobile-language { display:grid!important; }
26045
+ .tabs { width:100%; margin:14px 0 24px; }
26046
+ .tabs a { flex:1; justify-content:center; padding:0 10px; }
26047
+ .tabs a span { display:none; }
26048
+ .page-heading { align-items:flex-start; }
26049
+ .page-heading h1 { font-size:24px; }
26050
+ .page-heading p { font-size:13px; }
26051
+ .stats-grid,.stats-grid.three { grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
26052
+ .stat-card { min-height:105px; padding:15px; }
26053
+ .toolbar { align-items:stretch; flex-direction:column; }
26054
+ .search-box { width:100%; flex-basis:auto; }
26055
+ .filters { order:2; }
26056
+ .overview-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
26057
+ .save-row { align-items:flex-end; flex-direction:column-reverse; }
26058
+ .save-row .btn { width:100%; }
26059
+ .responsive-table { display:block; padding:10px; }
26060
+ .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; }
26061
+ .responsive-table tbody { display:grid; gap:10px; }
26062
+ .responsive-table tr { display:grid; gap:0; padding:10px 12px; border:1px solid var(--border); border-radius:11px; background:var(--surface-2); }
26063
+ .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; }
26064
+ .responsive-table td::before { content:attr(data-label); color:var(--text-3); font-size:10px; font-weight:730; letter-spacing:.045em; text-transform:uppercase; }
26065
+ .responsive-table td[data-primary] { display:block; padding-bottom:10px; margin-bottom:4px; border-bottom:1px solid var(--border); }
26066
+ .responsive-table td[data-primary]::before { display:none; }
26067
+ .responsive-table .task-prompt { max-width:100%; }
26068
+ .responsive-table .actions { justify-content:flex-start; }
26069
+ }
26070
+ @media (max-width:520px) {
26071
+ .stats-grid,.stats-grid.three { grid-template-columns:1fr 1fr; }
26072
+ .stat-card { min-height:98px; }
26073
+ .stat-value { font-size:24px; }
26074
+ .page-heading p { max-width:95%; }
26075
+ .field { grid-template-columns:minmax(0,1fr) 105px; }
26076
+ .overview-grid { grid-template-columns:1fr 1fr; padding:12px; }
26077
+ .pagination .summary { max-width:150px; text-align:center; }
26078
+ }
26079
+ @media (prefers-reduced-motion:reduce) {
26080
+ *,*::before,*::after { scroll-behavior:auto!important; animation-duration:.01ms!important; animation-iteration-count:1!important; transition-duration:.01ms!important; }
26081
+ }
26082
+ </style>`;
26083
+ PAGE_KEYS = {
26084
+ tasks: { title: "page.tasks.title", description: "page.tasks.description" },
26085
+ templates: { title: "page.templates.title", description: "page.templates.description" },
26086
+ runs: { title: "page.runs.title", description: "page.runs.description" },
26087
+ system: { title: "page.system.title", description: "page.system.description" }
25392
26088
  };
25393
26089
  }
25394
26090
  });
@@ -25399,7 +26095,7 @@ __export(web_exports, {
25399
26095
  dashboardApp: () => dashboardApp,
25400
26096
  default: () => web_default
25401
26097
  });
25402
- import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync5, renameSync as renameSync2 } from "fs";
26098
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
25403
26099
  import { dirname as dirname8 } from "path";
25404
26100
  function parsePositiveInteger2(value) {
25405
26101
  if (!/^\d+$/.test(value)) return null;
@@ -25412,41 +26108,34 @@ function parseTaskStatus2(value) {
25412
26108
  function safeStatus(value) {
25413
26109
  return value && TASK_STATUSES2.has(value) ? value : "unknown";
25414
26110
  }
26111
+ function resolveLocale(c) {
26112
+ const requested = c.req.query("lang");
26113
+ if (requested === "en" || requested === "zh-CN") return requested;
26114
+ const cookie = c.req.header("Cookie") ?? "";
26115
+ const match2 = /(?:^|;\s*)supertask_locale=([^;]+)/.exec(cookie);
26116
+ if (match2) {
26117
+ try {
26118
+ const saved = decodeURIComponent(match2[1]);
26119
+ if (saved === "en" || saved === "zh-CN") return saved;
26120
+ } catch {
26121
+ }
26122
+ }
26123
+ const accepted = c.req.header("Accept-Language")?.toLowerCase() ?? "";
26124
+ return accepted.startsWith("en") ? "en" : "zh-CN";
26125
+ }
25415
26126
  function formatDuration(startAt, endAt) {
25416
- if (!startAt) return "-";
26127
+ if (!startAt) return "\u2014";
25417
26128
  const start = new Date(startAt).getTime();
25418
26129
  const end = endAt ? new Date(endAt).getTime() : Date.now();
25419
26130
  const seconds = Math.floor((end - start) / 1e3);
25420
26131
  if (seconds < 0) return "0s";
25421
26132
  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;");
26133
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
26134
+ return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m`;
26135
+ }
26136
+ function esc(value) {
26137
+ if (!value) return "";
26138
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
25450
26139
  }
25451
26140
  function readCurrentConfig() {
25452
26141
  const configPath = getConfigPath();
@@ -25465,94 +26154,51 @@ function writeConfig(cfg) {
25465
26154
  writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
25466
26155
  renameSync2(tempPath, configPath);
25467
26156
  }
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);}
26157
+ function statCard(value, label, tone, cardIcon, delay = "") {
26158
+ return `<div class="stat-card ${tone} reveal ${delay}">
26159
+ <div class="stat-top"><div class="stat-icon">${cardIcon}</div></div>
26160
+ <div class="stat-value">${value}</div><div class="stat-label">${label}</div>
26161
+ </div>`;
25492
26162
  }
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;
26163
+ function emptyState(title, hint, code = "") {
26164
+ return `<div class="empty-state"><div><div class="empty-icon">${icon("inbox")}</div>
26165
+ <h3>${title}</h3><p>${hint}</p>${code ? `<code>${code}</code>` : ""}</div></div>`;
26166
+ }
26167
+ function formatInterval(milliseconds, locale) {
26168
+ const formatter = new Intl.NumberFormat(locale === "en" ? "en-US" : "zh-CN", {
26169
+ maximumFractionDigits: 1
26170
+ });
26171
+ if (milliseconds < 6e4) {
26172
+ return t(locale, "schedule.seconds", { count: formatter.format(milliseconds / 1e3) });
26173
+ }
26174
+ if (milliseconds < 36e5) {
26175
+ return t(locale, "schedule.minutes", { count: formatter.format(milliseconds / 6e4) });
26176
+ }
26177
+ if (milliseconds < 864e5) {
26178
+ return t(locale, "schedule.hours", { count: formatter.format(milliseconds / 36e5) });
26179
+ }
26180
+ return t(locale, "schedule.days", { count: formatter.format(milliseconds / 864e5) });
26181
+ }
26182
+ function pagination(locale, basePath, page, pages, total, suffix = "") {
26183
+ const previous = page > 1 ? `<a class="btn" href="${basePath}?page=${page - 1}${suffix}">${icon("chevronLeft")}${t(locale, "pagination.previous")}</a>` : "";
26184
+ const next = page < pages ? `<a class="btn" href="${basePath}?page=${page + 1}${suffix}">${t(locale, "pagination.next")}${icon("chevronRight")}</a>` : "";
26185
+ return `<div class="pagination">${previous}<span class="summary">${t(locale, "pagination.summary", { page, pages, total })}</span>${next}</div>`;
26186
+ }
26187
+ var app, TASK_STATUSES2, dashboardApp, web_default;
25542
26188
  var init_web = __esm({
25543
26189
  "src/web/index.tsx"() {
25544
26190
  "use strict";
25545
26191
  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
26192
  init_drizzle_orm();
25552
26193
  init_db2();
26194
+ init_database_maintenance_service();
26195
+ init_task_run_service();
26196
+ init_task_service();
26197
+ init_task_template_service();
25553
26198
  init_config();
25554
26199
  init_health();
25555
26200
  init_job_templates();
26201
+ init_ui();
25556
26202
  app = new Hono2();
25557
26203
  TASK_STATUSES2 = /* @__PURE__ */ new Set([
25558
26204
  "pending",
@@ -25593,84 +26239,9 @@ var init_web = __esm({
25593
26239
  const health = getGatewayHealth();
25594
26240
  return c.json(health, health.status === "ok" ? 200 : 503);
25595
26241
  });
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
26242
  app.get("/", async (c) => {
25672
- const pageParam = c.req.query("page") || "1";
25673
- const page = parsePositiveInteger2(pageParam);
26243
+ const locale = resolveLocale(c);
26244
+ const page = parsePositiveInteger2(c.req.query("page") || "1");
25674
26245
  if (page === null) return c.text("invalid page", 400);
25675
26246
  const statusFilter = c.req.query("status") || "";
25676
26247
  const parsedStatus = statusFilter ? parseTaskStatus2(statusFilter) : null;
@@ -25681,8 +26252,7 @@ var init_web = __esm({
25681
26252
  TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
25682
26253
  TaskService.stats({})
25683
26254
  ]);
25684
- const taskIds = tasks4.map((t) => t.id);
25685
- const latestRuns = await TaskRunService.getLatestByTaskIds(taskIds);
26255
+ const latestRuns = await TaskRunService.getLatestByTaskIds(tasks4.map((task) => task.id));
25686
26256
  const counts = {
25687
26257
  pending: statsData.pending || 0,
25688
26258
  running: statsData.running || 0,
@@ -25690,253 +26260,211 @@ var init_web = __esm({
25690
26260
  failed: (statsData.failed || 0) + (statsData.dead_letter || 0),
25691
26261
  total: statsData.total || 0
25692
26262
  };
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) {
26263
+ const filteredTotal = parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
26264
+ const totalPages = Math.max(1, Math.ceil(filteredTotal / limit));
26265
+ const filterItems = [
26266
+ { status: "", label: t(locale, "filter.all") },
26267
+ { status: "pending", label: statusText(locale, "pending") },
26268
+ { status: "running", label: statusText(locale, "running") },
26269
+ { status: "done", label: statusText(locale, "done") },
26270
+ { status: "failed", label: statusText(locale, "failed") },
26271
+ { status: "dead_letter", label: statusText(locale, "dead_letter") },
26272
+ { status: "cancelled", label: statusText(locale, "cancelled") }
26273
+ ];
26274
+ const filters = filterItems.map(({ status, label }) => {
26275
+ const href = status ? `/?status=${status}` : "/";
26276
+ return `<a href="${href}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
26277
+ }).join("");
26278
+ const rows = tasks4.map((task) => {
25704
26279
  const status = safeStatus(task.status);
25705
- const st = status.toUpperCase();
25706
26280
  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>`;
26281
+ const searchable = esc(`${task.name} ${task.agent} ${task.prompt}`);
26282
+ return `<tr data-task-row data-search="${searchable}">
26283
+ <td class="faint" data-label="${t(locale, "table.id")}">#${task.id}</td>
26284
+ <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>
26285
+ <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(task.agent)}</span></td>
26286
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
26287
+ <td data-label="${t(locale, "table.duration")}" class="small ${task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
26288
+ <td data-label="${t(locale, "table.retries")}" class="muted small">${(task.retryCount ?? 0) > 0 ? task.retryCount : "\u2014"}</td>
26289
+ <td data-label="${t(locale, "table.actions")}"><div class="actions">
26290
+ <button type="button" class="btn" onclick="showDetail(${task.id})">${t(locale, "action.details")}</button>
26291
+ ${task.status === "failed" || task.status === "dead_letter" ? `<button type="button" class="btn btn-warning" onclick="retryTask(${task.id})">${t(locale, "action.retry")}</button>` : ""}
26292
+ ${["pending", "running", "failed"].includes(task.status ?? "") ? `<button type="button" class="btn btn-warning" onclick="cancelTask(${task.id})">${t(locale, "action.cancel")}</button>` : ""}
26293
+ ${task.status === "running" || executionActive ? "" : `<button type="button" class="btn btn-danger" onclick="deleteTask(${task.id})">${t(locale, "action.delete")}</button>`}
26294
+ </div></td>
26295
+ </tr>`;
26296
+ }).join("");
26297
+ const table = tasks4.length === 0 ? emptyState(t(locale, "empty.tasks"), t(locale, "empty.tasksHint")) : `<div class="table-wrap"><table class="responsive-table">
26298
+ <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>
26299
+ <tbody>${rows}</tbody>
26300
+ </table></div>
26301
+ <div id="search-empty" hidden>${emptyState(t(locale, "filter.noResults"), "")}</div>`;
26302
+ const suffix = statusFilter ? `&status=${statusFilter}` : "";
25727
26303
  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>
26304
+ <div class="stats-grid">
26305
+ ${statCard(counts.pending, t(locale, "stats.pending"), "tone-neutral", icon("clock"))}
26306
+ ${statCard(counts.running, t(locale, "stats.running"), "tone-blue", icon("activity"), "reveal-delay-1")}
26307
+ ${statCard(counts.done, t(locale, "stats.done"), "tone-green", icon("check"), "reveal-delay-1")}
26308
+ ${statCard(counts.failed, t(locale, "stats.failedDead"), "tone-red", icon("alert"), "reveal-delay-2")}
25733
26309
  </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));
26310
+ <div class="toolbar reveal reveal-delay-1">
26311
+ <div class="filters">${filters}</div>
26312
+ <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>
26313
+ </div>
26314
+ <section class="panel reveal reveal-delay-2">${table}</section>
26315
+ ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}`;
26316
+ return c.html(renderLayout({ locale, activeTab: "tasks", body }));
25741
26317
  });
25742
26318
  app.get("/templates", async (c) => {
26319
+ const locale = resolveLocale(c);
25743
26320
  const templates = await TaskTemplateService.list(100);
25744
- const enabled = templates.filter((t) => t.enabled).length;
26321
+ const enabled = templates.filter((template) => template.enabled).length;
25745
26322
  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>` : "";
26323
+ const rows = templates.map((template) => {
26324
+ const scheduleType = ["cron", "recurring", "delayed"].includes(template.scheduleType) ? template.scheduleType : "unknown";
26325
+ const typeLabel = scheduleType === "cron" ? t(locale, "schedule.cron") : scheduleType === "recurring" ? t(locale, "schedule.recurring") : scheduleType === "delayed" ? t(locale, "schedule.delayed") : t(locale, "schedule.unknown");
26326
+ let rule = "\u2014";
26327
+ if (template.scheduleType === "cron") rule = template.cronExpr || "\u2014";
26328
+ if (template.scheduleType === "recurring" && template.intervalMs) rule = formatInterval(template.intervalMs, locale);
26329
+ if (template.scheduleType === "delayed") rule = formatDateTime(template.runAt, locale);
26330
+ 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>`;
26331
+ return `<tr>
26332
+ <td class="faint" data-label="${t(locale, "table.id")}">#${template.id}</td>
26333
+ <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>
26334
+ <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>
26335
+ <td data-label="${t(locale, "table.type")}"><span class="tag t-${scheduleType}">${typeLabel}</span></td>
26336
+ <td data-label="${t(locale, "table.rule")}" class="m small">${esc(rule)}</td>
26337
+ <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>
26338
+ <td data-label="${t(locale, "table.lastRun")}" class="small muted">${formatRelative(template.lastRunAt, locale)}</td>
26339
+ <td data-label="${t(locale, "table.nextRun")}" class="small">${formatFuture(template.nextRunAt, locale)}</td>
26340
+ <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showTemplateDetail(${template.id})">${t(locale, "action.details")}</button>
26341
+ <button type="button" class="btn btn-primary" onclick="triggerTmpl(${template.id})">${t(locale, "action.trigger")}</button>${toggle}
26342
+ <button type="button" class="btn btn-danger" onclick="deleteTmpl(${template.id})">${t(locale, "action.delete")}</button></div></td>
26343
+ </tr>`;
26344
+ }).join("");
25774
26345
  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>
26346
+ <div class="stats-grid three">
26347
+ ${statCard(templates.length, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
26348
+ ${statCard(enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
26349
+ ${statCard(disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
25779
26350
  </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));
26351
+ <section class="panel reveal reveal-delay-2">
26352
+ <div class="panel-head"><h2>${t(locale, "page.templates.title")}</h2></div>
26353
+ ${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>`}
26354
+ </section>`;
26355
+ return c.html(renderLayout({ locale, activeTab: "templates", body }));
25788
26356
  });
25789
26357
  app.get("/runs", async (c) => {
26358
+ const locale = resolveLocale(c);
25790
26359
  const page = parsePositiveInteger2(c.req.query("page") || "1");
25791
26360
  if (page === null) return c.text("invalid page", 400);
25792
26361
  const limit = 50;
25793
26362
  const offset = (page - 1) * limit;
25794
- const { taskRuns: tr, tasks: tk } = schema_exports;
26363
+ const { taskRuns: taskRuns4, tasks: tasks4 } = schema_exports;
25795
26364
  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);
26365
+ id: taskRuns4.id,
26366
+ taskId: taskRuns4.taskId,
26367
+ sessionId: taskRuns4.sessionId,
26368
+ model: taskRuns4.model,
26369
+ status: taskRuns4.status,
26370
+ startedAt: taskRuns4.startedAt,
26371
+ finishedAt: taskRuns4.finishedAt,
26372
+ log: taskRuns4.log,
26373
+ heartbeatAt: taskRuns4.heartbeatAt,
26374
+ workerPid: taskRuns4.workerPid,
26375
+ childPid: taskRuns4.childPid,
26376
+ taskName: tasks4.name,
26377
+ taskAgent: tasks4.agent
26378
+ }).from(taskRuns4).innerJoin(tasks4, eq(taskRuns4.taskId, tasks4.id)).orderBy(desc(taskRuns4.startedAt), desc(taskRuns4.id)).limit(limit).offset(offset);
26379
+ const totalResult = await db.select({ count: sql`count(*)` }).from(taskRuns4);
25811
26380
  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) {
26381
+ const totalPages = Math.max(1, Math.ceil(total / limit));
26382
+ const logs = [];
26383
+ const rows = runs.map((run) => {
25816
26384
  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
26385
  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>` : "";
26386
+ 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>`);
26387
+ }
26388
+ return `<tr>
26389
+ <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
26390
+ <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>
26391
+ <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(run.taskAgent)}</span></td>
26392
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
26393
+ <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, run.finishedAt)}</td>
26394
+ <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td>
26395
+ <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showRunDetail(${run.id})">${t(locale, "action.details")}</button>
26396
+ ${run.log ? `<button type="button" class="btn" aria-expanded="false" onclick="toggleLog(${run.id},this)">${t(locale, "action.logs")}</button>` : ""}</div></td>
26397
+ </tr>`;
26398
+ }).join("");
25841
26399
  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>
26400
+ <div class="stats-grid">
26401
+ ${statCard(total, t(locale, "stats.records"), "tone-purple", icon("runs"))}
26402
+ ${statCard(runs.filter((run) => run.status === "done").length, t(locale, "stats.pageDone"), "tone-green", icon("check"), "reveal-delay-1")}
26403
+ ${statCard(runs.filter((run) => run.status === "failed").length, t(locale, "stats.pageFailed"), "tone-red", icon("alert"), "reveal-delay-1")}
26404
+ ${statCard(runs.filter((run) => run.status === "running").length, t(locale, "stats.pageRunning"), "tone-blue", icon("activity"), "reveal-delay-2")}
25847
26405
  </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));
26406
+ <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>
26407
+ ${logs.join("")}${pagination(locale, "/runs", page, totalPages, total)}`;
26408
+ return c.html(renderLayout({ locale, activeTab: "runs", body }));
25855
26409
  });
25856
26410
  app.get("/system", async (c) => {
26411
+ const locale = resolveLocale(c);
25857
26412
  const config = loadConfig();
25858
26413
  const configPath = getConfigPath();
25859
- const stats = await TaskService.stats({});
25860
- const runningRuns = await TaskRunService.getAllRunningRuns();
25861
- const templates = await TaskTemplateService.list(100);
26414
+ const [stats, runningRuns, templates] = await Promise.all([
26415
+ TaskService.stats({}),
26416
+ TaskRunService.getAllRunningRuns(),
26417
+ TaskTemplateService.list(100)
26418
+ ]);
25862
26419
  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>';
26420
+ const runRows = runningRuns.map((run) => {
26421
+ const session = run.sessionId ? `${run.sessionId.slice(4, 7)}***${run.sessionId.slice(-3)}` : "\u2014";
26422
+ 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>
26423
+ <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>
26424
+ <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>
26425
+ <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, null)}</td></tr>`;
26426
+ }).join("");
26427
+ 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
26428
  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>
26429
+ <form id="config-form" onsubmit="event.preventDefault();saveConfig()">
26430
+ <div class="settings-grid reveal">
26431
+ <section class="card settings-card"><h2 class="settings-title"><span>${icon("activity")}${t(locale, "system.worker")}</span></h2>
26432
+ <div class="field"><label for="mc">${t(locale, "system.maxConcurrency")}</label>${unitInput("mc", config.worker.maxConcurrency, 1, "\xD7", 20)}</div>
26433
+ <div class="field"><label for="pi">${t(locale, "system.pollInterval")}</label>${unitInput("pi", config.worker.pollIntervalMs, 100, t(locale, "system.milliseconds"))}</div>
26434
+ <div class="field"><label for="hi">${t(locale, "system.heartbeatInterval")}</label>${unitInput("hi", config.worker.heartbeatIntervalMs / 1e3, 5, t(locale, "system.seconds"))}</div>
26435
+ <div class="field"><label for="to">${t(locale, "system.taskTimeout")}</label>${unitInput("to", config.worker.taskTimeoutMs / 6e4, 1, t(locale, "system.minutes"))}</div>
26436
+ </section>
26437
+ <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>
26438
+ <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>
26439
+ <div class="field"><label for="si">${t(locale, "system.checkInterval")}</label>${unitInput("si", config.scheduler.checkIntervalMs, 100, t(locale, "system.milliseconds"))}</div>
26440
+ <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>
26441
+ </section>
26442
+ <section class="card settings-card reveal-delay-2"><h2 class="settings-title"><span>${icon("system")}${t(locale, "system.watchdog")}</span></h2>
26443
+ <div class="field"><label for="wt">${t(locale, "system.heartbeatTimeout")}</label>${unitInput("wt", config.watchdog.heartbeatTimeoutMs / 1e3, 10, t(locale, "system.seconds"))}</div>
26444
+ <div class="field"><label for="wci">${t(locale, "system.checkInterval")}</label>${unitInput("wci", config.watchdog.checkIntervalMs / 1e3, 1, t(locale, "system.seconds"))}</div>
26445
+ <div class="field"><label for="wcl">${t(locale, "system.cleanupInterval")}</label>${unitInput("wcl", config.watchdog.cleanupIntervalMs / 36e5, 1, t(locale, "system.hours"))}</div>
26446
+ <div class="field"><label for="rd">${t(locale, "system.retentionDays")}</label>${unitInput("rd", config.watchdog.retentionDays, 1, t(locale, "system.days"))}</div>
26447
+ </section>
25889
26448
  </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>
26449
+ <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
26450
  </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));
26451
+ <section class="panel reveal">
26452
+ <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: config.worker.maxConcurrency })}</h2></div>
26453
+ ${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>`}
26454
+ </section>
26455
+ <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.taskStats")}</h2></div><div class="overview-grid">
26456
+ <div class="overview-item"><span>${statusText(locale, "pending")}</span><strong>${stats.pending || 0}</strong></div>
26457
+ <div class="overview-item"><span>${statusText(locale, "running")}</span><strong style="color:var(--blue)">${stats.running || 0}</strong></div>
26458
+ <div class="overview-item"><span>${statusText(locale, "done")}</span><strong style="color:var(--green)">${stats.done || 0}</strong></div>
26459
+ <div class="overview-item"><span>${t(locale, "stats.failedDead")}</span><strong style="color:var(--red)">${(stats.failed || 0) + (stats.dead_letter || 0)}</strong></div>
26460
+ </div></section>
26461
+ <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.configFile")}</h2></div><div class="info-list">
26462
+ <div class="info-row"><span class="info-key">${t(locale, "system.path")}</span><span class="info-value m small">${esc(configPath)}</span></div>
26463
+ <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>
26464
+ </div></section>
26465
+ <section class="card danger-card reveal reveal-delay-2"><h2>${icon("alert")}${t(locale, "system.danger")}</h2><p>${t(locale, "system.dangerDescription")}</p>
26466
+ <button type="button" class="btn btn-danger" onclick="clearDatabase()">${icon("database")}${t(locale, "action.clearDatabase")}</button></section>`;
26467
+ return c.html(renderLayout({ locale, activeTab: "system", body }));
25940
26468
  });
25941
26469
  app.get("/api/tasks/:id", async (c) => {
25942
26470
  const id = parsePositiveInteger2(c.req.param("id"));
@@ -25956,9 +26484,9 @@ var init_web = __esm({
25956
26484
  app.get("/api/templates/:id", async (c) => {
25957
26485
  const id = parsePositiveInteger2(c.req.param("id"));
25958
26486
  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);
26487
+ const template = await TaskTemplateService.getById(id);
26488
+ if (!template) return c.json({ error: "not found" }, 404);
26489
+ return c.json(template);
25962
26490
  });
25963
26491
  app.post("/api/tasks/:id/retry", async (c) => {
25964
26492
  const id = parsePositiveInteger2(c.req.param("id"));
@@ -26002,14 +26530,14 @@ var init_web = __esm({
26002
26530
  app.delete("/api/templates/:id", async (c) => {
26003
26531
  const id = parsePositiveInteger2(c.req.param("id"));
26004
26532
  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);
26533
+ const deleted = await TaskTemplateService.delete(id);
26534
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
26007
26535
  });
26008
26536
  app.post("/api/templates/:id/trigger", async (c) => {
26009
26537
  const id = parsePositiveInteger2(c.req.param("id"));
26010
26538
  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);
26539
+ const template = await TaskTemplateService.getById(id);
26540
+ if (!template) return c.json({ error: "not found" }, 404);
26013
26541
  const task = await triggerTaskFromTemplate(id);
26014
26542
  if (!task) return c.json({ error: "maxInstances reached" }, 409);
26015
26543
  return c.json({ success: true, taskId: task.id });
@@ -26018,24 +26546,27 @@ var init_web = __esm({
26018
26546
  try {
26019
26547
  const body = await c.req.json();
26020
26548
  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 ?? {};
26549
+ const currentWorker = current.worker ?? {};
26550
+ const currentScheduler = current.scheduler ?? {};
26551
+ const currentWatchdog = current.watchdog ?? {};
26552
+ const bodyWorker = body.worker ?? {};
26553
+ const bodyScheduler = body.scheduler ?? {};
26554
+ const bodyWatchdog = body.watchdog ?? {};
26027
26555
  const merged = {
26028
26556
  ...current,
26029
26557
  ...body,
26030
26558
  configVersion: 2,
26031
- worker: { ...curW, ...bW },
26032
- scheduler: { ...curS, ...bS },
26033
- watchdog: { ...curD, ...bD }
26559
+ worker: { ...currentWorker, ...bodyWorker },
26560
+ scheduler: { ...currentScheduler, ...bodyScheduler },
26561
+ watchdog: { ...currentWatchdog, ...bodyWatchdog }
26034
26562
  };
26035
26563
  writeConfig(validateConfig(merged));
26036
26564
  return c.json({ success: true });
26037
- } catch (err) {
26038
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
26565
+ } catch (error) {
26566
+ return c.json({
26567
+ success: false,
26568
+ error: error instanceof Error ? error.message : String(error)
26569
+ }, 400);
26039
26570
  }
26040
26571
  });
26041
26572
  app.post("/api/database/clear", async (c) => {
@@ -26046,9 +26577,12 @@ var init_web = __esm({
26046
26577
  try {
26047
26578
  const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
26048
26579
  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);
26580
+ } catch (error) {
26581
+ const status = error instanceof DatabaseMaintenanceConflictError ? 409 : 500;
26582
+ return c.json({
26583
+ success: false,
26584
+ error: error instanceof Error ? error.message : String(error)
26585
+ }, status);
26052
26586
  }
26053
26587
  });
26054
26588
  dashboardApp = app;
@@ -26465,16 +26999,18 @@ function latestVersion() {
26465
26999
  const detail = `${result.stderr ?? ""}`.trim();
26466
27000
  throw new Error(`[supertask] \u67E5\u8BE2 npm latest \u5931\u8D25: ${detail || `\u9000\u51FA\u7801 ${result.status}`}`);
26467
27001
  }
26468
- let version2;
27002
+ let response;
26469
27003
  try {
26470
- version2 = JSON.parse(output);
27004
+ response = JSON.parse(output);
26471
27005
  } catch {
26472
27006
  throw new Error(`[supertask] npm latest \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790: ${output || "(empty)"}`);
26473
27007
  }
26474
- if (typeof version2 !== "string" || !isSemanticVersion(version2)) {
26475
- throw new Error(`[supertask] npm latest \u7248\u672C\u65E0\u6548: ${String(version2)}`);
27008
+ const versions = typeof response === "string" ? [response] : Array.isArray(response) && response.every((value) => typeof value === "string") ? response : [];
27009
+ const uniqueVersions = [...new Set(versions)];
27010
+ if (uniqueVersions.length !== 1 || !isSemanticVersion(uniqueVersions[0])) {
27011
+ throw new Error(`[supertask] npm latest \u7248\u672C\u65E0\u6548: ${String(response)}`);
26476
27012
  }
26477
- return version2;
27013
+ return uniqueVersions[0];
26478
27014
  }
26479
27015
  function resolveInstalledVersion(expectedVersion) {
26480
27016
  const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;