opencode-supertask 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9486,8 +9486,8 @@ var require_luxon = __commonJS({
9486
9486
  * Set whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals
9487
9487
  * @type {boolean}
9488
9488
  */
9489
- static set throwOnInvalid(t) {
9490
- throwOnInvalid = t;
9489
+ static set throwOnInvalid(t2) {
9490
+ throwOnInvalid = t2;
9491
9491
  }
9492
9492
  /**
9493
9493
  * Reset Luxon's global caches. Should only be necessary in testing scenarios.
@@ -10380,7 +10380,7 @@ var require_luxon = __commonJS({
10380
10380
  }, tokens = _Formatter.parseFormat(fmt), realTokens = tokens.reduce((found, {
10381
10381
  literal,
10382
10382
  val
10383
- }) => literal ? found : found.concat(val), []), collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t) => t)), durationInfo = {
10383
+ }) => literal ? found : found.concat(val), []), collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter((t2) => t2)), durationInfo = {
10384
10384
  isNegativeDuration: collapsed < 0,
10385
10385
  // this relies on "collapsed" being based on "shiftTo", which builds up the object
10386
10386
  // in order
@@ -12290,15 +12290,15 @@ var require_luxon = __commonJS({
12290
12290
  return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
12291
12291
  }
12292
12292
  function unitForToken(token, loc) {
12293
- 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) => ({
12294
- regex: RegExp(escapeToken(t.val)),
12293
+ 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) => ({
12294
+ regex: RegExp(escapeToken(t2.val)),
12295
12295
  deser: ([s2]) => s2,
12296
12296
  literal: true
12297
- }), unitate = (t) => {
12297
+ }), unitate = (t2) => {
12298
12298
  if (token.literal) {
12299
- return literal(t);
12299
+ return literal(t2);
12300
12300
  }
12301
- switch (t.val) {
12301
+ switch (t2.val) {
12302
12302
  // era
12303
12303
  case "G":
12304
12304
  return oneOf(loc.eras("short"), 0);
@@ -12413,7 +12413,7 @@ var require_luxon = __commonJS({
12413
12413
  case " ":
12414
12414
  return simple(/[^\S\n\r]/);
12415
12415
  default:
12416
- return literal(t);
12416
+ return literal(t2);
12417
12417
  }
12418
12418
  };
12419
12419
  const unit = unitate(token) || {
@@ -12615,15 +12615,15 @@ var require_luxon = __commonJS({
12615
12615
  return tokens;
12616
12616
  }
12617
12617
  function expandMacroTokens(tokens, locale) {
12618
- return Array.prototype.concat(...tokens.map((t) => maybeExpandMacroToken(t, locale)));
12618
+ return Array.prototype.concat(...tokens.map((t2) => maybeExpandMacroToken(t2, locale)));
12619
12619
  }
12620
12620
  var TokenParser = class {
12621
12621
  constructor(locale, format) {
12622
12622
  this.locale = locale;
12623
12623
  this.format = format;
12624
12624
  this.tokens = expandMacroTokens(Formatter.parseFormat(format), locale);
12625
- this.units = this.tokens.map((t) => unitForToken(t, locale));
12626
- this.disqualifyingUnit = this.units.find((t) => t.invalidReason);
12625
+ this.units = this.tokens.map((t2) => unitForToken(t2, locale));
12626
+ this.disqualifyingUnit = this.units.find((t2) => t2.invalidReason);
12627
12627
  if (!this.disqualifyingUnit) {
12628
12628
  const [regexString, handlers] = buildRegex(this.units);
12629
12629
  this.regex = RegExp(regexString, "i");
@@ -13448,7 +13448,7 @@ var require_luxon = __commonJS({
13448
13448
  */
13449
13449
  static parseFormatForOpts(formatOpts, localeOpts = {}) {
13450
13450
  const tokenList = formatOptsToTokens(formatOpts, Locale.fromObject(localeOpts));
13451
- return !tokenList ? null : tokenList.map((t) => t ? t.val : null).join("");
13451
+ return !tokenList ? null : tokenList.map((t2) => t2 ? t2.val : null).join("");
13452
13452
  }
13453
13453
  /**
13454
13454
  * Produce the the fully expanded format token for the locale
@@ -13459,7 +13459,7 @@ var require_luxon = __commonJS({
13459
13459
  */
13460
13460
  static expandFormat(fmt, localeOpts = {}) {
13461
13461
  const expanded = expandMacroTokens(Formatter.parseFormat(fmt), Locale.fromObject(localeOpts));
13462
- return expanded.map((t) => t.val).join("");
13462
+ return expanded.map((t2) => t2.val).join("");
13463
13463
  }
13464
13464
  static resetCache() {
13465
13465
  zoneOffsetTs = void 0;
@@ -16316,10 +16316,10 @@ var require_random = __commonJS({
16316
16316
  }
16317
16317
  function mulberry32(seed) {
16318
16318
  return () => {
16319
- let t = seed += 1831565813;
16320
- t = Math.imul(t ^ t >>> 15, t | 1);
16321
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
16322
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
16319
+ let t2 = seed += 1831565813;
16320
+ t2 = Math.imul(t2 ^ t2 >>> 15, t2 | 1);
16321
+ t2 ^= t2 + Math.imul(t2 ^ t2 >>> 7, t2 | 61);
16322
+ return ((t2 ^ t2 >>> 14) >>> 0) / 4294967296;
16323
16323
  };
16324
16324
  }
16325
16325
  function seededRandom(str) {
@@ -17790,7 +17790,7 @@ var init_request = __esm({
17790
17790
  });
17791
17791
 
17792
17792
  // node_modules/hono/dist/utils/html.js
17793
- var HtmlEscapedCallbackPhase, raw, escapeRe, stringBufferToString, escapeToBuffer, resolveCallbackSync, resolveCallback;
17793
+ var HtmlEscapedCallbackPhase, raw, resolveCallback;
17794
17794
  var init_html = __esm({
17795
17795
  "node_modules/hono/dist/utils/html.js"() {
17796
17796
  "use strict";
@@ -17805,80 +17805,6 @@ var init_html = __esm({
17805
17805
  escapedString.callbacks = callbacks;
17806
17806
  return escapedString;
17807
17807
  };
17808
- escapeRe = /[&<>'"]/;
17809
- stringBufferToString = async (buffer, callbacks) => {
17810
- let str = "";
17811
- callbacks ||= [];
17812
- const resolvedBuffer = await Promise.all(buffer);
17813
- for (let i = resolvedBuffer.length - 1; ; i--) {
17814
- str += resolvedBuffer[i];
17815
- i--;
17816
- if (i < 0) {
17817
- break;
17818
- }
17819
- let r = resolvedBuffer[i];
17820
- if (typeof r === "object") {
17821
- callbacks.push(...r.callbacks || []);
17822
- }
17823
- const isEscaped = r.isEscaped;
17824
- r = await (typeof r === "object" ? r.toString() : r);
17825
- if (typeof r === "object") {
17826
- callbacks.push(...r.callbacks || []);
17827
- }
17828
- if (r.isEscaped ?? isEscaped) {
17829
- str += r;
17830
- } else {
17831
- const buf = [str];
17832
- escapeToBuffer(r, buf);
17833
- str = buf[0];
17834
- }
17835
- }
17836
- return raw(str, callbacks);
17837
- };
17838
- escapeToBuffer = (str, buffer) => {
17839
- const match2 = str.search(escapeRe);
17840
- if (match2 === -1) {
17841
- buffer[0] += str;
17842
- return;
17843
- }
17844
- let escape;
17845
- let index2;
17846
- let lastIndex = 0;
17847
- for (index2 = match2; index2 < str.length; index2++) {
17848
- switch (str.charCodeAt(index2)) {
17849
- case 34:
17850
- escape = "&quot;";
17851
- break;
17852
- case 39:
17853
- escape = "&#39;";
17854
- break;
17855
- case 38:
17856
- escape = "&amp;";
17857
- break;
17858
- case 60:
17859
- escape = "&lt;";
17860
- break;
17861
- case 62:
17862
- escape = "&gt;";
17863
- break;
17864
- default:
17865
- continue;
17866
- }
17867
- buffer[0] += str.substring(lastIndex, index2) + escape;
17868
- lastIndex = index2 + 1;
17869
- }
17870
- buffer[0] += str.substring(lastIndex, index2);
17871
- };
17872
- resolveCallbackSync = (str) => {
17873
- const callbacks = str.callbacks;
17874
- if (!callbacks?.length) {
17875
- return str;
17876
- }
17877
- const buffer = [str];
17878
- const context = {};
17879
- callbacks.forEach((c) => c({ phase: HtmlEscapedCallbackPhase.Stringify, buffer, context }));
17880
- return buffer[0];
17881
- };
17882
17808
  resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => {
17883
17809
  if (typeof str === "object" && !(str instanceof String)) {
17884
17810
  if (!(str instanceof Promise)) {
@@ -18276,9 +18202,9 @@ var init_context = __esm({
18276
18202
  setDefaultContentType("application/json", headers)
18277
18203
  );
18278
18204
  };
18279
- html = (html2, arg, headers) => {
18280
- const res = (html22) => this.#newResponse(html22, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
18281
- return typeof html2 === "object" ? resolveCallback(html2, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html2);
18205
+ html = (html, arg, headers) => {
18206
+ const res = (html2) => this.#newResponse(html2, arg, setDefaultContentType("text/html; charset=UTF-8", headers));
18207
+ return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html);
18282
18208
  };
18283
18209
  /**
18284
18210
  * `.redirect()` can Redirect, default status code is 302.
@@ -19451,50 +19377,6 @@ var init_dist = __esm({
19451
19377
  }
19452
19378
  });
19453
19379
 
19454
- // node_modules/hono/dist/helper/html/index.js
19455
- var html;
19456
- var init_html2 = __esm({
19457
- "node_modules/hono/dist/helper/html/index.js"() {
19458
- "use strict";
19459
- init_html();
19460
- html = (strings, ...values) => {
19461
- const buffer = [""];
19462
- for (let i = 0, len = strings.length - 1; i < len; i++) {
19463
- buffer[0] += strings[i];
19464
- const children = Array.isArray(values[i]) ? values[i].flat(Infinity) : [values[i]];
19465
- for (let i2 = 0, len2 = children.length; i2 < len2; i2++) {
19466
- const child = children[i2];
19467
- if (typeof child === "string") {
19468
- escapeToBuffer(child, buffer);
19469
- } else if (typeof child === "number") {
19470
- ;
19471
- buffer[0] += child;
19472
- } else if (typeof child === "boolean" || child === null || child === void 0) {
19473
- continue;
19474
- } else if (typeof child === "object" && child.isEscaped) {
19475
- if (child.callbacks) {
19476
- buffer.unshift("", child);
19477
- } else {
19478
- const tmp = child.toString();
19479
- if (tmp instanceof Promise) {
19480
- buffer.unshift("", tmp);
19481
- } else {
19482
- buffer[0] += tmp;
19483
- }
19484
- }
19485
- } else if (child instanceof Promise) {
19486
- buffer.unshift("", child);
19487
- } else {
19488
- escapeToBuffer(child.toString(), buffer);
19489
- }
19490
- }
19491
- }
19492
- buffer[0] += strings.at(-1);
19493
- return buffer.length === 1 ? "callbacks" in buffer ? raw(resolveCallbackSync(raw(buffer[0], buffer.callbacks))) : raw(buffer[0]) : stringBufferToString(buffer, buffer.callbacks);
19494
- };
19495
- }
19496
- });
19497
-
19498
19380
  // src/core/services/database-maintenance.service.ts
19499
19381
  import { Database as Database3 } from "bun:sqlite";
19500
19382
  import { randomUUID as randomUUID2 } from "crypto";
@@ -19931,13 +19813,827 @@ var init_database_maintenance_service = __esm({
19931
19813
  }
19932
19814
  });
19933
19815
 
19816
+ // src/web/ui.ts
19817
+ function t(locale, key, values = {}) {
19818
+ const template = (locale === "en" ? EN : ZH)[key];
19819
+ return template.replace(/\{([a-zA-Z]+)\}/g, (_, name) => String(values[name] ?? `{${name}}`));
19820
+ }
19821
+ function statusText(locale, status) {
19822
+ const key = `status.${status}`;
19823
+ return key in ZH ? t(locale, key) : t(locale, "status.unknown");
19824
+ }
19825
+ function formatRelative(timestamp2, locale) {
19826
+ if (!timestamp2) return "\u2014";
19827
+ const deltaMs = timestamp2 - Date.now();
19828
+ const abs = Math.abs(deltaMs);
19829
+ const language = locale === "en" ? "en" : "zh-CN";
19830
+ const formatter = new Intl.RelativeTimeFormat(language, { numeric: "auto" });
19831
+ if (abs < 6e4) return formatter.format(Math.round(deltaMs / 1e3), "second");
19832
+ if (abs < 36e5) return formatter.format(Math.round(deltaMs / 6e4), "minute");
19833
+ if (abs < 864e5) return formatter.format(Math.round(deltaMs / 36e5), "hour");
19834
+ return formatter.format(Math.round(deltaMs / 864e5), "day");
19835
+ }
19836
+ function formatFuture(timestamp2, locale) {
19837
+ if (!timestamp2) return "\u2014";
19838
+ if (timestamp2 < Date.now()) return t(locale, "schedule.overdue");
19839
+ return formatRelative(timestamp2, locale);
19840
+ }
19841
+ function formatDateTime(value, locale) {
19842
+ if (!value) return "\u2014";
19843
+ const date = value instanceof Date ? value : new Date(value);
19844
+ return new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
19845
+ month: "short",
19846
+ day: "2-digit",
19847
+ hour: "2-digit",
19848
+ minute: "2-digit"
19849
+ }).format(date);
19850
+ }
19851
+ function icon(name, className = "icon") {
19852
+ const paths = {
19853
+ 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"/>',
19854
+ tasks: '<path d="M9 6h11M9 12h11M9 18h11"/><path d="M4 6h.01M4 12h.01M4 18h.01"/>',
19855
+ templates: '<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M16 3v4M8 3v4M3 10h18"/><path d="M12 14v3l2 1"/>',
19856
+ runs: '<path d="M4 19.5V4.5M4 19.5h16"/><path d="m7 15 3-4 3 2 5-6"/>',
19857
+ 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"/>',
19858
+ refresh: '<path d="M20 6v5h-5"/><path d="M18.5 15a7 7 0 1 1-.8-7.8L20 11"/>',
19859
+ search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/>',
19860
+ 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"/>',
19861
+ 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"/>',
19862
+ chevronLeft: '<path d="m15 18-6-6 6-6"/>',
19863
+ chevronRight: '<path d="m9 18 6-6-6-6"/>',
19864
+ 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"/>',
19865
+ close: '<path d="m6 6 12 12M18 6 6 18"/>',
19866
+ inbox: '<path d="M4 4h16l2 12h-6l-2 3h-4l-2-3H2L4 4Z"/><path d="M8 9h8"/>',
19867
+ activity: '<path d="M3 12h4l2-6 4 12 2-6h6"/>',
19868
+ check: '<path d="m5 12 4 4L19 6"/>',
19869
+ alert: '<path d="M12 3 2.8 19h18.4L12 3Z"/><path d="M12 9v4M12 17h.01"/>',
19870
+ clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
19871
+ 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"/>'
19872
+ };
19873
+ 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>`;
19874
+ }
19875
+ function clientMessages(locale) {
19876
+ const keys = [
19877
+ "action.cancel",
19878
+ "action.confirm",
19879
+ "action.copy",
19880
+ "action.delete",
19881
+ "action.refresh",
19882
+ "action.logs",
19883
+ "action.hideLogs",
19884
+ "details.copySuccess",
19885
+ "feedback.copyFailed",
19886
+ "dialog.cancelTask",
19887
+ "dialog.cancelTaskBody",
19888
+ "dialog.retryTask",
19889
+ "dialog.retryTaskBody",
19890
+ "dialog.deleteTask",
19891
+ "dialog.deleteTaskBody",
19892
+ "dialog.disableTemplate",
19893
+ "dialog.disableTemplateBody",
19894
+ "dialog.deleteTemplate",
19895
+ "dialog.deleteTemplateBody",
19896
+ "dialog.triggerTemplate",
19897
+ "dialog.triggerTemplateBody",
19898
+ "dialog.clearTitle",
19899
+ "dialog.clearBody",
19900
+ "dialog.clearInstruction",
19901
+ "feedback.retryFailed",
19902
+ "feedback.cancelFailed",
19903
+ "feedback.deleteFailed",
19904
+ "feedback.requestFailed",
19905
+ "feedback.triggered",
19906
+ "feedback.configSaved",
19907
+ "feedback.databaseCleared",
19908
+ "filter.noResults"
19909
+ ];
19910
+ return Object.fromEntries(keys.map((key) => [key, t(locale, key)]));
19911
+ }
19912
+ function renderLayout(options) {
19913
+ const { locale, activeTab, body } = options;
19914
+ const page = PAGE_KEYS[activeTab];
19915
+ const nav = [
19916
+ { id: "tasks", href: "/", icon: "tasks" },
19917
+ { id: "templates", href: "/templates", icon: "templates" },
19918
+ { id: "runs", href: "/runs", icon: "runs" },
19919
+ { id: "system", href: "/system", icon: "system" }
19920
+ ];
19921
+ const ui = JSON.stringify(clientMessages(locale)).replace(/</g, "\\u003c");
19922
+ const language = locale === "en" ? "en" : "zh-CN";
19923
+ return `<!DOCTYPE html>
19924
+ <html lang="${language}">
19925
+ <head>
19926
+ <meta charset="UTF-8">
19927
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
19928
+ <meta name="color-scheme" content="light dark">
19929
+ <meta name="theme-color" content="#f5f7fb">
19930
+ <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">
19931
+ <title>${t(locale, page.title)} \xB7 SuperTask</title>
19932
+ <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>
19933
+ ${STYLES}
19934
+ </head>
19935
+ <body>
19936
+ <a class="skip-link" href="#main">${t(locale, "a11y.skip")}</a>
19937
+ <div class="app-shell">
19938
+ <header class="topbar">
19939
+ <div class="brand">
19940
+ <div class="brand-mark">${icon("brand")}</div>
19941
+ <div>
19942
+ <div class="brand-name">${t(locale, "app.name")} <span>${t(locale, "app.dashboard")}</span></div>
19943
+ <div class="brand-tagline">${t(locale, "app.tagline")}</div>
19944
+ </div>
19945
+ </div>
19946
+ <div class="top-actions">
19947
+ <div class="local-chip"><span class="live-dot"></span>${t(locale, "app.local")}</div>
19948
+ <div class="control language-switch" role="group" aria-label="${t(locale, "language.label")}">
19949
+ <button type="button" class="${locale === "zh-CN" ? "active" : ""}" onclick="setLocale('zh-CN')" aria-pressed="${locale === "zh-CN"}">\u4E2D</button>
19950
+ <button type="button" class="${locale === "en" ? "active" : ""}" onclick="setLocale('en')" aria-pressed="${locale === "en"}">EN</button>
19951
+ </div>
19952
+ <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>
19953
+ <label class="select-wrap" aria-label="${t(locale, "theme.label")}">
19954
+ ${icon("sun")}
19955
+ <select id="theme-select" class="control" onchange="setTheme(this.value)" title="${t(locale, "theme.label")}">
19956
+ <option value="system">${t(locale, "theme.system")}</option>
19957
+ <option value="light">${t(locale, "theme.light")}</option>
19958
+ <option value="dark">${t(locale, "theme.dark")}</option>
19959
+ </select>
19960
+ </label>
19961
+ <button type="button" class="icon-button" onclick="refreshPage(this)" aria-label="${t(locale, "action.refresh")}" title="${t(locale, "action.refresh")}">${icon("refresh")}</button>
19962
+ </div>
19963
+ </header>
19964
+ <nav class="tabs" aria-label="Primary">
19965
+ ${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("")}
19966
+ </nav>
19967
+ <main id="main" tabindex="-1">
19968
+ <div class="page-heading reveal">
19969
+ <div><h1>${t(locale, page.title)}</h1><p>${t(locale, page.description)}</p></div>
19970
+ </div>
19971
+ ${body}
19972
+ </main>
19973
+ <footer>${t(locale, "app.footer")}</footer>
19974
+ </div>
19975
+ <div id="toast-region" class="toast-region" role="status" aria-live="polite"></div>
19976
+ <dialog id="detail-dialog">
19977
+ <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>
19978
+ <div class="dialog-body"><pre id="detail-content" class="json-view"></pre></div>
19979
+ <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>
19980
+ </dialog>
19981
+ <dialog id="confirm-dialog">
19982
+ <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>
19983
+ <div class="dialog-body"><p id="confirm-body" class="confirm-copy"></p></div>
19984
+ <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>
19985
+ </dialog>
19986
+ <dialog id="danger-dialog">
19987
+ <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>
19988
+ <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>
19989
+ <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>
19990
+ </dialog>
19991
+ <script>
19992
+ const UI=${ui};
19993
+ const text=(key,values={})=>(UI[key]||key).replace(/{([a-zA-Z]+)}/g,(_,name)=>String(values[name]??'{'+name+'}'));
19994
+ const themeMedia=matchMedia('(prefers-color-scheme: dark)');
19995
+ 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';}
19996
+ function setTheme(preference){localStorage.setItem('supertask-theme',preference);applyTheme(preference);}
19997
+ function setLocale(value){document.cookie='supertask_locale='+encodeURIComponent(value)+'; Path=/; Max-Age=31536000; SameSite=Lax';location.reload();}
19998
+ themeMedia.addEventListener?.('change',()=>{if((localStorage.getItem('supertask-theme')||'system')==='system')applyTheme('system');});
19999
+ applyTheme(localStorage.getItem('supertask-theme')||'system');
20000
+ requestAnimationFrame(()=>document.documentElement.classList.add('ui-ready'));
20001
+ function refreshPage(button){button.classList.add('refreshing');button.setAttribute('aria-label','${t(locale, "a11y.refreshing")}');location.reload();}
20002
+ 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);}
20003
+ async function readJson(response){const data=await response.json().catch(()=>({}));if(!response.ok)throw new Error(data.error||text('feedback.requestFailed'));return data;}
20004
+ 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();});}
20005
+ 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);});}
20006
+ 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')}}
20007
+ 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')}}
20008
+ 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')}}
20009
+ 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')}}
20010
+ const showDetail=id=>showRecord('/api/tasks/'+id);const showRunDetail=id=>showRecord('/api/runs/'+id);const showTemplateDetail=id=>showRecord('/api/templates/'+id);
20011
+ async function copyDetails(){try{await navigator.clipboard.writeText(document.getElementById('detail-content').textContent);showToast(text('details.copySuccess'))}catch{showToast(text('feedback.copyFailed'),'error')}}
20012
+ async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
20013
+ 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')}}
20014
+ 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')}}
20015
+ 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')}}
20016
+ 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');}
20017
+ 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;}
20018
+ 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')}}
20019
+ 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')}}
20020
+ </script>
20021
+ </body>
20022
+ </html>`;
20023
+ }
20024
+ var ZH, EN, STYLES, PAGE_KEYS;
20025
+ var init_ui = __esm({
20026
+ "src/web/ui.ts"() {
20027
+ "use strict";
20028
+ ZH = {
20029
+ "app.name": "SuperTask",
20030
+ "app.dashboard": "\u63A7\u5236\u53F0",
20031
+ "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u8C03\u5EA6\u4E2D\u5FC3",
20032
+ "app.local": "\u672C\u5730\u8FD0\u884C",
20033
+ "app.footer": "Local-first \xB7 \u6570\u636E\u7559\u5728\u4F60\u7684\u8BBE\u5907\u4E0A",
20034
+ "nav.tasks": "\u4EFB\u52A1",
20035
+ "nav.templates": "\u8C03\u5EA6",
20036
+ "nav.runs": "\u6267\u884C\u8BB0\u5F55",
20037
+ "nav.system": "\u7CFB\u7EDF",
20038
+ "page.tasks.title": "\u4EFB\u52A1\u961F\u5217",
20039
+ "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",
20040
+ "page.templates.title": "\u8C03\u5EA6\u6A21\u677F",
20041
+ "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",
20042
+ "page.runs.title": "\u6267\u884C\u8BB0\u5F55",
20043
+ "page.runs.description": "\u8FFD\u8E2A\u6BCF\u6B21 Agent \u6267\u884C\u7684\u72B6\u6001\u3001\u8017\u65F6\u3001\u5FC3\u8DF3\u4E0E\u8F93\u51FA\u3002",
20044
+ "page.system.title": "\u7CFB\u7EDF\u8BBE\u7F6E",
20045
+ "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",
20046
+ "action.refresh": "\u5237\u65B0",
20047
+ "action.details": "\u8BE6\u60C5",
20048
+ "action.retry": "\u91CD\u8BD5",
20049
+ "action.cancel": "\u53D6\u6D88",
20050
+ "action.delete": "\u5220\u9664",
20051
+ "action.enable": "\u542F\u7528",
20052
+ "action.disable": "\u7981\u7528",
20053
+ "action.trigger": "\u7ACB\u5373\u89E6\u53D1",
20054
+ "action.logs": "\u67E5\u770B\u65E5\u5FD7",
20055
+ "action.hideLogs": "\u6536\u8D77\u65E5\u5FD7",
20056
+ "action.save": "\u4FDD\u5B58\u8BBE\u7F6E",
20057
+ "action.copy": "\u590D\u5236 JSON",
20058
+ "action.close": "\u5173\u95ED",
20059
+ "action.confirm": "\u786E\u8BA4",
20060
+ "action.clearDatabase": "\u6E05\u7A7A\u6570\u636E\u5E93",
20061
+ "status.pending": "\u5F85\u6267\u884C",
20062
+ "status.running": "\u8FD0\u884C\u4E2D",
20063
+ "status.done": "\u5DF2\u5B8C\u6210",
20064
+ "status.failed": "\u5931\u8D25",
20065
+ "status.dead_letter": "\u6B7B\u4FE1",
20066
+ "status.cancelled": "\u5DF2\u53D6\u6D88",
20067
+ "status.unknown": "\u672A\u77E5",
20068
+ "stats.total": "\u603B\u4EFB\u52A1",
20069
+ "stats.pending": "\u5F85\u6267\u884C",
20070
+ "stats.running": "\u8FD0\u884C\u4E2D",
20071
+ "stats.done": "\u5DF2\u5B8C\u6210",
20072
+ "stats.failedDead": "\u5931\u8D25\u4E0E\u6B7B\u4FE1",
20073
+ "stats.templates": "\u6A21\u677F\u603B\u6570",
20074
+ "stats.enabled": "\u5DF2\u542F\u7528",
20075
+ "stats.disabled": "\u5DF2\u7981\u7528",
20076
+ "stats.records": "\u6267\u884C\u603B\u6570",
20077
+ "stats.pageDone": "\u672C\u9875\u6210\u529F",
20078
+ "stats.pageFailed": "\u672C\u9875\u5931\u8D25",
20079
+ "stats.pageRunning": "\u672C\u9875\u8FD0\u884C\u4E2D",
20080
+ "filter.all": "\u5168\u90E8",
20081
+ "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001Agent \u6216\u63D0\u793A\u8BCD",
20082
+ "filter.noResults": "\u6CA1\u6709\u7B26\u5408\u5F53\u524D\u641C\u7D22\u6761\u4EF6\u7684\u4EFB\u52A1",
20083
+ "table.id": "ID",
20084
+ "table.task": "\u4EFB\u52A1",
20085
+ "table.name": "\u540D\u79F0",
20086
+ "table.agent": "Agent",
20087
+ "table.status": "\u72B6\u6001",
20088
+ "table.duration": "\u8017\u65F6",
20089
+ "table.retries": "\u91CD\u8BD5",
20090
+ "table.actions": "\u64CD\u4F5C",
20091
+ "table.type": "\u7C7B\u578B",
20092
+ "table.rule": "\u89C4\u5219",
20093
+ "table.lastRun": "\u4E0A\u6B21\u6267\u884C",
20094
+ "table.nextRun": "\u4E0B\u6B21\u6267\u884C",
20095
+ "table.run": "Run",
20096
+ "table.heartbeat": "\u5FC3\u8DF3",
20097
+ "table.session": "Session",
20098
+ "table.model": "\u6A21\u578B",
20099
+ "table.startedAt": "\u542F\u52A8\u65F6\u95F4",
20100
+ "table.pid": "PID",
20101
+ "pagination.previous": "\u4E0A\u4E00\u9875",
20102
+ "pagination.next": "\u4E0B\u4E00\u9875",
20103
+ "pagination.summary": "\u7B2C {page} \u9875\uFF0C\u5171 {pages} \u9875 \xB7 {total} \u6761",
20104
+ "empty.tasks": "\u961F\u5217\u91CC\u8FD8\u6CA1\u6709\u4EFB\u52A1",
20105
+ "empty.tasksHint": "\u901A\u8FC7 OpenCode \u63D2\u4EF6\u6216 supertask add \u521B\u5EFA\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u3002",
20106
+ "empty.templates": "\u8FD8\u6CA1\u6709\u8C03\u5EA6\u6A21\u677F",
20107
+ "empty.templatesHint": "\u4F7F\u7528 CLI \u521B\u5EFA\uFF1Asupertask template add",
20108
+ "empty.runs": "\u8FD8\u6CA1\u6709\u6267\u884C\u8BB0\u5F55",
20109
+ "empty.running": "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1",
20110
+ "schedule.cron": "Cron",
20111
+ "schedule.recurring": "\u5FAA\u73AF",
20112
+ "schedule.delayed": "\u5EF6\u8FDF",
20113
+ "schedule.unknown": "\u672A\u77E5",
20114
+ "schedule.enabled": "\u5DF2\u542F\u7528",
20115
+ "schedule.disabled": "\u5DF2\u7981\u7528",
20116
+ "schedule.minutes": "{count} \u5206\u949F",
20117
+ "schedule.seconds": "{count} \u79D2",
20118
+ "schedule.hours": "{count} \u5C0F\u65F6",
20119
+ "schedule.days": "{count} \u5929",
20120
+ "schedule.overdue": "\u5DF2\u5230\u671F",
20121
+ "system.worker": "Worker",
20122
+ "system.scheduler": "Scheduler",
20123
+ "system.watchdog": "Watchdog",
20124
+ "system.maxConcurrency": "\u6700\u5927\u5E76\u53D1",
20125
+ "system.pollInterval": "\u8F6E\u8BE2\u95F4\u9694",
20126
+ "system.heartbeatInterval": "\u5FC3\u8DF3\u95F4\u9694",
20127
+ "system.taskTimeout": "\u4EFB\u52A1\u8D85\u65F6",
20128
+ "system.schedulerEnabled": "\u542F\u7528\u8C03\u5EA6",
20129
+ "system.checkInterval": "\u68C0\u67E5\u95F4\u9694",
20130
+ "system.heartbeatTimeout": "\u5FC3\u8DF3\u8D85\u65F6",
20131
+ "system.cleanupInterval": "\u6E05\u7406\u95F4\u9694",
20132
+ "system.retentionDays": "\u6570\u636E\u4FDD\u7559",
20133
+ "system.milliseconds": "\u6BEB\u79D2",
20134
+ "system.seconds": "\u79D2",
20135
+ "system.minutes": "\u5206\u949F",
20136
+ "system.hours": "\u5C0F\u65F6",
20137
+ "system.days": "\u5929",
20138
+ "system.activeTemplates": "\u6D3B\u8DC3\u6A21\u677F",
20139
+ "system.saveHint": "\u4FDD\u5B58\u540E\u9700\u91CD\u542F Gateway \u751F\u6548",
20140
+ "system.runningTasks": "\u5F53\u524D\u8FD0\u884C\u4EFB\u52A1\uFF08{running} / {limit} \u5E76\u53D1\uFF09",
20141
+ "system.taskStats": "\u4EFB\u52A1\u6982\u89C8",
20142
+ "system.configFile": "\u914D\u7F6E\u6587\u4EF6",
20143
+ "system.path": "\u8DEF\u5F84",
20144
+ "system.fileExists": "\u6587\u4EF6\u5B58\u5728",
20145
+ "system.yes": "\u662F",
20146
+ "system.noDefault": "\u5426\uFF0C\u5F53\u524D\u4F7F\u7528\u9ED8\u8BA4\u503C",
20147
+ "system.danger": "\u5371\u9669\u64CD\u4F5C",
20148
+ "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",
20149
+ "theme.label": "\u4E3B\u9898",
20150
+ "theme.system": "\u8DDF\u968F\u7CFB\u7EDF",
20151
+ "theme.light": "\u6D45\u8272",
20152
+ "theme.dark": "\u6DF1\u8272",
20153
+ "language.label": "\u8BED\u8A00",
20154
+ "details.title": "\u6570\u636E\u8BE6\u60C5",
20155
+ "details.subtitle": "\u539F\u59CB\u8BB0\u5F55\uFF08JSON\uFF09",
20156
+ "details.copySuccess": "JSON \u5DF2\u590D\u5236",
20157
+ "dialog.cancelTask": "\u53D6\u6D88\u4EFB\u52A1 #{id}\uFF1F",
20158
+ "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",
20159
+ "dialog.retryTask": "\u91CD\u8BD5\u4EFB\u52A1 #{id}\uFF1F",
20160
+ "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
20161
+ "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
20162
+ "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",
20163
+ "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
20164
+ "dialog.disableTemplateBody": "\u6A21\u677F\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
20165
+ "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
20166
+ "dialog.deleteTemplateBody": "\u6A21\u677F\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
20167
+ "dialog.triggerTemplate": "\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21\uFF1F",
20168
+ "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",
20169
+ "dialog.clearTitle": "\u786E\u8BA4\u6E05\u7A7A\u6570\u636E\u5E93",
20170
+ "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",
20171
+ "dialog.clearInstruction": "\u8F93\u5165 CLEAR \u4EE5\u786E\u8BA4",
20172
+ "feedback.retryFailed": "\u91CD\u8BD5\u5931\u8D25",
20173
+ "feedback.cancelFailed": "\u53D6\u6D88\u5931\u8D25",
20174
+ "feedback.deleteFailed": "\u5220\u9664\u5931\u8D25",
20175
+ "feedback.requestFailed": "\u8BF7\u6C42\u5931\u8D25",
20176
+ "feedback.triggered": "\u5DF2\u521B\u5EFA\u4EFB\u52A1 #{id}",
20177
+ "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F Gateway \u540E\u751F\u6548",
20178
+ "feedback.databaseCleared": "\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u5907\u4EFD\u4F4D\u4E8E\uFF1A{path}",
20179
+ "feedback.copyFailed": "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u5185\u5BB9",
20180
+ "a11y.skip": "\u8DF3\u5230\u4E3B\u8981\u5185\u5BB9",
20181
+ "a11y.refreshing": "\u6B63\u5728\u5237\u65B0"
20182
+ };
20183
+ EN = {
20184
+ "app.name": "SuperTask",
20185
+ "app.dashboard": "Dashboard",
20186
+ "app.tagline": "Reliable local agent orchestration",
20187
+ "app.local": "Running locally",
20188
+ "app.footer": "Local-first \xB7 Your data stays on this device",
20189
+ "nav.tasks": "Tasks",
20190
+ "nav.templates": "Schedules",
20191
+ "nav.runs": "Runs",
20192
+ "nav.system": "System",
20193
+ "page.tasks.title": "Task queue",
20194
+ "page.tasks.description": "Track priority, execution state, and retries, then act on tasks that need attention.",
20195
+ "page.templates.title": "Schedule templates",
20196
+ "page.templates.description": "Manage cron, delayed, and recurring work so repeated tasks run on time.",
20197
+ "page.runs.title": "Execution history",
20198
+ "page.runs.description": "Inspect the status, duration, heartbeat, and output of every agent run.",
20199
+ "page.system.title": "System settings",
20200
+ "page.system.description": "Tune Gateway behavior, inspect active work, and manage local data.",
20201
+ "action.refresh": "Refresh",
20202
+ "action.details": "Details",
20203
+ "action.retry": "Retry",
20204
+ "action.cancel": "Cancel",
20205
+ "action.delete": "Delete",
20206
+ "action.enable": "Enable",
20207
+ "action.disable": "Disable",
20208
+ "action.trigger": "Run now",
20209
+ "action.logs": "View log",
20210
+ "action.hideLogs": "Hide log",
20211
+ "action.save": "Save settings",
20212
+ "action.copy": "Copy JSON",
20213
+ "action.close": "Close",
20214
+ "action.confirm": "Confirm",
20215
+ "action.clearDatabase": "Clear database",
20216
+ "status.pending": "Pending",
20217
+ "status.running": "Running",
20218
+ "status.done": "Done",
20219
+ "status.failed": "Failed",
20220
+ "status.dead_letter": "Dead letter",
20221
+ "status.cancelled": "Cancelled",
20222
+ "status.unknown": "Unknown",
20223
+ "stats.total": "Total tasks",
20224
+ "stats.pending": "Pending",
20225
+ "stats.running": "Running",
20226
+ "stats.done": "Completed",
20227
+ "stats.failedDead": "Failed & dead",
20228
+ "stats.templates": "Templates",
20229
+ "stats.enabled": "Enabled",
20230
+ "stats.disabled": "Disabled",
20231
+ "stats.records": "Total runs",
20232
+ "stats.pageDone": "Succeeded here",
20233
+ "stats.pageFailed": "Failed here",
20234
+ "stats.pageRunning": "Running here",
20235
+ "filter.all": "All",
20236
+ "filter.searchTasks": "Search tasks, agents, or prompts on this page",
20237
+ "filter.noResults": "No tasks match this search",
20238
+ "table.id": "ID",
20239
+ "table.task": "Task",
20240
+ "table.name": "Name",
20241
+ "table.agent": "Agent",
20242
+ "table.status": "Status",
20243
+ "table.duration": "Duration",
20244
+ "table.retries": "Retries",
20245
+ "table.actions": "Actions",
20246
+ "table.type": "Type",
20247
+ "table.rule": "Rule",
20248
+ "table.lastRun": "Last run",
20249
+ "table.nextRun": "Next run",
20250
+ "table.run": "Run",
20251
+ "table.heartbeat": "Heartbeat",
20252
+ "table.session": "Session",
20253
+ "table.model": "Model",
20254
+ "table.startedAt": "Started",
20255
+ "table.pid": "PID",
20256
+ "pagination.previous": "Previous",
20257
+ "pagination.next": "Next",
20258
+ "pagination.summary": "Page {page} of {pages} \xB7 {total} items",
20259
+ "empty.tasks": "Your queue is empty",
20260
+ "empty.tasksHint": "Create the first task with the OpenCode plugin or supertask add.",
20261
+ "empty.templates": "No schedule templates yet",
20262
+ "empty.templatesHint": "Create one with: supertask template add",
20263
+ "empty.runs": "No execution history yet",
20264
+ "empty.running": "No tasks are running right now",
20265
+ "schedule.cron": "Cron",
20266
+ "schedule.recurring": "Recurring",
20267
+ "schedule.delayed": "Delayed",
20268
+ "schedule.unknown": "Unknown",
20269
+ "schedule.enabled": "Enabled",
20270
+ "schedule.disabled": "Disabled",
20271
+ "schedule.minutes": "{count} min",
20272
+ "schedule.seconds": "{count} sec",
20273
+ "schedule.hours": "{count} hr",
20274
+ "schedule.days": "{count} days",
20275
+ "schedule.overdue": "Overdue",
20276
+ "system.worker": "Worker",
20277
+ "system.scheduler": "Scheduler",
20278
+ "system.watchdog": "Watchdog",
20279
+ "system.maxConcurrency": "Max concurrency",
20280
+ "system.pollInterval": "Poll interval",
20281
+ "system.heartbeatInterval": "Heartbeat interval",
20282
+ "system.taskTimeout": "Task timeout",
20283
+ "system.schedulerEnabled": "Enable scheduler",
20284
+ "system.checkInterval": "Check interval",
20285
+ "system.heartbeatTimeout": "Heartbeat timeout",
20286
+ "system.cleanupInterval": "Cleanup interval",
20287
+ "system.retentionDays": "Data retention",
20288
+ "system.milliseconds": "ms",
20289
+ "system.seconds": "seconds",
20290
+ "system.minutes": "minutes",
20291
+ "system.hours": "hours",
20292
+ "system.days": "days",
20293
+ "system.activeTemplates": "Active templates",
20294
+ "system.saveHint": "Restart Gateway to apply saved settings",
20295
+ "system.runningTasks": "Active tasks ({running} / {limit} concurrent)",
20296
+ "system.taskStats": "Task overview",
20297
+ "system.configFile": "Configuration file",
20298
+ "system.path": "Path",
20299
+ "system.fileExists": "File exists",
20300
+ "system.yes": "Yes",
20301
+ "system.noDefault": "No, using defaults",
20302
+ "system.danger": "Danger zone",
20303
+ "system.dangerDescription": "A verified backup is created first, then tasks, runs, and templates are cleared transactionally. Active work blocks this operation.",
20304
+ "theme.label": "Theme",
20305
+ "theme.system": "System",
20306
+ "theme.light": "Light",
20307
+ "theme.dark": "Dark",
20308
+ "language.label": "Language",
20309
+ "details.title": "Data details",
20310
+ "details.subtitle": "Raw record (JSON)",
20311
+ "details.copySuccess": "JSON copied",
20312
+ "dialog.cancelTask": "Cancel task #{id}?",
20313
+ "dialog.cancelTaskBody": "A running task will terminate its process tree on the next worker poll.",
20314
+ "dialog.retryTask": "Retry task #{id}?",
20315
+ "dialog.retryTaskBody": "The task returns to pending and its automatic retry budget is reset.",
20316
+ "dialog.deleteTask": "Delete task #{id}?",
20317
+ "dialog.deleteTaskBody": "The task and its execution history will be permanently deleted.",
20318
+ "dialog.disableTemplate": "Disable this schedule?",
20319
+ "dialog.disableTemplateBody": "It will stop creating new tasks automatically. Existing tasks are unchanged.",
20320
+ "dialog.deleteTemplate": "Delete this schedule?",
20321
+ "dialog.deleteTemplateBody": "This template configuration will be permanently deleted.",
20322
+ "dialog.triggerTemplate": "Run this schedule now?",
20323
+ "dialog.triggerTemplateBody": "A new task is created from the template, subject to its instance limit.",
20324
+ "dialog.clearTitle": "Confirm database clear",
20325
+ "dialog.clearBody": "This deletes every task, run, and schedule template after creating a backup.",
20326
+ "dialog.clearInstruction": "Type CLEAR to confirm",
20327
+ "feedback.retryFailed": "Retry failed",
20328
+ "feedback.cancelFailed": "Cancellation failed",
20329
+ "feedback.deleteFailed": "Delete failed",
20330
+ "feedback.requestFailed": "Request failed",
20331
+ "feedback.triggered": "Task #{id} created",
20332
+ "feedback.configSaved": "Settings saved. Restart Gateway to apply them.",
20333
+ "feedback.databaseCleared": "Database cleared. Backup: {path}",
20334
+ "feedback.copyFailed": "Copy failed. Select the content manually.",
20335
+ "a11y.skip": "Skip to main content",
20336
+ "a11y.refreshing": "Refreshing"
20337
+ };
20338
+ STYLES = `
20339
+ <style>
20340
+ :root {
20341
+ color-scheme: light;
20342
+ --bg:#f5f7fb; --bg-glow:rgba(99,102,241,.12); --surface:#ffffff; --surface-2:#f8fafc;
20343
+ --surface-3:#eef2f7; --text:#172033; --text-2:#58657a; --text-3:#8792a5;
20344
+ --border:#dfe5ee; --border-strong:#cbd4e1; --primary:#5957d9; --primary-hover:#4846c7;
20345
+ --primary-soft:#eeedff; --green:#15805d; --green-soft:#e7f7f0; --red:#c63f4f; --red-soft:#fdecef;
20346
+ --yellow:#a66608; --yellow-soft:#fff4d9; --blue:#2563b8; --blue-soft:#e8f1ff; --purple:#7552c8;
20347
+ --shadow-sm:0 1px 2px rgba(16,24,40,.04); --shadow-md:0 12px 30px rgba(30,41,59,.08);
20348
+ --shadow-lg:0 24px 60px rgba(30,41,59,.18); --radius:14px; --radius-sm:9px;
20349
+ --focus:0 0 0 3px rgba(89,87,217,.22);
20350
+ }
20351
+ :root[data-theme="dark"] {
20352
+ color-scheme: dark;
20353
+ --bg:#090d15; --bg-glow:rgba(99,102,241,.18); --surface:#111722; --surface-2:#151d2a;
20354
+ --surface-3:#1c2635; --text:#edf2f8; --text-2:#a6b1c2; --text-3:#738095;
20355
+ --border:#273244; --border-strong:#344258; --primary:#8b87ff; --primary-hover:#a19eff;
20356
+ --primary-soft:#242347; --green:#48c78e; --green-soft:#16362b; --red:#ff7180; --red-soft:#3b1e27;
20357
+ --yellow:#f0b34b; --yellow-soft:#392d18; --blue:#6ea8ff; --blue-soft:#192d4b; --purple:#b89cff;
20358
+ --shadow-sm:0 1px 2px rgba(0,0,0,.25); --shadow-md:0 16px 36px rgba(0,0,0,.28);
20359
+ --shadow-lg:0 28px 70px rgba(0,0,0,.45); --focus:0 0 0 3px rgba(139,135,255,.25);
20360
+ }
20361
+ * { box-sizing:border-box; }
20362
+ html { min-height:100%; background:var(--bg); }
20363
+ body { min-height:100vh; margin:0; color:var(--text); background:
20364
+ radial-gradient(circle at 10% -10%,var(--bg-glow),transparent 34rem),var(--bg);
20365
+ font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
20366
+ font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; }
20367
+ button,input,select { font:inherit; }
20368
+ button,a,select,input { -webkit-tap-highlight-color:transparent; }
20369
+ a { color:inherit; }
20370
+ code,.mono,.m { font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace; }
20371
+ .skip-link { position:fixed; left:16px; top:-60px; z-index:200; padding:10px 14px; border-radius:8px;
20372
+ color:#fff; background:var(--primary); transition:top .2s ease; }
20373
+ .skip-link:focus { top:16px; }
20374
+ .app-shell { width:min(1440px,100%); margin:0 auto; padding:0 28px 28px; }
20375
+ .topbar { min-height:80px; display:flex; align-items:center; justify-content:space-between; gap:20px;
20376
+ border-bottom:1px solid var(--border); }
20377
+ .brand { display:flex; align-items:center; gap:12px; min-width:0; }
20378
+ .brand-mark { width:38px; height:38px; display:grid; place-items:center; color:#fff; border-radius:11px;
20379
+ background:linear-gradient(145deg,#7773ff,#514ec8); box-shadow:0 9px 22px rgba(89,87,217,.28); }
20380
+ .brand-mark .icon { width:23px; height:23px; }
20381
+ .brand-name { display:flex; align-items:baseline; gap:7px; font-size:17px; font-weight:760; letter-spacing:-.025em; }
20382
+ .brand-name span { color:var(--text-3); font-size:12px; font-weight:650; letter-spacing:.02em; text-transform:uppercase; }
20383
+ .brand-tagline { color:var(--text-2); font-size:12px; margin-top:1px; }
20384
+ .top-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
20385
+ .local-chip { display:inline-flex; align-items:center; gap:7px; color:var(--text-2); font-size:12px; padding:7px 10px;
20386
+ border:1px solid var(--border); border-radius:999px; background:color-mix(in srgb,var(--surface) 78%,transparent); }
20387
+ .live-dot { width:7px; height:7px; border-radius:50%; background:var(--green); box-shadow:0 0 0 4px var(--green-soft); }
20388
+ .control { height:36px; border:1px solid var(--border); border-radius:9px; background:var(--surface);
20389
+ color:var(--text-2); box-shadow:var(--shadow-sm); }
20390
+ .select-wrap { position:relative; display:flex; align-items:center; }
20391
+ .select-wrap>.icon { width:15px; height:15px; position:absolute; left:10px; pointer-events:none; color:var(--text-3); }
20392
+ .select-wrap select { appearance:none; padding:0 29px 0 31px; cursor:pointer; outline:none; }
20393
+ .select-wrap::after { content:""; position:absolute; right:11px; width:6px; height:6px; border-right:1.5px solid currentColor;
20394
+ border-bottom:1.5px solid currentColor; transform:rotate(45deg) translateY(-2px); pointer-events:none; color:var(--text-3); }
20395
+ .language-switch { display:flex; padding:3px; gap:2px; }
20396
+ .language-switch button { height:28px; min-width:34px; padding:0 8px; border:0; border-radius:6px; color:var(--text-3);
20397
+ background:transparent; cursor:pointer; font-size:12px; font-weight:650; }
20398
+ .language-switch button.active { background:var(--surface-3); color:var(--text); }
20399
+ .icon-button { width:36px; height:36px; display:grid; place-items:center; border:1px solid var(--border); border-radius:9px;
20400
+ color:var(--text-2); background:var(--surface); box-shadow:var(--shadow-sm); cursor:pointer; }
20401
+ .icon-button .icon { width:17px; height:17px; }
20402
+ .icon-button:hover,.control:hover { border-color:var(--border-strong); color:var(--text); }
20403
+ .icon-button.refreshing .icon { animation:spin .7s linear infinite; }
20404
+ .tabs { display:flex; gap:6px; margin:18px 0 30px; padding:5px; width:max-content; max-width:100%; overflow-x:auto;
20405
+ border:1px solid var(--border); border-radius:12px; background:color-mix(in srgb,var(--surface) 82%,transparent); box-shadow:var(--shadow-sm); }
20406
+ .tabs a { display:flex; align-items:center; gap:8px; min-height:36px; padding:0 14px; border-radius:8px; color:var(--text-2);
20407
+ font-weight:650; font-size:13px; text-decoration:none; white-space:nowrap; transition:background .16s ease,color .16s ease,box-shadow .16s ease; }
20408
+ .tabs a .icon { width:16px; height:16px; }
20409
+ .tabs a:hover { color:var(--text); background:var(--surface-2); }
20410
+ .tabs a.active { color:var(--primary); background:var(--surface); box-shadow:0 1px 4px rgba(16,24,40,.08); }
20411
+ main { outline:none; }
20412
+ .page-heading { display:flex; justify-content:space-between; align-items:flex-end; gap:20px; margin-bottom:22px; }
20413
+ .page-heading h1 { margin:0; font-size:28px; line-height:1.2; letter-spacing:-.035em; }
20414
+ .page-heading p { margin:7px 0 0; color:var(--text-2); max-width:720px; }
20415
+ .stats-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:14px; margin-bottom:20px; }
20416
+ .stats-grid.three { grid-template-columns:repeat(3,minmax(0,1fr)); }
20417
+ .stat-card { position:relative; min-height:118px; padding:19px; overflow:hidden; border:1px solid var(--border); border-radius:var(--radius);
20418
+ background:linear-gradient(145deg,var(--surface),color-mix(in srgb,var(--surface-2) 72%,var(--surface)));
20419
+ box-shadow:var(--shadow-sm); transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease; }
20420
+ .stat-card:hover { transform:translateY(-2px); border-color:var(--border-strong); box-shadow:var(--shadow-md); }
20421
+ .stat-card::after { content:""; position:absolute; width:90px; height:90px; right:-38px; top:-45px; border-radius:50%; background:var(--tone-soft); }
20422
+ .stat-top { display:flex; justify-content:space-between; align-items:center; }
20423
+ .stat-icon { width:32px; height:32px; display:grid; place-items:center; border-radius:9px; color:var(--tone); background:var(--tone-soft); }
20424
+ .stat-icon .icon { width:17px; height:17px; }
20425
+ .stat-value { margin-top:13px; font-size:28px; font-weight:760; line-height:1; letter-spacing:-.04em; color:var(--tone); }
20426
+ .stat-label { margin-top:7px; color:var(--text-2); font-size:12px; font-weight:650; }
20427
+ .tone-neutral { --tone:var(--text-2); --tone-soft:var(--surface-3); }
20428
+ .tone-blue { --tone:var(--blue); --tone-soft:var(--blue-soft); }
20429
+ .tone-green { --tone:var(--green); --tone-soft:var(--green-soft); }
20430
+ .tone-red { --tone:var(--red); --tone-soft:var(--red-soft); }
20431
+ .tone-purple { --tone:var(--purple); --tone-soft:var(--primary-soft); }
20432
+ .toolbar { display:flex; justify-content:space-between; align-items:center; gap:14px; margin:0 0 12px; }
20433
+ .filters { display:flex; gap:6px; overflow-x:auto; padding:2px; }
20434
+ .filter-chip { display:inline-flex; align-items:center; min-height:34px; padding:0 12px; border:1px solid var(--border); border-radius:9px;
20435
+ color:var(--text-2); background:var(--surface); text-decoration:none; font-size:12px; font-weight:650; white-space:nowrap; }
20436
+ .filter-chip:hover { border-color:var(--border-strong); color:var(--text); }
20437
+ .filter-chip.active { border-color:color-mix(in srgb,var(--primary) 38%,var(--border)); color:var(--primary); background:var(--primary-soft); }
20438
+ .search-box { position:relative; width:min(320px,100%); flex:0 1 320px; }
20439
+ .search-box .icon { position:absolute; left:11px; top:50%; width:16px; height:16px; color:var(--text-3); transform:translateY(-50%); }
20440
+ .search-box input { width:100%; height:36px; padding:0 12px 0 36px; border:1px solid var(--border); border-radius:9px;
20441
+ outline:none; color:var(--text); background:var(--surface); box-shadow:var(--shadow-sm); }
20442
+ .search-box input::placeholder { color:var(--text-3); }
20443
+ .search-box input:focus { border-color:var(--primary); box-shadow:var(--focus); }
20444
+ .panel,.card { border:1px solid var(--border); border-radius:var(--radius); background:var(--surface); box-shadow:var(--shadow-sm); }
20445
+ .panel { overflow:hidden; margin-bottom:16px; }
20446
+ .panel-head { min-height:52px; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:0 18px;
20447
+ border-bottom:1px solid var(--border); }
20448
+ .panel-head h2,.panel-head h3 { margin:0; font-size:14px; letter-spacing:-.01em; }
20449
+ .table-wrap { width:100%; overflow-x:auto; }
20450
+ table { width:100%; border-collapse:separate; border-spacing:0; font-size:13px; }
20451
+ th { height:42px; padding:0 13px; color:var(--text-3); background:var(--surface-2); border-bottom:1px solid var(--border);
20452
+ font-size:11px; font-weight:730; letter-spacing:.045em; text-align:left; text-transform:uppercase; white-space:nowrap; }
20453
+ td { padding:12px 13px; border-bottom:1px solid var(--border); vertical-align:middle; }
20454
+ tbody tr:last-child td { border-bottom:0; }
20455
+ tbody tr { transition:background .14s ease; }
20456
+ tbody tr:hover { background:color-mix(in srgb,var(--primary-soft) 30%,transparent); }
20457
+ .task-name { font-weight:680; color:var(--text); }
20458
+ .task-prompt { max-width:520px; margin-top:3px; color:var(--text-2); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
20459
+ .muted,.mu { color:var(--text-2); }
20460
+ .faint { color:var(--text-3); }
20461
+ .small,.sm { font-size:12px; }
20462
+ .tag { display:inline-flex; align-items:center; min-height:23px; max-width:180px; padding:0 8px; border:1px solid var(--border);
20463
+ border-radius:7px; color:var(--text-2); background:var(--surface-2); font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
20464
+ .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; }
20465
+ .badge::before { content:""; width:6px; height:6px; border-radius:50%; background:currentColor; }
20466
+ .b-pending { color:var(--text-2); background:var(--surface-3); }
20467
+ .b-running { color:var(--blue); background:var(--blue-soft); }
20468
+ .b-running::before { animation:pulse 1.7s ease-in-out infinite; }
20469
+ .b-done { color:var(--green); background:var(--green-soft); }
20470
+ .b-failed { color:var(--red); background:var(--red-soft); }
20471
+ .b-dead_letter { color:var(--yellow); background:var(--yellow-soft); }
20472
+ .b-cancelled,.b-unknown { color:var(--text-3); background:var(--surface-3); }
20473
+ .t-cron { color:var(--purple); } .t-recurring { color:var(--blue); } .t-delayed { color:var(--yellow); }
20474
+ .actions { display:flex; align-items:center; flex-wrap:wrap; gap:5px; }
20475
+ .btn { min-height:32px; display:inline-flex; align-items:center; justify-content:center; gap:6px; padding:0 11px; border:1px solid var(--border);
20476
+ border-radius:8px; color:var(--text-2); background:var(--surface); text-decoration:none; cursor:pointer; font-size:12px; font-weight:650;
20477
+ transition:transform .12s ease,background .15s ease,border-color .15s ease,color .15s ease,box-shadow .15s ease; }
20478
+ .btn:hover { color:var(--text); border-color:var(--border-strong); background:var(--surface-2); }
20479
+ .btn:active { transform:scale(.97); }
20480
+ .btn:disabled { opacity:.5; cursor:not-allowed; transform:none; }
20481
+ .btn-primary { color:#fff; border-color:var(--primary); background:var(--primary); }
20482
+ .btn-primary:hover { color:#fff; border-color:var(--primary-hover); background:var(--primary-hover); }
20483
+ .btn-danger { color:var(--red); }
20484
+ .btn-danger:hover { color:var(--red); border-color:color-mix(in srgb,var(--red) 55%,var(--border)); background:var(--red-soft); }
20485
+ .btn-warning:hover { color:var(--yellow); border-color:color-mix(in srgb,var(--yellow) 55%,var(--border)); background:var(--yellow-soft); }
20486
+ .btn .icon { width:14px; height:14px; }
20487
+ .pagination { display:flex; justify-content:center; align-items:center; gap:10px; margin:18px 0 4px; }
20488
+ .pagination .summary { color:var(--text-2); font-size:12px; }
20489
+ .empty-state { display:grid; place-items:center; min-height:230px; padding:36px; text-align:center; }
20490
+ .empty-icon { width:48px; height:48px; display:grid; place-items:center; border-radius:14px; color:var(--primary); background:var(--primary-soft); }
20491
+ .empty-icon .icon { width:23px; height:23px; }
20492
+ .empty-state h3 { margin:13px 0 4px; font-size:15px; }
20493
+ .empty-state p { margin:0; color:var(--text-2); font-size:12px; }
20494
+ .empty-state code { display:inline-block; margin-top:10px; padding:5px 8px; border-radius:6px; background:var(--surface-3); }
20495
+ .settings-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:14px; margin-bottom:16px; }
20496
+ .settings-card { padding:18px; }
20497
+ .settings-title { display:flex; align-items:center; justify-content:space-between; margin:0 0 16px; font-size:14px; }
20498
+ .settings-title span:first-child { display:flex; align-items:center; gap:8px; }
20499
+ .settings-title .icon { width:17px; height:17px; color:var(--primary); }
20500
+ .field { display:grid; grid-template-columns:minmax(0,1fr) 112px; align-items:center; gap:12px; margin:11px 0; }
20501
+ .field label { color:var(--text-2); font-size:12px; }
20502
+ .input-unit { position:relative; }
20503
+ .input-unit input { width:100%; height:36px; padding:0 47px 0 10px; border:1px solid var(--border); border-radius:8px; outline:none;
20504
+ color:var(--text); background:var(--surface-2); }
20505
+ .input-unit span { position:absolute; right:9px; top:50%; transform:translateY(-50%); color:var(--text-3); font-size:10px; pointer-events:none; }
20506
+ .input-unit input:focus { border-color:var(--primary); box-shadow:var(--focus); background:var(--surface); }
20507
+ .switch-field { display:flex; align-items:center; justify-content:space-between; gap:12px; margin:11px 0; color:var(--text-2); font-size:12px; }
20508
+ .switch { position:relative; width:42px; height:24px; flex:0 0 auto; }
20509
+ .switch input { position:absolute; opacity:0; }
20510
+ .switch span { position:absolute; inset:0; border-radius:999px; background:var(--surface-3); border:1px solid var(--border-strong); cursor:pointer; transition:.2s ease; }
20511
+ .switch span::after { content:""; position:absolute; width:17px; height:17px; left:2px; top:2px; border-radius:50%; background:var(--surface);
20512
+ box-shadow:0 1px 3px rgba(0,0,0,.22); transition:transform .2s ease; }
20513
+ .switch input:checked+span { background:var(--primary); border-color:var(--primary); }
20514
+ .switch input:checked+span::after { transform:translateX(18px); }
20515
+ .save-row { display:flex; align-items:center; justify-content:flex-end; gap:12px; margin:0 0 24px; }
20516
+ .info-list { padding:4px 18px; }
20517
+ .info-row { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; padding:13px 0; border-bottom:1px solid var(--border); }
20518
+ .info-row:last-child { border-bottom:0; }
20519
+ .info-key { color:var(--text-2); }
20520
+ .info-value { font-weight:650; text-align:right; overflow-wrap:anywhere; }
20521
+ .overview-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; padding:18px; }
20522
+ .overview-item { padding:13px; border-radius:10px; background:var(--surface-2); }
20523
+ .overview-item span { color:var(--text-2); font-size:11px; }
20524
+ .overview-item strong { display:block; margin-top:5px; font-size:19px; }
20525
+ .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)); }
20526
+ .danger-card h2 { display:flex; align-items:center; gap:8px; margin:0 0 5px; color:var(--red); font-size:14px; }
20527
+ .danger-card h2 .icon { width:17px; height:17px; }
20528
+ .danger-card p { max-width:800px; margin:0 0 14px; color:var(--text-2); font-size:12px; }
20529
+ .log-panel { margin:12px 0; animation:reveal .18s ease both; }
20530
+ .log-box { max-height:360px; overflow:auto; padding:16px; color:var(--text-2); background:#0b1018; font-family:"SFMono-Regular",Consolas,monospace;
20531
+ font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
20532
+ :root[data-theme="light"] .log-box { color:#dbe5f3; }
20533
+ dialog { width:min(760px,calc(100% - 32px)); padding:0; border:1px solid var(--border); border-radius:16px; color:var(--text);
20534
+ background:var(--surface); box-shadow:var(--shadow-lg); }
20535
+ dialog[open] { animation:dialog-in .18s ease both; }
20536
+ dialog::backdrop { background:rgba(8,12,20,.62); backdrop-filter:blur(3px); animation:fade-in .18s ease both; }
20537
+ .dialog-head { display:flex; align-items:center; justify-content:space-between; gap:14px; padding:17px 18px; border-bottom:1px solid var(--border); }
20538
+ .dialog-head h2 { margin:0; font-size:15px; }
20539
+ .dialog-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
20540
+ .dialog-body { max-height:70vh; overflow:auto; padding:18px; }
20541
+ .dialog-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; padding:14px 18px; border-top:1px solid var(--border); }
20542
+ .json-view { min-height:160px; margin:0; padding:15px; overflow:auto; border:1px solid var(--border); border-radius:10px; color:var(--text-2);
20543
+ background:var(--surface-2); font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
20544
+ .confirm-copy { color:var(--text-2); margin:0; }
20545
+ .confirm-copy strong { display:block; margin-bottom:5px; color:var(--text); font-size:15px; }
20546
+ .danger-input { width:100%; height:40px; margin-top:14px; padding:0 12px; border:1px solid var(--border); border-radius:9px; outline:none;
20547
+ color:var(--text); background:var(--surface-2); font-family:"SFMono-Regular",Consolas,monospace; text-transform:uppercase; }
20548
+ .danger-input:focus { border-color:var(--red); box-shadow:0 0 0 3px color-mix(in srgb,var(--red) 22%,transparent); }
20549
+ .toast-region { position:fixed; top:18px; right:18px; z-index:300; display:grid; gap:8px; pointer-events:none; }
20550
+ .toast { min-width:260px; max-width:min(420px,calc(100vw - 36px)); display:flex; align-items:flex-start; gap:10px; padding:12px 14px;
20551
+ 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; }
20552
+ .toast .icon { width:18px; height:18px; flex:0 0 auto; margin-top:1px; }
20553
+ .toast.ok .icon { color:var(--green); } .toast.error .icon { color:var(--red); }
20554
+ .toast.leaving { animation:toast-out .18s ease both; }
20555
+ footer { display:flex; justify-content:center; padding:24px 0 4px; color:var(--text-3); font-size:11px; }
20556
+ [hidden] { display:none!important; }
20557
+ :focus-visible { outline:none; box-shadow:var(--focus); }
20558
+ .ui-ready,.ui-ready * { transition-property:background-color,border-color,color,box-shadow; transition-duration:.16s; transition-timing-function:ease; }
20559
+ .reveal { animation:reveal .28s ease both; }
20560
+ .reveal-delay-1 { animation-delay:.04s; } .reveal-delay-2 { animation-delay:.08s; }
20561
+ @keyframes spin { to { transform:rotate(360deg); } }
20562
+ @keyframes pulse { 0%,100% { opacity:1; box-shadow:0 0 0 0 currentColor; } 50% { opacity:.7; box-shadow:0 0 0 4px transparent; } }
20563
+ @keyframes reveal { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:none; } }
20564
+ @keyframes dialog-in { from { opacity:0; transform:translateY(8px) scale(.985); } to { opacity:1; transform:none; } }
20565
+ @keyframes fade-in { from { opacity:0; } to { opacity:1; } }
20566
+ @keyframes toast-in { from { opacity:0; transform:translateY(-8px) scale(.98); } to { opacity:1; transform:none; } }
20567
+ @keyframes toast-out { to { opacity:0; transform:translateY(-6px) scale(.98); } }
20568
+ @media (max-width:1000px) {
20569
+ .stats-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
20570
+ .settings-grid { grid-template-columns:1fr; }
20571
+ .local-chip { display:none; }
20572
+ }
20573
+ @media (max-width:720px) {
20574
+ .app-shell { padding:0 16px 20px; }
20575
+ .topbar { min-height:72px; }
20576
+ .brand-tagline,.brand-name span { display:none; }
20577
+ .top-actions { gap:5px; }
20578
+ .select-wrap select { width:42px; color:transparent; padding:0; }
20579
+ .select-wrap>.icon { left:12px; color:var(--text-2); }
20580
+ .select-wrap::after { display:none; }
20581
+ .language-switch { display:none; }
20582
+ .mobile-language { display:grid!important; }
20583
+ .tabs { width:100%; margin:14px 0 24px; }
20584
+ .tabs a { flex:1; justify-content:center; padding:0 10px; }
20585
+ .tabs a span { display:none; }
20586
+ .page-heading { align-items:flex-start; }
20587
+ .page-heading h1 { font-size:24px; }
20588
+ .page-heading p { font-size:13px; }
20589
+ .stats-grid,.stats-grid.three { grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
20590
+ .stat-card { min-height:105px; padding:15px; }
20591
+ .toolbar { align-items:stretch; flex-direction:column; }
20592
+ .search-box { width:100%; flex-basis:auto; }
20593
+ .filters { order:2; }
20594
+ .overview-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
20595
+ .save-row { align-items:flex-end; flex-direction:column-reverse; }
20596
+ .save-row .btn { width:100%; }
20597
+ .responsive-table { display:block; padding:10px; }
20598
+ .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; }
20599
+ .responsive-table tbody { display:grid; gap:10px; }
20600
+ .responsive-table tr { display:grid; gap:0; padding:10px 12px; border:1px solid var(--border); border-radius:11px; background:var(--surface-2); }
20601
+ .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; }
20602
+ .responsive-table td::before { content:attr(data-label); color:var(--text-3); font-size:10px; font-weight:730; letter-spacing:.045em; text-transform:uppercase; }
20603
+ .responsive-table td[data-primary] { display:block; padding-bottom:10px; margin-bottom:4px; border-bottom:1px solid var(--border); }
20604
+ .responsive-table td[data-primary]::before { display:none; }
20605
+ .responsive-table .task-prompt { max-width:100%; }
20606
+ .responsive-table .actions { justify-content:flex-start; }
20607
+ }
20608
+ @media (max-width:520px) {
20609
+ .stats-grid,.stats-grid.three { grid-template-columns:1fr 1fr; }
20610
+ .stat-card { min-height:98px; }
20611
+ .stat-value { font-size:24px; }
20612
+ .page-heading p { max-width:95%; }
20613
+ .field { grid-template-columns:minmax(0,1fr) 105px; }
20614
+ .overview-grid { grid-template-columns:1fr 1fr; padding:12px; }
20615
+ .pagination .summary { max-width:150px; text-align:center; }
20616
+ }
20617
+ @media (prefers-reduced-motion:reduce) {
20618
+ *,*::before,*::after { scroll-behavior:auto!important; animation-duration:.01ms!important; animation-iteration-count:1!important; transition-duration:.01ms!important; }
20619
+ }
20620
+ </style>`;
20621
+ PAGE_KEYS = {
20622
+ tasks: { title: "page.tasks.title", description: "page.tasks.description" },
20623
+ templates: { title: "page.templates.title", description: "page.templates.description" },
20624
+ runs: { title: "page.runs.title", description: "page.runs.description" },
20625
+ system: { title: "page.system.title", description: "page.system.description" }
20626
+ };
20627
+ }
20628
+ });
20629
+
19934
20630
  // src/web/index.tsx
19935
20631
  var web_exports = {};
19936
20632
  __export(web_exports, {
19937
20633
  dashboardApp: () => dashboardApp,
19938
20634
  default: () => web_default
19939
20635
  });
19940
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "fs";
20636
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
19941
20637
  import { dirname as dirname6 } from "path";
19942
20638
  function parsePositiveInteger(value) {
19943
20639
  if (!/^\d+$/.test(value)) return null;
@@ -19950,41 +20646,34 @@ function parseTaskStatus(value) {
19950
20646
  function safeStatus(value) {
19951
20647
  return value && TASK_STATUSES.has(value) ? value : "unknown";
19952
20648
  }
20649
+ function resolveLocale(c) {
20650
+ const requested = c.req.query("lang");
20651
+ if (requested === "en" || requested === "zh-CN") return requested;
20652
+ const cookie = c.req.header("Cookie") ?? "";
20653
+ const match2 = /(?:^|;\s*)supertask_locale=([^;]+)/.exec(cookie);
20654
+ if (match2) {
20655
+ try {
20656
+ const saved = decodeURIComponent(match2[1]);
20657
+ if (saved === "en" || saved === "zh-CN") return saved;
20658
+ } catch {
20659
+ }
20660
+ }
20661
+ const accepted = c.req.header("Accept-Language")?.toLowerCase() ?? "";
20662
+ return accepted.startsWith("en") ? "en" : "zh-CN";
20663
+ }
19953
20664
  function formatDuration(startAt, endAt) {
19954
- if (!startAt) return "-";
20665
+ if (!startAt) return "\u2014";
19955
20666
  const start = new Date(startAt).getTime();
19956
20667
  const end = endAt ? new Date(endAt).getTime() : Date.now();
19957
20668
  const seconds = Math.floor((end - start) / 1e3);
19958
20669
  if (seconds < 0) return "0s";
19959
20670
  if (seconds < 60) return `${seconds}s`;
19960
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m${seconds % 60}s`;
19961
- return `${Math.floor(seconds / 3600)}h${Math.floor(seconds % 3600 / 60)}m`;
20671
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
20672
+ return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m`;
19962
20673
  }
19963
- function timeAgo(ms) {
19964
- if (!ms) return "-";
19965
- const diff = Date.now() - ms;
19966
- if (diff < 6e4) return `${Math.floor(diff / 1e3)}\u79D2\u524D`;
19967
- if (diff < 36e5) return `${Math.floor(diff / 6e4)}\u5206\u949F\u524D`;
19968
- if (diff < 864e5) return `${Math.floor(diff / 36e5)}\u5C0F\u65F6\u524D`;
19969
- return `${Math.floor(diff / 864e5)}\u5929\u524D`;
19970
- }
19971
- function timeUntil(ms) {
19972
- if (!ms) return "-";
19973
- const diff = ms - Date.now();
19974
- if (diff < 0) return "\u5DF2\u5230\u671F";
19975
- if (diff < 6e4) return `${Math.floor(diff / 1e3)}\u79D2\u540E`;
19976
- if (diff < 36e5) return `${Math.floor(diff / 6e4)}\u5206\u949F\u540E`;
19977
- if (diff < 864e5) return `${Math.floor(diff / 36e5)}\u5C0F\u65F6\u540E`;
19978
- return `${Math.floor(diff / 864e5)}\u5929\u540E`;
19979
- }
19980
- function formatDate(ts) {
19981
- if (!ts) return "-";
19982
- const d = ts instanceof Date ? ts : new Date(ts);
19983
- return d.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
19984
- }
19985
- function esc(s) {
19986
- if (!s) return "";
19987
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
20674
+ function esc(value) {
20675
+ if (!value) return "";
20676
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
19988
20677
  }
19989
20678
  function readCurrentConfig() {
19990
20679
  const configPath = getConfigPath();
@@ -20003,94 +20692,51 @@ function writeConfig(cfg) {
20003
20692
  writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
20004
20693
  renameSync2(tempPath, configPath);
20005
20694
  }
20006
- function renderLayout(title, activeTab, body) {
20007
- return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
20008
- <script>
20009
- 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();}
20010
- 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();}
20011
- 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();}
20012
- 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');}}
20013
- 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');}}
20014
- 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');}}
20015
- async function enableTmpl(id){await fetch('/api/templates/'+id+'/enable',{method:'POST'});location.reload();}
20016
- async function disableTmpl(id){if(!confirm('\u786E\u5B9A\u7981\u7528\u6B64\u6A21\u677F?'))return;await fetch('/api/templates/'+id+'/disable',{method:'POST'});location.reload();}
20017
- 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();}
20018
- 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');}}
20019
- function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
20020
-
20021
- async function clearDatabase(){
20022
- 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;
20023
- 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;
20024
- try{
20025
- const r=await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})});
20026
- const d=await r.json();
20027
- if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u81EA\u52A8\u5907\u4EFD\uFF1A'+d.backupPath);location.reload();}
20028
- else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
20029
- }catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
20695
+ function statCard(value, label, tone, cardIcon, delay = "") {
20696
+ return `<div class="stat-card ${tone} reveal ${delay}">
20697
+ <div class="stat-top"><div class="stat-icon">${cardIcon}</div></div>
20698
+ <div class="stat-value">${value}</div><div class="stat-label">${label}</div>
20699
+ </div>`;
20030
20700
  }
20031
-
20032
- async function saveConfig(){
20033
- const form=document.getElementById('config-form');
20034
- const data={
20035
- worker:{
20036
- maxConcurrency:Number(form.mc.value),
20037
- pollIntervalMs:Number(form.pi.value),
20038
- heartbeatIntervalMs:Number(form.hi.value)*1000,
20039
- taskTimeoutMs:Number(form.to.value)*60000,
20040
- },
20041
- scheduler:{
20042
- enabled:form.se.checked,
20043
- checkIntervalMs:Number(form.si.value),
20044
- },
20045
- watchdog:{
20046
- heartbeatTimeoutMs:Number(form.wt.value)*1000,
20047
- checkIntervalMs:Number(form.wci.value)*1000,
20048
- cleanupIntervalMs:Number(form.wcl.value)*3600000,
20049
- retentionDays:Number(form.rd.value),
20050
- }
20051
- };
20052
- try{
20053
- const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
20054
- const d=await r.json();
20055
- 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);}
20056
- else{alert('\u4FDD\u5B58\u5931\u8D25: '+d.error);}
20057
- }catch(e){alert('\u4FDD\u5B58\u5931\u8D25: '+e.message);}
20701
+ function emptyState(title, hint, code = "") {
20702
+ return `<div class="empty-state"><div><div class="empty-icon">${icon("inbox")}</div>
20703
+ <h3>${title}</h3><p>${hint}</p>${code ? `<code>${code}</code>` : ""}</div></div>`;
20058
20704
  }
20059
- </script>
20060
- </head>
20061
- <body>
20062
- <div id="toast" class="toast toast-ok"></div>
20063
- <div class="c">
20064
- <header>
20065
- <div><h1>SuperTask Dashboard</h1><span class="mu sm">\u4EFB\u52A1\u8C03\u5EA6\u7BA1\u7406\u4E2D\u5FC3</span></div>
20066
- <div><a href="${activeTab === "tasks" ? "/" : "/" + activeTab}" class="rf">\u5237\u65B0</a></div>
20067
- </header>
20068
- <nav class="tabs">
20069
- <a href="/" class="${activeTab === "tasks" ? "active" : ""}">\u4EFB\u52A1\u961F\u5217</a>
20070
- <a href="/templates" class="${activeTab === "templates" ? "active" : ""}">\u5B9A\u65F6\u4EFB\u52A1</a>
20071
- <a href="/runs" class="${activeTab === "runs" ? "active" : ""}">\u6267\u884C\u65E5\u5FD7</a>
20072
- <a href="/system" class="${activeTab === "system" ? "active" : ""}">\u7CFB\u7EDF\u72B6\u6001</a>
20073
- </nav>
20074
- ${body}
20075
- </div>
20076
- <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>
20077
- </body></html>`;
20705
+ function formatInterval(milliseconds, locale) {
20706
+ const formatter = new Intl.NumberFormat(locale === "en" ? "en-US" : "zh-CN", {
20707
+ maximumFractionDigits: 1
20708
+ });
20709
+ if (milliseconds < 6e4) {
20710
+ return t(locale, "schedule.seconds", { count: formatter.format(milliseconds / 1e3) });
20711
+ }
20712
+ if (milliseconds < 36e5) {
20713
+ return t(locale, "schedule.minutes", { count: formatter.format(milliseconds / 6e4) });
20714
+ }
20715
+ if (milliseconds < 864e5) {
20716
+ return t(locale, "schedule.hours", { count: formatter.format(milliseconds / 36e5) });
20717
+ }
20718
+ return t(locale, "schedule.days", { count: formatter.format(milliseconds / 864e5) });
20078
20719
  }
20079
- var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
20720
+ function pagination(locale, basePath, page, pages, total, suffix = "") {
20721
+ const previous = page > 1 ? `<a class="btn" href="${basePath}?page=${page - 1}${suffix}">${icon("chevronLeft")}${t(locale, "pagination.previous")}</a>` : "";
20722
+ const next = page < pages ? `<a class="btn" href="${basePath}?page=${page + 1}${suffix}">${t(locale, "pagination.next")}${icon("chevronRight")}</a>` : "";
20723
+ return `<div class="pagination">${previous}<span class="summary">${t(locale, "pagination.summary", { page, pages, total })}</span>${next}</div>`;
20724
+ }
20725
+ var app, TASK_STATUSES, dashboardApp, web_default;
20080
20726
  var init_web = __esm({
20081
20727
  "src/web/index.tsx"() {
20082
20728
  "use strict";
20083
20729
  init_dist();
20084
- init_html2();
20085
- init_task_service();
20086
- init_task_run_service();
20087
- init_task_template_service();
20088
- init_database_maintenance_service();
20089
20730
  init_drizzle_orm();
20090
20731
  init_db2();
20732
+ init_database_maintenance_service();
20733
+ init_task_run_service();
20734
+ init_task_service();
20735
+ init_task_template_service();
20091
20736
  init_config();
20092
20737
  init_health();
20093
20738
  init_job_templates();
20739
+ init_ui();
20094
20740
  app = new Hono2();
20095
20741
  TASK_STATUSES = /* @__PURE__ */ new Set([
20096
20742
  "pending",
@@ -20131,84 +20777,9 @@ var init_web = __esm({
20131
20777
  const health = getGatewayHealth();
20132
20778
  return c.json(health, health.status === "ok" ? 200 : 503);
20133
20779
  });
20134
- SHARED_STYLES = html`
20135
- <style>
20136
- :root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
20137
- * { box-sizing:border-box; }
20138
- body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; background:var(--bg); color:var(--t1); margin:0; line-height:1.5; }
20139
- .c { max-width:1280px; margin:0 auto; padding:20px; }
20140
- header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; border-bottom:1px solid var(--border); padding-bottom:16px; }
20141
- h1 { font-size:22px; margin:0; color:#fff; }
20142
- nav.tabs { display:flex; gap:0; margin-bottom:20px; border-bottom:1px solid var(--border); }
20143
- 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; }
20144
- nav.tabs a:hover { color:var(--t1); }
20145
- nav.tabs a.active { color:#fff; border-bottom-color:var(--blue); }
20146
- .g4 { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:16px; margin-bottom:24px; }
20147
- .g3 { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:16px; margin-bottom:24px; }
20148
- .card { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:16px; }
20149
- .sv { font-size:28px; font-weight:bold; }
20150
- .sl { color:var(--t2); font-size:12px; text-transform:uppercase; margin-top:4px; }
20151
- .panel { background:var(--card); border:1px solid var(--border); border-radius:8px; overflow:hidden; margin-bottom:16px; }
20152
- .ph { padding:12px 16px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; }
20153
- .ph h3 { margin:0; font-size:14px; }
20154
- table { width:100%; border-collapse:collapse; font-size:13px; }
20155
- th { background:#21262d; color:var(--t2); font-weight:600; text-align:left; padding:8px 12px; white-space:nowrap; }
20156
- td { padding:8px 12px; border-bottom:1px solid var(--border); vertical-align:top; }
20157
- tr:last-child td { border-bottom:none; }
20158
- tr:hover { background:rgba(255,255,255,0.02); }
20159
- .badge { display:inline-block; padding:2px 8px; border-radius:12px; font-size:11px; font-weight:600; }
20160
- .b-pending { background:rgba(110,118,129,0.3); color:#8b949e; }
20161
- .b-running { background:rgba(56,139,253,0.15); color:#58a6ff; }
20162
- .b-done { background:rgba(46,160,67,0.15); color:#3fb950; }
20163
- .b-failed { background:rgba(248,81,73,0.15); color:#f85149; }
20164
- .b-dead_letter { background:rgba(210,153,34,0.15); color:#d29922; }
20165
- .b-cancelled { background:rgba(110,118,129,0.2); color:#6e7681; }
20166
- .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; }
20167
- .btn:hover { background:#30363d; color:#fff; }
20168
- .btn-sm { padding:2px 6px; font-size:11px; }
20169
- .btn-primary { background:var(--green); border-color:var(--green); color:#fff; }
20170
- .btn-primary:hover { opacity:0.85; color:#fff; }
20171
- .btn-danger:hover { color:var(--red); border-color:var(--red); }
20172
- .btn-warn:hover { color:var(--yellow); border-color:var(--yellow); }
20173
- .rf { background:var(--green); color:white; border:none; padding:6px 16px; border-radius:6px; font-weight:600; cursor:pointer; text-decoration:none; }
20174
- .rf:hover { opacity:0.9; }
20175
- .m { font-family:monospace; font-size:12px; }
20176
- .mu { color:var(--t2); }
20177
- .sm { font-size:12px; }
20178
- .el { max-width:400px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:inline-block; vertical-align:middle; }
20179
- .tag { display:inline-block; background:#21262d; padding:1px 6px; border-radius:4px; font-size:11px; }
20180
- .t-cron { color:var(--purple); }
20181
- .t-recurring { color:var(--blue); }
20182
- .t-delayed { color:var(--yellow); }
20183
- .pn { margin-top:16px; display:flex; justify-content:center; gap:10px; align-items:center; }
20184
- .ir { display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px solid var(--border); font-size:13px; }
20185
- .ir:last-child { border-bottom:none; }
20186
- .ik { color:var(--t2); }
20187
- .iv { font-weight:500; }
20188
- .form-row { display:flex; gap:12px; align-items:center; margin-bottom:12px; }
20189
- .form-row label { color:var(--t2); font-size:13px; min-width:100px; }
20190
- .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; }
20191
- .form-row input:focus, .form-row select:focus { outline:none; border-color:var(--blue); }
20192
- .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; }
20193
- dialog { background:var(--card); color:var(--t1); border:1px solid var(--border); border-radius:8px; padding:0; max-width:900px; width:90%; }
20194
- dialog::backdrop { background:rgba(0,0,0,0.6); }
20195
- .dh { padding:16px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; }
20196
- .db { padding:16px; max-height:70vh; overflow-y:auto; }
20197
- pre { margin:0; white-space:pre-wrap; font-size:12px; }
20198
- .cb { background:transparent; border:none; color:var(--t2); cursor:pointer; font-size:20px; }
20199
- .mt8 { margin-top:8px; }
20200
- .mt16 { margin-top:16px; }
20201
- .mb0 { margin-bottom:0; }
20202
- .ta-center { text-align:center; }
20203
- .p30 { padding:30px; }
20204
- .toast { position:fixed; top:20px; right:20px; padding:12px 20px; border-radius:6px; color:#fff; font-size:14px; z-index:9999; display:none; }
20205
- .toast-ok { background:var(--green); }
20206
- .toast-err { background:var(--red); }
20207
- </style>
20208
- `;
20209
20780
  app.get("/", async (c) => {
20210
- const pageParam = c.req.query("page") || "1";
20211
- const page = parsePositiveInteger(pageParam);
20781
+ const locale = resolveLocale(c);
20782
+ const page = parsePositiveInteger(c.req.query("page") || "1");
20212
20783
  if (page === null) return c.text("invalid page", 400);
20213
20784
  const statusFilter = c.req.query("status") || "";
20214
20785
  const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
@@ -20219,8 +20790,7 @@ var init_web = __esm({
20219
20790
  TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
20220
20791
  TaskService.stats({})
20221
20792
  ]);
20222
- const taskIds = tasks4.map((t) => t.id);
20223
- const latestRuns = await TaskRunService.getLatestByTaskIds(taskIds);
20793
+ const latestRuns = await TaskRunService.getLatestByTaskIds(tasks4.map((task) => task.id));
20224
20794
  const counts = {
20225
20795
  pending: statsData.pending || 0,
20226
20796
  running: statsData.running || 0,
@@ -20228,253 +20798,211 @@ var init_web = __esm({
20228
20798
  failed: (statsData.failed || 0) + (statsData.dead_letter || 0),
20229
20799
  total: statsData.total || 0
20230
20800
  };
20231
- const totalPages = Math.ceil(counts.total / limit);
20232
- let filterBtns = `<div style="margin-bottom:12px;display:flex;gap:6px;">
20233
- <a href="/" class="btn ${!statusFilter ? "btn-primary" : ""}">\u5168\u90E8</a>
20234
- <a href="/?status=pending" class="btn ${statusFilter === "pending" ? "btn-primary" : ""}">Pending</a>
20235
- <a href="/?status=running" class="btn ${statusFilter === "running" ? "btn-primary" : ""}">Running</a>
20236
- <a href="/?status=done" class="btn ${statusFilter === "done" ? "btn-primary" : ""}">Done</a>
20237
- <a href="/?status=failed" class="btn ${statusFilter === "failed" ? "btn-primary" : ""}">Failed</a>
20238
- <a href="/?status=dead_letter" class="btn ${statusFilter === "dead_letter" ? "btn-primary" : ""}">Dead Letter</a>
20239
- </div>`;
20240
- let rows = "";
20241
- for (const task of tasks4) {
20801
+ const filteredTotal = parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
20802
+ const totalPages = Math.max(1, Math.ceil(filteredTotal / limit));
20803
+ const filterItems = [
20804
+ { status: "", label: t(locale, "filter.all") },
20805
+ { status: "pending", label: statusText(locale, "pending") },
20806
+ { status: "running", label: statusText(locale, "running") },
20807
+ { status: "done", label: statusText(locale, "done") },
20808
+ { status: "failed", label: statusText(locale, "failed") },
20809
+ { status: "dead_letter", label: statusText(locale, "dead_letter") },
20810
+ { status: "cancelled", label: statusText(locale, "cancelled") }
20811
+ ];
20812
+ const filters = filterItems.map(({ status, label }) => {
20813
+ const href = status ? `/?status=${status}` : "/";
20814
+ return `<a href="${href}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
20815
+ }).join("");
20816
+ const rows = tasks4.map((task) => {
20242
20817
  const status = safeStatus(task.status);
20243
- const st = status.toUpperCase();
20244
20818
  const executionActive = latestRuns.get(task.id)?.status === "running";
20245
- rows += `<tr>
20246
- <td class="mu">#${task.id}</td>
20247
- <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
20248
- <td><span class="tag">${esc(task.agent)}</span></td>
20249
- <td><span class="badge b-${status}">${st}</span></td>
20250
- <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
20251
- <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
20252
- <td>
20253
- <button class="btn btn-sm" onclick="showDetail(${task.id})">\u8BE6\u60C5</button>
20254
- ${task.status === "failed" || task.status === "dead_letter" ? `<button class="btn btn-sm btn-warn" onclick="retryTask(${task.id})">\u91CD\u8BD5</button>` : ""}
20255
- ${["pending", "running", "failed"].includes(task.status ?? "") ? `<button class="btn btn-sm btn-warn" onclick="cancelTask(${task.id})">\u53D6\u6D88</button>` : ""}
20256
- ${task.status === "running" || executionActive ? "" : `<button class="btn btn-sm btn-danger" onclick="deleteTask(${task.id})">\u5220\u9664</button>`}
20257
- </td></tr>`;
20258
- }
20259
- const qp = statusFilter ? `&status=${statusFilter}` : "";
20260
- let paging = `<div class="pn">`;
20261
- if (page > 1) paging += `<a href="/?page=${page - 1}${qp}" class="btn">\u4E0A\u4E00\u9875</a>`;
20262
- paging += `<span class="mu sm">\u7B2C ${page} \u9875 / \u5171 ${totalPages} \u9875 (${counts.total} \u6761)</span>`;
20263
- if (page < totalPages) paging += `<a href="/?page=${page + 1}${qp}" class="btn">\u4E0B\u4E00\u9875</a>`;
20264
- paging += `</div>`;
20819
+ const searchable = esc(`${task.name} ${task.agent} ${task.prompt}`);
20820
+ return `<tr data-task-row data-search="${searchable}">
20821
+ <td class="faint" data-label="${t(locale, "table.id")}">#${task.id}</td>
20822
+ <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>
20823
+ <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(task.agent)}</span></td>
20824
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
20825
+ <td data-label="${t(locale, "table.duration")}" class="small ${task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
20826
+ <td data-label="${t(locale, "table.retries")}" class="muted small">${(task.retryCount ?? 0) > 0 ? task.retryCount : "\u2014"}</td>
20827
+ <td data-label="${t(locale, "table.actions")}"><div class="actions">
20828
+ <button type="button" class="btn" onclick="showDetail(${task.id})">${t(locale, "action.details")}</button>
20829
+ ${task.status === "failed" || task.status === "dead_letter" ? `<button type="button" class="btn btn-warning" onclick="retryTask(${task.id})">${t(locale, "action.retry")}</button>` : ""}
20830
+ ${["pending", "running", "failed"].includes(task.status ?? "") ? `<button type="button" class="btn btn-warning" onclick="cancelTask(${task.id})">${t(locale, "action.cancel")}</button>` : ""}
20831
+ ${task.status === "running" || executionActive ? "" : `<button type="button" class="btn btn-danger" onclick="deleteTask(${task.id})">${t(locale, "action.delete")}</button>`}
20832
+ </div></td>
20833
+ </tr>`;
20834
+ }).join("");
20835
+ const table = tasks4.length === 0 ? emptyState(t(locale, "empty.tasks"), t(locale, "empty.tasksHint")) : `<div class="table-wrap"><table class="responsive-table">
20836
+ <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>
20837
+ <tbody>${rows}</tbody>
20838
+ </table></div>
20839
+ <div id="search-empty" hidden>${emptyState(t(locale, "filter.noResults"), "")}</div>`;
20840
+ const suffix = statusFilter ? `&status=${statusFilter}` : "";
20265
20841
  const body = `
20266
- <div class="g4">
20267
- <div class="card"><div class="sv" style="color:var(--t2)">${counts.pending}</div><div class="sl">Pending</div></div>
20268
- <div class="card"><div class="sv" style="color:var(--blue)">${counts.running}</div><div class="sl">Running</div></div>
20269
- <div class="card"><div class="sv" style="color:var(--green)">${counts.done}</div><div class="sl">Done</div></div>
20270
- <div class="card"><div class="sv" style="color:var(--red)">${counts.failed}</div><div class="sl">Failed / Dead</div></div>
20842
+ <div class="stats-grid">
20843
+ ${statCard(counts.pending, t(locale, "stats.pending"), "tone-neutral", icon("clock"))}
20844
+ ${statCard(counts.running, t(locale, "stats.running"), "tone-blue", icon("activity"), "reveal-delay-1")}
20845
+ ${statCard(counts.done, t(locale, "stats.done"), "tone-green", icon("check"), "reveal-delay-1")}
20846
+ ${statCard(counts.failed, t(locale, "stats.failedDead"), "tone-red", icon("alert"), "reveal-delay-2")}
20847
+ </div>
20848
+ <div class="toolbar reveal reveal-delay-1">
20849
+ <div class="filters">${filters}</div>
20850
+ <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>
20271
20851
  </div>
20272
- ${filterBtns}
20273
- <div class="panel"><table>
20274
- <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>
20275
- <tbody>${rows}</tbody>
20276
- </table></div>
20277
- ${paging}`;
20278
- return c.html(renderLayout("\u4EFB\u52A1\u961F\u5217", "tasks", body));
20852
+ <section class="panel reveal reveal-delay-2">${table}</section>
20853
+ ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}`;
20854
+ return c.html(renderLayout({ locale, activeTab: "tasks", body }));
20279
20855
  });
20280
20856
  app.get("/templates", async (c) => {
20857
+ const locale = resolveLocale(c);
20281
20858
  const templates = await TaskTemplateService.list(100);
20282
- const enabled = templates.filter((t) => t.enabled).length;
20859
+ const enabled = templates.filter((template) => template.enabled).length;
20283
20860
  const disabled = templates.length - enabled;
20284
- let rows = "";
20285
- for (const t of templates) {
20286
- const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
20287
- const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
20288
- const typeClass = "tag t-" + scheduleType;
20289
- let rule = "-";
20290
- if (t.scheduleType === "cron") rule = t.cronExpr || "-";
20291
- else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
20292
- else if (t.scheduleType === "delayed") rule = formatDate(t.runAt);
20293
- const statusBadge = t.enabled ? '<span class="badge b-done">\u542F\u7528</span>' : '<span class="badge b-cancelled">\u7981\u7528</span>';
20294
- 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>`;
20295
- rows += `<tr>
20296
- <td class="mu">#${t.id}</td>
20297
- <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
20298
- <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>
20299
- <td><span class="${typeClass}">${typeLabel}</span></td>
20300
- <td class="m sm">${esc(rule)}</td>
20301
- <td>${statusBadge}</td>
20302
- <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
20303
- <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
20304
- <td>
20305
- <button class="btn btn-sm" onclick="showTemplateDetail(${t.id})">\u8BE6\u60C5</button>
20306
- <button class="btn btn-sm btn-primary" onclick="triggerTmpl(${t.id})">\u89E6\u53D1</button>
20307
- ${toggleBtn}
20308
- <button class="btn btn-sm btn-danger" onclick="deleteTmpl(${t.id})">\u5220\u9664</button>
20309
- </td></tr>`;
20310
- }
20311
- 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>` : "";
20861
+ const rows = templates.map((template) => {
20862
+ const scheduleType = ["cron", "recurring", "delayed"].includes(template.scheduleType) ? template.scheduleType : "unknown";
20863
+ const typeLabel = scheduleType === "cron" ? t(locale, "schedule.cron") : scheduleType === "recurring" ? t(locale, "schedule.recurring") : scheduleType === "delayed" ? t(locale, "schedule.delayed") : t(locale, "schedule.unknown");
20864
+ let rule = "\u2014";
20865
+ if (template.scheduleType === "cron") rule = template.cronExpr || "\u2014";
20866
+ if (template.scheduleType === "recurring" && template.intervalMs) rule = formatInterval(template.intervalMs, locale);
20867
+ if (template.scheduleType === "delayed") rule = formatDateTime(template.runAt, locale);
20868
+ 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>`;
20869
+ return `<tr>
20870
+ <td class="faint" data-label="${t(locale, "table.id")}">#${template.id}</td>
20871
+ <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>
20872
+ <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>
20873
+ <td data-label="${t(locale, "table.type")}"><span class="tag t-${scheduleType}">${typeLabel}</span></td>
20874
+ <td data-label="${t(locale, "table.rule")}" class="m small">${esc(rule)}</td>
20875
+ <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>
20876
+ <td data-label="${t(locale, "table.lastRun")}" class="small muted">${formatRelative(template.lastRunAt, locale)}</td>
20877
+ <td data-label="${t(locale, "table.nextRun")}" class="small">${formatFuture(template.nextRunAt, locale)}</td>
20878
+ <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showTemplateDetail(${template.id})">${t(locale, "action.details")}</button>
20879
+ <button type="button" class="btn btn-primary" onclick="triggerTmpl(${template.id})">${t(locale, "action.trigger")}</button>${toggle}
20880
+ <button type="button" class="btn btn-danger" onclick="deleteTmpl(${template.id})">${t(locale, "action.delete")}</button></div></td>
20881
+ </tr>`;
20882
+ }).join("");
20312
20883
  const body = `
20313
- <div class="g3">
20314
- <div class="card"><div class="sv" style="color:var(--purple)">${templates.length}</div><div class="sl">\u6A21\u677F\u603B\u6570</div></div>
20315
- <div class="card"><div class="sv" style="color:var(--green)">${enabled}</div><div class="sl">\u5DF2\u542F\u7528</div></div>
20316
- <div class="card"><div class="sv" style="color:var(--t2)">${disabled}</div><div class="sl">\u5DF2\u7981\u7528</div></div>
20884
+ <div class="stats-grid three">
20885
+ ${statCard(templates.length, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
20886
+ ${statCard(enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
20887
+ ${statCard(disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
20317
20888
  </div>
20318
- <div class="panel">
20319
- <div class="ph"><h3>\u8C03\u5EA6\u6A21\u677F</h3></div>
20320
- <table>
20321
- <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>
20322
- <tbody>${rows}${emptyRow}</tbody>
20323
- </table>
20324
- </div>`;
20325
- return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
20889
+ <section class="panel reveal reveal-delay-2">
20890
+ <div class="panel-head"><h2>${t(locale, "page.templates.title")}</h2></div>
20891
+ ${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>`}
20892
+ </section>`;
20893
+ return c.html(renderLayout({ locale, activeTab: "templates", body }));
20326
20894
  });
20327
20895
  app.get("/runs", async (c) => {
20896
+ const locale = resolveLocale(c);
20328
20897
  const page = parsePositiveInteger(c.req.query("page") || "1");
20329
20898
  if (page === null) return c.text("invalid page", 400);
20330
20899
  const limit = 50;
20331
20900
  const offset = (page - 1) * limit;
20332
- const { taskRuns: tr, tasks: tk } = schema_exports;
20901
+ const { taskRuns: taskRuns4, tasks: tasks4 } = schema_exports;
20333
20902
  const runs = await db.select({
20334
- id: tr.id,
20335
- taskId: tr.taskId,
20336
- sessionId: tr.sessionId,
20337
- model: tr.model,
20338
- status: tr.status,
20339
- startedAt: tr.startedAt,
20340
- finishedAt: tr.finishedAt,
20341
- log: tr.log,
20342
- heartbeatAt: tr.heartbeatAt,
20343
- workerPid: tr.workerPid,
20344
- childPid: tr.childPid,
20345
- taskName: tk.name,
20346
- taskAgent: tk.agent
20347
- }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
20348
- const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
20903
+ id: taskRuns4.id,
20904
+ taskId: taskRuns4.taskId,
20905
+ sessionId: taskRuns4.sessionId,
20906
+ model: taskRuns4.model,
20907
+ status: taskRuns4.status,
20908
+ startedAt: taskRuns4.startedAt,
20909
+ finishedAt: taskRuns4.finishedAt,
20910
+ log: taskRuns4.log,
20911
+ heartbeatAt: taskRuns4.heartbeatAt,
20912
+ workerPid: taskRuns4.workerPid,
20913
+ childPid: taskRuns4.childPid,
20914
+ taskName: tasks4.name,
20915
+ taskAgent: tasks4.agent
20916
+ }).from(taskRuns4).innerJoin(tasks4, eq(taskRuns4.taskId, tasks4.id)).orderBy(desc(taskRuns4.startedAt), desc(taskRuns4.id)).limit(limit).offset(offset);
20917
+ const totalResult = await db.select({ count: sql`count(*)` }).from(taskRuns4);
20349
20918
  const total = Number(totalResult[0]?.count ?? 0);
20350
- const totalPages = Math.ceil(total / limit);
20351
- let rows = "";
20352
- const logsHtml = [];
20353
- for (const run of runs) {
20919
+ const totalPages = Math.max(1, Math.ceil(total / limit));
20920
+ const logs = [];
20921
+ const rows = runs.map((run) => {
20354
20922
  const status = safeStatus(run.status);
20355
- const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
20356
- const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
20357
- rows += `<tr>
20358
- <td class="mu">#${run.id}</td>
20359
- <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
20360
- ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
20361
- <td><span class="tag">${esc(run.taskAgent)}</span></td>
20362
- <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
20363
- <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
20364
- <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
20365
- <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
20366
- </tr>`;
20367
20923
  if (run.log) {
20368
- logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
20369
- <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
20370
- <div class="log-box">${esc(run.log)}</div></div></div>`);
20371
- }
20372
- }
20373
- let paging = `<div class="pn">`;
20374
- if (page > 1) paging += `<a href="/runs?page=${page - 1}" class="btn">\u4E0A\u4E00\u9875</a>`;
20375
- paging += `<span class="mu sm">\u7B2C ${page} \u9875 / \u5171 ${totalPages} \u9875 (${total} \u6761)</span>`;
20376
- if (page < totalPages) paging += `<a href="/runs?page=${page + 1}" class="btn">\u4E0B\u4E00\u9875</a>`;
20377
- paging += `</div>`;
20378
- const emptyRow = runs.length === 0 ? `<tr><td colspan="7" class="ta-center mu p30">\u6682\u65E0\u6267\u884C\u8BB0\u5F55</td></tr>` : "";
20924
+ 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>`);
20925
+ }
20926
+ return `<tr>
20927
+ <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
20928
+ <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>
20929
+ <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(run.taskAgent)}</span></td>
20930
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
20931
+ <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, run.finishedAt)}</td>
20932
+ <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td>
20933
+ <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showRunDetail(${run.id})">${t(locale, "action.details")}</button>
20934
+ ${run.log ? `<button type="button" class="btn" aria-expanded="false" onclick="toggleLog(${run.id},this)">${t(locale, "action.logs")}</button>` : ""}</div></td>
20935
+ </tr>`;
20936
+ }).join("");
20379
20937
  const body = `
20380
- <div class="g4">
20381
- <div class="card"><div class="sv">${total}</div><div class="sl">\u603B\u8BB0\u5F55</div></div>
20382
- <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>
20383
- <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>
20384
- <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>
20938
+ <div class="stats-grid">
20939
+ ${statCard(total, t(locale, "stats.records"), "tone-purple", icon("runs"))}
20940
+ ${statCard(runs.filter((run) => run.status === "done").length, t(locale, "stats.pageDone"), "tone-green", icon("check"), "reveal-delay-1")}
20941
+ ${statCard(runs.filter((run) => run.status === "failed").length, t(locale, "stats.pageFailed"), "tone-red", icon("alert"), "reveal-delay-1")}
20942
+ ${statCard(runs.filter((run) => run.status === "running").length, t(locale, "stats.pageRunning"), "tone-blue", icon("activity"), "reveal-delay-2")}
20385
20943
  </div>
20386
- <div class="panel"><table>
20387
- <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>
20388
- <tbody>${rows}${emptyRow}</tbody>
20389
- </table></div>
20390
- ${logsHtml.join("")}
20391
- ${paging}`;
20392
- return c.html(renderLayout("\u6267\u884C\u65E5\u5FD7", "runs", body));
20944
+ <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>
20945
+ ${logs.join("")}${pagination(locale, "/runs", page, totalPages, total)}`;
20946
+ return c.html(renderLayout({ locale, activeTab: "runs", body }));
20393
20947
  });
20394
20948
  app.get("/system", async (c) => {
20949
+ const locale = resolveLocale(c);
20395
20950
  const config = loadConfig();
20396
20951
  const configPath = getConfigPath();
20397
- const stats = await TaskService.stats({});
20398
- const runningRuns = await TaskRunService.getAllRunningRuns();
20399
- const templates = await TaskTemplateService.list(100);
20952
+ const [stats, runningRuns, templates] = await Promise.all([
20953
+ TaskService.stats({}),
20954
+ TaskRunService.getAllRunningRuns(),
20955
+ TaskTemplateService.list(100)
20956
+ ]);
20400
20957
  const configExists = existsSync6(configPath);
20401
- let runRows = "";
20402
- if (runningRuns.length > 0) {
20403
- for (const run of runningRuns) {
20404
- const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
20405
- runRows += `<tr>
20406
- <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
20407
- <td class="m sm">${esc(shortS)}</td>
20408
- <td class="sm">${esc(run.model) || "-"}</td>
20409
- <td class="sm">${formatDate(run.startedAt)}</td>
20410
- <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
20411
- <td class="m sm">W:${run.workerPid ?? "-"} C:${run.childPid ?? "-"}</td>
20412
- <td class="sm">${formatDuration(run.startedAt, null)}</td>
20413
- </tr>`;
20414
- }
20415
- }
20416
- const schedulerStatus = config.scheduler.enabled ? '<span class="badge b-done">\u5DF2\u542F\u7528</span>' : '<span class="badge b-cancelled">\u5DF2\u7981\u7528</span>';
20417
- const configFileStatus = configExists ? '<span class="badge b-done">\u662F</span>' : '<span class="badge b-cancelled">\u5426 (\u4F7F\u7528\u9ED8\u8BA4\u503C)</span>';
20958
+ const runRows = runningRuns.map((run) => {
20959
+ const session = run.sessionId ? `${run.sessionId.slice(4, 7)}***${run.sessionId.slice(-3)}` : "\u2014";
20960
+ 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>
20961
+ <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>
20962
+ <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>
20963
+ <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, null)}</td></tr>`;
20964
+ }).join("");
20965
+ 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>`;
20418
20966
  const body = `
20419
- <form id="config-form" onsubmit="event.preventDefault();saveConfig();">
20420
- <div class="g3">
20421
- <div class="card">
20422
- <h3 style="margin:0 0 12px;font-size:14px">Worker \u914D\u7F6E</h3>
20423
- <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>
20424
- <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>
20425
- <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>
20426
- <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>
20427
- </div>
20428
- <div class="card">
20429
- <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
20430
- <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
20431
- <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>
20432
- <div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
20967
+ <form id="config-form" onsubmit="event.preventDefault();saveConfig()">
20968
+ <div class="settings-grid reveal">
20969
+ <section class="card settings-card"><h2 class="settings-title"><span>${icon("activity")}${t(locale, "system.worker")}</span></h2>
20970
+ <div class="field"><label for="mc">${t(locale, "system.maxConcurrency")}</label>${unitInput("mc", config.worker.maxConcurrency, 1, "\xD7", 20)}</div>
20971
+ <div class="field"><label for="pi">${t(locale, "system.pollInterval")}</label>${unitInput("pi", config.worker.pollIntervalMs, 100, t(locale, "system.milliseconds"))}</div>
20972
+ <div class="field"><label for="hi">${t(locale, "system.heartbeatInterval")}</label>${unitInput("hi", config.worker.heartbeatIntervalMs / 1e3, 5, t(locale, "system.seconds"))}</div>
20973
+ <div class="field"><label for="to">${t(locale, "system.taskTimeout")}</label>${unitInput("to", config.worker.taskTimeoutMs / 6e4, 1, t(locale, "system.minutes"))}</div>
20974
+ </section>
20975
+ <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>
20976
+ <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>
20977
+ <div class="field"><label for="si">${t(locale, "system.checkInterval")}</label>${unitInput("si", config.scheduler.checkIntervalMs, 100, t(locale, "system.milliseconds"))}</div>
20978
+ <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>
20979
+ </section>
20980
+ <section class="card settings-card reveal-delay-2"><h2 class="settings-title"><span>${icon("system")}${t(locale, "system.watchdog")}</span></h2>
20981
+ <div class="field"><label for="wt">${t(locale, "system.heartbeatTimeout")}</label>${unitInput("wt", config.watchdog.heartbeatTimeoutMs / 1e3, 10, t(locale, "system.seconds"))}</div>
20982
+ <div class="field"><label for="wci">${t(locale, "system.checkInterval")}</label>${unitInput("wci", config.watchdog.checkIntervalMs / 1e3, 1, t(locale, "system.seconds"))}</div>
20983
+ <div class="field"><label for="wcl">${t(locale, "system.cleanupInterval")}</label>${unitInput("wcl", config.watchdog.cleanupIntervalMs / 36e5, 1, t(locale, "system.hours"))}</div>
20984
+ <div class="field"><label for="rd">${t(locale, "system.retentionDays")}</label>${unitInput("rd", config.watchdog.retentionDays, 1, t(locale, "system.days"))}</div>
20985
+ </section>
20433
20986
  </div>
20434
- <div class="card">
20435
- <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
20436
- <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>
20437
- <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>
20438
- <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>
20439
- <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>
20440
- </div>
20441
- </div>
20442
- <div style="text-align:center;margin-bottom:24px">
20443
- <button type="submit" class="rf" style="font-size:14px;padding:10px 30px">\u4FDD\u5B58\u914D\u7F6E</button>
20444
- <span class="mu sm" style="margin-left:12px">\u4FDD\u5B58\u540E\u9700\u91CD\u542F Gateway \u751F\u6548</span>
20445
- </div>
20987
+ <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>
20446
20988
  </form>
20447
-
20448
- <div class="panel mt8">
20449
- <div class="ph"><h3>\u5F53\u524D\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1 (${runningRuns.length} / ${config.worker.maxConcurrency} \u5E76\u53D1)</h3></div>
20450
- ${runningRuns.length > 0 ? `<table>
20451
- <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>
20452
- <tbody>${runRows}</tbody>
20453
- </table>` : `<div class="ta-center mu p30">\u5F53\u524D\u65E0\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1</div>`}
20454
- </div>
20455
-
20456
- <div class="card mt16">
20457
- <h3 style="margin:0 0 12px;font-size:14px">\u4EFB\u52A1\u7EDF\u8BA1</h3>
20458
- <div class="g4 mb0">
20459
- <div><span class="mu sm">Pending:</span> <strong>${stats.pending || 0}</strong></div>
20460
- <div><span class="mu sm">Running:</span> <strong style="color:var(--blue)">${stats.running || 0}</strong></div>
20461
- <div><span class="mu sm">Done:</span> <strong style="color:var(--green)">${stats.done || 0}</strong></div>
20462
- <div><span class="mu sm">Failed/Dead:</span> <strong style="color:var(--red)">${(stats.failed || 0) + (stats.dead_letter || 0)}</strong></div>
20463
- </div>
20464
- </div>
20465
-
20466
- <div class="card mt16">
20467
- <h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
20468
- <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
20469
- <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
20470
- </div>
20471
-
20472
- <div class="card mt16" style="border-color:var(--red)">
20473
- <h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
20474
- <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>
20475
- <button class="btn btn-danger" style="border-color:var(--red);color:var(--red);padding:6px 16px" onclick="clearDatabase()">\u6E05\u7A7A\u6570\u636E\u5E93</button>
20476
- </div>`;
20477
- return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
20989
+ <section class="panel reveal">
20990
+ <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: config.worker.maxConcurrency })}</h2></div>
20991
+ ${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>`}
20992
+ </section>
20993
+ <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.taskStats")}</h2></div><div class="overview-grid">
20994
+ <div class="overview-item"><span>${statusText(locale, "pending")}</span><strong>${stats.pending || 0}</strong></div>
20995
+ <div class="overview-item"><span>${statusText(locale, "running")}</span><strong style="color:var(--blue)">${stats.running || 0}</strong></div>
20996
+ <div class="overview-item"><span>${statusText(locale, "done")}</span><strong style="color:var(--green)">${stats.done || 0}</strong></div>
20997
+ <div class="overview-item"><span>${t(locale, "stats.failedDead")}</span><strong style="color:var(--red)">${(stats.failed || 0) + (stats.dead_letter || 0)}</strong></div>
20998
+ </div></section>
20999
+ <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.configFile")}</h2></div><div class="info-list">
21000
+ <div class="info-row"><span class="info-key">${t(locale, "system.path")}</span><span class="info-value m small">${esc(configPath)}</span></div>
21001
+ <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>
21002
+ </div></section>
21003
+ <section class="card danger-card reveal reveal-delay-2"><h2>${icon("alert")}${t(locale, "system.danger")}</h2><p>${t(locale, "system.dangerDescription")}</p>
21004
+ <button type="button" class="btn btn-danger" onclick="clearDatabase()">${icon("database")}${t(locale, "action.clearDatabase")}</button></section>`;
21005
+ return c.html(renderLayout({ locale, activeTab: "system", body }));
20478
21006
  });
20479
21007
  app.get("/api/tasks/:id", async (c) => {
20480
21008
  const id = parsePositiveInteger(c.req.param("id"));
@@ -20494,9 +21022,9 @@ var init_web = __esm({
20494
21022
  app.get("/api/templates/:id", async (c) => {
20495
21023
  const id = parsePositiveInteger(c.req.param("id"));
20496
21024
  if (id === null) return c.json({ error: "invalid id" }, 400);
20497
- const tmpl = await TaskTemplateService.getById(id);
20498
- if (!tmpl) return c.json({ error: "not found" }, 404);
20499
- return c.json(tmpl);
21025
+ const template = await TaskTemplateService.getById(id);
21026
+ if (!template) return c.json({ error: "not found" }, 404);
21027
+ return c.json(template);
20500
21028
  });
20501
21029
  app.post("/api/tasks/:id/retry", async (c) => {
20502
21030
  const id = parsePositiveInteger(c.req.param("id"));
@@ -20540,14 +21068,14 @@ var init_web = __esm({
20540
21068
  app.delete("/api/templates/:id", async (c) => {
20541
21069
  const id = parsePositiveInteger(c.req.param("id"));
20542
21070
  if (id === null) return c.json({ error: "invalid id" }, 400);
20543
- const ok = await TaskTemplateService.delete(id);
20544
- return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
21071
+ const deleted = await TaskTemplateService.delete(id);
21072
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
20545
21073
  });
20546
21074
  app.post("/api/templates/:id/trigger", async (c) => {
20547
21075
  const id = parsePositiveInteger(c.req.param("id"));
20548
21076
  if (id === null) return c.json({ error: "invalid id" }, 400);
20549
- const tmpl = await TaskTemplateService.getById(id);
20550
- if (!tmpl) return c.json({ error: "not found" }, 404);
21077
+ const template = await TaskTemplateService.getById(id);
21078
+ if (!template) return c.json({ error: "not found" }, 404);
20551
21079
  const task = await triggerTaskFromTemplate(id);
20552
21080
  if (!task) return c.json({ error: "maxInstances reached" }, 409);
20553
21081
  return c.json({ success: true, taskId: task.id });
@@ -20556,24 +21084,27 @@ var init_web = __esm({
20556
21084
  try {
20557
21085
  const body = await c.req.json();
20558
21086
  const current = readCurrentConfig();
20559
- const curW = current.worker ?? {};
20560
- const curS = current.scheduler ?? {};
20561
- const curD = current.watchdog ?? {};
20562
- const bW = body.worker ?? {};
20563
- const bS = body.scheduler ?? {};
20564
- const bD = body.watchdog ?? {};
21087
+ const currentWorker = current.worker ?? {};
21088
+ const currentScheduler = current.scheduler ?? {};
21089
+ const currentWatchdog = current.watchdog ?? {};
21090
+ const bodyWorker = body.worker ?? {};
21091
+ const bodyScheduler = body.scheduler ?? {};
21092
+ const bodyWatchdog = body.watchdog ?? {};
20565
21093
  const merged = {
20566
21094
  ...current,
20567
21095
  ...body,
20568
21096
  configVersion: 2,
20569
- worker: { ...curW, ...bW },
20570
- scheduler: { ...curS, ...bS },
20571
- watchdog: { ...curD, ...bD }
21097
+ worker: { ...currentWorker, ...bodyWorker },
21098
+ scheduler: { ...currentScheduler, ...bodyScheduler },
21099
+ watchdog: { ...currentWatchdog, ...bodyWatchdog }
20572
21100
  };
20573
21101
  writeConfig(validateConfig(merged));
20574
21102
  return c.json({ success: true });
20575
- } catch (err) {
20576
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
21103
+ } catch (error) {
21104
+ return c.json({
21105
+ success: false,
21106
+ error: error instanceof Error ? error.message : String(error)
21107
+ }, 400);
20577
21108
  }
20578
21109
  });
20579
21110
  app.post("/api/database/clear", async (c) => {
@@ -20584,9 +21115,12 @@ var init_web = __esm({
20584
21115
  try {
20585
21116
  const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
20586
21117
  return c.json({ success: true, ...result });
20587
- } catch (err) {
20588
- const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
20589
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
21118
+ } catch (error) {
21119
+ const status = error instanceof DatabaseMaintenanceConflictError ? 409 : 500;
21120
+ return c.json({
21121
+ success: false,
21122
+ error: error instanceof Error ? error.message : String(error)
21123
+ }, status);
20590
21124
  }
20591
21125
  });
20592
21126
  dashboardApp = app;